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