org.forgerock.openidm.shell.impl.RemoteCommandScope.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.shell.impl.RemoteCommandScope.java

Source

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2011-2015 ForgeRock AS.
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * http://forgerock.org/license/CDDLv1.0.html
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at http://forgerock.org/license/CDDLv1.0.html
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 */

package org.forgerock.openidm.shell.impl;

import java.io.File;
import java.io.FileFilter;
import java.io.InputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Descriptor;
import org.apache.felix.service.command.Parameter;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourcePath;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.config.persistence.ConfigBootstrapHelper;
import org.forgerock.openidm.core.IdentityServer;
import org.forgerock.openidm.shell.CustomCommandScope;
import org.forgerock.openidm.shell.felixgogo.MetaVar;
import org.forgerock.services.context.RootContext;

/**
 * Command scope for remote operations.
 */
public class RemoteCommandScope extends CustomCommandScope {

    private static final String DOTTED_PLACEHOLDER = "............................................";

    private static final String IDM_PORT_DEFAULT = "8080";
    private static final String IDM_PORT_DESC = "Port of OpenIDM REST service. This will override any port in --url. Default "
            + IDM_PORT_DEFAULT;
    private static final String IDM_PORT_METAVAR = "PORT";

    private static final String IDM_URL_DEFAULT = "http://localhost:8080/openidm/";
    private static final String IDM_URL_DESC = "URL of OpenIDM REST service. Default " + IDM_URL_DEFAULT;
    private static final String IDM_URL_METAVAR = "URL";

    private static final String USER_PASS_DESC = "Server user and password";
    private static final String USER_PASS_METAVAR = "USER[:PASSWORD]";
    private static final String USER_PASS_DEFAULT = "";

    private static final String REPLACE_ALL_DESC = "Replace the entire config set by deleting the additional configuration";

    private final HttpRemoteJsonResource resource = new HttpRemoteJsonResource();

    private final ObjectMapper mapper = new ObjectMapper();

    /**
     * {@inheritDoc}
     */
    public Map<String, String> getFunctionMap() {
        Map<String, String> help = new HashMap<String, String>();
        help.put("configimport", getLongHeader("configimport"));
        help.put("configexport", getLongHeader("configexport"));
        help.put("configureconnector", getLongHeader("configureconnector"));
        help.put("update", getLongHeader("update"));
        return help;
    }

    /**
     * {@inheritDoc}
     */
    public String getScope() {
        return "remote";
    }

    private void processOptions(final String userPass, final String idmUrl, final String idmPort) {
        String username = "";
        String password = "";

        if (StringUtils.isNotBlank(userPass)) {
            int passwordIdx = userPass.indexOf(":") + 1;

            if (passwordIdx > 0) {
                username = userPass.substring(0, passwordIdx - 1);
                password = userPass.substring(passwordIdx);
            } else {
                username = userPass;
                password = new String(System.console().readPassword("Password:"));
            }

            resource.setUsername(username);
            resource.setPassword(password);
        }

        if (StringUtils.isNotBlank(idmUrl)) {
            resource.setBaseUri(idmUrl.endsWith("/") ? idmUrl : idmUrl + "/");
        }

        if (StringUtils.isNotBlank(idmPort)) {
            if (NumberUtils.isDigits(idmPort)) {
                resource.setPort(Integer.decode(idmPort));
            } else {
                throw new IllegalArgumentException("Port must be a number");
            }
        }
    }

