View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.frontend.cli;
20  
21  import java.lang.reflect.InvocationTargetException;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Modifier;
24  import java.text.Format;
25  import java.text.MessageFormat;
26  import java.text.NumberFormat;
27  import java.text.ParseException;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.StringTokenizer;
32  
33  import org.apache.commons.lang3.ClassUtils;
34  import org.apache.logging.log4j.LogManager;
35  import org.apache.logging.log4j.Logger;
36  import org.mycore.common.MCRClassTools;
37  import org.mycore.common.MCRException;
38  import org.mycore.common.config.MCRConfigurationException;
39  
40  /**
41   * Represents a command understood by the command line interface. A command has an external input syntax that the user
42   * uses to invoke the command and points to a method in a class that implements the command.
43   * 
44   * @see MCRCommandLineInterface
45   * @author Frank Lützenkirchen
46   * @author Jens Kupferschmidt
47   * @version $Revision$ $Date$
48   */
49  public class MCRCommand {
50  
51      private static final Logger LOGGER = LogManager.getLogger(MCRCommand.class);
52  
53      /** The input format used for invoking this command */
54      protected MessageFormat messageFormat;
55  
56      /** The java method that implements this command */
57      private Method method;
58  
59      /** The types of the invocation parameters */
60      protected Class<?>[] parameterTypes;
61  
62      /** The class providing the implementation method */
63      protected String className;
64  
65      /** The method implementing this command */
66      protected String methodName;
67  
68      /** The beginning of the message format up to the first parameter */
69      protected String suffix;
70  
71      /** The help text String */
72      protected String help;
73  
74      /**
75       * use this to overwrite this class.
76       */
77      protected MCRCommand() {
78      }
79  
80      /**
81       * Creates a new MCRCommand.
82       * 
83       * @param format
84       *            the command syntax, e.g. "save document {0} to directory {1}"
85       * @param methodSignature
86       *            the method to invoke, e.g. "miless.commandline.DocumentCommands.saveDoc int String"
87       * @param helpText
88       *            the helpt text for this command
89       */
90      public MCRCommand(String format, String methodSignature, String helpText) {
91          StringTokenizer st = new StringTokenizer(methodSignature, " ");
92  
93          String token = st.nextToken();
94          int point = token.lastIndexOf(".");
95  
96          className = token.substring(0, point);
97          methodName = token.substring(point + 1);
98          int numParameters = st.countTokens();
99          parameterTypes = new Class<?>[numParameters];
100         messageFormat = new MessageFormat(format, Locale.ROOT);
101 
102         for (int i = 0; i < numParameters; i++) {
103             token = st.nextToken();
104 
105             Format f = null;
106             switch (token) {
107             case "int":
108                 parameterTypes[i] = Integer.TYPE;
109                 f = NumberFormat.getIntegerInstance(Locale.ROOT);
110                 break;
111             case "long":
112                 parameterTypes[i] = Long.TYPE;
113                 f = NumberFormat.getIntegerInstance(Locale.ROOT);
114                 break;
115             case "String":
116                 parameterTypes[i] = String.class;
117                 break;
118             default:
119                 unsupportedArgException(methodSignature, token);
120             }
121             messageFormat.setFormat(i, f);
122         }
123 
124         int pos = format.indexOf("{");
125         suffix = pos == -1 ? format : format.substring(0, pos);
126 
127         if (helpText != null) {
128             help = helpText;
129         } else {
130             help = "No help text available for this command";
131         }
132     }
133 
134     private void unsupportedArgException(String methodSignature, String token) {
135         throw new MCRConfigurationException("Error while parsing command definitions for command line interface:\n"
136             + "Unsupported argument type '" + token + "' in command " + methodSignature);
137     }
138 
139     public MCRCommand(Method cmd) {
140         className = cmd.getDeclaringClass().getName();
141         methodName = cmd.getName();
142         parameterTypes = cmd.getParameterTypes();
143         org.mycore.frontend.cli.annotation.MCRCommand cmdAnnotation = cmd
144             .getAnnotation(org.mycore.frontend.cli.annotation.MCRCommand.class);
145         help = cmdAnnotation.help();
146         messageFormat = new MessageFormat(cmdAnnotation.syntax(), Locale.ROOT);
147         setMethod(cmd);
148 
149         for (int i = 0; i < parameterTypes.length; i++) {
150             Class<?> paramtype = parameterTypes[i];
151             if (ClassUtils.isAssignable(paramtype, Integer.class, true)
152                 || ClassUtils.isAssignable(paramtype, Long.class, true)) {
153                 messageFormat.setFormat(i, NumberFormat.getIntegerInstance(Locale.ROOT));
154             } else if (!String.class.isAssignableFrom(paramtype)) {
155                 unsupportedArgException(className + "." + methodName, paramtype.getName());
156             }
157         }
158 
159         int pos = cmdAnnotation.syntax().indexOf("{");
160         suffix = pos == -1 ? cmdAnnotation.syntax() : cmdAnnotation.syntax().substring(0, pos);
161     }
162 
163     private void initMethod(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException {
164         if (method == null) {
165             setMethod(Class.forName(className, true, classLoader).getMethod(methodName, parameterTypes));
166         }
167     }
168 
169     /**
170      * The method return the helpt text of this command.
171      * 
172      * @return the help text as String
173      */
174     public String getHelpText() {
175         return help;
176     }
177 
178     /**
179      * Parses an input string and tries to match it with the message format used to invoke this command.
180      * 
181      * @param commandLine
182      *            The input from the command line
183      * @return null, if the input does not match the message format; otherwise an array holding the parameter values
184      *         from the command line
185      */
186     protected Object[] parseCommandLine(String commandLine) {
187         try {
188             return messageFormat.parse(commandLine);
189         } catch (ParseException ex) {
190             return null;
191         }
192     }
193 
194     /**
195      * Transforms the parameters found by the MessageFormat parse method into such that can be used to invoke the method
196      * implementing this command
197      * 
198      * @param commandParameters
199      *            The parameters as returned by the <code>parseCommandLine</code> method
200      */
201     private void prepareInvocationParameters(Object[] commandParameters) {
202 
203         for (int i = 0; i < commandParameters.length; i++) {
204             if (parameterTypes[i] == Integer.TYPE) {
205                 commandParameters[i] = ((Number) commandParameters[i]).intValue();
206             }
207         }
208     }
209 
210     /**
211      * Tries to invoke the method that implements the behavior of this command given the user input from the command
212      * line. This is only done when the command line syntax matches the syntax used by this command.
213      * 
214      * @return null, if the command syntax did not match and the command was not invoked, otherwise a List of commands
215      *         is returned which may be empty or otherwise contains commands that should be processed next
216      * @param input
217      *            The command entered by the user at the command prompt
218      * @throws IllegalAccessException
219      *             when the method can not be invoked
220      * @throws InvocationTargetException
221      *             when an exception is thrown by the invoked method
222      * @throws ClassNotFoundException
223      *             when the class providing the method could not be found
224      * @throws NoSuchMethodException
225      *             when the method specified does not exist
226      */
227     public List<String> invoke(String input) throws IllegalAccessException, InvocationTargetException,
228         ClassNotFoundException, NoSuchMethodException {
229         return invoke(input, MCRClassTools.getClassLoader());
230     }
231 
232     @SuppressWarnings({ "unchecked", "rawtypes" })
233     public List<String> invoke(String input, ClassLoader classLoader) throws IllegalAccessException,
234         InvocationTargetException, ClassNotFoundException, NoSuchMethodException {
235         if (!input.startsWith(suffix)) {
236             return null;
237         }
238 
239         Object[] commandParameters = parseCommandLine(input);
240 
241         if (commandParameters == null) {
242             LOGGER.info("No match for syntax: {}", getSyntax());
243             return null;
244         }
245         LOGGER.info("Syntax matched (executed): {}", getSyntax());
246 
247         initMethod(classLoader);
248         prepareInvocationParameters(commandParameters);
249         Object result = method.invoke(null, commandParameters);
250         if (result instanceof List && !((List) result).isEmpty() && ((List) result).get(0) instanceof String) {
251             return (List<String>) result;
252         } else {
253             return new ArrayList<>();
254         }
255     }
256 
257     /**
258      * Returns the input syntax to be used for invoking this command from the command prompt.
259      * 
260      * @return the input syntax for this command
261      */
262     public final String getSyntax() {
263         return messageFormat.toPattern();
264     }
265 
266     public void outputHelp() {
267         MCRCommandLineInterface.output(getSyntax());
268         MCRCommandLineInterface.output("    " + getHelpText());
269         MCRCommandLineInterface.output("");
270     }
271 
272     /**
273      * @param method
274      *            the method to set
275      */
276     public void setMethod(Method method) {
277         if (!Modifier.isStatic(method.getModifiers())) {
278             throw new MCRException("MCRCommand method needs to be static: " + method);
279         }
280         this.method = method;
281     }
282 }