View Javadoc

1   /*
2   Karma core - Core of the Karma application
3   Copyright (C) 2004  Toolforge <www.toolforge.nl>
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library 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 GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, write to the Free Software
17  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  */
19  package nl.toolforge.karma.core.cmd;
20  
21  import nl.toolforge.core.util.listener.ChangeListener;
22  import nl.toolforge.core.util.listener.ListenerManager;
23  import nl.toolforge.core.util.listener.ListenerManagerException;
24  import nl.toolforge.karma.core.KarmaRuntimeException;
25  import nl.toolforge.karma.core.boot.WorkingContext;
26  import nl.toolforge.karma.core.boot.WorkingContextConfiguration;
27  import nl.toolforge.karma.core.cmd.event.CommandFailedEvent;
28  import nl.toolforge.karma.core.cmd.event.CommandFinishedEvent;
29  import nl.toolforge.karma.core.cmd.event.CommandResponseListener;
30  import nl.toolforge.karma.core.cmd.event.CommandStartedEvent;
31  import nl.toolforge.karma.core.cmd.event.ErrorEvent;
32  import nl.toolforge.karma.core.cmd.event.MessageEvent;
33  import nl.toolforge.karma.core.cmd.event.SimpleMessage;
34  import nl.toolforge.karma.core.location.LocationException;
35  import nl.toolforge.karma.core.manifest.Manifest;
36  import nl.toolforge.karma.core.manifest.ManifestException;
37  import nl.toolforge.karma.core.manifest.ManifestFactory;
38  import nl.toolforge.karma.core.manifest.ManifestLoader;
39  import nl.toolforge.karma.core.manifest.ManifestStructure;
40  import nl.toolforge.karma.core.module.Module;
41  import org.apache.commons.logging.Log;
42  import org.apache.commons.logging.LogFactory;
43  
44  import java.io.File;
45  import java.util.ArrayList;
46  import java.util.Collection;
47  import java.util.HashMap;
48  import java.util.Iterator;
49  import java.util.Map;
50  
51  /***
52   * <p>The command context is the class that provides a runtime for commands to run in. The command context maintains
53   * access to the current manifest and all commands that are valid. A <code>CommandContext</code> must be initialized
54   * through its {@link #init} method so it can initialize all resources it requires to properly run commands. The
55   * <code>init</code> method can only be run once.
56   *
57   * @author D.A. Smedes
58   * @version $Id: CommandContext.java,v 1.83 2004/11/16 22:31:57 asmedes Exp $
59   */
60  public final class CommandContext implements ChangeListener {
61  
62    private static final Log logger = LogFactory.getLog(CommandContext.class);
63  
64    private Manifest currentManifest;
65    private Map modificationMap = new HashMap();
66    private boolean managed = false;
67  
68    private static ListenerManager manager;
69  
70    private CommandResponseHandler handler = null;
71    private WorkingContext workingContext = null;
72    private CommandResponse commandResponse = null;
73  
74    /***
75     * Constructs a <code>CommandContext</code>, in which commands are run.
76     */
77    public CommandContext(WorkingContext workingContext) {
78      this.workingContext = workingContext;
79    }
80  
81    public WorkingContext getWorkingContext() {
82      return workingContext;
83    }
84  
85    /***
86     * Initializes the context to run commands. This method should only be called once on a <code>CommandContext</code>.
87     *
88     * @param handler        The {@link CommandResponseHandler} object that will be passed to all commands run through this
89     *                       context.
90     * @param updateStores   If this paramter is true, the CommandContext will update the local manifest and location store
91     *                       with the latest manifests and locations.
92     */
93    public synchronized void init(CommandResponseHandler handler, boolean updateStores) throws CommandException {
94  
95      if (handler == null) {
96        throw new IllegalArgumentException("CommandResponseHandler may not be null.");
97      }
98  
99      // Initialize a command response object.
100     //
101     commandResponse = new CommandResponse();
102     commandResponse.addCommandResponseListener(handler);
103     setHandler(handler);
104 
105     // Use a command to initialize this further.
106     //
107     Command command = new KarmaInitializationCommand(updateStores);
108 
109     command.setContext(this);
110     command.registerCommandResponseListener(getHandler());
111 
112     CommandStartedEvent startEvent = new CommandStartedEvent(command);
113     //commandResponse.addEvent(startEvent);
114 
115     try {
116       command.execute();
117     } catch (CommandException c) {
118       commandResponse.addEvent(new ErrorEvent(command, c.getErrorCode(), c.getMessageArguments()));
119       //commandResponse.addEvent(new CommandFailedEvent(command, c));
120       throw c;
121     }
122 
123     command.deregisterCommandResponseListener(handler);
124     command.cleanUp();
125   }
126 
127 
128   private synchronized void setFileModificationTimes() {
129 
130     Manifest manifest = currentManifest;
131 
132     WorkingContextConfiguration config = workingContext.getConfiguration();
133 
134     Long lastMod = new Long(new File(config.getManifestStore().getModule().getBaseDir(), manifest.getName() + ".xml").lastModified());
135     modificationMap.put(manifest, lastMod);
136 
137     try {
138       Collection includes = manifest.getIncludes();
139       for (Iterator i = includes.iterator(); i.hasNext();) {
140 
141         manifest = (Manifest) i.next();
142 
143         lastMod = new Long(new File(config.getManifestStore().getModule().getBaseDir(), manifest.getName() + ".xml").lastModified());
144         modificationMap.put(manifest, lastMod);
145       }
146     } catch (Exception e) {
147       logger.error(e);
148       modificationMap.clear();
149     }
150   }
151 
152   /***
153    * Implementation of the {@link ChangeListener} interface. This method reloads the
154    * current manifest to allow changes to be reflected without having to restart Karma.
155    */
156   public synchronized void process() {
157 
158     boolean reload = false;
159 
160     try {
161 
162       Collection manifests = new ArrayList();
163       manifests.add(currentManifest);
164       manifests.addAll(currentManifest.getIncludes());
165 
166       for (Iterator i = manifests.iterator(); i.hasNext();) {
167 
168         Manifest m = (Manifest) i.next();
169         long lastMod = ((Long) modificationMap.get(m)).longValue();
170 
171         WorkingContextConfiguration config = workingContext.getConfiguration();
172 
173         // todo omslachtig. direct via een getmanifeststore()-achtige.
174         File f = new File(config.getManifestStore().getModule().getBaseDir(), m.getName() + ".xml");
175         if (!f.exists()) {
176           currentManifest = null;
177           throw new ManifestException(ManifestException.MANIFEST_FILE_NOT_FOUND, new Object[] {m.getName()});
178         }
179 
180         if (f.lastModified() > lastMod) {
181           reload = true;
182           break;
183         }
184       }
185 
186       if (reload) {
187 
188         // One of the manifests in the tree has been changed on disk, reload the full structure.
189         //
190         ManifestStructure reloadedStructure =
191             getWorkingContext().getManifestLoader().load(currentManifest.getName());
192         currentManifest = new ManifestFactory().create(workingContext, reloadedStructure);
193 
194         setFileModificationTimes();
195 
196         String message = "\nManifest " + getCurrentManifest().getName() + " has changed on disk. Reloaded automatically.\n";
197         logger.info(message);
198 
199         commandResponse.addEvent(new MessageEvent(new SimpleMessage(message)));
200 
201         return;
202       }
203 
204     } catch (ManifestException m) {
205 
206       // Catches the ManifestException in case the manifest file has disappeared as well.
207       //
208       managed = false;
209       manager.suspendListener(this);
210 
211       logger.error(m);
212 
213       // todo in karma-core-1.1 this should be improved. Right now, the probability of this process failing is remote.
214       //
215       throw new KarmaRuntimeException(m.getErrorCode(), m.getMessageArguments());
216     } catch (Exception e) {
217 
218       managed = false;
219       manager.suspendListener(this);
220 
221       logger.error("Error while processing manifests during automatic reload; " + e.getMessage(), e);
222     }
223   }
224 
225   /***
226    * Gets the currently active manifest.
227    *
228    * @return The currently active manifest, or <code>null</code> when no manifest is current.
229    */
230   public Manifest getCurrentManifest() {
231     return currentManifest;
232   }
233 
234   /***
235    * Changes the current manifest for this context. This method loads the manifest with the <code>manifestName</code>
236    * name.
237    *
238    * @param manifestName
239    * @throws ManifestException When the manifest could not be changed. See {@link ManifestException#MANIFEST_LOAD_ERROR}.
240    */
241   public void changeCurrentManifest(String manifestName) throws ManifestException, LocationException {
242 
243     ManifestFactory manifestFactory = new ManifestFactory();
244     ManifestLoader loader = new ManifestLoader(workingContext);
245     Manifest newManifest = manifestFactory.create(workingContext, loader.load(manifestName));
246 
247     // If we are here, loading the new manifest was succesfull.
248     //
249     currentManifest = newManifest;
250 
251     register();
252   }
253 
254   /***
255    * Changes the current manifest for this context. This method assumes a loaded manifest.
256    *
257    * @param newManifest
258    */
259   public void changeCurrentManifest(Manifest newManifest) {
260     currentManifest = newManifest;
261 
262     if (currentManifest != null) {
263       register();
264     }
265   }
266 
267   /***
268    * Registers this <code>CommandContext</code> for automatic manifest file update changes.
269    */
270   synchronized void register() {
271 
272     setFileModificationTimes();
273 
274     if (!managed) {
275 
276       manager = ListenerManager.getInstance();
277       try {
278         manager.register(this);
279       } catch (ListenerManagerException e) {
280         logger.error(e);
281         throw new KarmaRuntimeException(e.getMessage());
282       }
283 
284       manager.start();
285 
286       managed = true;
287     }
288   }
289 
290   /***
291    * Gets all manifests.
292    *
293    * @return See <code>ManifestLoader.getAllManifests()</code>.
294    */
295   public Collection getAllManifests() {
296     return workingContext.getManifestCollector().getAllManifests();
297   }
298 
299   /***
300    * <p>Executes a command. Interface applications should use this method to actually execute a command. When a
301    * <code>KarmaException</code> is thrown an interface applications should <b>*** NOT ***</b> quit program execution as
302    * a result of this exception. It should be handled nicely.
303    *
304    * @param commandLine The command to execute. A full command line is passed as a parameter.
305    * @throws CommandException A whole lot. Interface applications should <b>*** NOT ***</b> quit program execution as a
306    *   result of this exception. It should be handled nicely.
307    */
308   public void execute(String commandLine) throws CommandException {
309 
310     Command command = null;
311     try {
312       command = CommandFactory.getInstance().getCommand(commandLine);
313     } catch (CommandException c) {
314       logger.error(c.getMessage());
315       commandResponse.addEvent(new ErrorEvent(c.getErrorCode(), c.getMessageArguments()));
316       throw c;
317     } catch (CommandLoadException e) {
318       logger.error(e.getMessage());
319       throw new CommandException(e, e.getErrorCode(),  e.getMessageArguments());
320     }
321     execute(command);
322   }
323 
324   /***
325    * Exceutes <code>command</code>.
326    *
327    * @param command The command to execute.
328    * @throws CommandException
329    */
330   public void execute(Command command) throws CommandException {
331 
332     if (command == null) {
333       throw new IllegalArgumentException("Invalid command; command cannot be null.");
334     }
335 
336     // Store a reference to this context in the command
337     //
338     command.setContext(this);
339     command.registerCommandResponseListener(getHandler());
340     // Register the response handler with this context, so commands have a reference to it.
341     //
342     //todo what happens when an exception occurs in the execute wrt deregister?
343 
344     CommandStartedEvent startEvent = new CommandStartedEvent(command);
345     commandResponse.addEvent(startEvent);
346 
347     try {
348       command.execute();
349     } catch (CommandException c) {
350       logger.error(c.getMessage());
351       commandResponse.addEvent(new ErrorEvent(command, c.getErrorCode(), c.getMessageArguments()));
352       commandResponse.addEvent(new CommandFailedEvent(command, c));
353       throw c;
354     }
355     commandResponse.addEvent(new CommandFinishedEvent(command, startEvent.getTime()));
356 
357     command.deregisterCommandResponseListener(getHandler());
358     command.cleanUp();
359   }
360 
361   private void setHandler(CommandResponseHandler handler) {
362     this.handler = handler;
363   }
364 
365   private CommandResponseListener getHandler() {
366     return handler;
367   }
368 
369   /***
370    * Checks if a manifest is active for this context.
371    *
372    * @return <code>true</code> if a manifest is active for the context, or <code>false</code> if no manifest is active.
373    */
374   public boolean isManifestLoaded() {
375     return currentManifest != null;
376   }
377 
378   /***
379    * <p>Some module-types (e.g. source modules) have a physical location on disk where the module can be located. This
380    * method returns a valid reference to that location. When the module-root is located at
381    * <code>/home/jensen/dev/modules/CORE-conversion</code>, <code>getLocalPath()</code> will return a <code>File</code>
382    * handle to that directory.
383    *
384    * @param module The module for which the local path should be retrieved.
385    *
386    * @return A <code>File</code> handle to the module directory on a local disk.
387    *
388    * todo consider moving it to Module.
389    */
390   public File getLocalPath(Module module) {
391 
392     File localPath = new File(getBase(), module.getName());
393     logger.debug("getLocalPath() = " + localPath.getPath());
394 
395     return localPath;
396   }
397 
398   /***
399    * Helper to get the module base for the current manifest.
400    */
401   private File getBase() {
402     return new File(workingContext.getProjectBaseDirectory(), getCurrentManifest().getName());
403   }
404 
405   /***
406    * Sets the workingContext for this command context.
407    *
408    * @param workingContext
409    */
410   public void setWorkingContext(WorkingContext workingContext) {
411     this.workingContext = workingContext;
412   }
413 }