001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *      Copyright 2014-2015 ForgeRock AS.
024 */
025package org.forgerock.opendj.server.setup.cli;
026
027import static com.forgerock.opendj.cli.Utils.filterExitCode;
028import static com.forgerock.opendj.cli.Utils.LINE_SEPARATOR;
029import static com.forgerock.opendj.cli.Utils.checkJavaVersion;
030import static com.forgerock.opendj.cli.CliMessages.*;
031import static com.forgerock.opendj.cli.CliConstants.*;
032
033import java.io.PrintStream;
034import java.util.ArrayList;
035import java.util.Collection;
036import java.util.HashSet;
037import java.util.LinkedHashSet;
038import java.util.Set;
039
040import org.forgerock.i18n.LocalizableMessage;
041import org.forgerock.i18n.LocalizableMessageBuilder;
042import org.forgerock.i18n.slf4j.LocalizedLogger;
043
044import com.forgerock.opendj.cli.Argument;
045import com.forgerock.opendj.cli.ArgumentException;
046import com.forgerock.opendj.cli.BooleanArgument;
047import com.forgerock.opendj.cli.ClientException;
048import com.forgerock.opendj.cli.CommonArguments;
049import com.forgerock.opendj.cli.ConsoleApplication;
050import com.forgerock.opendj.cli.FileBasedArgument;
051import com.forgerock.opendj.cli.IntegerArgument;
052import com.forgerock.opendj.cli.ReturnCode;
053import com.forgerock.opendj.cli.StringArgument;
054import com.forgerock.opendj.cli.SubCommand;
055import com.forgerock.opendj.cli.SubCommandArgumentParser;
056import com.forgerock.opendj.cli.Utils;
057
058/**
059 * This class implements the new CLI for OpenDJ3 setup.
060 */
061public final class SetupCli extends ConsoleApplication {
062
063    /**
064     * Setup's logger.
065     */
066    private static final LocalizedLogger LOG = LocalizedLogger.getLoggerForThisClass();
067
068    /**
069     * TODO remove that after implementation in config.
070     *
071     * @return The installation path.
072     */
073    static String getInstallationPath() {
074        return "/home/violette/OpenDJ-3.0.0/";
075    }
076
077    /**
078     * TODO remove that after implementation in config.
079     *
080     * @return The instance path.
081     */
082    static String getInstancePath() {
083        return "/home/violette/OpenDJ-3.0.0/";
084    }
085
086
087    private SubCommandArgumentParser argParser;
088
089    private BooleanArgument cli;
090    private BooleanArgument addBaseEntry;
091    private BooleanArgument skipPortCheck;
092    private BooleanArgument enableWindowsService;
093    private BooleanArgument doNotStart;
094    private BooleanArgument enableStartTLS;
095    private BooleanArgument generateSelfSignedCertificate;
096    private StringArgument hostName;
097    private BooleanArgument usePkcs11;
098    private FileBasedArgument directoryManagerPwdFile;
099    private FileBasedArgument keyStorePasswordFile;
100    private IntegerArgument ldapPort;
101    private IntegerArgument adminConnectorPort;
102    private IntegerArgument ldapsPort;
103    private IntegerArgument jmxPort;
104    private IntegerArgument sampleData;
105    private StringArgument baseDN;
106    private StringArgument importLDIF;
107    private StringArgument rejectedImportFile;
108    private StringArgument skippedImportFile;
109    private StringArgument directoryManagerDN;
110    private StringArgument directoryManagerPwdString;
111    private StringArgument useJavaKeyStore;
112    private StringArgument useJCEKS;
113    private StringArgument usePkcs12;
114    private StringArgument keyStorePassword;
115    private StringArgument certNickname;
116    private IntegerArgument connectTimeout;
117    private BooleanArgument acceptLicense;
118
119    /** Sub-commands. */
120    private SubCommand createDirectoryServer;
121    private SubCommand createProxy;
122
123    /** Register the global arguments. */
124    private BooleanArgument noPrompt;
125    private BooleanArgument quietMode;
126    private BooleanArgument verbose;
127    private StringArgument propertiesFile;
128    private BooleanArgument noPropertiesFile;
129    private BooleanArgument showUsage;
130
131    private SetupCli() {
132        // Nothing to do.
133    }
134
135    /** To allow tests. */
136    SetupCli(PrintStream out, PrintStream err) {
137        super(out, err);
138    }
139
140    /**
141     * The main method for setup tool.
142     *
143     * @param args
144     *            The command-line arguments provided to this program.
145     */
146    public static void main(final String[] args) {
147        final int retCode = new SetupCli().run(args);
148        System.exit(filterExitCode(retCode));
149    }
150
151    /** Create the command-line argument parser for use with this program. */
152    int run(final String[] args) {
153        // TODO Activate logger when the instance/installation path will be resolved.
154        // SetupLog.initLogFileHandler();
155
156        try {
157            checkJavaVersion();
158        } catch (ClientException e) {
159            errPrintln(e.getMessageObject());
160            return ReturnCode.JAVA_VERSION_INCOMPATIBLE.get();
161        }
162
163        try {
164            argParser = new SubCommandArgumentParser("setup", INFO_SETUP_DESCRIPTION.get(), true);
165            initializeArguments();
166        } catch (ArgumentException e) {
167            final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(e.getMessage());
168            errPrintln(message);
169            return ReturnCode.CLIENT_SIDE_PARAM_ERROR.get();
170        }
171
172        // Parse the command-line arguments provided to this program.
173        try {
174            argParser.parseArguments(args);
175
176            if (argParser.usageOrVersionDisplayed()) {
177                // If we should just display usage or version information, then print it and exit.
178                return ReturnCode.SUCCESS.get();
179            }
180        } catch (final ArgumentException e) {
181            final LocalizableMessage message = ERR_ERROR_PARSING_ARGS.get(e.getMessage());
182            errPrintln(message);
183            return ReturnCode.CLIENT_SIDE_PARAM_ERROR.get();
184        }
185
186        // Verifying provided informations.
187        try {
188            final LinkedHashSet<LocalizableMessage> errorMessages = new LinkedHashSet<>();
189            checkServerPassword(errorMessages);
190            checkProvidedPorts(errorMessages);
191            checkImportDataArguments(errorMessages);
192            checkSecurityArguments(errorMessages);
193            if (errorMessages.size() > 0) {
194                throw new ArgumentException(ERR_CANNOT_INITIALIZE_ARGS.get(
195                        getMessageFromCollection(errorMessages, LINE_SEPARATOR)));
196            }
197        } catch (final ArgumentException e) {
198            errPrintln(e.getMessageObject());
199            return ReturnCode.CLIENT_SIDE_PARAM_ERROR.get();
200        }
201
202        // Starts setup process.
203        try {
204            fillSetupSettings();
205            runSetupInstallation();
206        } catch (ClientException ex) {
207            return ex.getReturnCode();
208        } catch (Exception ex) {
209            // TODO
210            //println(Style.ERROR, LocalizableMessage.raw("...?"));
211            return ReturnCode.ERROR_UNEXPECTED.get();
212        }
213        return ReturnCode.SUCCESS.get();
214    }
215
216    /**
217     * Initialize setup's arguments by default.
218     *
219     * @throws ArgumentException
220     *             If an exception occurs during the initialization of the arguments.
221     */
222    private void initializeArguments() throws ArgumentException {
223        // Options.
224        acceptLicense = addGlobal(CommonArguments.getAcceptLicense());
225        cli = addGlobal(CommonArguments.getCLI());
226        baseDN = addGlobal(CommonArguments.getBaseDN());
227        addBaseEntry = addGlobal(CommonArguments.getAddBaseEntry());
228        importLDIF = addGlobal(CommonArguments.getLDIFFile(INFO_DESCRIPTION_IMPORTLDIF.get()));
229        rejectedImportFile = addGlobal(CommonArguments.getRejectedImportLdif());
230        skippedImportFile = addGlobal(CommonArguments.getSkippedImportFile());
231        sampleData = addGlobal(CommonArguments.getSampleData());
232        ldapPort = addGlobal(CommonArguments.getLDAPPort(DEFAULT_LDAP_PORT));
233        ldapsPort = addGlobal(CommonArguments.getLDAPSPort(DEFAULT_LDAPS_PORT));
234        adminConnectorPort = addGlobal(CommonArguments.getAdminLDAPPort(DEFAULT_ADMIN_PORT));
235        jmxPort = addGlobal(CommonArguments.getJMXPort(DEFAULT_JMX_PORT));
236        skipPortCheck = addGlobal(CommonArguments.getSkipPortCheck());
237        directoryManagerDN = addGlobal(CommonArguments.getRootDN());
238        directoryManagerPwdString = addGlobal(CommonArguments.getRootDNPwd());
239        directoryManagerPwdFile = addGlobal(CommonArguments.getRootDNPwdFile());
240        enableWindowsService = addGlobal(CommonArguments.getEnableWindowsService());
241        doNotStart = addGlobal(CommonArguments.getDoNotStart());
242        enableStartTLS = addGlobal(CommonArguments.getEnableTLS());
243        generateSelfSignedCertificate = addGlobal(CommonArguments.getGenerateSelfSigned());
244        hostName = addGlobal(CommonArguments.getHostName(Utils.getDefaultHostName()));
245        usePkcs11 = addGlobal(CommonArguments.getUsePKCS11Keystore());
246        useJavaKeyStore = addGlobal(CommonArguments.getUseJavaKeyStore());
247        useJCEKS = addGlobal(CommonArguments.getUseJCEKS());
248        usePkcs12 = addGlobal(CommonArguments.getUsePKCS12KeyStore());
249        keyStorePassword = addGlobal(CommonArguments.getKeyStorePassword());
250        keyStorePasswordFile = addGlobal(CommonArguments.getKeyStorePasswordFile());
251        certNickname = addGlobal(CommonArguments.getCertNickName());
252        connectTimeout = CommonArguments.getConnectTimeOut();
253
254        // Utility Input Output Options.
255        noPrompt = addGlobal(CommonArguments.getNoPrompt());
256        quietMode = addGlobal(CommonArguments.getQuiet());
257        verbose = addGlobal(CommonArguments.getVerbose());
258        propertiesFile = addGlobal(CommonArguments.getPropertiesFile());
259        noPropertiesFile = addGlobal(CommonArguments.getNoPropertiesFile());
260        showUsage = addGlobal(CommonArguments.getShowUsage());
261
262        //Sub-commands && their arguments
263        final ArrayList<SubCommand> subCommandList = new ArrayList<>(2);
264        createDirectoryServer = new SubCommand(argParser, "create-directory-server",
265                INFO_SETUP_SUBCOMMAND_CREATE_DIRECTORY_SERVER.get());
266        // TODO to complete.
267        createProxy = new SubCommand(argParser, "create-proxy",
268                INFO_SETUP_SUBCOMMAND_CREATE_PROXY.get());
269        subCommandList.add(createDirectoryServer);
270        subCommandList.add(createProxy);
271
272        argParser.setUsageGroupArgument(showUsage, subCommandList);
273
274        // Register the global arguments.
275        argParser.addArgument(showUsage);
276        argParser.setUsageArgument(showUsage, getOutputStream());
277        argParser.addArgument(noPropertiesFile);
278        argParser.setNoPropertiesFileArgument(noPropertiesFile);
279        argParser.addArgument(propertiesFile);
280        argParser.setFilePropertiesArgument(propertiesFile);
281        argParser.addArgument(quietMode);
282        argParser.addArgument(verbose);
283        argParser.addArgument(noPrompt);
284        argParser.addArgument(acceptLicense);
285    }
286
287    private <A extends Argument> A addGlobal(A arg) throws ArgumentException {
288        argParser.addGlobalArgument(arg);
289        return arg;
290    }
291
292    /** {@inheritDoc} */
293    @Override
294    public boolean isInteractive() {
295        return !noPrompt.isPresent();
296    }
297
298    /** {@inheritDoc} */
299    @Override
300    public boolean isQuiet() {
301        return quietMode.isPresent();
302    }
303
304    /**
305     * Automatically accepts the license if it's present.
306     *
307     * @return {@code true} if license is accepted by default.
308     */
309    private boolean isAcceptLicense() {
310        return acceptLicense.isPresent();
311    }
312
313    /** {@inheritDoc} */
314    @Override
315    public boolean isVerbose() {
316        return verbose.isPresent();
317    }
318
319    /**
320     * Returns whether the command was launched in CLI mode or not.
321     *
322     * @return <CODE>true</CODE> if the command was launched to use CLI mode and <CODE>false</CODE> otherwise.
323     */
324    public boolean isCli() {
325        return cli.isPresent();
326    }
327
328    /**
329     * Returns whether the command was launched to setup proxy or not.
330     *
331     * @return <CODE>true</CODE> if the command was launched to setup a proxy and <CODE>false</CODE> otherwise.
332     */
333    public boolean isCreateProxy() {
334        return argParser.getSubCommand("create-proxy") != null;
335    }
336
337    /**
338     * Checks that there are no conflicts with the provided ports (like if the user provided the same port for different
339     * protocols).
340     *
341     * @param errorMessages
342     *            the list of messages to which we add the error messages describing the problems encountered during the
343     *            execution of the checking.
344     */
345    private void checkProvidedPorts(final Collection<LocalizableMessage> errorMessages) {
346        // Check that the provided ports do not match.
347        try {
348            final Set<Integer> ports = new HashSet<>();
349            ports.add(ldapPort.getIntValue());
350
351            checkPortArgument(adminConnectorPort, ports, errorMessages);
352
353            if (jmxPort.isPresent()) {
354                checkPortArgument(jmxPort, ports, errorMessages);
355            }
356            if (ldapsPort.isPresent()) {
357                checkPortArgument(ldapsPort, ports, errorMessages);
358            }
359        } catch (ArgumentException ae) {
360            LOG.error(LocalizableMessage.raw("Unexpected error. "
361                    + "Assuming that it is caused by a previous parsing issue: " + ae, ae));
362        }
363    }
364
365    private void checkPortArgument(IntegerArgument portArg, final Set<Integer> ports,
366            final Collection<LocalizableMessage> errorMessages) throws ArgumentException {
367        if (ports.contains(portArg.getIntValue())) {
368            errorMessages.add(ERR_PORT_ALREADY_SPECIFIED.get(portArg.getIntValue()));
369        } else {
370            ports.add(portArg.getIntValue());
371        }
372    }
373
374    /**
375     * Checks that there are no conflicts with the import data arguments.
376     *
377     * @param errorMessages
378     *            the list of messages to which we add the error messages describing the problems encountered during the
379     *            execution of the checking.
380     */
381    private void checkImportDataArguments(final Collection<LocalizableMessage> errorMessages) {
382        // Make sure that the user didn't provide conflicting arguments.
383        if (addBaseEntry.isPresent()) {
384            if (importLDIF.isPresent()) {
385                errorMessages.add(ERR_TOOL_CONFLICTING_ARGS.get(addBaseEntry.getLongIdentifier(),
386                        importLDIF.getLongIdentifier()));
387            } else if (sampleData.isPresent()) {
388                errorMessages.add(ERR_TOOL_CONFLICTING_ARGS.get(addBaseEntry.getLongIdentifier(),
389                        sampleData.getLongIdentifier()));
390            }
391        } else if (importLDIF.isPresent() && sampleData.isPresent()) {
392            errorMessages.add(ERR_TOOL_CONFLICTING_ARGS.get(importLDIF.getLongIdentifier(),
393                    sampleData.getLongIdentifier()));
394        }
395
396        if (rejectedImportFile.isPresent() && addBaseEntry.isPresent()) {
397            errorMessages.add(ERR_TOOL_CONFLICTING_ARGS.get(addBaseEntry.getLongIdentifier(),
398                    rejectedImportFile.getLongIdentifier()));
399        } else if (rejectedImportFile.isPresent() && sampleData.isPresent()) {
400            errorMessages.add(ERR_TOOL_CONFLICTING_ARGS.get(rejectedImportFile.getLongIdentifier(),
401                    sampleData.getLongIdentifier()));
402        }
403
404        if (skippedImportFile.isPresent() && addBaseEntry.isPresent()) {
405            errorMessages.add(ERR_TOOL_CONFLICTING_ARGS.get(addBaseEntry.getLongIdentifier(),
406                    skippedImportFile.getLongIdentifier()));
407        } else if (skippedImportFile.isPresent() && sampleData.isPresent()) {
408            errorMessages.add(ERR_TOOL_CONFLICTING_ARGS.get(skippedImportFile.getLongIdentifier(),
409                    sampleData.getLongIdentifier()));
410        }
411
412        if (noPrompt.isPresent() && !baseDN.isPresent() && baseDN.getDefaultValue() == null) {
413            final Argument[] args = { importLDIF, addBaseEntry, sampleData };
414            for (final Argument arg : args) {
415                if (arg.isPresent()) {
416                    errorMessages.add(ERR_ARGUMENT_NO_BASE_DN_SPECIFIED.get("--" + arg.getLongIdentifier()));
417                }
418            }
419        }
420    }
421
422    /**
423     * Checks that there are no conflicts with the security arguments. If we are in no prompt mode, check that all the
424     * information required has been provided (but not if this information is valid: we do not try to open the keystores
425     * or to check that the LDAPS port is in use).
426     *
427     * @param errorMessages
428     *            the list of messages to which we add the error messages describing the problems encountered during the
429     *            execution of the checking.
430     */
431    private void checkSecurityArguments(final Collection<LocalizableMessage> errorMessages) {
432        final boolean certificateRequired = ldapsPort.isPresent() || enableStartTLS.isPresent();
433
434        int certificateType = 0;
435        if (generateSelfSignedCertificate.isPresent()) {
436            certificateType++;
437        }
438        if (useJavaKeyStore.isPresent()) {
439            certificateType++;
440        }
441        if (useJCEKS.isPresent()) {
442            certificateType++;
443        }
444        if (usePkcs11.isPresent()) {
445            certificateType++;
446        }
447        if (usePkcs12.isPresent()) {
448            certificateType++;
449        }
450
451        if (certificateType > 1) {
452            errorMessages.add(ERR_SEVERAL_CERTIFICATE_TYPE_SPECIFIED.get());
453        }
454
455        if (certificateRequired && noPrompt.isPresent() && certificateType == 0) {
456            errorMessages.add(ERR_CERTIFICATE_REQUIRED_FOR_SSL_OR_STARTTLS.get());
457        }
458
459        if (certificateType == 1) {
460            if (!generateSelfSignedCertificate.isPresent()) {
461                // Check that we have only a password.
462                if (keyStorePassword.isPresent() && keyStorePasswordFile.isPresent()) {
463                    final LocalizableMessage message = ERR_TWO_CONFLICTING_ARGUMENTS.get(
464                            keyStorePassword.getLongIdentifier(), keyStorePasswordFile.getLongIdentifier());
465                    errorMessages.add(message);
466                }
467
468                // Check that we have one password in no prompt mode.
469                if (noPrompt.isPresent() && !keyStorePassword.isPresent() && !keyStorePasswordFile.isPresent()) {
470                    final LocalizableMessage message = ERR_NO_KEYSTORE_PASSWORD.get(
471                            keyStorePassword.getLongIdentifier(), keyStorePasswordFile.getLongIdentifier());
472                    errorMessages.add(message);
473                }
474            }
475            if (noPrompt.isPresent() && !ldapsPort.isPresent() && !enableStartTLS.isPresent()) {
476                final LocalizableMessage message = ERR_SSL_OR_STARTTLS_REQUIRED.get(ldapsPort.getLongIdentifier(),
477                        enableStartTLS.getLongIdentifier());
478                errorMessages.add(message);
479            }
480        }
481    }
482
483    /**
484     * Checks that there are no conflicts with the directory manager passwords. If we are in no prompt mode, check that
485     * the password was provided.
486     *
487     * @param errorMessages
488     *            the list of messages to which we add the error messages describing the problems encountered during the
489     *            execution of the checking.
490     */
491    private void checkServerPassword(Collection<LocalizableMessage> errorMessages) {
492        if (directoryManagerPwdString.isPresent() && directoryManagerPwdFile.isPresent()) {
493            errorMessages.add(ERR_TWO_CONFLICTING_ARGUMENTS.get(
494                    directoryManagerPwdString.getLongIdentifier(), directoryManagerPwdFile.getLongIdentifier()));
495        }
496
497        if (noPrompt.isPresent() && !directoryManagerPwdString.isPresent()
498                && !directoryManagerPwdFile.isPresent()) {
499            errorMessages.add(ERR_NO_ROOT_PASSWORD.get(directoryManagerPwdString.getLongIdentifier(),
500                    directoryManagerPwdFile.getLongIdentifier()));
501        }
502    }
503
504
505    /**
506     * This is a helper method that gets a LocalizableMessage representation of the elements in the Collection of
507     * Messages. The LocalizableMessage will display the different elements separated by the separator String.
508     * TODO move this function.
509     * @param col
510     *            the collection containing the messages.
511     * @param separator
512     *            the separator String to be used.
513     * @return the message representation for the collection; LocalizableMessage.EMPTY if null.
514     */
515    private static LocalizableMessage getMessageFromCollection(final Collection<LocalizableMessage> col,
516            final String separator) {
517        if (col == null || col.isEmpty()) {
518            return LocalizableMessage.EMPTY;
519        } else {
520            LocalizableMessageBuilder mb = null;
521            for (final LocalizableMessage m : col) {
522                if (mb == null) {
523                    mb = new LocalizableMessageBuilder(m);
524                } else {
525                    mb.append(separator).append(m);
526                }
527            }
528            return mb.toMessage();
529        }
530    }
531
532    /**
533     * Fills the setup components according to the arguments provided by the user.
534     * @throws ArgumentException
535     */
536    private void fillSetupSettings() throws ArgumentException {
537        // TODO ...
538    }
539
540    /**
541     * Launches the setup process.
542     * @throws ClientException
543     */
544    private void runSetupInstallation() throws ClientException {
545        // TODO move that function to another class.
546    }
547}