View Javadoc

1   package org.springframework.roo.shell;
2   
3   import java.io.BufferedReader;
4   import java.io.File;
5   import java.io.FileInputStream;
6   import java.io.FileNotFoundException;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.io.InputStreamReader;
10  import java.net.URL;
11  import java.text.DateFormat;
12  import java.util.Date;
13  import java.util.Properties;
14  import java.util.Set;
15  import java.util.SortedSet;
16  import java.util.TreeSet;
17  import java.util.jar.Manifest;
18  import java.util.logging.Logger;
19  
20  import org.apache.felix.scr.annotations.Component;
21  import org.apache.felix.scr.annotations.Reference;
22  import org.osgi.service.component.ComponentContext;
23  import org.springframework.roo.shell.event.AbstractShellStatusPublisher;
24  import org.springframework.roo.shell.event.ShellStatus;
25  import org.springframework.roo.support.logging.HandlerUtils;
26  import org.springframework.roo.support.util.Assert;
27  import org.springframework.roo.support.util.TemplateUtils;
28  
29  /**
30   * Provides a base {@link Shell} implementation.
31   * 
32   * @author Ben Alex
33   *
34   */
35  @Component(componentAbstract=true)
36  public abstract class AbstractShell extends AbstractShellStatusPublisher implements Shell {
37  
38      @Reference private ExecutionStrategy executionStrategy;
39      @Reference private Parser parser;
40  
41  	protected final Logger logger = HandlerUtils.getLogger(getClass());
42      protected boolean inBlockComment = false;
43      protected ExitShellRequest exitShellRequest = null;
44  	public static String shellPrompt = "roo> ";
45  	public static String completionKeys = "TAB";
46  	
47  	@CliCommand(value={"script"}, help="Parses the specified resource file and executes its commands")
48  	public void script(@CliOption(key={"","file"}, help="The file to locate and execute", mandatory=true) File resource,
49  						@CliOption(key="lineNumbers", mandatory=false, specifiedDefaultValue="true", unspecifiedDefaultValue="false", help="Display line numbers when executing the script") boolean lineNumbers) {
50  		Assert.notNull(resource, "Resource to parser is required");
51  		long started = new Date().getTime();
52  		InputStream inputStream = null;
53  		try {
54  			inputStream = new FileInputStream(resource);
55  		} catch (FileNotFoundException tryTheClassLoaderInstead) {}
56  		
57  		if (inputStream == null) {
58  			// Try to find the resource via the classloader
59  			Set<URL> urls = TemplateUtils.findUrls(getContext().getBundleContext(), "/" + resource.getName());
60  			// Handle search system failure
61  			Assert.notNull(urls, "Unable to process classpath bundles to locate the script");
62  			// Handle the file simply not being present, but the search being OK
63  			Assert.notEmpty(urls, "Resource '" + resource + "' not found on disk or in classpath");
64  			Assert.isTrue(urls.size() == 1, "More than one '" + resource + "' was found in the classpath; unable to continue");
65  			try {
66  				inputStream = urls.iterator().next().openStream();
67  			} catch (IOException e) {
68  				throw new IllegalStateException(e);
69  			}
70  		}
71  		
72  	    try {
73  	    	BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
74  	        String line;
75  	        int i = 0;
76  	        while ((line = in.readLine()) != null) {
77  	        	i++;
78  	        	if (lineNumbers) {
79  		        	logger.fine("Line " + i + ": " + line);
80  	        	} else {
81  		        	logger.fine(line);
82  	        	}
83  	        	if (!"".equals(line.trim())) {
84  		        	boolean success = executeScriptLine(line);
85  		        	if (!success) {
86  		        		// Abort script processing, given something went wrong
87  		        		throw new IllegalStateException("Script execution aborted");
88  		        	}
89  	        	}
90  	        }
91  	        in.close();
92  	    } catch (IOException e) {
93  	    	throw new IllegalStateException(e);
94  	    }
95  	    logger.fine("Milliseconds required: " + (new Date().getTime() - started));
96  	}
97  	
98  	/**
99  	 * Execute the single line from a script.
100 	 * <p>
101 	 * This method can be overridden by sub-classes to pre-process script lines. 
102 	 */
103 	protected boolean executeScriptLine(String line) {
104 		return executeCommand(line);
105 	}
106 	
107 	public boolean executeCommand(String line) {
108 		// another command was attempted
109     	setShellStatus(ShellStatus.PARSING);
110 
111     	long lastWaitMessage = System.currentTimeMillis(); 
112     	while (executionStrategy == null || !executionStrategy.isReadyForCommands()) {
113     		// Wait
114     		try {
115 				Thread.sleep(500);
116 			} catch (InterruptedException ignore) {}
117 			if (System.currentTimeMillis() > (lastWaitMessage + (3000*5))) {
118 				lastWaitMessage = System.currentTimeMillis();
119 				logger.finest("Waiting for process manager to become available - " + new Date().toString());
120 			}
121     	}
122 
123     	try {
124 			// We support simple block comments; ie a single pair per line
125 			if (!inBlockComment && line.contains("/*")) {
126 				blockCommentBegin();
127 				String lhs = line.substring(0, line.lastIndexOf("/*"));
128 				if (line.contains("*/")) {
129 					line = lhs + line.substring(line.lastIndexOf("*/")+2);
130 					blockCommentFinish();
131 				} else {
132 					line = lhs;
133 				}
134 			}
135 			if (inBlockComment) {
136 				if (!line.contains("*/")) {
137 					return true;
138 				}
139 				blockCommentFinish();
140 				line = line.substring(line.lastIndexOf("*/")+2);
141 			}
142 			// We also support inline comments (but only at start of line, otherwise valid 
143 			// command options like http://www.helloworld.com will fail as per ROO-517)
144 			if (!inBlockComment && line.trim().startsWith("//")) {
145 				line = line.substring(0, line.indexOf("//"));
146 			}
147 			// convert any TAB characters to whitespace (ROO-527)
148 			line = line.replace('\t', ' ');
149 			if ("".equals(line.trim())) {
150 		    	setShellStatus(ShellStatus.EXECUTION_COMPLETE);
151 				return true;
152 			}
153 			ParseResult parseResult = parser.parse(line);
154 			if (parseResult == null) {
155 				return false;
156 			} else {
157 				setShellStatus(ShellStatus.EXECUTING);
158 		    	Object result = executionStrategy.execute(parseResult);
159 		    	setShellStatus(ShellStatus.EXECUTION_RESULT_PROCESSING);
160 		    	if (result != null) {
161 		    		if (result instanceof ExitShellRequest) {
162 		    			exitShellRequest = (ExitShellRequest) result;
163 		    		} else if (result instanceof Iterable<?>) {
164 		    			for (Object o : (Iterable<?>)result) {
165 		    				logger.info(o.toString());
166 		    			}
167 		    		} else {
168 	    				logger.info(result.toString());
169 		    		}
170 		    	}
171 		    }
172 		} catch (RuntimeException ex) {
173 	    	setShellStatus(ShellStatus.EXECUTION_RESULT_PROCESSING);
174 			// We rely on execution strategy to log it
175 	    	//Throwable root = ExceptionUtils.extractRootCause(ex);
176 			//logger.log(Level.FINE, root.getMessage());
177 	    	try {
178 	    		logCommandIfRequired(line, false);
179 	    	} catch (Exception ignoreIt) {}
180 			return false;
181 		} finally {
182 			setShellStatus(ShellStatus.EXECUTION_COMPLETE);
183 		}
184 		logCommandIfRequired(line, true);
185 		return true;
186 	}
187 	
188 	/**
189 	 * Allows a subclass to log the execution of a well-formed command. This is invoked after a command
190 	 * has completed, and indicates whether the command returned normally or returned an exception. Note
191 	 * that attempted commands that are not well-formed (eg they are missing a mandatory argument) will
192 	 * never be presented to this method, as the command execution is never actually attempted in those
193 	 * cases. This method is only invoked if an attempt is made to execute a particular command.
194 	 * 
195 	 * <p>
196 	 * Implementations should consider specially handling the "script" commands, and also
197 	 * indicating whether a command was successful or not. Implementations that wish to behave
198 	 * consistently with other {@link AbstractShell} subclasses are encouraged to simply override
199 	 * {@link #logCommandToOutput(String)} instead, and only override this method if you actually
200 	 * need to fine-tune the output logic.
201 	 *  
202 	 * @param line the parsed line (any comments have been removed; never null)
203 	 * @param successful if the command was successful or not
204 	 */
205 	protected void logCommandIfRequired(String line, boolean successful) {
206 		if (line.startsWith("script")) {
207 			logCommandToOutput((successful ? "// " : "// [failed] ") + line);
208 		} else {
209 			logCommandToOutput((successful ? "" : "// [failed] ") + line);
210 		}
211 	}
212 	
213 	/**
214 	 * Allows a subclass to actually write the resulting logged command to some form of output. This
215 	 * frees subclasses from needing to implement the logic within {@link #logCommandIfRequired(String, boolean)}.
216 	 *
217 	 * <p>
218 	 * Implementations should invoke {@link #getExitShellRequest()} to monitor any attempts to exit the shell and
219 	 * release resources such as output log files.
220 	 * 
221 	 * @param processedLine the line that should be appended to some type of output (excluding the \n character)
222 	 */
223 	protected void logCommandToOutput(String processedLine) {
224 		// logger.severe(processedLine);
225 	}
226 
227 	/**
228 	 * Base implementation of the {@link Shell#setPromptPath(String)} method, designed for simple shell
229 	 * implementations. Advanced implementations (eg those that support ANSI codes etc) will likely want
230 	 * to override this method and set the {@link #shellPrompt} variable directly.
231 	 * 
232 	 * @param path to set (can be null or empty; must NOT be formatted in any special way eg ANSI codes)
233 	 */
234 	public void setPromptPath(String path) {
235 		if ("".equals(path) || path == null) {
236 			shellPrompt = "roo> ";
237 		} else {
238 			shellPrompt = path + " roo> ";
239 		}
240 	}
241 
242 	public ExitShellRequest getExitShellRequest() {
243 		return exitShellRequest;
244 	}
245 
246 	@CliCommand(value={"//", ";"}, help="Inline comment markers (start of line only)")
247 	public void inlineComment() {}
248 
249 	@CliCommand(value={"/*"}, help="Start of block comment")
250 	public void blockCommentBegin() {
251 		Assert.isTrue(!inBlockComment, "Cannot open a new block comment when one already active");
252 		inBlockComment = true;
253 	}
254 
255 	@CliCommand(value={"*/"}, help="End of block comment")
256 	public void blockCommentFinish() {
257 		Assert.isTrue(inBlockComment, "Cannot close a block comment when it has not been opened");
258 		inBlockComment = false;
259 	}
260 
261 	@CliCommand(value={"system properties"}, help="Shows the shell's properties")
262 	public String props() {
263 		Properties properties = System.getProperties();
264 		SortedSet<String> data = new TreeSet<String>();
265 		for (Object property : properties.keySet()) {
266 			Object value = properties.get(property);
267 			data.add(property + " = " + value);
268 		}
269 		
270 		StringBuilder sb = new StringBuilder();
271 		for (String line : data) {
272 			sb.append(line).append(System.getProperty("line.separator"));
273 		}
274 		return sb.toString();
275 	}
276 
277 	@CliCommand(value={"date"}, help="Displays the local date and time")
278 	public String date() {
279 		return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date());
280 	}
281 
282 	@CliCommand(value={"version"}, help="Displays shell version")
283 	public String version(@CliOption(key="", help="Special version flags") String extra) {
284     	StringBuilder sb = new StringBuilder();
285 		
286     	if ("jaime".equals(extra)) {
287     		sb.append("               /\\ /l").append(System.getProperty("line.separator"));
288     		sb.append("               ((.Y(!").append(System.getProperty("line.separator"));
289     		sb.append("                \\ |/").append(System.getProperty("line.separator"));
290     		sb.append("                /  6~6,").append(System.getProperty("line.separator"));
291     		sb.append("                \\ _    +-.").append(System.getProperty("line.separator"));
292     		sb.append("                 \\`-=--^-' \\").append(System.getProperty("line.separator"));
293     		sb.append("                  \\   \\     |\\--------------------------+").append(System.getProperty("line.separator"));
294     		sb.append("                 _/    \\    |  Thanks for loading Roo!  |").append(System.getProperty("line.separator"));
295     		sb.append("                (  .    Y   +---------------------------+").append(System.getProperty("line.separator"));
296     		sb.append("               /\"\\ `---^--v---.").append(System.getProperty("line.separator"));
297     		sb.append("              / _ `---\"T~~\\/~\\/").append(System.getProperty("line.separator"));
298     		sb.append("             / \" ~\\.      !").append(System.getProperty("line.separator"));
299     		sb.append("       _    Y      Y.~~~ /'").append(System.getProperty("line.separator"));
300     		sb.append("      Y^|   |      | Roo 7").append(System.getProperty("line.separator"));
301     		sb.append("      | l   |     / .   /'").append(System.getProperty("line.separator"));
302     		sb.append("      | `L  | Y .^/   ~T").append(System.getProperty("line.separator"));
303     		sb.append("      |  l  ! | |/  | |               ____  ____  ____").append(System.getProperty("line.separator"));
304     		sb.append("      | .`\\/' | Y   | !              / __ \\/ __ \\/ __ \\").append(System.getProperty("line.separator"));
305     		sb.append("      l  \"~   j l   j L______       / /_/ / / / / / / /").append(System.getProperty("line.separator"));
306     		sb.append("       \\,____{ __\"\" ~ __ ,\\_,\\_    / _, _/ /_/ / /_/ /").append(System.getProperty("line.separator"));
307     		sb.append("    ~~~~~~~~~~~~~~~~~~~~~~~~~~~   /_/ |_|\\____/\\____/").append(" ").append(versionInfo()).append(System.getProperty("line.separator"));
308     		return sb.toString();
309     	}
310     	
311     	sb.append("    ____  ____  ____  ").append(System.getProperty("line.separator")); 
312 		sb.append("   / __ \\/ __ \\/ __ \\ ").append(System.getProperty("line.separator"));
313 		sb.append("  / /_/ / / / / / / / ").append(System.getProperty("line.separator"));
314 		sb.append(" / _, _/ /_/ / /_/ /  ").append(System.getProperty("line.separator"));
315 		sb.append("/_/ |_|\\____/\\____/   ").append(" ").append(versionInfo()).append(System.getProperty("line.separator"));
316 		sb.append(System.getProperty("line.separator"));
317 		
318 		return sb.toString();
319 	}
320 	
321 	public static String versionInfo() {
322 		// Try to determine the bundle version
323 		String bundleVersion = null;
324 		String gitCommitHash = null;
325 		try {
326 			String classContainer = AbstractShell.class.getProtectionDomain().getCodeSource().getLocation().toString();
327 			if (classContainer.endsWith(".jar")) {
328 				// Attempt to obtain the "Bundle-Version" version from the manifest
329 				URL manifestUrl = new URL("jar:" + classContainer + "!/META-INF/MANIFEST.MF");
330 				Manifest manifest = new Manifest(manifestUrl.openStream());
331 				bundleVersion = manifest.getMainAttributes().getValue("Bundle-Version");
332 				gitCommitHash = manifest.getMainAttributes().getValue("Git-Commit-Hash");
333 			}
334 		} catch (Exception ignoreAndMoveOn) {}
335 		
336 		StringBuilder sb = new StringBuilder();
337 		
338 		if (bundleVersion != null) {
339 			sb.append(bundleVersion);
340 		}
341 		
342 		if (gitCommitHash != null) {
343 			if (sb.length() > 0) {
344 				sb.append(" "); // to separate from version
345 			}
346 			sb.append("[rev ");
347 			sb.append(gitCommitHash.substring(0,7));
348 			sb.append("]");
349 		}
350 		
351 		if (sb.length() == 0) {
352 			sb.append("UNKNOWN VERSION");
353 		}
354 		
355 		return sb.toString();
356 	}
357 
358 	public String getShellPrompt() {
359 		return shellPrompt;
360 	}
361 	
362 	/**
363 	 * Obtains the home directory for the current shell instance.
364 	 *
365 	 * <p>
366 	 * Note: calls the {@link #getHomeAsString()} method to allow subclasses to provide the home directory location as 
367 	 * string using different environment-specific strategies. 
368  	 *
369 	 * <p>
370 	 * If the path indicated by {@link #getHomeAsString()} exists and refers to a directory, that directory
371 	 * is returned.
372 	 * 
373 	 * <p>
374 	 * If the path indicated by {@link #getHomeAsString()} exists and refers to a file, an exception is thrown.
375 	 * 
376 	 * <p>
377 	 * If the path indicated by {@link #getHomeAsString()} does not exist, it will be created as a directory.
378 	 * If this fails, an exception will be thrown.
379 	 * 
380 	 * @return the home directory for the current shell instance (which is guaranteed to exist and be a directory)
381 	 */
382 	public File getHome() {
383 		String rooHome = getHomeAsString();
384 		File f = new File(rooHome);
385 		Assert.isTrue(!f.exists() || (f.exists() && f.isDirectory()), "Path '" + f.getAbsolutePath() + "' must be a directory, or it must not exist");
386 		if (!f.exists()) {
387 			f.mkdirs();
388 		}
389 		Assert.isTrue(f.exists() && f.isDirectory(), "Path '" + f.getAbsolutePath() + "' is not a directory; please specify roo.home system property correctly");
390 		return f;
391 	}
392 
393 	protected abstract String getHomeAsString();
394 
395 	protected abstract ComponentContext getContext();
396 	
397 	protected final Parser getParser() {
398 		return parser;
399 	}
400 }