    /**
     * Handles the Update (upgrade/patch) process.
     * @param session session that invoked the command.
     * @param userPass user:passwd to be used on rest calls.
     * @param idmUrl url to the idm instance.
     * @param idmPort port that idm is running on.
     * @param acceptLicense if true, the accept license step is skipped.
     * @param archive The simple file name of the archive that is already in bin/update.
     */
    @Descriptor("Update the system with the provided update file.")
    public void update(CommandSession session,
            @Descriptor(USER_PASS_DESC) @MetaVar(USER_PASS_METAVAR) @Parameter(names = { "-u",
                    "--user" }, absentValue = USER_PASS_DEFAULT) final String userPass,

            @Descriptor(IDM_URL_DESC) @MetaVar(IDM_URL_METAVAR) @Parameter(names = {
                    "--url" }, absentValue = IDM_URL_DEFAULT) final String idmUrl,

            @Descriptor(IDM_PORT_DESC) @MetaVar(IDM_PORT_METAVAR) @Parameter(names = { "-P",
                    "--port" }, absentValue = IDM_PORT_DEFAULT) final String idmPort,

            @Descriptor("Automatically accepts the product license (if present). "
                    + "Defaults to 'false' to ask for acceptance.") @Parameter(names = {
                            "--acceptLicense" }, presentValue = "true", absentValue = "false") final boolean acceptLicense,

            @Descriptor("Timeout value to wait for jobs to finish. "
                    + "Defaults to -1 to exit immediately if jobs are running.") @MetaVar("TIME") @Parameter(names = {
                            "--maxJobsFinishWaitTimeMs" }, absentValue = "-1") final long maxJobsFinishWaitTimeMs,

            @Descriptor("Timeout value to wait for update process to complete. Defaults to 30000 ms.") @MetaVar("TIME") @Parameter(names = {
                    "--maxUpdateWaitTimeMs" }, absentValue = "30000") final long maxUpdateWaitTimeMs,

            @Descriptor("Log file path. (optional) Defaults to logs/update.log") @MetaVar("LOG_FILE") @Parameter(names = {
                    "-l", "--log" }, absentValue = "logs/update.log") final String logFilePath,

            @Descriptor("Log only to the log file.") @Parameter(names = { "-Q",
                    "--Quiet" }, presentValue = "true", absentValue = "false") final boolean quietMode,

            @Descriptor("Filename of the Update archive within bin/update.") String archive) {

        processOptions(userPass, idmUrl, idmPort);
        UpdateCommandConfig config = new UpdateCommandConfig().setUpdateArchive(archive).setLogFilePath(logFilePath)
                .setQuietMode(quietMode).setAcceptedLicense(acceptLicense)
                .setMaxJobsFinishWaitTimeMs(maxJobsFinishWaitTimeMs).setMaxUpdateWaitTimeMs(maxUpdateWaitTimeMs);

        new UpdateCommand(session, resource, config).execute(new RootContext());
    }

    /**
     * Import the configuration set from a local "conf" directory.
     *
     * @param session the command session
     * @param userPass the username/password
     * @param idmUrl the url of the OpenIDM instance
     * @param idmPort the OpenIDM instance's port
     * @param replaceall whether or not to replace the config
     */
    @Descriptor("Imports the configuration set from local 'conf' directory.")
    public void configimport(final CommandSession session,

            @Descriptor(USER_PASS_DESC) @MetaVar(USER_PASS_METAVAR) @Parameter(names = { "-u",
                    "--user" }, absentValue = USER_PASS_DEFAULT) final String userPass,

            @Descriptor(IDM_URL_DESC) @MetaVar(IDM_URL_METAVAR) @Parameter(names = {
                    "--url" }, absentValue = IDM_URL_DEFAULT) final String idmUrl,

            @Descriptor(IDM_PORT_DESC) @MetaVar(IDM_PORT_METAVAR) @Parameter(names = { "-P",
                    "--port" }, absentValue = IDM_PORT_DEFAULT) final String idmPort,

            @Descriptor(REPLACE_ALL_DESC) @Parameter(names = { "-r", "--replaceall",
                    "--replaceAll" }, presentValue = "true", absentValue = "false") final boolean replaceall) {
        configimport(session, userPass, idmUrl, idmPort, replaceall, "conf");
    }

