de.hasait.clap.CLAP.java Source code

Java tutorial

Introduction

Here is the source code for de.hasait.clap.CLAP.java

Source

/*
 * Copyright (C) 2013 by Sebastian Hasait (sebastian at hasait dot de)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.hasait.clap;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyFactory;

import de.hasait.clap.impl.CLAPHelpCategoryImpl;
import de.hasait.clap.impl.CLAPHelpNode;
import de.hasait.clap.impl.CLAPNodeList;
import de.hasait.clap.impl.CLAPParseContext;
import de.hasait.clap.impl.CLAPResultImpl;
import de.hasait.clap.impl.CLAPUsageCategoryImpl;
import de.hasait.clap.impl.SwingDialogUICallback;
import de.hasait.clap.impl.SystemConsoleUICallback;

/**
 * Entry point to CLAP library.
 */
public final class CLAP implements CLAPNode {

    public static final int UNLIMITED_ARG_COUNT = -1;

    private static final String NLSKEY_CLAP_ERROR_ERROR_MESSAGES_SPLIT = "clap.error.errorMessagesSplit"; //$NON-NLS-1$
    private static final String NLSKEY_CLAP_ERROR_ERROR_MESSAGE_SPLIT = "clap.error.errorMessageSplit"; //$NON-NLS-1$
    private static final String NLSKEY_CLAP_ERROR_VALIDATION_FAILED = "clap.error.validationFailed"; //$NON-NLS-1$
    private static final String NLSKEY_CLAP_ERROR_AMBIGUOUS_RESULT = "clap.error.ambiguousResult"; //$NON-NLS-1$
    private static final String NLSKEY_CLAP_ERROR_INVALID_TOKEN_LIST = "clap.error.invalidTokenList"; //$NON-NLS-1$
    private static final String NLSKEY_ENTER_PASSWORD = "clap.enterpassword"; //$NON-NLS-1$
    private static final String NLSKEY_ENTER_LINE = "clap.enterline"; //$NON-NLS-1$
    private static final String NLSKEY_DEFAULT_HELP_CATEGORY = "clap.defaultHelpCategory"; //$NON-NLS-1$
    private static final String NLSKEY_DEFAULT_USAGE_CATEGORY = "clap.defaultUsageCategory"; //$NON-NLS-1$

    private final ResourceBundle _nls;

    private final char _shortOptPrefix;
    private final String _longOptPrefix;
    private final String _longOptEquals;

    private final Map<Class<?>, CLAPConverter<?>> _converters;

    private final CLAPNodeList _root;

    private CLAPUICallback _uiCallback;

    /**
     * @param pNLS {@link ResourceBundle} used for messages.
     */
    public CLAP(final ResourceBundle pNLS) {
        super();

        _nls = pNLS;

        _shortOptPrefix = '-';
        _longOptPrefix = "--"; //$NON-NLS-1$
        _longOptEquals = "="; //$NON-NLS-1$

        _converters = new HashMap<Class<?>, CLAPConverter<?>>();
        initDefaultConverters();

        _root = new CLAPNodeList(this);
        _root.setHelpCategory(1000, NLSKEY_DEFAULT_HELP_CATEGORY);
        _root.setUsageCategory(1000, NLSKEY_DEFAULT_USAGE_CATEGORY);

        if (System.console() != null) {
            _uiCallback = new SystemConsoleUICallback();
        } else {
            _uiCallback = new SwingDialogUICallback(null);
        }
    }

    @Override
    public <V> CLAPValue<V> addClass(final Class<V> pClass) {
        return _root.addClass(pClass);
    }

    public <R> void addConverter(final Class<R> pResultClass, final CLAPConverter<? extends R> pConverter) {
        Class<? super R> currentClass = pResultClass;
        while (currentClass != null) {
            if (pResultClass.equals(currentClass) || !_converters.containsKey(currentClass)) {
                _converters.put(currentClass, pConverter);
            }
            currentClass = currentClass.getSuperclass();
        }
    }

    @Override
    public CLAPNode addDecision() {
        return _root.addDecision();
    }

    @Override
    public final <V> CLAPValue<V> addDecision(final Class<V> pResultClass,
            final Class<? extends V>... pBranchClasses) {
        return _root.addDecision(pResultClass, pBranchClasses);
    }

