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 }