    /**
     * Imports the configuration set from a local file or directory.
     *
     * @param session the command session
     * @param userPass the username/password
     * @param idmUrl the url of the OpenIDM instance
     * @param idmPort the OpenIDM instance's port
     * @param replaceall whether or not to replace the config
     * @param source the source directory
     */
    @Descriptor("Imports the configuration set from local file/directory.")
    public void configimport(final CommandSession session,

            @Descriptor(USER_PASS_DESC) @MetaVar(USER_PASS_METAVAR) @Parameter(names = { "-u",
                    "--user" }, absentValue = USER_PASS_DEFAULT) final String userPass,

            @Descriptor(IDM_URL_DESC) @MetaVar(IDM_URL_METAVAR) @Parameter(names = {
                    "--url" }, absentValue = IDM_URL_DEFAULT) final String idmUrl,

            @Descriptor(IDM_PORT_DESC) @MetaVar(IDM_PORT_METAVAR) @Parameter(names = { "-P",
                    "--port" }, absentValue = IDM_PORT_DEFAULT) final String idmPort,

            @Descriptor(REPLACE_ALL_DESC) @Parameter(names = { "-r", "--replaceall",
                    "--replaceAll" }, presentValue = "true", absentValue = "false") final boolean replaceall,

            @Descriptor("source directory") final String source) {
        processOptions(userPass, idmUrl, idmPort);

        PrintStream console = session.getConsole();
        File file = IdentityServer.getFileForPath(source);
        console.println("...................................................................");
        if (file.isDirectory()) {
            console.println("[ConfigImport] Load JSON configuration files from:");
            console.append("[ConfigImport] \t").println(file.getAbsolutePath());

            FileFilter filter = new FileFilter() {
                public boolean accept(File f) {
                    return f.getName().endsWith(".json");
                }
            };

            // Read the files from the provided source directory.
            File[] files = file.listFiles(filter);
            Map<String, File> localConfigSet = new HashMap<String, File>(files.length);
            for (File subFile : files) {
                if (subFile.isDirectory()) {
                    continue;
                }
                String configName = subFile.getName().replaceFirst("-", "/");
                configName = ConfigBootstrapHelper.unqualifyPid(configName.substring(0, configName.length() - 5));
                if (configName.indexOf("-") > -1) {
                    console.append(
                            "[WARN] Invalid file name found with multiple '-' character. The normalized config id: ");
                    console.println(configName);
                }
                localConfigSet.put(configName, subFile);
            }

            // Read the remote configs that are currently active.
            Map<String, JsonValue> remoteConfigSet = new HashMap<String, JsonValue>();
            try {
                ResourceResponse responseValue = resource.read(null, Requests.newReadRequest("config"));
                Iterator<JsonValue> iterator = responseValue.getContent().get("configurations").iterator();
                while (iterator.hasNext()) {
                    JsonValue configValue = iterator.next();
                    // TODO catch JsonValueExceptions
                    String id = ConfigBootstrapHelper.unqualifyPid(configValue.get("_id").required().asString());
                    // Remove apache configurations
                    if (!id.startsWith("org.apache")) {
                        remoteConfigSet.put(id, configValue);
                    }
                }
            } catch (ResourceException e) {
                console.append("Remote operation failed: ").println(e.getMessage());
                return;
            } catch (Exception e) {
                console.append("Operation failed: ").println(e.getMessage());
                return;
            }

            final ResourcePath configResource = ResourcePath.valueOf("config");
            for (Map.Entry<String, File> entry : localConfigSet.entrySet()) {
                String sourceConfigId = entry.getKey();
                try {
                    if (remoteConfigSet.containsKey(sourceConfigId)) {
                        // Update
                        UpdateRequest updateRequest = Requests.newUpdateRequest(
                                configResource.concat(sourceConfigId),
                                new JsonValue(mapper.readValue(entry.getValue(), Map.class)));

                        resource.update(null, updateRequest);
                        // If the update succeeded, remove the entry from 'remoteConfigSet' - this prevents it
                        // from being deleted below.  If this update fails, the entry will remain in remoteConfigSet
                        // and will be deleted from the remote IDM instance.
                        remoteConfigSet.remove(sourceConfigId);
                    } else {
                        // Create
                        CreateRequest createRequest = Requests.newCreateRequest(
                                configResource.concat(sourceConfigId),
                                new JsonValue(mapper.readValue(entry.getValue(), Map.class)));
                        resource.create(null, createRequest);
                    }
                    prettyPrint(console, "ConfigImport", sourceConfigId, null);
                } catch (Exception e) {
                    prettyPrint(console, "ConfigImport", sourceConfigId, e.getMessage());
                }
            }

            // Delete all additional config objects
            if (replaceall) {
                for (String configId : remoteConfigSet.keySet()) {
                    if (isProtectedConfigId(configId)) {
                        prettyPrint(console, "ConfigDelete", configId,
                                "Protected configuration can not be deleted");
                        continue;
                    }

                    try {
                        // configId is concatenated to avoid file paths from getting url encoded -> '/'-> '%2f'
                        resource.delete(null, Requests.newDeleteRequest(configResource.concat(configId)));
                        prettyPrint(console, "ConfigDelete", configId, null);
                    } catch (Exception e) {
                        prettyPrint(console, "ConfigDelete", configId, e.getMessage());
                    }
                }
            }

        } else if (file.exists()) {
            // TODO import archive file
            console.println("Input path must be a directory not a file.");
        } else {
            console.append("[ConfigImport] Configuration directory not found at: ");
            console.println(file.getAbsolutePath());
        }
    }

    private boolean isProtectedConfigId(String configId) {
        return "authentication".equals(configId) || "router".equals(configId) || "audit".equals(configId)
                || configId.startsWith("repo");
    }