    @Override
    public CLAPValue<Boolean> addFlag(final Character pShortKey, final String pLongKey, final boolean pRequired,
            final String pDescriptionNLSKey) {
        return _root.addFlag(pShortKey, pLongKey, pRequired, pDescriptionNLSKey);
    }

    @Override
    public void addKeyword(final String pKeyword) {
        _root.addKeyword(pKeyword);
    }

    @Override
    public CLAPNode addNodeList() {
        return _root.addNodeList();
    }

    @Override
    public <V> CLAPValue<V> addOption(final Class<V> pResultClass, final Character pShortKey, final String pLongKey,
            final boolean pRequired, final Integer pArgCount, final Character pMultiArgSplit,
            final String pDescriptionNLSKey, final String pArgUsageNLSKey) {
        return _root.addOption(pResultClass, pShortKey, pLongKey, pRequired, pArgCount, pMultiArgSplit,
                pDescriptionNLSKey, pArgUsageNLSKey);
    }

    @Override
    public <V> CLAPValue<V> addOption1(final Class<V> pResultClass, final Character pShortKey,
            final String pLongKey, final boolean pRequired, final String pDescriptionNLSKey,
            final String pArgUsageNLSKey) {
        return _root.addOption1(pResultClass, pShortKey, pLongKey, pRequired, pDescriptionNLSKey, pArgUsageNLSKey);
    }

    @Override
    public <V> CLAPValue<V> addOptionU(final Class<V> pResultClass, final Character pShortKey,
            final String pLongKey, final boolean pRequired, final Character pMultiArgSplit,
            final String pDescriptionNLSKey, final String pArgUsageNLSKey) {
        return _root.addOptionU(pResultClass, pShortKey, pLongKey, pRequired, pMultiArgSplit, pDescriptionNLSKey,
                pArgUsageNLSKey);
    }

    public <R> CLAPConverter<? extends R> getConverter(final Class<R> pResultClass) {
        if (!_converters.containsKey(pResultClass)) {
            try {
                addStringConstructorConverter(pResultClass);
            } catch (final Exception e) {
                throw new RuntimeException(MessageFormat.format("No converter for {0} found", pResultClass), e); //$NON-NLS-1$
            }
        }

        return (CLAPConverter<? extends R>) _converters.get(pResultClass);
    }

    public <T> T getLineOrReadInteractivly(final T pObject, final String pCancelNLSKey,
            final boolean pSetAfterRead) {
        final GetOrReadInteractivlyLogic logic = new GetOrReadInteractivlyLogic() {

            @Override
            protected String readInteractivly(final String pPrompt) {
                return _uiCallback.readLine(pPrompt);
            }

        };
        return logic.getOrReadInteractivly(pObject, NLSKEY_ENTER_LINE, pCancelNLSKey, pSetAfterRead);
    }

    public String getLongOptEquals() {
        return _longOptEquals;
    }

    public String getLongOptPrefix() {
        return _longOptPrefix;
    }

    public ResourceBundle getNLS() {
        return _nls;
    }

    public <T> T getPasswordOrReadInteractivly(final T pObject, final String pCancelNLSKey,
            final boolean pSetAfterRead) {
        final GetOrReadInteractivlyLogic logic = new GetOrReadInteractivlyLogic() {

            @Override
            protected String readInteractivly(final String pPrompt) {
                return _uiCallback.readPassword(pPrompt);
            }

        };
        return logic.getOrReadInteractivly(pObject, NLSKEY_ENTER_PASSWORD, pCancelNLSKey, pSetAfterRead);
    }

    public char getShortOptPrefix() {
        return _shortOptPrefix;
    }

    public CLAPUICallback getUICallback() {
        return _uiCallback;
    }

    public final String nls(final String pKey, final Object... pArguments) {
        String pattern = null;
        if (_nls != null && pKey != null) {
            try {
                pattern = _nls.getString(pKey);
            } catch (final MissingResourceException e) {
                pattern = null;
            }
        }
        if (pattern == null) {
            pattern = pKey != null ? pKey : ""; //$NON-NLS-1$
            for (int i = 0; i < pArguments.length; i++) {
                pattern += " {" + i + "}"; //$NON-NLS-1$ //$NON-NLS-2$
            }
        }
        try {
            return MessageFormat.format(pattern, pArguments);
        } catch (final Exception e) {
            return pattern + "!" + StringUtils.join(pArguments, ", ") + "!"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        }
    }

