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