    /**
     * Exports all configuration to the "conf" folder.
     *
     * @param session the command session
     * @param userPass the username/password
     * @param idmUrl the url of the OpenIDM instance
     * @param idmPort the OpenIDM instance's port
     */
    @Descriptor("Exports all configurations to 'conf' folder.")
    public void configexport(CommandSession session,

            @Descriptor(USER_PASS_DESC) @MetaVar(USER_PASS_METAVAR) @Parameter(names = { "-u",
                    "--user" }, absentValue = USER_PASS_DEFAULT) final String userPass,

            @Descriptor(IDM_URL_DESC) @MetaVar(IDM_URL_METAVAR) @Parameter(names = {
                    "--url" }, absentValue = IDM_URL_DEFAULT) final String idmUrl,

            @Descriptor(IDM_PORT_DESC) @MetaVar(IDM_PORT_METAVAR) @Parameter(names = { "-P",
                    "--port" }, absentValue = IDM_PORT_DEFAULT) final String idmPort) {
        configexport(session, userPass, idmUrl, idmPort, "conf");
    }

    /**
     * Exports all configurations.
     *
     * @param session the command session
     * @param userPass the username/password
     * @param idmUrl the url of the OpenIDM instance
     * @param idmPort the OpenIDM instance's port
     * @param target the target directory
     */
    @Descriptor("Exports all configurations.")
    public void configexport(CommandSession session,

            @Descriptor(USER_PASS_DESC) @MetaVar(USER_PASS_METAVAR) @Parameter(names = { "-u",
                    "--user" }, absentValue = USER_PASS_DEFAULT) final String userPass,

            @Descriptor(IDM_URL_DESC) @MetaVar(IDM_URL_METAVAR) @Parameter(names = {
                    "--url" }, absentValue = IDM_URL_DEFAULT) final String idmUrl,

            @Descriptor(IDM_PORT_DESC) @MetaVar(IDM_PORT_METAVAR) @Parameter(names = { "-P",
                    "--port" }, absentValue = IDM_PORT_DEFAULT) final String idmPort,

            @Descriptor("target directory") String target) {
        processOptions(userPass, idmUrl, idmPort);

        File targetDir = IdentityServer.getFileForPath(target);
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }

        session.getConsole().println("[ConfigExport] Export JSON configurations to:");
        session.getConsole().append("[ConfigExport] \t").println(targetDir.getAbsolutePath());

        try {
            ResourceResponse responseValue = resource.read(null, Requests.newReadRequest("config"));
            Iterator<JsonValue> iterator = responseValue.getContent().get("configurations").iterator();
            String bkpPostfix = "." + (new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss")).format(new Date()) + ".bkp";
            while (iterator.hasNext()) {
                String id = iterator.next().get("_id").required().asString();
                if (!id.startsWith("org.apache")) {
                    try {
                        responseValue = resource.read(null, Requests.newReadRequest("config/" + id));
                        if (null != responseValue.getContent() && !responseValue.getContent().isNull()) {
                            File configFile = new File(targetDir, id.replace("/", "-") + ".json");
                            if (configFile.exists()) {
                                configFile.renameTo(
                                        new File(configFile.getParentFile(), configFile.getName() + bkpPostfix));
                            }
                            mapper.writerWithDefaultPrettyPrinter().writeValue(configFile,
                                    responseValue.getContent().getObject());
                            prettyPrint(session.getConsole(), "ConfigExport", id, null);
                        }
                    } catch (Exception e) {
                        prettyPrint(session.getConsole(), "ConfigExport", id, e.getMessage());
                    }
                }
            }
        } catch (ResourceException e) {
            session.getConsole().append("Remote operation failed: ").println(e.getMessage());
        } catch (Exception e) {
            session.getConsole().append("Operation failed: ").println(e.getMessage());
        }
    }

    private void export(InputStream console, PrintStream out, String[] args) {
        out.println("Exported");
    }

