001    /*
002     * Copyright (C) 2010 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.command;
021    
022    import org.crsh.util.Strings;
023    import org.crsh.util.TypeResolver;
024    import org.kohsuke.args4j.CmdLineException;
025    import org.kohsuke.args4j.CmdLineParser;
026    import org.kohsuke.args4j.Option;
027    
028    import java.io.PrintWriter;
029    import java.io.StringWriter;
030    import java.lang.annotation.Annotation;
031    import java.lang.reflect.Field;
032    import java.util.Collections;
033    import java.util.List;
034    import java.util.Map;
035    import java.util.concurrent.ConcurrentHashMap;
036    import java.util.regex.Pattern;
037    
038    /**
039     * The base command.
040     *
041     * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
042     * @version $Revision$
043     * @param <C> the consumed type
044     * @param <P> the produced type
045     */
046    public abstract class BaseCommand<C, P> extends GroovyCommand implements ShellCommand, CommandInvoker<C, P> {
047    
048      private static final class MetaData {
049    
050        /** . */
051        private static final ConcurrentHashMap<String, MetaData> metaDataCache = new ConcurrentHashMap<String, MetaData>();
052    
053        static MetaData getMetaData(Class<?> clazz) {
054          MetaData metaData = metaDataCache.get(clazz.getName());
055          if (metaData == null || !metaData.isValid(clazz)) {
056            metaData = new MetaData(clazz);
057            metaDataCache.put(clazz.getName(), metaData);
058          }
059          return metaData;
060        }
061    
062        /** . */
063        private static final Pattern ARGS4J = Pattern.compile("^org\\.kohsuke\\.args4j\\.?$");
064    
065        /** . */
066        private final int descriptionFramework;
067    
068        /** . */
069        private final String fqn;
070    
071        /** . */
072        private final int identityHashCode;
073    
074        private MetaData(Class<?> clazz) {
075          this.descriptionFramework = findDescriptionFramework(clazz);
076          this.fqn = clazz.getName();
077          this.identityHashCode = System.identityHashCode(clazz);
078        }
079    
080        private boolean isValid(Class<?> clazz) {
081          return identityHashCode == System.identityHashCode(clazz) && fqn.equals(clazz.getName());
082        }
083    
084        private int findDescriptionFramework(Class<?> clazz) {
085          if (clazz == null) {
086            throw new NullPointerException();
087          }
088          Class<?> superClazz = clazz.getSuperclass();
089          int bs;
090          if (superClazz != null) {
091            bs = findDescriptionFramework(superClazz);
092          } else {
093            bs = 0;
094          }
095          for (Field f : clazz.getDeclaredFields()) {
096            for (Annotation annotation : f.getDeclaredAnnotations()) {
097              String packageName = annotation.annotationType().getPackage().getName();
098              if (ARGS4J.matcher(packageName).matches()) {
099                bs |= 0x01;
100              }
101            }
102          }
103          return bs;
104        }
105      }
106    
107      /** . */
108      private InvocationContext<C, P> context;
109    
110      /** . */
111      private boolean unquoteArguments;
112    
113      /** . */
114      private Class<C> consumedType;
115    
116      /** . */
117      private Class<P> producedType;
118    
119      /** . */
120      private final MetaData metaData;
121    
122      /** . */
123      private String[] args;
124    
125      /** . */
126      private String line;
127    
128      /** . */
129      @Option(name = "-h", aliases = "--help")
130      private boolean help;
131    
132      protected BaseCommand() {
133        this.context = null;
134        this.unquoteArguments = true;
135        this.consumedType = (Class<C>)TypeResolver.resolve(getClass(), CommandInvoker.class, 0);
136        this.producedType = (Class<P>)TypeResolver.resolve(getClass(), CommandInvoker.class, 1);
137        this.metaData = MetaData.getMetaData(getClass());
138        this.args = null;
139        this.line = null;
140      }
141    
142      public Class<P> getProducedType() {
143        return producedType;
144      }
145    
146      public Class<C> getConsumedType() {
147        return consumedType;
148      }
149    
150      /**
151       * Returns true if the command wants its arguments to be unquoted.
152       *
153       * @return true if arguments must be unquoted
154       */
155      public final boolean getUnquoteArguments() {
156        return unquoteArguments;
157      }
158    
159      public final void setUnquoteArguments(boolean unquoteArguments) {
160        this.unquoteArguments = unquoteArguments;
161      }
162    
163      protected final String readLine(String msg) {
164        return readLine(msg, true);
165      }
166    
167      protected final String readLine(String msg, boolean echo) {
168        if (context == null) {
169          throw new IllegalStateException("No current context");
170        }
171        return context.readLine(msg, echo);
172      }
173    
174      @Override
175      protected final InvocationContext<?, ?> getContext() {
176        return context;
177      }
178    
179      public final Map<String, String> complete(CommandContext context, String line) {
180        return Collections.emptyMap();
181      }
182    
183      public String describe(String line, DescriptionMode mode) {
184        Description description = getClass().getAnnotation(Description.class);
185    
186        //
187        switch (mode) {
188          case DESCRIBE:
189            return description != null ? description.value() : null;
190          case USAGE:
191            StringWriter sw = new StringWriter();
192            PrintWriter pw = new PrintWriter(sw);
193    
194            //
195            if (description != null) {
196              pw.write(description.value());
197              pw.write("\n");
198            }
199    
200            //
201            switch (metaData.descriptionFramework) {
202              default:
203                System.out.println("Not only one description framework");
204              case 0:
205                break;
206              case 1:
207                CmdLineParser parser = new CmdLineParser(this);
208                parser.printUsage(pw, null);
209                break;
210              case 2:
211                throw new UnsupportedOperationException();
212            }
213    
214            //
215            return sw.toString();
216          default:
217            return null;
218        }
219      }
220    
221      public final CommandInvoker<?, ?> createInvoker(String line) {
222        List<String> chunks = Strings.chunks(line);
223        this.args = chunks.toArray(new String[chunks.size()]);
224        this.line = line;
225        return this;
226      }
227    
228      public final void invoke(InvocationContext<C, P> context) throws ScriptException {
229        if (context == null) {
230          throw new NullPointerException();
231        }
232        if (args == null) {
233          throw new NullPointerException();
234        }
235    
236        // Remove surrounding quotes if there are
237        if (unquoteArguments) {
238          String[] foo = new String[args.length];
239          for (int i = 0;i < args.length;i++) {
240            String arg = args[i];
241            if (arg.charAt(0) == '\'') {
242              if (arg.charAt(arg.length() - 1) == '\'') {
243                arg = arg.substring(1, arg.length() - 1);
244              }
245            } else if (arg.charAt(0) == '"') {
246              if (arg.charAt(arg.length() - 1) == '"') {
247                arg = arg.substring(1, arg.length() - 1);
248              }
249            }
250            foo[i] = arg;
251          }
252          args = foo;
253        }
254    
255        //
256        switch (metaData.descriptionFramework) {
257          default:
258            System.out.println("Not only one description framework");
259          case 0:
260            break;
261          case 1:
262            try {
263              CmdLineParser parser = new CmdLineParser(this);
264              parser.parseArgument(args);
265            }
266            catch (CmdLineException e) {
267              throw new ScriptException(e.getMessage(), e);
268            }
269            break;
270        }
271    
272        //
273        if (help) {
274          String usage = describe(line, DescriptionMode.USAGE);
275          if (usage != null) {
276            context.getWriter().println(usage);
277          }
278        } else {
279          try {
280            this.context = context;
281    
282            //
283            execute(context);
284          }
285          finally {
286            this.context = null;
287          }
288        }
289      }
290    
291      protected abstract void execute(InvocationContext<C, P> context) throws ScriptException;
292    
293    }