001: /* Copyright 2002 The JA-SIG Collaborative. All rights reserved.
002: * See license distributed with this file and
003: * available online at http://www.uportal.org/license.html
004: */
005:
006: package org.jasig.portal.car;
007:
008: import java.io.File;
009: import java.io.FileFilter;
010: import java.io.IOException;
011: import java.io.InputStream;
012: import java.net.URL;
013: import java.util.Enumeration;
014: import java.util.Hashtable;
015: import java.util.Iterator;
016: import java.util.Map;
017: import java.util.Properties;
018: import java.util.StringTokenizer;
019: import java.util.Vector;
020: import java.util.Map.Entry;
021: import java.util.jar.JarFile;
022: import java.util.zip.ZipEntry;
023:
024: import javax.servlet.Servlet;
025: import javax.servlet.ServletContext;
026:
027: import org.apache.commons.logging.Log;
028: import org.apache.commons.logging.LogFactory;
029: import org.jasig.portal.PortalException;
030: import org.jasig.portal.PortalSessionManager;
031: import org.jasig.portal.properties.PropertiesManager;
032: import org.jasig.portal.utils.SAX2BufferImpl;
033: import org.xml.sax.ContentHandler;
034: import org.xml.sax.SAXException;
035:
036: /**
037: * Provides access to resources stored in channel archive files or CARs for
038: * short.
039: * @author Mark Boyd {@link <a href="mailto:mark.boyd@engineer.com">mark.boyd@engineer.com</a>}
040: * @version $Revision: 36690 $
041: */
042: public class CarResources {
043:
044: // static, class variables
045: private static final Log log = LogFactory
046: .getLog(CarResources.class);
047: private static CarResources instance = null;
048: private static CarClassLoader loader = null;
049:
050: public final static String RCS_ID = "@(#) $Header$";
051:
052: static final String DEPLOYMENT_DESCRIPTOR = "META-INF/comp.xml";
053: private static final String WELL_KNOWN_DIR = "/WEB-INF/cars";
054: private static final String CAR_DIR_PROP_NAME = "org.jasig.portal.car.CarResources.directory";
055:
056: public static final String CAR_WORKER_ID = "carRsrc";
057: public static final String CAR_RESOURCE_PARM = CAR_WORKER_ID;
058: private static final Map.Entry[] ENTRY_ARRAY = new Map.Entry[] {};
059: private static final String[] STRING_ARRAY = new String[] {};
060:
061: // instance variables
062:
063: private Hashtable resourceJars = new Hashtable();
064: private Hashtable carsByJars = new Hashtable();
065: private Hashtable carContents = new Hashtable();
066: private Hashtable carsByPath = new Hashtable();
067:
068: private SAX2BufferImpl services = new SAX2BufferImpl();
069: private Properties workers = new Properties();
070: private boolean carsLoaded = false;
071:
072: private Vector jarsWithDescriptors = new Vector();
073:
074: private String carDirPath = null;
075: private boolean carDirExists = false;
076:
077: /**
078: A fileFilter for obtaining a list of CARs.
079: */
080: private FileFilter carFilter = new FileFilter() {
081: public boolean accept(File path) {
082: return path.getName().endsWith(".car");
083: }
084: };
085:
086: /**
087: A fileFilter for obtaining a list of directories.
088: */
089: private FileFilter dirFilter = new FileFilter() {
090: public boolean accept(File file) {
091: return file.isDirectory();
092: }
093: };
094:
095: static {
096: instance = new CarResources();
097: loader = new CarClassLoader(CarResources.class.getClassLoader());
098: instance.processDescriptors();
099: }
100:
101: /**
102: Instantiate a CarResources object and load information about all CARs
103: and their contained resources.
104: */
105: private CarResources() {
106: try {
107: loadCars();
108: } catch (Exception e) {
109: log.error("An Exception occurred while loading "
110: + "channel archives. Any channels "
111: + "deployed via CARs will not be " + "available.",
112: e);
113: }
114: }
115:
116: /**
117: * Process the descriptors of the channel archives if any.
118: */
119: private void processDescriptors() {
120: if (carsLoaded == true) {
121: for (Enumeration jars = jarsWithDescriptors.elements(); jars
122: .hasMoreElements();) {
123: JarFile jarFile = null;
124: try {
125: jarFile = (JarFile) jars.nextElement();
126: DescriptorHandler handler = new DescriptorHandler(
127: jarFile);
128: handler.getWorkers(workers);
129: handler.getServices(services);
130: } catch (Exception e) {
131: log.error(
132: "An Exception occurred while processing deployment "
133: + "descriptor "
134: + DEPLOYMENT_DESCRIPTOR + " in "
135: + jarFile.getName() + ". ", e);
136: }
137: }
138: }
139: }
140:
141: /**
142: Return the single instance of CarResources.
143: */
144: public static CarResources getInstance() {
145: return instance;
146: }
147:
148: /**
149: Return the single instance of CarClassLoader.
150: */
151: public ClassLoader getClassLoader() {
152: return loader;
153: }
154:
155: /**
156: Return a File object representing the well-known channel archive base
157: directory '/WEB-INF/cars' where channel archives are located.
158: */
159: private File getWellKnownDir() {
160: Servlet servlet = PortalSessionManager.getInstance();
161:
162: if (servlet == null)
163: return null;
164:
165: ServletContext ctx = servlet.getServletConfig()
166: .getServletContext();
167: String carDirRealPath = ctx.getRealPath(WELL_KNOWN_DIR);
168:
169: if (carDirRealPath == null) {
170: log.error("Channel Archives will not be "
171: + " loaded. Unable to aquire the real "
172: + "path to '" + WELL_KNOWN_DIR + "'. This "
173: + "can occur if the portal is deployed "
174: + "as a WAR and directories can not be "
175: + "created within its directory "
176: + "structure. Alternatively, you can "
177: + "specify a fully qualified path as "
178: + "the value of a '" + CAR_DIR_PROP_NAME
179: + "' property in portal.properties.");
180: return null;
181: }
182:
183: File carDir = new File(carDirRealPath);
184:
185: if (!carDir.exists()) {
186: if (log.isInfoEnabled())
187: log.info("Channel Archives can not be "
188: + " loaded. CAR directory '" + carDirRealPath
189: + "' does not exist.");
190: return null;
191: }
192: carDirExists = true;
193: this .carDirPath = carDirRealPath;
194: return carDir;
195: }
196:
197: /**
198: Return a File object representing the channel archive base
199: directory whose fully-qualified path is specified by the
200: 'org.jasig.portal.car.CarResources.directory' property in
201: portal.properties.
202: */
203: private File getPropertySpecifiedDir() {
204: String carDirPath = null;
205: File carDir = null;
206:
207: try {
208: carDirPath = PropertiesManager
209: .getProperty(CAR_DIR_PROP_NAME);
210: carDir = new File(carDirPath);
211: } catch (RuntimeException re) {
212: if (log.isInfoEnabled())
213: log.info("CAR directory property '" + CAR_DIR_PROP_NAME
214: + "' not specified. Defaulting to "
215: + "well-known directory '" + WELL_KNOWN_DIR
216: + "'.");
217: return null;
218: }
219:
220: if (!carDir.exists()) {
221: log.error("CAR directory '" + carDirPath
222: + "' specified by property '" + CAR_DIR_PROP_NAME
223: + "' does not exist. "
224: + "Channel Archives can not be "
225: + "loaded from this directory.");
226: return null;
227: }
228: carDirExists = true;
229: this .carDirPath = carDirPath;
230: return carDir;
231: }
232:
233: /**
234: Load information about all installed CARs and their contained resources.
235: */
236: private void loadCars() {
237: File carDir = getPropertySpecifiedDir();
238:
239: if (carDir == null)
240: carDir = getWellKnownDir();
241:
242: if (carDir != null) {
243: scanDir(carDir);
244: if (log.isInfoEnabled())
245: log.info("Channel Archives Loaded: "
246: + carsByPath.size() + " from '"
247: + this .carDirPath + "'");
248: }
249: carsLoaded = true;
250: }
251:
252: /**
253: Scan the passed in directory loading any cars there-in and calling
254: this method for any nested directories.
255: */
256: private void scanDir(File dir) {
257:
258: // first get all of the cars in this directory
259: File[] cars = dir.listFiles(carFilter);
260:
261: if (cars != null && cars.length != 0)
262: for (int i = 0; i < cars.length; i++)
263: loadCarEntries(cars[i]);
264:
265: // now get all of the sub-directories to be scanned
266: File[] dirs = dir.listFiles(dirFilter);
267:
268: if (dirs != null && dirs.length != 0)
269: for (int i = 0; i < dirs.length; i++)
270: scanDir(dirs[i]);
271: }
272:
273: /**
274: Load information about the passed in CAR and any contained resources.
275: */
276: private void loadCarEntries(File car) {
277: JarFile jar = null;
278:
279: try {
280: jar = new JarFile(car);
281: } catch (IOException ioe) {
282: log.error("CAR "
283: + getCarPath(car)
284: + " could not be loaded. Details: "
285: + (ioe.getMessage() != null ? ioe.getMessage()
286: : ioe.getClass().getName()), ioe);
287: return;
288: }
289: Vector entryList = new Vector();
290: carsByJars.put(jar, car);
291: carsByPath.put(getCarPath(car), car);
292:
293: Enumeration entries = jar.entries();
294:
295: while (entries.hasMoreElements()) {
296: ZipEntry entry = (ZipEntry) entries.nextElement();
297:
298: if (!entry.isDirectory()) {
299: String name = entry.getName();
300:
301: if (name.equals(DEPLOYMENT_DESCRIPTOR)) {
302: jarsWithDescriptors.add(jar);
303: } else
304: // add to map of which jar holds this resource
305: resourceJars.put(name, jar);
306:
307: // add to list of contents for this car
308: entryList.add(name);
309: }
310: }
311: carContents.put(car, entryList);
312: }
313:
314: /**
315: Push into the passed in properties object workers defined
316: in any component archive's deployment descriptor.
317: */
318: public void getWorkers(Properties workers) {
319: for (Iterator itr = this .workers.entrySet().iterator(); itr
320: .hasNext();) {
321: Map.Entry entry = (Entry) itr.next();
322: if (!workers.containsKey(entry.getKey()))
323: workers.put(entry.getKey(), entry.getValue());
324: }
325: }
326:
327: /**
328: Returns true if any archive included a deployment descriptor.
329: */
330: public boolean hasDescriptors() {
331: return jarsWithDescriptors.size() > 0;
332: }
333:
334: /**
335: Push into the passed in content handler events for any services declared
336: in any component archive's deployment descriptor.
337: */
338: public void getServices(ContentHandler contentHandler)
339: throws SAXException {
340: this .services.outputBuffer(contentHandler);
341: }
342:
343: /**
344: Return an input stream for reading the raw bytes making up the resource
345: contained in one of the installed CARs. Returns null if the resource
346: is not found.
347:
348: */
349: public InputStream getResourceAsStream(String resource)
350: throws PortalException {
351: JarFile jar = (JarFile) resourceJars.get(resource);
352:
353: if (jar == null)
354: return null;
355:
356: ZipEntry entry = jar.getEntry(resource);
357:
358: if (entry == null)
359: return null;
360:
361: try {
362: return jar.getInputStream(entry);
363: } catch (IOException ioe) {
364: throw new PortalException("Unable to get input stream for "
365: + resource);
366: }
367: }
368:
369: /**
370: Return the size of the indicated resource or -1 if the resource is not
371: found or its size is unknown.
372: */
373: public long getResourceSize(String resource) {
374: JarFile jar = (JarFile) resourceJars.get(resource);
375:
376: if (jar == null)
377: return -1;
378:
379: ZipEntry entry = jar.getEntry(resource);
380:
381: if (entry == null)
382: return -1;
383: return entry.getSize();
384: }
385:
386: /**
387: Returns a URL to the requested entry if found in one of the installed
388: CARs or null if not found.
389: */
390: public URL findResource(String entry) {
391: if (entry == null)
392: return null;
393:
394: // resolve entries that refer to a parent directory
395: // using a regular expression.
396: entry = resolveRegExpr(entry);
397:
398: JarFile jar = (JarFile) resourceJars.get(entry);
399: if (jar == null)
400: return null;
401: File carFile = (File) carsByJars.get(jar);
402: if (carFile == null) // should never happen!
403: return null;
404: String url = "jar:file:" + carFile.getAbsolutePath() + "!/"
405: + entry;
406: try {
407: return new URL(url);
408: } catch (java.net.MalformedURLException me) {
409: }
410: return null;
411: }
412:
413: /**
414: Returns the path of the CAR containing the indicated resource. This
415: path is relative to the CAR directory configured via the property in
416: portal.properties. If a CAR for that entry is not found it
417: returns null.
418: */
419: public String getContainingCarPath(String entry) {
420: if (entry == null)
421: return null;
422: JarFile jar = (JarFile) resourceJars.get(entry);
423: if (jar == null)
424: return null;
425: File carFile = (File) carsByJars.get(jar);
426: if (carFile == null) // should never happen!
427: return null;
428: return getCarPath(carFile);
429: }
430:
431: /**
432: Returns true if the indicated resource is available, false otherwise.
433: The resource is identified by its complete path within the CAR file.
434: */
435: public boolean containsResource(String resource) {
436: return resourceJars.containsKey(resource);
437: }
438:
439: /**
440: Returns a String array of car file paths relative to the car directory
441: specified via the property in portal.properties.
442: */
443: public String[] listCars() {
444: Map.Entry[] entries = null;
445:
446: entries = (Map.Entry[]) carsByJars.entrySet().toArray(
447: ENTRY_ARRAY);
448: String[] carNames = new String[entries.length];
449:
450: for (int i = 0; i < entries.length; i++)
451: carNames[i] = getCarPath((File) entries[i].getValue());
452: return carNames;
453: }
454:
455: /**
456: Returns a list of resources available in the car identified by the
457: passed in relative car file path name. This name is the path to the
458: car file relative to the car directory. If no car file is found for
459: the passed-in path then null is returned.
460: */
461: public String[] listCarResources(String carPath) {
462: File car = (File) carsByPath.get(carPath);
463: if (car == null)
464: return null;
465:
466: Vector contents = (Vector) carContents.get(car);
467:
468: if (contents == null)
469: return null; // should never happen
470:
471: return (String[]) contents.toArray(STRING_ARRAY);
472: }
473:
474: /**
475: Return the path of a car file relative to the car directory.
476: */
477: private String getCarPath(File car) {
478: String carPath = car.getAbsolutePath();
479: return carPath.substring(carDirPath.length() + 1);
480: }
481:
482: /**
483: Returns an enumeration of String objects each containing the path of a
484: resource available from the installed CARs.
485: */
486: public String[] listAllResources() {
487: return (String[]) resourceJars.keySet().toArray(STRING_ARRAY);
488: }
489:
490: /**
491: * Home-grown version of the String replace method. This one replaces
492: * the supplied String (generally a regular expression '../') with the
493: * supplied replacement. It returns the original String as is if no
494: * matches were found or a modified version of it if matches were found.
495: *
496: * @param entry the String to search for the regExpr.
497: * @param regExpr the regular expression to find and replace
498: * @param replacement the String to replace the regExpr with
499: * @return A modified String of match(es) were found, otherwise the
500: * original String unmodified.
501: **/
502: private String replace(String entry, String regExpr,
503: String replacement) {
504: String copy = entry;
505: int beginIdx = 0;
506: int endIdx = copy.indexOf(regExpr);
507: StringBuffer buff = new StringBuffer();
508:
509: while (endIdx != -1) {
510: // grab portion of the copied string up to the
511: // reg expr.
512: String newStr = copy.substring(beginIdx, endIdx);
513:
514: // replace original version of copy(ed) string with
515: // only a substring from the reg expr (+3 for reg expr
516: // length) to the end of the string
517: copy = copy.substring(endIdx + 3, copy.length());
518:
519: // append the string taken up to the reg expr to the
520: // buffer and add a replacement character to replace
521: // the reg expr.
522: buff.append(newStr).append(replacement);
523:
524: // see if another reg expr exists in the remaining
525: // copy(ed) string.
526: endIdx = copy.indexOf(regExpr);
527:
528: // if there are no more reg expr in the copy(ed) string,
529: // append the copy and call it good.
530: if (endIdx == -1)
531: buff.append(copy);
532: }
533:
534: // if there was a reg expr in the original entry, then the
535: // buffer wouldn't be 0 length.
536: if (buff.toString().length() > 0)
537: entry = buff.toString();
538:
539: if (log.isDebugEnabled())
540: log.debug("CarResources replace() - returned entry is: "
541: + entry);
542: return entry;
543: }
544:
545: /**
546: * Resolves the String entry and removes any regular expression
547: * patterns that would indicate a directory move (i.e. '../').
548: * The returned String is the supplied 'entry' String minus the
549: * '../' pattern and the directory directly preceding it if any.
550: *
551: * @param entry the String entry to resolve
552: * @return the modified String minus the reg expr.
553: **/
554: private String resolveRegExpr(String entry) {
555: // first it's necessary to replace any reg expr '../'
556: // with a different character, in this case a '~'.
557: // this allows the StringTokenizer to parse the
558: // entry into the appropriate tokens.
559: String replacement = "~";
560: entry = replace(entry, "../", replacement);
561:
562: // now the real fun starts. If the entry had been modified,
563: // (i.e. had a reg expr), then it will now be tokenized so
564: // that a new String can be constructed.
565: if (entry.indexOf(replacement) != -1) {
566: String delim = "/";
567: StringBuffer sb = new StringBuffer();
568:
569: if (log.isDebugEnabled())
570: log.debug("CarResources resolveRegExpr() - "
571: + " Parsing resource name: " + entry);
572:
573: StringTokenizer st = new StringTokenizer(entry, replacement);
574: int tokens = st.countTokens();
575: int count = 1;
576:
577: while (st.hasMoreTokens()) {
578: // parse each token separately to correctly climb back
579: // up a directory
580: String token = st.nextToken();
581:
582: if (log.isDebugEnabled())
583: log.debug("CarResources resolveRegExpr() - "
584: + "Token is now: " + token);
585:
586: StringTokenizer st1 = new StringTokenizer(token, delim);
587: int childTokens = st1.countTokens();
588: int childCount = 1;
589:
590: while (st1.hasMoreTokens()) {
591: String childToken = st1.nextToken();
592:
593: if (log.isDebugEnabled())
594: log.debug("CarResources resolveRegExpr() - "
595: + "Child token is: " + childToken);
596:
597: // if there are more child tokens, then add the most
598: // recent one to the buffer along with the delimiter
599: if (childCount < childTokens) {
600: sb.append(childToken);
601: sb.append(delim);
602: } else if (count == tokens) {
603: // if the original entry began with '../', like
604: // ( ../somedir ),
605: // then this would basically remove the ../ and
606: // return the rest of the string unchanged.
607: sb.append(childToken);
608: } else
609: // ignore last token
610: break;
611: childCount++;
612: }
613: count++;
614: }
615: entry = sb.toString();
616: }
617:
618: if (log.isDebugEnabled())
619: log.debug("CarResources resolveRegExpr() - "
620: + "resolved entry is: " + entry);
621: return entry;
622: }
623: }
|