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.generation;
018:
019: import java.io.ByteArrayInputStream;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.util.Iterator;
023: import java.util.Map;
024: import java.util.Properties;
025:
026: import javax.servlet.http.HttpServletRequest;
027:
028: import org.apache.avalon.framework.activity.Disposable;
029: import org.apache.avalon.framework.configuration.Configurable;
030: import org.apache.avalon.framework.configuration.Configuration;
031: import org.apache.avalon.framework.configuration.ConfigurationException;
032: import org.apache.avalon.framework.parameters.Parameters;
033: import org.apache.avalon.framework.service.ServiceException;
034: import org.apache.avalon.framework.service.ServiceManager;
035: import org.apache.cocoon.ProcessingException;
036: import org.apache.cocoon.ResourceNotFoundException;
037: import org.apache.cocoon.caching.CacheableProcessingComponent;
038: import org.apache.cocoon.components.source.SourceUtil;
039: import org.apache.cocoon.environment.ObjectModelHelper;
040: import org.apache.cocoon.environment.Request;
041: import org.apache.cocoon.environment.SourceResolver;
042: import org.apache.cocoon.environment.http.HttpEnvironment;
043: import org.apache.cocoon.util.PostInputStream;
044: import org.apache.cocoon.xml.dom.DOMBuilder;
045: import org.apache.cocoon.xml.dom.DOMStreamer;
046: import org.apache.excalibur.source.Source;
047: import org.apache.excalibur.source.SourceException;
048: import org.apache.excalibur.source.SourceValidity;
049: import org.apache.excalibur.xml.xpath.XPathProcessor;
050: import org.apache.xerces.parsers.AbstractSAXParser;
051: import org.cyberneko.html.HTMLConfiguration;
052: import org.w3c.dom.Document;
053: import org.w3c.dom.NodeList;
054: import org.xml.sax.InputSource;
055: import org.xml.sax.SAXException;
056:
057: /**
058: * @cocoon.sitemap.component.documentation
059: * The neko html generator reads HTML from a source, converts it to XHTML
060: * and generates SAX Events. It uses the NekoHTML library to do this.
061: *
062: * @cocoon.sitemap.component.name nekohtml
063: * @cocoon.sitemap.component.label content
064: * @cocoon.sitemap.component.logger sitemap.generator.nekohtml
065: * @cocoon.sitemap.component.documentation.caching
066: * Uses the last modification date of the xml document for validation
067: *
068: * @cocoon.sitemap.component.pooling.max 32
069: *
070: * @author <a href="mailto:dims@yahoo.com">Davanum Srinivas</a>
071: * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
072: * @author <a href="mailto:barozzi@nicolaken.com">Nicola Ken Barozzi</a>
073: * @author <a href="mailto:gianugo@apache.org">Gianugo Rabellino</a>
074: *
075: * @version CVS $Id: NekoHTMLGenerator.java 433543 2006-08-22 06:22:54Z crossley $
076: */
077: public class NekoHTMLGenerator extends ServiceableGenerator implements
078: Configurable, CacheableProcessingComponent, Disposable {
079:
080: /** The parameter that specifies what request attribute to use, if any */
081: public static final String FORM_NAME = "form-name";
082:
083: /** The source, if coming from a file */
084: private Source inputSource;
085:
086: /** The source, if coming from the request */
087: private InputStream requestStream;
088:
089: /** XPATH expression */
090: private String xpath = null;
091:
092: /** XPath Processor */
093: private XPathProcessor processor = null;
094:
095: /** Neko properties */
096: private Properties properties;
097:
098: public void service(ServiceManager manager) throws ServiceException {
099: super .service(manager);
100: this .processor = (XPathProcessor) this .manager
101: .lookup(XPathProcessor.ROLE);
102: }
103:
104: public void configure(Configuration config)
105: throws ConfigurationException {
106:
107: String configUrl = config.getChild("neko-config")
108: .getValue(null);
109:
110: if (configUrl != null) {
111: org.apache.excalibur.source.SourceResolver resolver = null;
112: Source configSource = null;
113: try {
114: resolver = (org.apache.excalibur.source.SourceResolver) this .manager
115: .lookup(org.apache.excalibur.source.SourceResolver.ROLE);
116: configSource = resolver.resolveURI(configUrl);
117: if (getLogger().isDebugEnabled()) {
118: getLogger().debug(
119: "Loading configuration from "
120: + configSource.getURI());
121: }
122:
123: this .properties = new Properties();
124: this .properties.load(configSource.getInputStream());
125:
126: } catch (Exception e) {
127: getLogger().warn(
128: "Cannot load configuration from " + configUrl);
129: throw new ConfigurationException(
130: "Cannot load configuration from " + configUrl,
131: e);
132: } finally {
133: if (null != resolver) {
134: this .manager.release(resolver);
135: resolver.release(configSource);
136: }
137: }
138: }
139: }
140:
141: /**
142: * Recycle this component.
143: * All instance variables are set to <code>null</code>.
144: */
145: public void recycle() {
146: if (this .inputSource != null) {
147: this .resolver.release(this .inputSource);
148: this .inputSource = null;
149: this .requestStream = null;
150: }
151: this .xpath = null;
152: super .recycle();
153: }
154:
155: /**
156: * Setup the html generator.
157: * Try to get the last modification date of the source for caching.
158: */
159: public void setup(SourceResolver resolver, Map objectModel,
160: String src, Parameters par) throws ProcessingException,
161: SAXException, IOException {
162: super .setup(resolver, objectModel, src, par);
163:
164: Request request = ObjectModelHelper.getRequest(objectModel);
165:
166: if (src == null) {
167: // Handle this request as the StreamGenerator does (from the POST
168: // request or from a request parameter), but try to make sure
169: // that the output will be well-formed
170:
171: String contentType = request.getContentType();
172:
173: if (contentType == null) {
174: throw new IOException(
175: "Content-type was not specified for this request");
176: } else if (contentType
177: .startsWith("application/x-www-form-urlencoded")
178: || contentType.startsWith("multipart/form-data")) {
179: String requested = parameters.getParameter(FORM_NAME,
180: null);
181: if (requested == null) {
182: throw new ProcessingException(
183: "NekoHtmlGenerator with no \"src\" parameter expects a sitemap parameter called '"
184: + FORM_NAME
185: + "' for handling form data");
186: }
187:
188: String sXml = request.getParameter(requested);
189:
190: requestStream = new ByteArrayInputStream(sXml
191: .getBytes());
192:
193: } else if (contentType.startsWith("text/plain")
194: || contentType.startsWith("text/xml")
195: || contentType.startsWith("application/xml")) {
196:
197: HttpServletRequest httpRequest = (HttpServletRequest) objectModel
198: .get(HttpEnvironment.HTTP_REQUEST_OBJECT);
199: if (httpRequest == null) {
200: throw new ProcessingException(
201: "This functionality only works in an http environment.");
202: }
203: int len = request.getContentLength();
204: if (len > 0) {
205: requestStream = new PostInputStream(httpRequest
206: .getInputStream(), len);
207: } else {
208: throw new IOException("getContentLen() == 0");
209: }
210: } else {
211: throw new IOException("Unexpected getContentType(): "
212: + request.getContentType());
213: }
214:
215: }
216:
217: xpath = request.getParameter("xpath");
218: if (xpath == null)
219: xpath = par.getParameter("xpath", null);
220:
221: // append the request parameter to the URL if necessary
222: if (par.getParameterAsBoolean("copy-parameters", false)
223: && request.getQueryString() != null) {
224: StringBuffer query = new StringBuffer(super .source);
225: query.append(super .source.indexOf("?") == -1 ? '?' : '&');
226: query.append(request.getQueryString());
227: super .source = query.toString();
228: }
229:
230: try {
231: if (source != null)
232: this .inputSource = resolver.resolveURI(super .source);
233: } catch (SourceException se) {
234: throw SourceUtil.handle(
235: "Unable to resolve " + super .source, se);
236: }
237: }
238:
239: /**
240: * Generate the unique key.
241: * This key must be unique inside the space of this component.
242: * This method must be invoked before the generateValidity() method.
243: *
244: * @return The generated key or <code>0</code> if the component
245: * is currently not cacheable.
246: */
247: public java.io.Serializable getKey() {
248: if (this .inputSource == null)
249: return null;
250:
251: if (this .xpath != null) {
252: StringBuffer buffer = new StringBuffer(this .inputSource
253: .getURI());
254: buffer.append(':').append(this .xpath);
255: return buffer.toString();
256: } else {
257: return this .inputSource.getURI();
258: }
259: }
260:
261: /**
262: * Generate the validity object.
263: * Before this method can be invoked the generateKey() method
264: * must be invoked.
265: *
266: * @return The generated validity object or <code>null</code> if the
267: * component is currently not cacheable.
268: */
269: public SourceValidity getValidity() {
270: if (this .inputSource == null)
271: return null;
272: return this .inputSource.getValidity();
273: }
274:
275: /**
276: * Generate XML data.
277: */
278: public void generate() throws IOException, SAXException,
279: ProcessingException {
280: try {
281: HtmlSaxParser parser = new HtmlSaxParser(this .properties);
282:
283: if (inputSource != null)
284: requestStream = this .inputSource.getInputStream();
285:
286: if (xpath != null) {
287: DOMBuilder builder = new DOMBuilder();
288: parser.setContentHandler(builder);
289: parser.parse(new InputSource(requestStream));
290: Document doc = builder.getDocument();
291:
292: DOMStreamer domStreamer = new DOMStreamer(
293: this .contentHandler, this .lexicalHandler);
294: this .contentHandler.startDocument();
295: NodeList nl = processor.selectNodeList(doc, xpath);
296: int length = nl.getLength();
297: for (int i = 0; i < length; i++) {
298: domStreamer.stream(nl.item(i));
299: }
300: this .contentHandler.endDocument();
301: } else {
302: parser.setContentHandler(this .contentHandler);
303: parser.parse(new InputSource(requestStream));
304: }
305: requestStream.close();
306: } catch (IOException e) {
307: throw new ResourceNotFoundException(
308: "Could not get resource "
309: + this .inputSource.getURI(), e);
310: } catch (SAXException e) {
311: throw e;
312: } catch (Exception e) {
313: throw new ProcessingException(
314: "Exception in NekoHTMLGenerator.generate()", e);
315: }
316: }
317:
318: public void dispose() {
319: if (this .manager != null) {
320: this .manager.release(this .processor);
321: this .manager = null;
322: }
323: this .processor = null;
324: super .dispose();
325: }
326:
327: public static class HtmlSaxParser extends AbstractSAXParser {
328:
329: public HtmlSaxParser(Properties properties) {
330: super (getConfig(properties));
331: }
332:
333: private static HTMLConfiguration getConfig(Properties properties) {
334: HTMLConfiguration config = new HTMLConfiguration();
335: config.setProperty(
336: "http://cyberneko.org/html/properties/names/elems",
337: "lower");
338: if (properties != null) {
339: for (Iterator i = properties.keySet().iterator(); i
340: .hasNext();) {
341: String name = (String) i.next();
342: if (name.indexOf("/features/") > -1) {
343: config.setFeature(name, Boolean
344: .getBoolean(properties
345: .getProperty(name)));
346: } else if (name.indexOf("/properties/") > -1) {
347: config.setProperty(name, properties
348: .getProperty(name));
349: }
350: }
351: }
352: return config;
353: }
354: }
355: }
|