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