001    /*
002     * Copyright (C) 2003-2009 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    package org.crsh.plugin;
020    
021    import org.crsh.vfs.FS;
022    import org.crsh.vfs.File;
023    import org.crsh.vfs.Path;
024    import org.crsh.vfs.Resource;
025    import org.slf4j.Logger;
026    import org.slf4j.LoggerFactory;
027    
028    import java.io.IOException;
029    import java.io.InputStream;
030    import java.util.*;
031    import java.util.concurrent.ScheduledExecutorService;
032    import java.util.concurrent.ScheduledThreadPoolExecutor;
033    import java.util.concurrent.TimeUnit;
034    import java.util.regex.Matcher;
035    import java.util.regex.Pattern;
036    
037    /**
038     * The plugin context.
039     *
040     * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
041     * @version $Revision$
042     */
043    public final class PluginContext {
044    
045      /** . */
046      private static final Pattern p = Pattern.compile("(.+)\\.groovy");
047    
048      /** . */
049      private static final Logger log = LoggerFactory.getLogger(PluginContext.class);
050    
051      /** . */
052      final PluginManager manager;
053    
054      /** . */
055      private final ClassLoader loader;
056    
057      /** . */
058      private final String version;
059    
060      /** . */
061      private ScheduledExecutorService executor;
062    
063      /** . */
064      private volatile List<File> dirs;
065    
066      /** . */
067      private final Map<PropertyDescriptor<?>, Property<?>> properties;
068    
069      /** . */
070      private final FS cmdFS;
071    
072      /** . */
073      private final Map<String, ?> attributes;
074    
075      /** . */
076      private final FS confFS;
077    
078      /** . */
079      private boolean started;
080    
081      /**
082       * Create a new plugin context.
083       *
084       * @param discovery the plugin discovery
085       * @param cmdFS the command file system
086       * @param attributes the attributes
087       * @param confFS the conf file system
088       * @param loader the loader
089       * @throws NullPointerException if any parameter argument is null
090       */
091      public PluginContext(
092        PluginDiscovery discovery,
093        Map<String, ?> attributes,
094        FS cmdFS,
095        FS confFS,
096        ClassLoader loader) throws NullPointerException {
097        if (discovery == null) {
098          throw new NullPointerException("No null plugin disocovery accepted");
099        }
100        if (confFS == null) {
101          throw new NullPointerException("No null configuration file system accepted");
102        }
103        if (cmdFS == null) {
104          throw new NullPointerException("No null command file system accepted");
105        }
106        if (loader == null) {
107          throw new NullPointerException();
108        }
109        if (attributes == null) {
110          throw new NullPointerException();
111        }
112    
113        //
114        String version = null;
115        try {
116          Properties props = new Properties();
117          InputStream in = getClass().getClassLoader().getResourceAsStream("META-INF/maven/org.crsh/crsh.shell.core/pom.properties");
118          if (in != null) {
119            props.load(in);
120            version = props.getProperty("version");
121          }
122        } catch (Exception e) {
123          log.error("Could not load maven properties", e);
124        }
125    
126        //
127        if (version == null) {
128          log.warn("No version found will use unknown value instead");
129          version = "unknown";
130        }
131    
132        //
133        this.loader = loader;
134        this.attributes = attributes;
135        this.version = version;
136        this.dirs = Collections.emptyList();
137        this.cmdFS = cmdFS;
138        this.properties = new HashMap<PropertyDescriptor<?>, Property<?>>();
139        this.started = false;
140        this.manager = new PluginManager(this, discovery);
141        this.confFS = confFS;
142      }
143    
144      public Iterable<CRaSHPlugin<?>> getPlugins() {
145        return manager.getPlugins();
146      }
147    
148      public String getVersion() {
149        return version;
150      }
151    
152      public Map<String, ?> getAttributes() {
153        return attributes;
154      }
155    
156      /**
157       * Returns a context property or null if it cannot be found.
158       *
159       * @param desc the property descriptor
160       * @param <T> the property parameter type
161       * @return the property value
162       * @throws NullPointerException if the descriptor argument is null
163       */
164      public <T> T getProperty(PropertyDescriptor<T> desc) throws NullPointerException {
165        if (desc == null) {
166          throw new NullPointerException();
167        }
168        Property<T> property = (Property<T>)properties.get(desc);
169        return property != null ? property.getValue() : desc.defaultValue;
170      }
171    
172      /**
173       * Returns a context property or null if it cannot be found.
174       *
175       * @param propertyName the name of the property
176       * @param type the property type
177       * @param <T> the property parameter type
178       * @return the property value
179       * @throws NullPointerException if the descriptor argument is null
180       */
181      public <T> T getProperty(String propertyName, Class<T> type) throws NullPointerException {
182        if (propertyName == null) {
183          throw new NullPointerException("No null property name accepted");
184        }
185        if (type == null) {
186          throw new NullPointerException("No null property type accepted");
187        }
188        for (PropertyDescriptor<?> pd : properties.keySet())
189        {
190          if (pd.name.equals(propertyName) && type.isAssignableFrom(pd.type))
191          {
192            return type.cast(getProperty(pd));
193          }
194        }
195        return null;
196      }
197    
198      /**
199       * Set a context property to a new value. If the provided value is null, then the property is removed.
200       *
201       * @param desc the property descriptor
202       * @param value the property value
203       * @param <T> the property parameter type
204       * @throws NullPointerException if the descriptor argument is null
205       */
206      public <T> void setProperty(PropertyDescriptor<T> desc, T value) throws NullPointerException {
207        if (desc == null) {
208          throw new NullPointerException();
209        }
210        if (value == null) {
211          log.debug("Removing property " + desc.name);
212          properties.remove(desc);
213        } else {
214          Property<T> property = new Property<T>(desc, value);
215          log.debug("Setting property " + desc.name + " to value " + property.getValue());
216          properties.put(desc, property);
217        }
218      }
219    
220      /**
221       * Set a context property to a new value. If the provided value is null, then the property is removed.
222       *
223       * @param desc the property descriptor
224       * @param value the property value
225       * @param <T> the property parameter type
226       * @throws NullPointerException if the descriptor argument is null
227       * @throws IllegalArgumentException if the string value cannot be converted to the property type
228       */
229      public <T> void setProperty(PropertyDescriptor<T> desc, String value) throws NullPointerException, IllegalArgumentException {
230        if (desc == null) {
231          throw new NullPointerException();
232        }
233        if (value == null) {
234          log.debug("Removing property " + desc.name);
235          properties.remove(desc);
236        } else {
237          Property<T> property = desc.toProperty(value);
238          log.debug("Setting property " + desc.name + " to value " + property.getValue());
239          properties.put(desc, property);
240        }
241      }
242    
243      /**
244       * Load a resource from the context.
245       *
246       * @param resourceId the resource id
247       * @param resourceKind the resource kind
248       * @return the resource or null if it cannot be found
249       */
250      public Resource loadResource(String resourceId, ResourceKind resourceKind) {
251        Resource res = null;
252        try {
253    
254          //
255          switch (resourceKind) {
256            case LIFECYCLE:
257              if ("login".equals(resourceId) || "logout".equals(resourceId)) {
258                StringBuilder sb = new StringBuilder();
259                long timestamp = Long.MIN_VALUE;
260                for (File path : dirs) {
261                  File f = path.child(resourceId + ".groovy", false);
262                  if (f != null) {
263                    Resource sub = f.getResource();
264                    if (sub != null) {
265                      sb.append(sub.getContent()).append('\n');
266                      timestamp = Math.max(timestamp, sub.getTimestamp());
267                    }
268                  }
269                }
270                return new Resource(sb.toString(), timestamp);
271              }
272              break;
273            case COMMAND:
274              // Find the resource first, we find for the first found
275              for (File path : dirs) {
276                File f = path.child(resourceId + ".groovy", false);
277                if (f != null) {
278                  res = f.getResource();
279                }
280              }
281              break;
282            case CONFIG:
283              String path = "/" + resourceId;
284              File file = confFS.get(Path.get(path));
285              if (file != null) {
286                res = file.getResource();
287              }
288          }
289        } catch (IOException e) {
290          log.warn("Could not obtain resource " + resourceId, e);
291        }
292        return res;
293      }
294    
295      /**
296       * List the resources id for a specific resource kind.
297       *
298       * @param kind the resource kind
299       * @return the resource ids
300       */
301      public List<String> listResourceId(ResourceKind kind) {
302        switch (kind) {
303          case COMMAND:
304            SortedSet<String> all = new TreeSet<String>();
305            try {
306              for (File path : dirs) {
307                for (File file : path.children()) {
308                  String name = file.getName();
309                  Matcher matcher = p.matcher(name);
310                  if (matcher.matches()) {
311                    all.add(matcher.group(1));
312                  }
313                }
314              }
315            }
316            catch (IOException e) {
317              e.printStackTrace();
318            }
319            all.remove("login");
320            all.remove("logout");
321            return new ArrayList<String>(all);
322          default:
323            return Collections.emptyList();
324        }
325      }
326    
327      /**
328       * Returns the classloader associated with this context.
329       *
330       * @return the class loader
331       */
332      public ClassLoader getLoader() {
333        return loader;
334      }
335    
336      /**
337       * Returns the plugins associated with this context.
338       *
339       * @param pluginType the plugin type
340       * @param <T> the plugin generic type
341       * @return the plugins
342       */
343      public <T> Iterable<T> getPlugins(Class<T> pluginType) {
344        return manager.getPlugins(pluginType);
345      }
346    
347      /**
348       * Refresh the fs system view. This is normally triggered by the periodic job but it can be manually
349       * invoked to trigger explicit refreshes.
350       */
351      public void refresh() {
352        try {
353          File commands = cmdFS.get(Path.get("/"));
354          List<File> newDirs = new ArrayList<File>();
355          newDirs.add(commands);
356          for (File path : commands.children()) {
357            if (path.isDir()) {
358              newDirs.add(path);
359            }
360          }
361          dirs = newDirs;
362        }
363        catch (IOException e) {
364          e.printStackTrace();
365        }
366      }
367    
368      synchronized void start() {
369        if (!started) {
370    
371          // Start refresh
372          Integer refreshRate = getProperty(PropertyDescriptor.VFS_REFRESH_PERIOD);
373          TimeUnit timeUnit = getProperty(PropertyDescriptor.VFS_REFRESH_UNIT);
374          if (refreshRate != null && refreshRate > 0) {
375            TimeUnit tu = timeUnit != null ? timeUnit : TimeUnit.SECONDS;
376            executor =  new ScheduledThreadPoolExecutor(1);
377            executor.scheduleWithFixedDelay(new Runnable() {
378              public void run() {
379                refresh();
380              }
381            }, 0, refreshRate, tu);
382          }
383    
384          // Init plugins
385          manager.getPlugins(Object.class);
386    
387          //
388          started = true;
389        } else {
390          log.warn("Attempt to double start");
391        }
392      }
393    
394      synchronized void stop() {
395    
396        //
397        if (started) {
398    
399          // Shutdown manager
400          manager.shutdown();
401    
402          //
403          if (executor != null) {
404            ScheduledExecutorService tmp = executor;
405            executor = null;
406            tmp.shutdown();
407          }
408        } else {
409          log.warn("Attempt to stop when stopped");
410        }
411      }
412    }