001: /**
002: * @author garethc
003: * Date: Jan 7, 2003
004: */package vqwiki;
005:
006: import org.apache.log4j.Logger;
007: import org.w3c.dom.Document;
008: import org.w3c.dom.Element;
009: import org.w3c.dom.NodeList;
010: import org.xml.sax.SAXException;
011: import vqwiki.lex.LexExtender;
012: import vqwiki.utils.Utilities;
013:
014: import javax.xml.parsers.ParserConfigurationException;
015: import java.io.File;
016: import java.io.IOException;
017: import java.io.InputStream;
018: import java.util.ArrayList;
019: import java.util.Enumeration;
020: import java.util.List;
021: import java.util.zip.ZipEntry;
022: import java.util.zip.ZipFile;
023: import java.net.URL;
024: import java.net.URLDecoder;
025: import java.security.AccessControlException;
026: import java.io.UnsupportedEncodingException;
027:
028: /**
029: * Manager for loading plugins from zip files. The plugins directory is a directory under
030: * the Wiki home directory (where the topic contents live) named "plugins". A plugin occupies
031: * one zip file and needs to include a file named the same as the zip file but with
032: * and "xml" file extension in the WEB-INF/classes directory.
033: * So if the plugin file is called test.zip, then the included file
034: * should be called test.xml.
035: * <p/>
036: * The plugin zip is extracted directly to the wiki installation directory, so any class files
037: * should be in the path WEB-INF/classes and any jars should be in WEB-INF/lib.
038: * <p/>
039: * The plugin xml file can contain various entries that will cause mappings to be inserted in the
040: * different mapping repositories.
041: */
042: public class PluginManager {
043:
044: private static PluginManager instance;
045: public static final String PLUGINS_DIR = "plugins";
046: private static final Logger logger = Logger
047: .getLogger(PluginManager.class);
048: private static final String TAG_PLUGIN = "plugin";
049: private static final String TAG_EXTERNAL_LEX = "external-lex";
050: private static final String TAG_TOPIC_LISTENER = "topic-listener";
051: private static final String TAG_ACTION = "action";
052: private static final String ATTR_TAG = "tag";
053: private static final String ATTR_CLASS = "class";
054: private static final String ATTR_NAME = "name";
055: private static final String ATTR_REDIRECT = "redirect";
056: private static final String ATTR_PSEUDOTOPIC = "pseudotopic";
057:
058: /** Listeners for the WikiBase to use */
059: private List topicListeners = new ArrayList();
060: /** Number of millis to wait after a plugin has been unzipped so that the class loader has a chance to load the classes */
061: private static final long PAUSE = 3000;
062:
063: /**
064: * Get an instance of the manager
065: *
066: * @return singleton instance
067: */
068: public synchronized static PluginManager getInstance() {
069: if (instance == null) {
070: instance = new PluginManager();
071: }
072: return instance;
073: }
074:
075: /**
076: * Hide default constructor
077: */
078: private PluginManager() {
079: }
080:
081: /**
082: * Look for plugins and install them as required
083: */
084: public void installAll() {
085: File pluginDir = new File(Environment.dir(), PLUGINS_DIR);
086: // create if necessary
087: pluginDir.mkdir();
088: logger.debug("Looking for plugins in " + pluginDir);
089: File[] files = pluginDir.listFiles();
090: if (files == null) {
091: return;
092: }
093: if (files.length == 0) {
094: return;
095: }
096: for (int i = 0; i < files.length; i++) {
097: File file = files[i];
098: String name = file.getName();
099: if (name.endsWith(".zip")) {
100: name = name.substring(0, name.length() - 4);
101: logger.debug("Plugin found: " + name);
102: install(name, file);
103: }
104: }
105: }
106:
107: /**
108: * Return the name the plugin XML file will have
109: *
110: * @param name plugin name
111: * @return name
112: */
113: private String getPluginPropertiesFilename(String name) {
114: StringBuffer buffer = new StringBuffer();
115: buffer.append("/");
116: buffer.append(name);
117: buffer.append(".xml");
118: String pluginPropertiesFilename = buffer.toString();
119: return pluginPropertiesFilename;
120: }
121:
122: /**
123: * Install the plugin
124: *
125: * @param name plugin name
126: * @param file zip file containing the plugin
127: */
128: private void install(String name, File file) {
129: try {
130: String installedVersion = getInstalledPluginVersion(name);
131: String newVersion = readPluginAttributeFromPlugin(name,
132: file, "version");
133: logger
134: .debug("installed plugin version: "
135: + installedVersion + ", new version: "
136: + newVersion);
137: if (installedVersion == null
138: || newVersion.compareTo(installedVersion) > 0) {
139: logger.info("Installing plugin: " + name);
140: String realPath = Environment.getInstance()
141: .getRealPath();
142: if (realPath == null) {
143: logger
144: .error("installation directory is null, plugin manager must be running without any requests having been made!");
145: return;
146: }
147: Utilities.unzip(file, new File(realPath));
148: // give the app server a chance to load the classes
149: Thread.sleep(PAUSE);
150: } else {
151: logger
152: .info("plugin of same version already installed: "
153: + name);
154: }
155: } catch (Exception e) {
156: logger.error(
157: "unable to determine current version of plugin: "
158: + name, e);
159: }
160: Document doc;
161: String pluginPropertiesFilename = null;
162: String res = null;
163: try {
164: pluginPropertiesFilename = getPluginPropertiesFilename(name);
165: URL resource = PluginManager.class
166: .getResource(pluginPropertiesFilename);
167: if (resource == null) {
168: logger.error("plugin properties file not found: "
169: + pluginPropertiesFilename);
170: return;
171: } else {
172: String defaultencoding = null;
173: try {
174: defaultencoding = System
175: .getProperty("file.encoding");
176: } catch (AccessControlException ae) {
177: logger
178: .warn("This application server doesn't allow to access "
179: + "file.encoding with System.getProperty. Set default "
180: + "encoding for filename-URL to UTF-8");
181: defaultencoding = "UTF-8";
182: }
183: try {
184: res = URLDecoder.decode(resource.getFile(),
185: defaultencoding);
186: } catch (UnsupportedEncodingException e) {
187: logger
188: .error(
189: "The platform's default encoding is not supported in the JDK.",
190: e);
191: try {
192: res = URLDecoder.decode(resource.getFile(),
193: "UTF-8");
194: } catch (UnsupportedEncodingException e1) {
195: logger
196: .fatal(
197: "Even UTF-8 is not supported by this JDK!",
198: e1);
199: }
200: }
201: }
202: doc = Utilities.parseDocumentFromFile(res);
203: } catch (Exception e) {
204: logger.error("Error parsing plugin properties XML file: "
205: + pluginPropertiesFilename, e);
206: return;
207: }
208: logger.debug("Reading plugin configuration");
209: NodeList rootList = doc.getElementsByTagName(TAG_PLUGIN);
210: if (rootList.getLength() == 0) {
211: return;
212: }
213: Element root = (Element) rootList.item(0);
214: NodeList externalLexEntries = root
215: .getElementsByTagName(TAG_EXTERNAL_LEX);
216: for (int i = 0; i < externalLexEntries.getLength(); i++) {
217: logger.debug("Making plugin external lex entry");
218: Element externalLexElement = (Element) externalLexEntries
219: .item(i);
220: String className = externalLexElement
221: .getAttribute(ATTR_CLASS);
222: String tagName = externalLexElement.getAttribute(ATTR_TAG);
223: try {
224: LexExtender.getInstance().addLexerEntry(tagName,
225: className);
226: } catch (IOException e) {
227: logger.error("error adding lexer entry", e);
228: }
229: }
230: NodeList actionEntries = root.getElementsByTagName(TAG_ACTION);
231: for (int i = 0; i < actionEntries.getLength(); i++) {
232: Element actionElement = (Element) actionEntries.item(i);
233: String actionName = actionElement.getAttribute(ATTR_NAME);
234: String className = actionElement.getAttribute(ATTR_CLASS);
235: try {
236: ActionManager.getInstance().addMapping(actionName,
237: className);
238: } catch (IOException e) {
239: logger.error("error adding action", e);
240: }
241: String pseudotopic = actionElement
242: .getAttribute(ATTR_PSEUDOTOPIC);
243: if (pseudotopic != null && !"".equals(pseudotopic)) {
244: PseudoTopicHandler.getInstance().addMapping(
245: pseudotopic, "Wiki?action=" + actionName);
246: }
247: }
248: NodeList topicListeners = root
249: .getElementsByTagName(TAG_TOPIC_LISTENER);
250: for (int i = 0; i < topicListeners.getLength(); i++) {
251: Element topicListenerElement = (Element) topicListeners
252: .item(i);
253: String className = topicListenerElement
254: .getAttribute(ATTR_CLASS);
255: logger.debug("registering topic listener: " + className);
256: try {
257: Class clazz = Class.forName(className);
258: TopicListener listener = (TopicListener) clazz
259: .newInstance();
260: this .topicListeners.add(listener);
261: } catch (Exception e) {
262: logger
263: .error(
264: "error creating topic listener and registering it",
265: e);
266: }
267: }
268: }
269:
270: /**
271: * Get the version of the currently installed version of the plugin
272: *
273: * @param name plugin name
274: * @return version or null if not installed
275: * @throws IOException
276: * @throws ParserConfigurationException
277: * @throws SAXException
278: */
279: private String getInstalledPluginVersion(String name)
280: throws IOException, ParserConfigurationException,
281: SAXException {
282: InputStream in = getClass().getResourceAsStream(
283: "/" + name + ".xml");
284: if (in == null) {
285: return null;
286: }
287: Document doc = Utilities.parseDocumentFromInputStream(in);
288: return getPluginAttributeFromDocument(doc, "version");
289: }
290:
291: /**
292: * Read an attribute from the top-level plugin element in the plugin's descriptor XML
293: *
294: * @param pluginName name of the plugin
295: * @param pluginZipFile the zip file containing the plugin
296: * @param attributeName attribute name
297: * @return attribute value or null if not found
298: */
299: private String readPluginAttributeFromPlugin(String pluginName,
300: File pluginZipFile, String attributeName) {
301: Enumeration entries;
302: ZipFile zipFile;
303: try {
304: zipFile = new ZipFile(pluginZipFile);
305: entries = zipFile.entries();
306: while (entries.hasMoreElements()) {
307: ZipEntry entry = (ZipEntry) entries.nextElement();
308: if (entry.getName().equals(
309: "WEB-INF/classes/" + pluginName + ".xml")) {
310: logger.debug("found descriptor");
311: InputStream xmlIn = null;
312: try {
313: xmlIn = zipFile.getInputStream(entry);
314: Document doc = Utilities
315: .parseDocumentFromInputStream(xmlIn);
316: return getPluginAttributeFromDocument(doc,
317: attributeName);
318: } catch (Exception e) {
319: logger.error("", e);
320: } finally {
321: if (xmlIn != null) {
322: xmlIn.close();
323: }
324: }
325: }
326: }
327: zipFile.close();
328: } catch (IOException ioe) {
329: logger.error("Unzipping error: " + ioe);
330: }
331: return null;
332: }
333:
334: /**
335: * Return a plugin attribute from a plugin descriptor document
336: *
337: * @param doc document
338: * @param attributeName attribute name
339: * @return value or null if not found
340: */
341: private String getPluginAttributeFromDocument(Document doc,
342: String attributeName) {
343: NodeList pluginElements = doc.getElementsByTagName(TAG_PLUGIN);
344: if (pluginElements.getLength() != 1) {
345: logger
346: .error("there must be one and only one plugin element in descriptor");
347: return null;
348: }
349: Element element = (Element) pluginElements.item(0);
350: String attribute = element.getAttribute(attributeName);
351: logger.debug("found attribute " + attributeName + " = "
352: + attribute);
353: return attribute;
354: }
355:
356: /**
357: * Topic listeners found in plugins
358: *
359: * @return listeners
360: */
361: public List getTopicListeners() {
362: return topicListeners;
363: }
364: }
|