    public CLAPResult parse(final String... pArgs) {
        final Set<CLAPParseContext> contextsWithInvalidToken = new HashSet<CLAPParseContext>();
        final List<CLAPParseContext> parsedContexts = new ArrayList<CLAPParseContext>();
        final LinkedList<CLAPParseContext> activeContexts = new LinkedList<CLAPParseContext>();
        activeContexts.add(new CLAPParseContext(this, pArgs));
        while (!activeContexts.isEmpty()) {
            final CLAPParseContext context = activeContexts.removeFirst();
            if (context.hasMoreTokens()) {
                final CLAPParseContext[] result = _root.parse(context);
                if (result != null) {
                    for (final CLAPParseContext nextContext : result) {
                        activeContexts.add(nextContext);
                    }
                } else {
                    contextsWithInvalidToken.add(context);
                }
            } else {
                parsedContexts.add(context);
            }
        }
        if (parsedContexts.isEmpty()) {
            int maxArgIndex = Integer.MIN_VALUE;
            final Set<String> invalidTokensOfBestContexts = new HashSet<String>();
            for (final CLAPParseContext context : contextsWithInvalidToken) {
                final int currentArgIndex = context.getCurrentArgIndex();
                if (currentArgIndex > maxArgIndex) {
                    invalidTokensOfBestContexts.clear();
                }
                if (currentArgIndex >= maxArgIndex) {
                    maxArgIndex = currentArgIndex;
                    invalidTokensOfBestContexts.add(context.currentArg());
                }
            }
            throw new CLAPException(
                    nls(NLSKEY_CLAP_ERROR_INVALID_TOKEN_LIST, StringUtils.join(invalidTokensOfBestContexts, ", "))); //$NON-NLS-1$
        }

        final Map<CLAPParseContext, List<String>> contextErrorMessages = new HashMap<CLAPParseContext, List<String>>();
        final Set<CLAPResultImpl> results = new LinkedHashSet<CLAPResultImpl>();
        for (final CLAPParseContext context : parsedContexts) {
            final List<String> errorMessages = new ArrayList<String>();
            _root.validate(context, errorMessages);
            if (errorMessages.isEmpty()) {
                final CLAPResultImpl result = new CLAPResultImpl();
                _root.fillResult(context, result);
                results.add(result);
            } else {
                contextErrorMessages.put(context, errorMessages);
            }
        }

        if (results.isEmpty()) {
            int minErrorMessages = Integer.MAX_VALUE;
            final List<String> errorMessagesOfBestContexts = new ArrayList<String>();
            for (final Entry<CLAPParseContext, List<String>> entry : contextErrorMessages.entrySet()) {
                final int countErrorMessages = entry.getValue().size();
                if (countErrorMessages < minErrorMessages) {
                    errorMessagesOfBestContexts.clear();
                }
                if (countErrorMessages <= minErrorMessages) {
                    minErrorMessages = countErrorMessages;
                    errorMessagesOfBestContexts
                            .add(StringUtils.join(entry.getValue(), nls(NLSKEY_CLAP_ERROR_ERROR_MESSAGE_SPLIT)));
                }
            }
            throw new CLAPException(nls(NLSKEY_CLAP_ERROR_VALIDATION_FAILED,
                    StringUtils.join(errorMessagesOfBestContexts, nls(NLSKEY_CLAP_ERROR_ERROR_MESSAGES_SPLIT))));
        }

        if (results.size() > 1) {
            throw new CLAPException(nls(NLSKEY_CLAP_ERROR_AMBIGUOUS_RESULT));
        }

        return results.iterator().next();
    }

