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 }