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    
020    package org.crsh.cli.impl.lang;
021    
022    import org.crsh.cli.impl.descriptor.CommandDescriptorImpl;
023    import org.crsh.cli.descriptor.ArgumentDescriptor;
024    import org.crsh.cli.descriptor.Description;
025    import org.crsh.cli.impl.descriptor.IntrospectionException;
026    import org.crsh.cli.descriptor.OptionDescriptor;
027    import org.crsh.cli.descriptor.ParameterDescriptor;
028    import org.crsh.cli.impl.ParameterType;
029    import org.crsh.cli.Argument;
030    import org.crsh.cli.Command;
031    import org.crsh.cli.Option;
032    import org.crsh.cli.Required;
033    import org.crsh.cli.type.ValueTypeFactory;
034    
035    import java.lang.annotation.Annotation;
036    import java.lang.reflect.Field;
037    import java.lang.reflect.Method;
038    import java.lang.reflect.Type;
039    import java.util.ArrayList;
040    import java.util.Arrays;
041    import java.util.Collections;
042    import java.util.LinkedHashMap;
043    import java.util.List;
044    import java.util.Map;
045    import java.util.logging.Level;
046    import java.util.logging.Logger;
047    
048    /** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */
049    public 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    }