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