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