    public void printHelp(final PrintStream pPrintStream) {
        final Map<CLAPHelpCategoryImpl, Set<CLAPHelpNode>> nodes = new TreeMap<CLAPHelpCategoryImpl, Set<CLAPHelpNode>>();
        _root.collectHelpNodes(nodes, null);

        int maxLength = 0;
        for (final Entry<CLAPHelpCategoryImpl, Set<CLAPHelpNode>> entry : nodes.entrySet()) {
            for (final CLAPHelpNode node : entry.getValue()) {
                final int length = node.getHelpID().length();
                if (length > maxLength) {
                    maxLength = length;
                }
            }
        }
        maxLength += 6; // space to description

        for (final Entry<CLAPHelpCategoryImpl, Set<CLAPHelpNode>> entry : nodes.entrySet()) {
            pPrintStream.println();
            pPrintStream.println(nls(entry.getKey().getTitleNLSKey()));
            for (final CLAPHelpNode node : entry.getValue()) {
                pPrintStream.println();
                pPrintStream.print("  "); //$NON-NLS-1$
                pPrintStream.print(StringUtils.rightPad(node.getHelpID(), maxLength - 2));
                final String descriptionNLSKey = node.getDescriptionNLSKey();
                if (descriptionNLSKey != null) {
                    pPrintStream.println(nls(descriptionNLSKey));
                } else {
                    pPrintStream.println();
                }
            }
        }
    }

    public void printUsage(final PrintStream pPrintStream) {
        final Map<CLAPUsageCategoryImpl, StringBuilder> categories = new TreeMap<CLAPUsageCategoryImpl, StringBuilder>();
        _root.printUsage(categories, null, null);
        for (final Entry<CLAPUsageCategoryImpl, StringBuilder> entry : categories.entrySet()) {
            pPrintStream.print(nls(entry.getKey().getTitleNLSKey()));
            pPrintStream.print(": "); //$NON-NLS-1$
            pPrintStream.print(entry.getValue().toString());
            pPrintStream.println();
        }
    }

    @Override
    public void setHelpCategory(final int pOrder, final String pTitleNLSKey) {
        _root.setHelpCategory(pOrder, pTitleNLSKey);
    }

    public void setUICallback(final CLAPUICallback pUICallback) {
        if (pUICallback == null) {
            throw new IllegalArgumentException();
        }
        _uiCallback = pUICallback;
    }

    @Override
    public void setUsageCategory(final int pOrder, final String pTitleNLSKey) {
        _root.setUsageCategory(pOrder, pTitleNLSKey);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + ":" + _root; //$NON-NLS-1$
    }

    private <R> void addPrimitiveConverter(final Class<?> pWrapperClass, final Class<R> pPrimitiveClass)
            throws Exception {
        final Method parseMethod = pWrapperClass
                .getMethod("parse" + StringUtils.capitalize(pPrimitiveClass.getSimpleName()), String.class); //$NON-NLS-1$
        final CLAPConverter<R> methodConverter = new CLAPConverter<R>() {

            @Override
            public R convert(final String pInput) {
                try {
                    return (R) parseMethod.invoke(null, pInput);
                } catch (final Exception e) {
                    throw runtimeException(e);
                }
            }

        };
        addConverter(pPrimitiveClass, methodConverter);
    }

    private <R> void addStringConstructorConverter(final Class<R> pStringConstructorClass) throws Exception {
        final Constructor<R> constructor = pStringConstructorClass.getConstructor(String.class);

        final CLAPConverter<R> constructorConverter = new CLAPConverter<R>() {

            @Override
            public R convert(final String pInput) {
                try {
                    return constructor.newInstance(pInput);
                } catch (final Exception e) {
                    throw runtimeException(e);
                }
            }

        };

        addConverter(pStringConstructorClass, constructorConverter);
    }

