/**
* Copyright (C) 2013 Kametic <epo.jemba@kametic.com>
*
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3, 29 June 2007;
* or any later version
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.gnu.org/licenses/lgpl-3.0.txt
*
* 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 org.nuunframework.cli;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.lang.ArrayUtils;
import org.nuunframework.cli.api.NuunCliHandler;
import org.nuunframework.kernel.commons.AssertUtils;
import org.nuunframework.kernel.commons.specification.AbstractSpecification;
import org.nuunframework.kernel.commons.specification.Specification;
import org.nuunframework.kernel.context.InitContext;
import org.nuunframework.kernel.plugin.AbstractPlugin;
import org.nuunframework.kernel.plugin.InitState;
import org.nuunframework.kernel.plugin.PluginException;
import org.nuunframework.kernel.plugin.request.ClasspathScanRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class NuunCliPlugin extends AbstractPlugin
{
private Logger logger = LoggerFactory.getLogger(NuunCliPlugin.class);
private Specification<Class<?>> optionDefsSpecification;
private Options optionsAggregated;
private String[] lineArguments;
private CommandLine commandLine;
private Map<Class<?>, CommandLine> contextualCommandLineMap;
private CommandLineParser providedParser;
Specification<Class<?>> cliHandlerSpec = null;
private Map<Class, Class> bindings;
private Map<Class<?>, Options> byClassOptions;
/**
*
*/
public NuunCliPlugin()
{
bindings = Maps.newHashMap();
contextualCommandLineMap = Maps.newHashMap();
optionsAggregated = new Options();
}
@Override
public String name()
{
return "nuun-cli-plugin";
}
/*
* (non-Javadoc)
* @see org.nuunframework.kernel.plugin.AbstractPlugin#init(org.nuunframework.kernel.context.InitContext)
*/
@Override
public InitState init(InitContext initContext)
{
// Parser initialization
CommandLineParser parser = null;
if (providedParser == null)
{
parser = new PosixParser();
}
else {
parser = providedParser;
}
/////////// handler CliHandler
{
Collection<Class<?>> collection = initContext.scannedTypesBySpecification().get(cliHandlerSpec);
for (Class<?> class1 : collection) {
bindings.put(NuunCliHandler.class, class1);
// for now only one CommandLineHandler
break;
}
}
//////////// Define options and command lines
Collection<Class<?>> collection = initContext.scannedTypesBySpecification().get(optionDefsSpecification);
byClassOptions = createOptions(collection);
for (Entry<Class<?>, Options> entry : byClassOptions.entrySet())
{
try
{
commandLine = parser.parse( entry.getValue(), lineArguments);
contextualCommandLineMap.put(entry.getKey(), commandLine);
}
catch (ParseException e)
{
logger.debug("Error during option parse for class " + entry.getKey());
}
}
return InitState.INITIALIZED;
}
/**
* @param lineArguments2
* @return
*/
private String treat(String[] lineArguments2)
{
StringBuilder bulder = new StringBuilder();
for (String string : lineArguments2)
{
bulder.append( string).append(' ');
}
return bulder.toString();
}
private Map<Class<?>, Options> createOptions(Collection<Class<?>> collection)
{
Set< String> shortOptions = Sets.newHashSet();
Set< String> longOptions = Sets.newHashSet();
Map< Class<?>,Options> optionsMap = Maps.newHashMap();
for (Class<?> class1 : collection)
{
logger.info("CLASS " + class1.getSimpleName());
Set<Field> fields = annotatedFields(class1, NuunOption.class);
Options internalOptions = new Options();
for(Field field : fields)
{
logger.info("Class : " + field.getDeclaringClass().getSimpleName() + " / " + field.getName());
Option option = createOptionFromField(field);
if ( !Strings.isNullOrEmpty(option.getOpt()) && shortOptions.contains(option.getOpt()))
{
exception("Short option " + option.getOpt() + " already exists!" );
}
if ( !Strings.isNullOrEmpty(option.getLongOpt()) && longOptions.contains(option.getLongOpt()))
{
exception("Long option " + option.getLongOpt() + " already exists!" );
}
if ( Strings.isNullOrEmpty(option.getOpt()) && Strings.isNullOrEmpty(option.getLongOpt()))
{
exception("NuunOption defined on " + field + " has no opt nor longOpt." );
}
internalOptions.addOption(option);
// global one
optionsAggregated.addOption(option);
shortOptions.add(option.getOpt());
longOptions.add(option.getLongOpt());
}
optionsMap.put(class1, internalOptions);
}
return optionsMap;
}
public void provideParser(CommandLineParser parser)
{
this.providedParser = parser;
}
/* (non-Javadoc)
* @see org.nuunframework.kernel.plugin.AbstractPlugin#provideContainerContext(java.lang.Object)
*/
@Override
public void provideContainerContext(Object containerContext)
{
if (containerContext instanceof String[])
{
lineArguments = (String[]) containerContext;
}
else
{
lineArguments = new String[0];
}
}
/* (non-Javadoc)
* @see org.nuunframework.kernel.plugin.AbstractPlugin#dependencyInjectionDef()
*/
@Override
public Object dependencyInjectionDef()
{
return new NuunCliModule(commandLine,this.optionsAggregated, contextualCommandLineMap , byClassOptions ,bindings);
}
private void exception (String message, Object...objects)
{
logger.error(message, objects);
if (objects != null && objects.length == 1 && ( Throwable.class.isAssignableFrom( objects[0].getClass())))
{
throw new PluginException(message, (Throwable) objects[0]);
}
else
{
throw new PluginException(message, objects);
}
}
private void logException (String message, Object...objects)
{
logger.error(message, objects);
PluginException pluginException = null;
if (objects != null && objects.length == 1 && ( Throwable.class.isAssignableFrom( objects[0].getClass())))
{
pluginException = new PluginException(message, (Throwable) objects[0]);
}
else
{
pluginException = new PluginException(message, objects);
}
logger.error( "Nuun Cli Plugin Exception : ", pluginException);
}
private Option createOptionFromField (Field field )
{
Option option = null;
// Cli Option Builder is completly static :-/
// so we synchronized it ...
synchronized ( OptionBuilder.class )
{
// reset the builder creating a dummy option
OptionBuilder.withLongOpt("dummy");
OptionBuilder.create();
//
NuunOption nuunOption = field.getAnnotation(NuunOption.class);
if (nuunOption == null)
{
for (Annotation anno : field.getAnnotations() )
{
if ( AssertUtils.hasAnnotationDeep(anno.annotationType(), NuunOption.class ) )
{
nuunOption = AssertUtils.annotationProxyOf(NuunOption.class, anno);
break;
}
}
}
// longopt
if ( ! Strings.isNullOrEmpty(nuunOption.longOpt()))
{
OptionBuilder.withLongOpt( nuunOption.longOpt());
}
// description
if ( ! Strings.isNullOrEmpty(nuunOption.description()))
{
OptionBuilder.withDescription(nuunOption.description());
}
// required
OptionBuilder.isRequired((nuunOption.required()));
// arg
OptionBuilder.hasArg(( nuunOption.arg () ));
// args
if (nuunOption.args())
{
if (nuunOption.numArgs() > 0)
{
OptionBuilder.hasArgs(nuunOption.numArgs());
}
else
{
OptionBuilder.hasArgs();
}
}
// is optional
if ( nuunOption.optionalArg() )
{
OptionBuilder.hasOptionalArg() ;
}
// nuun
OptionBuilder.withValueSeparator(nuunOption.valueSeparator());
// opt
if ( ! Strings.isNullOrEmpty(nuunOption.opt()))
{
option = OptionBuilder.create(nuunOption.opt());
}
else
{
option = OptionBuilder.create();
}
}
return option;
}
private Set<Field> annotatedFields(Class<?> class_ , Class<? extends Annotation> annoClass)
{
Set<Field> fields = new HashSet<Field>();
for(Field field : class_.getDeclaredFields())
{
if (field.isAnnotationPresent(annoClass))
{
fields.add(field);
}
else {
for (Annotation annotation : field.getAnnotations())
{
if ( hasAnnotationDeep(annotation.annotationType(), annoClass))
{
// return true;
fields.add(field);
}
}
}
}
return fields;
}
/*
* (non-Javadoc)
* @see org.nuunframework.kernel.plugin.AbstractPlugin#classpathScanRequests()
*/
@SuppressWarnings("unchecked")
@Override
public Collection<ClasspathScanRequest> classpathScanRequests()
{
optionDefsSpecification = fieldAnnotatedWith(NuunOption.class);
cliHandlerSpec = and (
ancestorImplements(NuunCliHandler.class), //
not(classIsInterface()) , //
not(classIsAbstract()) //
);
return classpathScanRequestBuilder() //
.specification(optionDefsSpecification) //
.specification(cliHandlerSpec)
.build();
}
protected Specification<Class<?>> fieldAnnotatedWith (final Class<? extends Annotation> annotationClass)
{
return new AbstractSpecification<Class<?>> ()
{
@Override
public boolean isSatisfiedBy(Class<?> candidate) {
if (candidate != null)
{
try
{
for (Field field : candidate.getDeclaredFields())
{
if ( field.isAnnotationPresent(annotationClass) )
{
return true;
}
for (Annotation annotation : field.getAnnotations())
{
if ( hasAnnotationDeep(annotation.annotationType(), annotationClass))
{
return true;
}
}
}
}
catch (Throwable throwable)
{
logger.trace("fieldAnnotatedWith : " +candidate + " missing " + throwable );
}
}
return false;
}
};
}
boolean hasAnnotationDeep( Class<? extends Annotation> from , Class<? extends Annotation> toFind)
{
if (from.equals(toFind))
{
return true;
}
for (Annotation anno : from.getAnnotations())
{
Class<? extends Annotation> annoClass = anno.annotationType();
if (!annoClass.getPackage().getName().startsWith("java.lang") && hasAnnotationDeep(annoClass, toFind))
{
return true;
}
}
return false;
}
/*******************************************************************/
/************* SPECIFICATIONS ******************/
/*******************************************************************/
protected Specification<Class<?>> classIsInterface() {
return new AbstractSpecification<Class<?>>() {
@Override
public boolean isSatisfiedBy(Class<?> candidate) {
return candidate != null && candidate.isInterface();
}
};
}
protected Specification<Class<?>> classIsAbstract() {
return new AbstractSpecification<Class<?>>() {
@Override
public boolean isSatisfiedBy(Class<?> candidate) {
return candidate != null && Modifier.isAbstract(candidate.getModifiers());
}
};
}
protected Specification<Class<?>> ancestorImplements (final Class<?> interfaceClass)
{
return new AbstractSpecification<Class<?>>() {
@Override
public boolean isSatisfiedBy(Class<?> candidate) {
if (candidate == null) return false;
boolean result = false;
Class<?>[] allInterfacesAndClasses = getAllInterfacesAndClasses(candidate);
for (Class<?> clazz : allInterfacesAndClasses) {
if (! clazz.isInterface() )
{
for (Class<?> i : clazz.getInterfaces())
{
if (i.equals(interfaceClass))
{
result = true;
break;
}
}
}
}
return result;
}
};
}
Class<?>[] getAllInterfacesAndClasses(Class<?> clazz) {
return getAllInterfacesAndClasses(new Class[] { clazz } );
}
//This method walks up the inheritance hierarchy to make sure we get every class/interface that could
//possibly contain the declaration of the annotated method we're looking for.
@SuppressWarnings("unchecked")
Class<?>[] getAllInterfacesAndClasses(Class<?>[] classes) {
if (0 == classes.length) {
return classes;
} else {
List<Class<?>> extendedClasses = new ArrayList<Class<?>>();
// all interfaces hierarchy
for (Class<?> clazz : classes) {
if (clazz != null) {
Class<?>[] interfaces = clazz.getInterfaces();
if (interfaces != null) {
extendedClasses
.addAll((List<? extends Class<?>>) Arrays
.asList(interfaces));
}
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
extendedClasses
.addAll((List<? extends Class<?>>) Arrays
.asList(superclass));
}
}
}
// Class::getInterfaces() gets only interfaces/classes
// implemented/extended directly by a given class.
// We need to walk the whole way up the tree.
return (Class[]) ArrayUtils.addAll(classes,
getAllInterfacesAndClasses(extendedClasses
.toArray(new Class[extendedClasses.size()])));
}
}
// class OptionsDefSpecification extends AbstractSpecification<Class<?>>
// {
// public OptionsDefSpecification()
// {
// }
//
// @Override
// public boolean isSatisfiedBy(Class<?> candidate)
// {
// return (candidate != null && candidate.isAnnotation() &&
//
// ( candidate.isAnnotationPresent(NuunOption.class)) /*|| annotationAnnotatedWith(candidate, MetaNuunOption.class) */ );
// }
//
//
// private boolean annotationAnnotatedWith (Class<?> candidate, Class<? extends Annotation> anno)
// {
// for (Annotation a : candidate.getAnnotations())
// {
// if (a.annotationType().getAnnotation(anno) != null)
// {
// return true;
// }
// }
//
// return false;
// }
// }
}