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.descriptor.Description;
023import org.crsh.cli.impl.descriptor.IntrospectionException;
024import org.crsh.cli.descriptor.ParameterDescriptor;
025import org.crsh.cli.impl.ParameterType;
026import org.crsh.cli.Argument;
027import org.crsh.cli.Command;
028import org.crsh.cli.Option;
029import org.crsh.cli.Required;
030import org.crsh.cli.type.ValueTypeFactory;
031
032import java.lang.annotation.Annotation;
033import java.lang.reflect.Field;
034import java.lang.reflect.Method;
035import java.lang.reflect.Type;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.Collections;
039import java.util.LinkedHashMap;
040import java.util.List;
041import java.util.Map;
042import java.util.logging.Level;
043import java.util.logging.Logger;
044
045/** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */
046public class CommandFactory {
047
048  /** . */
049  public static final CommandFactory DEFAULT = new CommandFactory();
050
051  /** . */
052  private static final Logger log = Logger.getLogger(CommandFactory.class.getName());
053
054  /** . */
055  protected final ValueTypeFactory valueTypeFactory;
056
057  public CommandFactory() {
058    this.valueTypeFactory = ValueTypeFactory.DEFAULT;
059  }
060
061  public CommandFactory(ClassLoader loader) throws NullPointerException {
062    this(new ValueTypeFactory(loader));
063  }
064
065  public CommandFactory(ValueTypeFactory valueTypeFactory) throws NullPointerException {
066    if (valueTypeFactory == null) {
067      throw new NullPointerException("No null value type factory accepted");
068    }
069
070    //
071    this.valueTypeFactory = valueTypeFactory;
072  }
073
074
075  private List<Method> findAllMethods(Class<?> introspected) throws IntrospectionException {
076    List<Method> methods;
077    Class<?> superIntrospected = introspected.getSuperclass();
078    if (superIntrospected == null) {
079      methods = new ArrayList<Method>();
080    } else {
081      methods = findAllMethods(superIntrospected);
082      for (Method method : introspected.getDeclaredMethods()) {
083        if (method.getAnnotation(Command.class) != null) {
084          methods.add(method);
085        }
086      }
087    }
088    return methods;
089  }
090
091  public <T> ObjectCommandDescriptor<T> create(Class<T> type) throws IntrospectionException {
092
093    // Find all command methods
094    List<Method> methods = findAllMethods(type);
095
096    //
097    if (methods.size() == 1 && methods.get(0).getName().equals("main")) {
098      MethodDescriptor<T> methodDescriptor = create(null, type.getSimpleName().toLowerCase(), methods.get(0));
099      for (ParameterDescriptor parameter : parameters(type)) {
100        methodDescriptor.addParameter(parameter);
101      }
102      return methodDescriptor;
103    } else {
104      Map<String, MethodDescriptor<T>> methodMap = new LinkedHashMap<String, MethodDescriptor<T>>();
105      ClassDescriptor<T> classDescriptor = new ClassDescriptor<T>(type, methodMap, new Description(type));
106      for (Method method : methods) {
107        MethodDescriptor<T> methodDescriptor = create(classDescriptor, method.getName().toLowerCase(), method);
108        methodMap.put(methodDescriptor.getName(), methodDescriptor);
109      }
110      for (ParameterDescriptor parameter : parameters(type)) {
111        classDescriptor.addParameter(parameter);
112      }
113      return classDescriptor;
114    }
115  }
116
117  private <T> MethodDescriptor<T> create(ClassDescriptor<T> classDescriptor, String name, Method method) {
118    Description info = new Description(method);
119    MethodDescriptor<T> methodDescriptor = new MethodDescriptor<T>(
120        classDescriptor,
121        method,
122        name,
123        info);
124
125    Type[] parameterTypes = method.getGenericParameterTypes();
126    Annotation[][] parameterAnnotationMatrix = method.getParameterAnnotations();
127    for (int i = 0;i < parameterAnnotationMatrix.length;i++) {
128
129      Annotation[] parameterAnnotations = parameterAnnotationMatrix[i];
130      Type parameterType = parameterTypes[i];
131      Tuple tuple = get(parameterAnnotations);
132
133      MethodArgumentBinding binding = new MethodArgumentBinding(i);
134      ParameterDescriptor parameter = create(
135          binding,
136          parameterType,
137          tuple.argumentAnn,
138          tuple.optionAnn,
139          tuple.required,
140          tuple.descriptionAnn,
141          tuple.ann);
142      if (parameter != null) {
143        methodDescriptor.addParameter(parameter);
144      } else {
145        log.log(Level.FINE, "Method argument with index " + i + " of method " + method + " is not annotated");
146      }
147    }
148    return methodDescriptor;
149  }
150
151  private ParameterDescriptor create(
152      Binding binding,
153      Type type,
154      Argument argumentAnn,
155      Option optionAnn,
156      boolean required,
157      Description info,
158      Annotation ann) throws IntrospectionException {
159
160    //
161    if (argumentAnn != null) {
162      if (optionAnn != null) {
163        throw new IntrospectionException();
164      }
165
166      //
167      return new BoundArgumentDescriptor(
168          binding,
169          argumentAnn.name(),
170          ParameterType.create(valueTypeFactory, type),
171          info,
172          required,
173          false,
174          argumentAnn.unquote(),
175          argumentAnn.completer(),
176          ann);
177    } else if (optionAnn != null) {
178      return new BoundOptionDescriptor(
179          binding,
180          ParameterType.create(valueTypeFactory, type),
181          Collections.unmodifiableList(Arrays.asList(optionAnn.names())),
182          info,
183          required,
184          false,
185          optionAnn.unquote(),
186          optionAnn.completer(),
187          ann);
188    } else {
189      return null;
190    }
191  }
192
193  private static Tuple get(Annotation... ab) {
194    Argument argumentAnn = null;
195    Option optionAnn = null;
196    Boolean required = null;
197    Description description = new Description(ab);
198    Annotation info = null;
199    for (Annotation parameterAnnotation : ab) {
200      if (parameterAnnotation instanceof Option) {
201        optionAnn = (Option)parameterAnnotation;
202      } else if (parameterAnnotation instanceof Argument) {
203        argumentAnn = (Argument)parameterAnnotation;
204      } else if (parameterAnnotation instanceof Required) {
205        required = ((Required)parameterAnnotation).value();
206      } else if (info == null) {
207
208        // Look at annotated annotations
209        Class<? extends Annotation> a = parameterAnnotation.annotationType();
210        if (a.getAnnotation(Option.class) != null) {
211          optionAnn = a.getAnnotation(Option.class);
212          info = parameterAnnotation;
213        } else if (a.getAnnotation(Argument.class) != null) {
214          argumentAnn =  a.getAnnotation(Argument.class);
215          info = parameterAnnotation;
216        }
217
218        //
219        if (info != null) {
220
221          //
222          description = new Description(description, new Description(a));
223
224          //
225          if (required == null) {
226            Required metaReq = a.getAnnotation(Required.class);
227            if (metaReq != null) {
228              required = metaReq.value();
229            }
230          }
231        }
232      }
233    }
234
235    //
236    return new Tuple(argumentAnn, optionAnn, required != null && required,description, info);
237  }
238
239  /**
240   * Jus grouping some data for conveniency
241   */
242  protected static class Tuple {
243    final Argument argumentAnn;
244    final Option optionAnn;
245    final boolean required;
246    final Description descriptionAnn;
247    final Annotation ann;
248    private Tuple(Argument argumentAnn, Option optionAnn, boolean required, Description info, Annotation ann) {
249      this.argumentAnn = argumentAnn;
250      this.optionAnn = optionAnn;
251      this.required = required;
252      this.descriptionAnn = info;
253      this.ann = ann;
254    }
255  }
256
257  private List<ParameterDescriptor> parameters(Class<?> introspected) throws IntrospectionException {
258    List<ParameterDescriptor> parameters;
259    Class<?> superIntrospected = introspected.getSuperclass();
260    if (superIntrospected == null) {
261      parameters = new ArrayList<ParameterDescriptor>();
262    } else {
263      parameters = parameters(superIntrospected);
264      for (Field f : introspected.getDeclaredFields()) {
265        Tuple tuple = get(f.getAnnotations());
266        ClassFieldBinding binding = new ClassFieldBinding(f);
267        ParameterDescriptor parameter = create(
268            binding,
269            f.getGenericType(),
270            tuple.argumentAnn,
271            tuple.optionAnn,
272            tuple.required,
273            tuple.descriptionAnn,
274            tuple.ann);
275        if (parameter != null) {
276          parameters.add(parameter);
277        }
278      }
279    }
280    return parameters;
281  }
282}