    private void initDefaultConverters() {
        try {
            final CLAPConverter<String> stringConverter = new CLAPConverter<String>() {

                @Override
                public String convert(final String pInput) {
                    return pInput;
                }

            };
            addConverter(String.class, stringConverter);

            final CLAPConverter<Boolean> booleanConverter = new CLAPConverter<Boolean>() {

                @Override
                public Boolean convert(final String pInput) {
                    if (pInput.equalsIgnoreCase("true")) { //$NON-NLS-1$
                        return true;
                    }
                    if (pInput.equalsIgnoreCase("false")) { //$NON-NLS-1$
                        return false;
                    }
                    if (pInput.equalsIgnoreCase("yes")) { //$NON-NLS-1$
                        return true;
                    }
                    if (pInput.equalsIgnoreCase("no")) { //$NON-NLS-1$
                        return false;
                    }
                    if (pInput.equalsIgnoreCase("on")) { //$NON-NLS-1$
                        return true;
                    }
                    if (pInput.equalsIgnoreCase("off")) { //$NON-NLS-1$
                        return false;
                    }
                    if (pInput.equalsIgnoreCase("enable")) { //$NON-NLS-1$
                        return true;
                    }
                    if (pInput.equalsIgnoreCase("disable")) { //$NON-NLS-1$
                        return false;
                    }
                    throw new RuntimeException(pInput);
                }

            };
            addConverter(Boolean.class, booleanConverter);
            addConverter(Boolean.TYPE, booleanConverter);

            final Class<?>[] someWrapperClasses = new Class<?>[] { Byte.class, Short.class, Integer.class,
                    Long.class, Float.class, Double.class };
            for (int i = 0; i < someWrapperClasses.length; i++) {
                final Class<?> wrapperClass = someWrapperClasses[i];
                addStringConstructorConverter(wrapperClass);
                final Class<?> primitiveClass = ClassUtils.wrapperToPrimitive(wrapperClass);
                if (primitiveClass != null) {
                    addPrimitiveConverter(wrapperClass, primitiveClass);
                }
            }
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }

    private RuntimeException runtimeException(final Throwable pThrowable) {
        if (pThrowable instanceof InvocationTargetException) {
            return runtimeException(((InvocationTargetException) pThrowable).getTargetException());
        }
        if (pThrowable instanceof RuntimeException) {
            return (RuntimeException) pThrowable;
        }
        return new RuntimeException(pThrowable);
    }

    private abstract class GetOrReadInteractivlyLogic {

        public <T> T getOrReadInteractivly(final T pObject, final String pPromptNLSKey, final String pCancelNLSKey,
                final boolean pSetAfterRead) {
            try {
                final BeanInfo beanInfo = Introspector.getBeanInfo(pObject.getClass());
                final Map<Method, String> readMethodToDescriptionMap = new HashMap<Method, String>();
                final Map<Method, Method> readMethodToWriteMethodMap = new HashMap<Method, Method>();
                for (final PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
                    final Method readMethod = propertyDescriptor.getReadMethod();
                    final Method writeMethod = propertyDescriptor.getWriteMethod();
                    if (readMethod != null && writeMethod != null
                            && propertyDescriptor.getPropertyType().equals(String.class)) {
                        final String description;
                        final CLAPOption clapOption = writeMethod.getAnnotation(CLAPOption.class);
                        final String descriptionNLSKey = clapOption == null ? null : clapOption.descriptionNLSKey();
                        if (descriptionNLSKey != null && descriptionNLSKey.trim().length() != 0) {
                            description = nls(descriptionNLSKey);
                        } else {
                            description = propertyDescriptor.getDisplayName();
                        }
                        readMethodToDescriptionMap.put(readMethod, description);
                        readMethodToWriteMethodMap.put(readMethod, writeMethod);
                    }
                }
                final ProxyFactory proxyFactory = new ProxyFactory();
                proxyFactory.setSuperclass(pObject.getClass());
                proxyFactory.setFilter(new MethodFilter() {
                    @Override
                    public boolean isHandled(final Method m) {
                        return readMethodToDescriptionMap.containsKey(m);
                    }
                });
                final MethodHandler handler = new MethodHandler() {
                    @Override
                    public Object invoke(final Object pSelf, final Method pMethod, final Method pProceed,
                            final Object[] pArgs) throws Throwable {
                        final String result = (String) pMethod.invoke(pObject, pArgs); // execute the original method.
                        if (result == null) {
                            final String description = readMethodToDescriptionMap.get(pMethod);
                            final String prompt = nls(pPromptNLSKey, description);
                            final String newResult = readInteractivly(prompt);
                            if (newResult == null) {
                                throw new RuntimeException(nls(pCancelNLSKey));
                            }
                            if (pSetAfterRead) {
                                readMethodToWriteMethodMap.get(pMethod).invoke(pObject, newResult);
                            }
                            return newResult;
                        }
                        return result;
                    }
                };
                final Class<T> proxyClass = proxyFactory.createClass();
                final T proxy = proxyClass.newInstance();
                ((Proxy) proxy).setHandler(handler);
                return proxy;
            } catch (final Exception e) {
                throw new RuntimeException(e);
            }
        }

        protected abstract String readInteractivly(String pPrompt);

    }

}