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