001/*
002 * Copyright (C) 2012 eXo Platform SAS.
003 *
004 * This is free software; you can redistribute it and/or modify it
005 * under the terms of the GNU Lesser General Public License as
006 * published by the Free Software Foundation; either version 2.1 of
007 * the License, or (at your option) any later version.
008 *
009 * This software is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public
015 * License along with this software; if not, write to the Free
016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
018 */
019
020package org.crsh.cli.impl.lang;
021
022import org.crsh.cli.Named;
023import org.crsh.cli.descriptor.Description;
024import org.crsh.cli.impl.descriptor.IntrospectionException;
025import org.crsh.cli.descriptor.ParameterDescriptor;
026import org.crsh.cli.impl.ParameterType;
027import org.crsh.cli.Argument;
028import org.crsh.cli.Command;
029import org.crsh.cli.Option;
030import org.crsh.cli.Required;
031import org.crsh.cli.type.ValueTypeFactory;
032
033import java.lang.annotation.Annotation;
034import java.lang.reflect.Field;
035import java.lang.reflect.Method;
036import java.lang.reflect.Type;
037import java.util.ArrayList;
038import java.util.Arrays;
039import java.util.Collections;
040import java.util.LinkedHashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.logging.Level;
044import java.util.logging.Logger;
045
046/** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */
047public class CommandFactory {
048
049  /** . */
050  public static final CommandFactory DEFAULT = new CommandFactory();
051
052  /** . */
053  private static final Logger log = Logger.getLogger(CommandFactory.class.getName());
054
055  /** . */
056  protected final ValueTypeFactory valueTypeFactory;
057
058  public CommandFactory() {
059    this.valueTypeFactory = ValueTypeFactory.DEFAULT;
060  }
061
062  public CommandFactory(ClassLoader loader) throws NullPointerException {
063    this(new ValueTypeFactory(loader));
064  }
065
066  public CommandFactory(ValueTypeFactory valueTypeFactory) throws NullPointerException {
067    if (valueTypeFactory == null) {
068      throw new NullPointerException("No null value type factory accepted");
069    }
070
071    //
072    this.valueTypeFactory = valueTypeFactory;
073  }
074
075
076  private List<Method> findAllMethods(Class<?> introspected) throws IntrospectionException {
077    List<Method> methods;
078    Class<?> superIntrospected = introspected.getSuperclass();
079    if (superIntrospected == null) {
080      methods = new ArrayList<Method>();
081    } else {
082      methods = findAllMethods(superIntrospected);
083      for (Method method : introspected.getDeclaredMethods()) {
084        if (method.getAnnotation(Command.class) != null) {
085          methods.add(method);
086        }
087      }
088    }
089    return methods;
090  }
091
092  public <T> ObjectCommandDescriptor<T> create(Class<T> type) throws IntrospectionException {
093
094    // Find all command methods
095    List<Method> methods = findAllMethods(type);
096
097    //
098    String commandName;
099    if (type.getAnnotation(Named.class) != null) {
100      commandName = type.getAnnotation(Named.class).value();
101    } else {
102      commandName = type.getSimpleName();
103    }
104
105    //
106    if (methods.size() == 1 && methods.get(0).getName().equals("main")) {
107      MethodDescriptor<T> methodDescriptor = create(null, commandName, methods.get(0));
108      for (ParameterDescriptor parameter : parameters(type)) {
109        methodDescriptor.addParameter(parameter);
110      }
111      return methodDescriptor;
112    } else {
113      Map<String, MethodDescriptor<T>> methodMap = new LinkedHashMap<String, MethodDescriptor<T>>();
114      ClassDescriptor<T> classDescriptor = new ClassDescriptor<T>(type, commandName, methodMap, new Description(type));
115      for (Method method : methods) {
116        String methodName;
117        if (method.getAnnotation(Named.class) != null) {
118          methodName = method.getAnnotation(Named.class).value();
119        } else {
120          methodName = method.getName();
121        }
122        MethodDescriptor<T> methodDescriptor = create(classDescriptor, methodName, method);
123        methodMap.put(methodDescriptor.getName(), methodDescriptor);
124      }
125      for (ParameterDescriptor parameter : parameters(type)) {
126        classDescriptor.addParameter(parameter);
127      }
128      return classDescriptor;
129    }
130  }
131
132  private <T> MethodDescriptor<T> create(ClassDescriptor<T> classDescriptor, String name, Method method) throws IntrospectionException {
133    Description info = new Description(method);
134    MethodDescriptor<T> methodDescriptor = new MethodDescriptor<T>(
135        classDescriptor,
136        method,
137        name,
138        info);
139
140    Type[] parameterTypes = method.getGenericParameterTypes();
141    Annotation[][] parameterAnnotationMatrix = method.getParameterAnnotations();
142    for (int i = 0;i < parameterAnnotationMatrix.length;i++) {
143
144      Annotation[] parameterAnnotations = parameterAnnotationMatrix[i];
145      Type parameterType = parameterTypes[i];
146      Tuple tuple = get(parameterAnnotations);
147
148      MethodArgumentBinding binding = new MethodArgumentBinding(i);
149      ParameterDescriptor parameter = create(
150          binding,
151          parameterType,
152          tuple.argumentAnn,
153          tuple.optionAnn,
154          tuple.required,
155          tuple.descriptionAnn,
156          tuple.ann);
157      if (parameter != null) {
158        methodDescriptor.addParameter(parameter);
159      } else {
160        log.log(Level.FINE, "Method argument with index " + i + " of method " + method + " is not annotated");
161      }
162    }
163    return methodDescriptor;
164  }
165
166  private ParameterDescriptor create(
167      Binding binding,
168      Type type,
169      Argument argumentAnn,
170      Option optionAnn,
171      boolean required,
172      Description info,
173      Annotation ann) throws IntrospectionException {
174
175    //
176    if (argumentAnn != null) {
177      if (optionAnn != null) {
178        throw new IntrospectionException();
179      }
180
181      //
182      return new BoundArgumentDescriptor(
183          binding,
184          argumentAnn.name(),
185          ParameterType.create(valueTypeFactory, type),
186          info,
187          required,
188          false,
189          argumentAnn.unquote(),
190          argumentAnn.completer(),
191          ann);
192    } else if (optionAnn != null) {
193      return new BoundOptionDescriptor(
194          binding,
195          ParameterType.create(valueTypeFactory, type),
196          Collections.unmodifiableList(Arrays.asList(optionAnn.names())),
197          info,
198          required,
199          false,
200          optionAnn.unquote(),
201          optionAnn.completer(),
202          ann);
203    } else {
204      return null;
205    }
206  }
207
208  private static Tuple get(Annotation... ab) {
209    Argument argumentAnn = null;
210    Option optionAnn = null;
211    Boolean required = null;
212    Description description = new Description(ab);
213    Annotation info = null;
214    for (Annotation parameterAnnotation : ab) {
215      if (parameterAnnotation instanceof Option) {
216        optionAnn = (Option)parameterAnnotation;
217      } else if (parameterAnnotation instanceof Argument) {
218        argumentAnn = (Argument)parameterAnnotation;
219      } else if (parameterAnnotation instanceof Required) {
220        required = ((Required)parameterAnnotation).value();
221      } else if (info == null) {
222
223        // Look at annotated annotations
224        Class<? extends Annotation> a = parameterAnnotation.annotationType();
225        if (a.getAnnotation(Option.class) != null) {
226          optionAnn = a.getAnnotation(Option.class);
227          info = parameterAnnotation;
228        } else if (a.getAnnotation(Argument.class) != null) {
229          argumentAnn =  a.getAnnotation(Argument.class);
230          info = parameterAnnotation;
231        }
232
233        //
234        if (info != null) {
235
236          //
237          description = new Description(description, new Description(a));
238
239          //
240          if (required == null) {
241            Required metaReq = a.getAnnotation(Required.class);
242            if (metaReq != null) {
243              required = metaReq.value();
244            }
245          }
246        }
247      }
248    }
249
250    //
251    return new Tuple(argumentAnn, optionAnn, required != null && required,description, info);
252  }
253
254  /**
255   * Jus grouping some data for conveniency
256   */
257  protected static class Tuple {
258    final Argument argumentAnn;
259    final Option optionAnn;
260    final boolean required;
261    final Description descriptionAnn;
262    final Annotation ann;
263    private Tuple(Argument argumentAnn, Option optionAnn, boolean required, Description info, Annotation ann) {
264      this.argumentAnn = argumentAnn;
265      this.optionAnn = optionAnn;
266      this.required = required;
267      this.descriptionAnn = info;
268      this.ann = ann;
269    }
270  }
271
272  private List<ParameterDescriptor> parameters(Class<?> introspected) throws IntrospectionException {
273    List<ParameterDescriptor> parameters;
274    Class<?> superIntrospected = introspected.getSuperclass();
275    if (superIntrospected == null) {
276      parameters = new ArrayList<ParameterDescriptor>();
277    } else {
278      parameters = parameters(superIntrospected);
279      for (Field f : introspected.getDeclaredFields()) {
280        Tuple tuple = get(f.getAnnotations());
281        ClassFieldBinding binding = new ClassFieldBinding(f);
282        ParameterDescriptor parameter = create(
283            binding,
284            f.getGenericType(),
285            tuple.argumentAnn,
286            tuple.optionAnn,
287            tuple.required,
288            tuple.descriptionAnn,
289            tuple.ann);
290        if (parameter != null) {
291          parameters.add(parameter);
292        }
293      }
294    }
295    return parameters;
296  }
297}