001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.bean;
018:
019: import java.io.File;
020: import java.io.FileInputStream;
021: import java.io.FileNotFoundException;
022: import java.io.IOException;
023: import java.io.OutputStream;
024: import java.util.ArrayList;
025: import java.util.Arrays;
026: import java.util.Collection;
027: import java.util.HashMap;
028: import java.util.Iterator;
029: import java.util.List;
030: import java.util.Map;
031: import java.util.TreeMap;
032:
033: import org.apache.avalon.excalibur.component.ExcaliburComponentManager;
034: import org.apache.avalon.excalibur.logger.LogKitLoggerManager;
035:
036: import org.apache.avalon.framework.configuration.Configuration;
037: import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
038: import org.apache.avalon.framework.container.ContainerUtil;
039: import org.apache.avalon.framework.context.DefaultContext;
040: import org.apache.avalon.framework.logger.LogKitLogger;
041: import org.apache.avalon.framework.logger.Logger;
042:
043: import org.apache.cocoon.Cocoon;
044: import org.apache.cocoon.CocoonAccess;
045: import org.apache.cocoon.Constants;
046: import org.apache.cocoon.ProcessingException;
047: import org.apache.cocoon.components.CocoonComponentManager;
048: import org.apache.cocoon.components.pipeline.ProcessingPipeline;
049: import org.apache.cocoon.environment.Environment;
050: import org.apache.cocoon.environment.commandline.CommandLineContext;
051: import org.apache.cocoon.environment.commandline.FileSavingEnvironment;
052: import org.apache.cocoon.environment.commandline.LinkSamplingEnvironment;
053: import org.apache.cocoon.util.ClassUtils;
054: import org.apache.cocoon.util.IOUtils;
055: import org.apache.cocoon.util.NetUtils;
056: import org.apache.cocoon.xml.ContentHandlerWrapper;
057: import org.apache.cocoon.xml.XMLConsumer;
058: import org.apache.commons.lang.SystemUtils;
059:
060: import org.apache.log.Hierarchy;
061: import org.apache.log.Priority;
062: import org.xml.sax.ContentHandler;
063:
064: /**
065: * The Cocoon Wrapper simplifies usage of the Cocoon object. Allows to create,
066: * configure Cocoon instance and process single requests.
067: *
068: * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
069: * @author <a href="mailto:nicolaken@apache.org">Nicola Ken Barozzi</a>
070: * @author <a href="mailto:vgritsenko@apache.org">Vadim Gritsenko</a>
071: * @author <a href="mailto:uv@upaya.co.uk">Upayavira</a>
072: * @version $Id: CocoonWrapper.java 433543 2006-08-22 06:22:54Z crossley $
073: */
074: public class CocoonWrapper {
075:
076: protected static final String DEFAULT_USER_AGENT = Constants.COMPLETE_NAME;
077: protected static final String DEFAULT_ACCEPT = "text/html, */*";
078:
079: // User Supplied Parameters
080: private String contextDir = Constants.DEFAULT_CONTEXT_DIR;
081: private String configFile = null;
082:
083: private String workDir = Constants.DEFAULT_WORK_DIR;
084: private String logKit = null;
085: protected String logger = null;
086: protected String logLevel = "ERROR";
087: protected String userAgent = DEFAULT_USER_AGENT;
088: protected String accept = DEFAULT_ACCEPT;
089: private List classList = new ArrayList();
090:
091: // Objects used alongside User Supplied Parameters
092: protected File context;
093: private File work;
094: private File conf;
095:
096: // Internal Objects
097: protected CommandLineContext cliContext;
098: private LogKitLoggerManager logManager;
099: protected Cocoon cocoon;
100: protected Logger log;
101: private HashMap empty = new HashMap();
102:
103: private boolean initialized = false;
104: private boolean useExistingCocoon = false;
105:
106: //
107: // INITIALISATION METHOD
108: //
109: public void initialize() throws Exception {
110: // @todo@ these should log then throw exceptions back to the caller, not use system.exit()
111:
112: // Create a new hierarchy. This is needed when CocoonBean is called from
113: // within a CocoonServlet call, in order not to mix logs
114: final Hierarchy hierarchy = new Hierarchy();
115:
116: final Priority priority = Priority.getPriorityForName(logLevel);
117: hierarchy.setDefaultPriority(priority);
118:
119: // Install a temporary logger so that getDir() can log if needed
120: this .log = new LogKitLogger(hierarchy.getLoggerFor(""));
121:
122: try {
123: // First of all, initialize the logging system
124:
125: // Setup the application context with context-dir and work-dir that
126: // can be used in logkit.xconf
127: this .context = getDir(this .contextDir, "context");
128: this .work = getDir(workDir, "working");
129: DefaultContext appContext = new DefaultContext();
130: appContext.put(Constants.CONTEXT_WORK_DIR, work);
131:
132: this .logManager = new LogKitLoggerManager(hierarchy);
133: this .logManager.enableLogging(log);
134:
135: if (this .logKit != null) {
136: final FileInputStream fis = new FileInputStream(logKit);
137: final DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();
138: final Configuration logKitConf = builder.build(fis);
139: final DefaultContext subcontext = new DefaultContext(
140: appContext);
141: subcontext.put("context-root", this .contextDir);
142: subcontext.put("context-work", this .workDir);
143: this .logManager.contextualize(subcontext);
144: this .logManager.configure(logKitConf);
145: if (logger != null) {
146: log = this .logManager.getLoggerForCategory(logger);
147: } else {
148: log = this .logManager
149: .getLoggerForCategory("cocoon");
150: }
151: }
152:
153: this .conf = getConfigurationFile(this .context,
154: this .configFile);
155:
156: cliContext = new CommandLineContext(contextDir);
157: cliContext.enableLogging(log);
158:
159: appContext.put(Constants.CONTEXT_ENVIRONMENT_CONTEXT,
160: cliContext);
161: appContext.put(Constants.CONTEXT_CLASS_LOADER,
162: CocoonWrapper.class.getClassLoader());
163: appContext.put(Constants.CONTEXT_CLASSPATH,
164: getClassPath(contextDir));
165: appContext.put(Constants.CONTEXT_UPLOAD_DIR, contextDir
166: + "upload-dir");
167: File cacheDir = getDir(workDir + File.separator
168: + "cache-dir", "cache");
169: appContext.put(Constants.CONTEXT_CACHE_DIR, cacheDir);
170: appContext.put(Constants.CONTEXT_CONFIG_URL, conf.toURL());
171: appContext.put(Constants.CONTEXT_DEFAULT_ENCODING,
172: "ISO-8859-1");
173:
174: loadClasses(classList);
175:
176: if (this .useExistingCocoon) {
177: cocoon = getCocoon();
178: }
179: if (cocoon == null) {
180: cocoon = new Cocoon();
181: ContainerUtil.enableLogging(cocoon, log);
182: ContainerUtil.contextualize(cocoon, appContext);
183: cocoon.setLoggerManager(logManager);
184: ContainerUtil.initialize(cocoon);
185: }
186: } catch (Exception e) {
187: log.fatalError("Exception caught", e);
188: throw e;
189: }
190: initialized = true;
191: }
192:
193: private Cocoon getCocoon() {
194: return new CocoonInstance().instance();
195: }
196:
197: private static class CocoonInstance extends CocoonAccess {
198: final Cocoon instance() {
199: return super .getCocoon();
200: }
201: }
202:
203: protected ExcaliburComponentManager getComponentManager() {
204: return cocoon.getComponentManager();
205: }
206:
207: /**
208: * Look around for the configuration file.
209: *
210: * @param dir a <code>File</code> where to look for configuration files
211: * @return a <code>File</code> representing the configuration
212: * @exception IOException if an error occurs
213: */
214: private File getConfigurationFile(File dir, String configFile)
215: throws IOException {
216: File conf;
217: if (configFile == null) {
218: conf = tryConfigurationFile(dir + File.separator
219: + Constants.DEFAULT_CONF_FILE);
220: if (conf == null) {
221: conf = tryConfigurationFile(dir + File.separator
222: + "WEB-INF" + File.separator
223: + Constants.DEFAULT_CONF_FILE);
224: }
225: if (conf == null) {
226: conf = tryConfigurationFile(SystemUtils.USER_DIR
227: + File.separator + Constants.DEFAULT_CONF_FILE);
228: }
229: if (conf == null) {
230: conf = tryConfigurationFile("/usr/local/etc/"
231: + Constants.DEFAULT_CONF_FILE);
232: }
233: } else {
234: conf = new File(configFile);
235: if (!conf.exists()) {
236: conf = new File(dir, configFile);
237: }
238: }
239: if (conf == null) {
240: log.error("Could not find the configuration file.");
241: throw new FileNotFoundException(
242: "The configuration file could not be found.");
243: }
244: return conf;
245: }
246:
247: /**
248: * Try loading the configuration file from a single location
249: */
250: private File tryConfigurationFile(String filename) {
251: if (log.isDebugEnabled()) {
252: log.debug("Trying configuration file at: " + filename);
253: }
254: File conf = new File(filename);
255: if (conf.canRead()) {
256: return conf;
257: } else {
258: return null;
259: }
260: }
261:
262: /**
263: * Get a <code>File</code> representing a directory.
264: *
265: * @param dir a <code>String</code> with a directory name
266: * @param type a <code>String</code> describing the type of directory
267: * @return a <code>File</code> value
268: * @exception IOException if an error occurs
269: */
270: private File getDir(String dir, String type) throws IOException {
271: if (log.isDebugEnabled()) {
272: log.debug("Getting handle to " + type + " directory '"
273: + dir + "'");
274: }
275: File d = new File(dir);
276:
277: if (!d.exists()) {
278: if (!d.mkdirs()) {
279: throw new IOException("Error creating " + type
280: + " directory '" + d + "'");
281: }
282: }
283:
284: if (!d.isDirectory()) {
285: throw new IOException("'" + d + "' is not a directory.");
286: }
287:
288: if (!d.canRead()) {
289: throw new IOException("Directory '" + d
290: + "' is not readable");
291: }
292:
293: if ("working".equals(type) && !d.canWrite()) {
294: throw new IOException("Directory '" + d
295: + "' is not writable");
296: }
297:
298: return d;
299: }
300:
301: protected void finalize() throws Throwable {
302: dispose();
303: super .finalize();
304: }
305:
306: protected void loadClasses(List classList) {
307: if (classList != null) {
308: for (Iterator i = classList.iterator(); i.hasNext();) {
309: String className = (String) i.next();
310: try {
311: if (log.isDebugEnabled()) {
312: log.debug("Trying to load class: " + className);
313: }
314: ClassUtils.loadClass(className).newInstance();
315: } catch (Exception e) {
316: if (log.isWarnEnabled()) {
317: log.warn("Could not force-load class: "
318: + className, e);
319: }
320: // Do not throw an exception, because it is not a fatal error.
321: }
322: }
323: }
324: }
325:
326: //
327: // GETTERS AND SETTERS FOR CONFIGURATION PROPERTIES
328: //
329:
330: /**
331: * Set LogKit configuration file name
332: * @param logKit LogKit configuration file
333: */
334: public void setLogKit(String logKit) {
335: this .logKit = logKit;
336: }
337:
338: /**
339: * Set log level. Default is DEBUG.
340: * @param logLevel log level
341: */
342: public void setLogLevel(String logLevel) {
343: this .logLevel = logLevel;
344: }
345:
346: /**
347: * Set logger category as default logger for the Cocoon engine
348: * @param logger logger category
349: */
350: public void setLogger(String logger) {
351: this .logger = logger;
352: }
353:
354: public String getLoggerName() {
355: return logger;
356: }
357:
358: /**
359: * Set context directory
360: * @param contextDir context directory
361: */
362: public void setContextDir(String contextDir) {
363: this .contextDir = contextDir;
364: }
365:
366: /**
367: * Set working directory
368: * @param workDir working directory
369: */
370: public void setWorkDir(String workDir) {
371: this .workDir = workDir;
372: }
373:
374: public void setConfigFile(String configFile) {
375: this .configFile = configFile;
376: }
377:
378: public void setAgentOptions(String userAgent) {
379: this .userAgent = userAgent;
380: }
381:
382: public void setAcceptOptions(String accept) {
383: this .accept = accept;
384: }
385:
386: public void addLoadedClass(String className) {
387: this .classList.add(className);
388: }
389:
390: public void addLoadedClasses(List classList) {
391: this .classList.addAll(classList);
392: }
393:
394: public void setUseExistingCocoon(boolean useExistingCocoon) {
395: this .useExistingCocoon = useExistingCocoon;
396: }
397:
398: /**
399: * Process single URI into given output stream.
400: *
401: * @param uri to process
402: * @param outputStream to write generated contents into
403: */
404: public void processURI(String uri, OutputStream outputStream)
405: throws Exception {
406:
407: if (!initialized) {
408: initialize();
409: }
410: log.info("Processing URI: " + uri);
411:
412: // Get parameters, headers, deparameterized URI and path from URI
413: final TreeMap parameters = new TreeMap();
414: final TreeMap headers = new TreeMap();
415: final String deparameterizedURI = NetUtils.deparameterize(uri,
416: parameters);
417: headers.put("user-agent", userAgent);
418: headers.put("accept", accept);
419:
420: int status = getPage(deparameterizedURI, 0L, parameters,
421: headers, null, null, outputStream);
422:
423: if (status >= 400) {
424: throw new ProcessingException("Resource not found: "
425: + status);
426: }
427: }
428:
429: /**
430: * Process single URI into given content handler, skipping final
431: * serializer
432: *
433: * @param uri to process
434: * @param handler to write generated contents into
435: */
436: public void processURI(String uri, ContentHandler handler)
437: throws Exception {
438:
439: if (!initialized) {
440: initialize();
441: }
442: log.info("Processing URI: " + uri);
443:
444: // Get parameters, headers, deparameterized URI and path from URI
445: final TreeMap parameters = new TreeMap();
446: final TreeMap headers = new TreeMap();
447: final String deparameterizedURI = NetUtils.deparameterize(uri,
448: parameters);
449: headers.put("user-agent", userAgent);
450: headers.put("accept", accept);
451:
452: int status = getPage(deparameterizedURI, 0L, parameters,
453: headers, null, null, handler);
454:
455: if (status >= 400) {
456: throw new ProcessingException("Resource not found: "
457: + status);
458: }
459: }
460:
461: public void dispose() {
462: if (this .initialized) {
463: this .initialized = false;
464: ContainerUtil.dispose(this .cocoon);
465: this .cocoon = null;
466: this .logManager.dispose();
467: if (log.isDebugEnabled()) {
468: log.debug("Disposed");
469: }
470: }
471: }
472:
473: /**
474: * Samples an URI for its links.
475: *
476: * @param deparameterizedURI a <code>String</code> value of an URI to start sampling from
477: * @param parameters a <code>Map</code> value containing request parameters
478: * @return a <code>Collection</code> of links
479: * @exception Exception if an error occurs
480: */
481: protected Collection getLinks(String deparameterizedURI,
482: Map parameters) throws Exception {
483:
484: final TreeMap headers = new TreeMap();
485: headers.put("user-agent", userAgent);
486: headers.put("accept", accept);
487:
488: LinkSamplingEnvironment env = new LinkSamplingEnvironment(
489: deparameterizedURI, context, null, parameters, headers,
490: cliContext, log);
491: processLenient(env);
492: return env.getLinks();
493: }
494:
495: /**
496: * Processes an URI for its content.
497: *
498: * @param deparameterizedURI a <code>String</code> value of an URI to start sampling from
499: * @param parameters a <code>Map</code> value containing request parameters
500: * @param links a <code>Map</code> value
501: * @param stream an <code>OutputStream</code> to write the content to
502: * @return a <code>String</code> value for the content
503: * @exception Exception if an error occurs
504: */
505: protected int getPage(String deparameterizedURI, long lastModified,
506: Map parameters, Map headers, Map links, List gatheredLinks,
507: OutputStream stream) throws Exception {
508:
509: headers.put("user-agent", userAgent);
510: headers.put("accept", accept);
511:
512: FileSavingEnvironment env = new FileSavingEnvironment(
513: deparameterizedURI, lastModified, context, null,
514: parameters, headers, links, gatheredLinks, cliContext,
515: stream, log);
516:
517: // Here Cocoon can throw an exception if there are errors in processing the page
518: cocoon.process(env);
519:
520: // if we get here, the page was created :-)
521: int status = env.getStatus();
522: if (!env.isModified()) {
523: status = -1;
524: }
525: return status;
526: }
527:
528: /**
529: * Processes an URI for its content.
530: *
531: * @param deparameterizedURI a <code>String</code> value of an URI to start sampling from
532: * @param parameters a <code>Map</code> value containing request parameters
533: * @param links a <code>Map</code> value
534: * @param handler an <code>ContentHandler</code> to send the content to
535: * @return a <code>String</code> value for the content
536: * @exception Exception if an error occurs
537: */
538: protected int getPage(String deparameterizedURI, long lastModified,
539: Map parameters, Map headers, Map links, List gatheredLinks,
540: ContentHandler handler) throws Exception {
541: FileSavingEnvironment env = new FileSavingEnvironment(
542: deparameterizedURI, lastModified, context, null,
543: parameters, headers, links, gatheredLinks, cliContext,
544: null, log);
545:
546: XMLConsumer consumer = new ContentHandlerWrapper(handler);
547: ProcessingPipeline pipeline = cocoon.buildPipeline(env);
548: CocoonComponentManager.enterEnvironment(env, cocoon
549: .getComponentManager(), cocoon);
550: try {
551: pipeline.prepareInternal(env);
552: pipeline.process(env, consumer);
553: } finally {
554: CocoonComponentManager.leaveEnvironment();
555: }
556:
557: // if we get here, the page was created :-)
558: int status = env.getStatus();
559: if (!env.isModified()) {
560: status = -1;
561: }
562: return status;
563: }
564:
565: /** Class <code>NullOutputStream</code> here. */
566: static class NullOutputStream extends OutputStream {
567: public void write(int b) throws IOException {
568: }
569:
570: public void write(byte b[]) throws IOException {
571: }
572:
573: public void write(byte b[], int off, int len)
574: throws IOException {
575: }
576: }
577:
578: /**
579: * Analyze the type of content for an URI.
580: *
581: * @param deparameterizedURI a <code>String</code> value to analyze
582: * @param parameters a <code>Map</code> value for the request
583: * @return a <code>String</code> value denoting the type of content
584: * @exception Exception if an error occurs
585: */
586: protected String getType(String deparameterizedURI, Map parameters)
587: throws Exception {
588:
589: final TreeMap headers = new TreeMap();
590: headers.put("user-agent", userAgent);
591: headers.put("accept", accept);
592:
593: FileSavingEnvironment env = new FileSavingEnvironment(
594: deparameterizedURI, context, null, parameters, headers,
595: empty, null, cliContext, new NullOutputStream(), log);
596: processLenient(env);
597: return env.getContentType();
598: }
599:
600: /**
601: * Try to process something but don't throw a ProcessingException.
602: *
603: * @param env the <code>Environment</code> to process
604: * @return boolean true if no error were cast, false otherwise
605: * @exception Exception if an error occurs, except RNFE
606: */
607: private boolean processLenient(Environment env) throws Exception {
608: try {
609: this .cocoon.process(env);
610: } catch (ProcessingException pe) {
611: return false;
612: }
613: return true;
614: }
615:
616: /**
617: * This builds the important ClassPath used by this class. It
618: * does so in a neutral way.
619: * It iterates in alphabetical order through every file in the
620: * lib directory and adds it to the classpath.
621: *
622: * Also, we add the files to the ClassLoader for the Cocoon system.
623: * In order to protect ourselves from skitzofrantic classloaders,
624: * we need to work with a known one.
625: *
626: * @param context The context path
627: * @return a <code>String</code> value
628: */
629: protected String getClassPath(final String context) {
630: StringBuffer buildClassPath = new StringBuffer();
631:
632: String classDir = context + "/WEB-INF/classes";
633: buildClassPath.append(classDir);
634:
635: File root = new File(context + "/WEB-INF/lib");
636: if (root.isDirectory()) {
637: File[] libraries = root.listFiles();
638: Arrays.sort(libraries);
639: for (int i = 0; i < libraries.length; i++) {
640: if (libraries[i].getAbsolutePath().endsWith(".jar")) {
641: buildClassPath
642: .append(File.pathSeparatorChar)
643: .append(
644: IOUtils
645: .getFullFilename(libraries[i]));
646: }
647: }
648: }
649:
650: buildClassPath.append(File.pathSeparatorChar).append(
651: SystemUtils.JAVA_CLASS_PATH);
652:
653: // Extra class path is necessary for non-classloader-aware java compilers to compile XSPs
654: // buildClassPath.append(File.pathSeparatorChar)
655: // .append(getExtraClassPath(context));
656:
657: if (log.isDebugEnabled()) {
658: log.debug("Context classpath: " + buildClassPath);
659: }
660: return buildClassPath.toString();
661: }
662: }
|