    /**
     * Generate connector configuration.
     *
     * @param session the command session
     * @param userPass the username/password
     * @param idmUrl the url of the OpenIDM instance
     * @param idmPort the OpenIDM instance's port
     * @param name the connector name
     */
    @Descriptor("Generate connector configuration.")
    public void configureconnector(CommandSession session,

            @Descriptor(USER_PASS_DESC) @MetaVar(USER_PASS_METAVAR) @Parameter(names = { "-u",
                    "--user" }, absentValue = USER_PASS_DEFAULT) final String userPass,

            @Descriptor(IDM_URL_DESC) @MetaVar(IDM_URL_METAVAR) @Parameter(names = {
                    "--url" }, absentValue = IDM_URL_DEFAULT) final String idmUrl,

            @Descriptor(IDM_PORT_DESC) @MetaVar(IDM_PORT_METAVAR) @Parameter(names = { "-P",
                    "--port" }, absentValue = IDM_PORT_DEFAULT) final String idmPort,

            @Descriptor("Name of the new connector configuration.") @MetaVar("CONNECTOR") @Parameter(names = { "-n",
                    "--name" }, absentValue = "test") String name) {
        processOptions(userPass, idmUrl, idmPort);

        try {
            // Prepare temp folder and file
            File temp = IdentityServer.getFileForPath("temp");
            if (!temp.exists()) {
                temp.mkdir();
            }
            // TODO Make safe file name

            if (StringUtils.isBlank(name) || !name.matches("\\w++")) {
                session.getConsole().append("The given name \"").append(name)
                        .println("\" must match [a-zA-Z_0-9] pattern");
                return;
            }

            File finalConfig = new File(temp, "provisioner.openicf-" + name + ".json");

            // Common request attributes
            ActionRequest request = Requests.newActionRequest("system", "CREATECONFIGURATION");

            JsonValue responseValue;
            Map<String, Object> configuration = null;

            // Phase#1 - Get available connectors
            if (!finalConfig.exists()) {
                responseValue = resource.action(null, request).getJsonContent();

                JsonValue connectorRef = responseValue.get("connectorRef");
                if (!connectorRef.isNull() && connectorRef.isList()) {
                    int i = 0;
                    for (JsonValue connector : connectorRef) {
                        String displayName = connector.get("displayName").asString();
                        if (null == displayName) {
                            displayName = connector.get("connectorName").asString();
                        }
                        String version = connector.get("bundleVersion").asString();
                        String connectorHostRef = connector.get("connectorHostRef").asString();

                        session.getConsole().append(Integer.toString(i)).append(". ").append(displayName);
                        if (null != connectorHostRef) {
                            session.getConsole().append(" Remote (").append(connectorHostRef).append(")");
                        }
                        session.getConsole().append(" version ").println(version);
                        i++;
                    }

                    session.getConsole().append(Integer.toString(connectorRef.size())).println(". Exit");
                    Scanner input = new Scanner(session.getKeyboard());
                    int index = -1;
                    do {
                        session.getConsole().append("Select [0..").append(Integer.toString(connectorRef.size()))
                                .append("]: ");
                        index = input.nextInt();
                    } while (index < 0 || index > connectorRef.size());
                    if (index == connectorRef.size()) {
                        return;
                    }
                    configuration = responseValue.asMap();

                    // If we don't getObject() JsonValue will wrap the object resulting in a JSON payload of
                    // { "connectorRef": { ..., "wrappedObject": ... }
                    configuration.put("connectorRef", connectorRef.get(index).getObject());
                } else {
                    session.getConsole().println("There are no available connector!");
                }
            } else {
                configuration = new JsonValue(mapper.readValue(finalConfig, Map.class)).asMap();
                session.getConsole().append("Configuration was found and read from: ")
                        .println(finalConfig.getAbsolutePath());
            }

            if (null == configuration) {
                return;
            }

            // Repeatable phase #2 and #3
            request.setContent(new JsonValue(configuration));
            responseValue = resource.action(null, request).getJsonContent();

            configuration = responseValue.asMap();
            configuration.put("name", name);

            mapper.writerWithDefaultPrettyPrinter().writeValue(finalConfig, configuration);

            session.getConsole().append(
                    "Edit the configuration file and run the command again. The configuration was saved to ")
                    .println(finalConfig.getAbsolutePath());

        } catch (ResourceException e) {
            session.getConsole().append("Remote operation failed: ").println(e.getMessage());
        } catch (Exception e) {
            session.getConsole().append("Operation failed: ").println(e.getMessage());
        }
    }

    private void prettyPrint(PrintStream out, String cmd, String name, String reason) {
        out.append("[").append(cmd).append("] ").append(name).append(" ")
                .append(DOTTED_PLACEHOLDER.substring(Math.min(name.length(), DOTTED_PLACEHOLDER.length())));
        if (null == reason) {
            out.println(" SUCCESS");
        } else {
            out.println(" FAILED");
            out.append("\t[").append(reason).println("]");
        }
    }
}