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