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 }