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.portal.transformation;
018:
019: import java.io.BufferedInputStream;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.PrintWriter;
023: import java.io.StringWriter;
024: import java.io.UnsupportedEncodingException;
025: import java.net.HttpURLConnection;
026: import java.net.MalformedURLException;
027: import java.net.URL;
028: import java.util.Enumeration;
029: import java.util.Map;
030:
031: import org.apache.avalon.framework.activity.Disposable;
032: import org.apache.avalon.framework.parameters.ParameterException;
033: import org.apache.avalon.framework.parameters.Parameterizable;
034: import org.apache.avalon.framework.parameters.Parameters;
035: import org.apache.avalon.framework.service.ServiceException;
036: import org.apache.avalon.framework.service.ServiceManager;
037: import org.apache.avalon.framework.service.Serviceable;
038: import org.apache.cocoon.ProcessingException;
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.portal.Constants;
043: import org.apache.cocoon.portal.PortalService;
044: import org.apache.cocoon.portal.coplet.CopletData;
045: import org.apache.cocoon.portal.coplet.CopletInstanceData;
046: import org.apache.cocoon.portal.profile.ProfileManager;
047: import org.apache.cocoon.portal.util.InputModuleHelper;
048: import org.apache.cocoon.transformation.AbstractTransformer;
049: import org.apache.cocoon.util.NetUtils;
050: import org.apache.cocoon.xml.XMLUtils;
051: import org.apache.cocoon.xml.dom.DOMStreamer;
052: import org.w3c.dom.Document;
053: import org.w3c.dom.Element;
054: import org.w3c.dom.NodeList;
055: import org.w3c.tidy.Configuration;
056: import org.w3c.tidy.Tidy;
057: import org.xml.sax.Attributes;
058: import org.xml.sax.SAXException;
059:
060: /**
061: * This transformer is used to insert the XHTML data from an request
062: * to an external application at the specified element ("envelope-tag" parameter).
063: * Nesessary connection data for the external request like sessionid, cookies,
064: * documentbase, the uri, etc. will be taken from the application coplet instance
065: * data.
066: * @author <a href="mailto:friedrich.klenner@rzb.at">Friedrich Klenner</a>
067: * @author <a href="mailto:gernot.koller@rizit.at">Gernot Koller</a>
068: *
069: * @version CVS $Id: ProxyTransformer.java 433543 2006-08-22 06:22:54Z crossley $
070: */
071: public class ProxyTransformer extends AbstractTransformer implements
072: Serviceable, Disposable, Parameterizable {
073:
074: /**
075: * Parameter for specifying the envelope tag
076: */
077: public static final String ENVELOPE_TAG_PARAMETER = "envelope-tag";
078:
079: public static final String PORTALNAME = "cocoon-portal-portalname";
080: public static final String COPLETID = "cocoon-portal-copletid";
081: public static final String PROXY_PREFIX = "proxy-";
082:
083: public static final String COPLET_ID_PARAM = "copletId";
084: public static final String PORTAL_NAME_PARAM = "portalName";
085:
086: // Coplet instance data keys
087: public static final String SESSIONTOKEN = "sessiontoken";
088: public static final String COOKIE = "cookie";
089: public static final String START_URI = "start-uri";
090: public static final String LINK = "link";
091: public static final String DOCUMENT_BASE = "documentbase";
092:
093: /**
094: * Parameter for specifying the java protocol handler (used for https)
095: */
096: public static final String PROTOCOL_HANDLER_PARAMETER = "protocol-handler";
097:
098: /**
099: * The document base uri
100: */
101: protected String documentBase;
102:
103: /**
104: * The current link to the external application
105: */
106: protected String link;
107:
108: /**
109: * The default value for the envelope Tag
110: */
111: protected String defaultEnvelopeTag;
112:
113: /**
114: * This tag will include the external XHMTL
115: */
116: protected String envelopeTag;
117:
118: /**
119: * The Avalon component manager
120: */
121: protected ServiceManager manager;
122:
123: /**
124: * The coplet instance data
125: */
126: protected CopletInstanceData copletInstanceData;
127:
128: /**
129: * The original request to the portal
130: */
131: protected Request request;
132:
133: /**
134: * The encoding (JTidy constant) if configured
135: */
136: protected int configuredEncoding;
137:
138: /**
139: * The user agent identification string if confiugured
140: */
141: protected String userAgent;
142:
143: /** The sitemap parameters */
144: protected Parameters parameters;
145:
146: /** Helper for resolving input modules. */
147: protected InputModuleHelper imHelper;
148:
149: /**
150: * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
151: */
152: public void service(ServiceManager manager) throws ServiceException {
153: this .manager = manager;
154: this .imHelper = new InputModuleHelper(manager);
155: }
156:
157: /**
158: * @see org.apache.avalon.framework.activity.Disposable#dispose()
159: */
160: public void dispose() {
161: if (this .imHelper != null) {
162: this .imHelper.dispose();
163: this .imHelper = null;
164: }
165: }
166:
167: /**
168: * For the proxy transformer the envelope-tag parameter can be specified.
169: * @see org.apache.avalon.framework.parameters.Parameterizable#parameterize(Parameters)
170: */
171: public void parameterize(Parameters parameters) {
172: this .defaultEnvelopeTag = parameters.getParameter(
173: ENVELOPE_TAG_PARAMETER, null);
174: }
175:
176: /**
177: * @see org.apache.cocoon.sitemap.SitemapModelComponent#setup(SourceResolver, Map, String, Parameters)
178: */
179: public void setup(SourceResolver resolver, Map objectModel,
180: String src, Parameters parameters)
181: throws ProcessingException, SAXException, IOException {
182: this .parameters = parameters;
183: this .request = ObjectModelHelper.getRequest(objectModel);
184:
185: this .copletInstanceData = getInstanceData(this .manager,
186: objectModel, parameters);
187:
188: final CopletData copletData = this .copletInstanceData
189: .getCopletData();
190:
191: this .link = (String) this .copletInstanceData
192: .getTemporaryAttribute(LINK);
193:
194: this .documentBase = (String) this .copletInstanceData
195: .getAttribute(DOCUMENT_BASE);
196:
197: if (this .link == null) {
198: final String startURI = (String) copletData
199: .getAttribute(START_URI);
200: this .link = this .imHelper.resolve(startURI);
201: }
202:
203: if (documentBase == null) {
204: this .documentBase = this .link.substring(0, this .link
205: .lastIndexOf('/') + 1);
206: copletInstanceData.setAttribute(DOCUMENT_BASE,
207: this .documentBase);
208: }
209:
210: this .configuredEncoding = encodingConstantFromString((String) copletData
211: .getAttribute("encoding"));
212: this .userAgent = (String) copletData.getAttribute("user-agent");
213: this .envelopeTag = parameters.getParameter(
214: ENVELOPE_TAG_PARAMETER, this .defaultEnvelopeTag);
215:
216: if (envelopeTag == null) {
217: throw new ProcessingException(
218: "Can not initialize ProxyTransformer - sitemap parameter 'envelope-tag' missing");
219: }
220: }
221:
222: /**
223: * @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
224: */
225: public void recycle() {
226: super .recycle();
227: this .envelopeTag = null;
228: this .userAgent = null;
229: this .documentBase = null;
230: this .link = null;
231: this .request = null;
232: this .parameters = null;
233: this .copletInstanceData = null;
234: }
235:
236: /**
237: * @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes)
238: */
239: public void startElement(String uri, String name, String raw,
240: Attributes attributes) throws SAXException {
241: super .startElement(uri, name, raw, attributes);
242:
243: if (name.equalsIgnoreCase(this .envelopeTag)) {
244: //super.startElement(uri, name, raw, attributes);
245: processRequest();
246: //super.endElement(uri, name, raw);
247: }
248: }
249:
250: /**
251: * Processes the request to the external application
252: */
253: protected void processRequest() throws SAXException {
254: try {
255: String remoteURI = null;
256: try {
257: remoteURI = resolveURI(link, documentBase);
258: } catch (MalformedURLException ex) {
259: throw new SAXException(ex);
260: }
261: boolean firstparameter = true;
262: boolean post = ("POST".equals(request.getMethod()));
263: int pos = remoteURI.indexOf('?');
264: final StringBuffer query = new StringBuffer();
265: if (pos != -1) {
266: if (!post) {
267: query.append('?');
268: }
269: query.append(remoteURI.substring(pos + 1));
270: firstparameter = true;
271: remoteURI = remoteURI.substring(0, pos);
272: }
273:
274: // append all parameters of the current request, except those where
275: // the name of the request parameter starts with "cocoon-portal-"
276: final Enumeration enumeration = request.getParameterNames();
277: while (enumeration.hasMoreElements()) {
278: String paramName = (String) enumeration.nextElement();
279:
280: if (!paramName.startsWith("cocoon-portal-")) {
281: String[] paramValues = request
282: .getParameterValues(paramName);
283: for (int i = 0; i < paramValues.length; i++) {
284: firstparameter = this .appendParameter(query,
285: firstparameter, post, paramName,
286: paramValues[i]);
287: }
288: }
289: }
290:
291: // now append parameters from the sitemap - if any
292: final String[] names = this .parameters.getNames();
293: for (int i = 0; i < names.length; i++) {
294: if (names[i].startsWith("add:")) {
295: final String value = this .parameters
296: .getParameter(names[i]);
297: if (value != null && value.trim().length() > 0) {
298: final String pName = names[i].substring(4);
299: firstparameter = this .appendParameter(query,
300: firstparameter, post, pName, value
301: .trim());
302: }
303: }
304:
305: }
306:
307: Document result = null;
308: try {
309: do {
310: if (this .getLogger().isDebugEnabled()) {
311: this .getLogger().debug(
312: "Invoking '" + remoteURI
313: + query.toString() + "', post="
314: + post);
315: }
316: HttpURLConnection connection = connect(request,
317: remoteURI, query.toString(), post);
318: remoteURI = checkForRedirect(connection,
319: documentBase);
320:
321: if (remoteURI == null) {
322: result = readXML(connection);
323: remoteURI = checkForRedirect(result,
324: documentBase);
325: }
326: } while (remoteURI != null);
327: } catch (IOException ex) {
328: throw new SAXException("Failed to retrieve remoteURI "
329: + remoteURI, ex);
330: }
331:
332: XMLUtils.stripDuplicateAttributes(result, null);
333:
334: DOMStreamer streamer = new DOMStreamer();
335: streamer.setContentHandler(contentHandler);
336: streamer.stream(result.getDocumentElement());
337: } catch (SAXException se) {
338: throw se;
339: } catch (Exception ex) {
340: throw new SAXException(ex);
341: }
342: }
343:
344: protected boolean appendParameter(StringBuffer buffer,
345: boolean firstparameter, boolean post, String name,
346: String value) throws UnsupportedEncodingException {
347: if (firstparameter) {
348: if (!post) {
349: buffer.append('?');
350: }
351: firstparameter = false;
352: } else {
353: buffer.append('&');
354: }
355:
356: buffer.append(NetUtils.encode(name, "utf-8"));
357: buffer.append('=');
358: buffer.append(NetUtils.encode(value, "utf-8"));
359:
360: return firstparameter;
361: }
362:
363: /**
364: * Check the http status code of the http response to detect any redirects.
365: * @param connection The HttpURLConnection
366: * @param documentBase The current documentBase (needed for relative redirects)
367: * @return the redirected URL or null if no redirects are detected.
368: * @throws IOException if exceptions occure while analysing the response
369: */
370: protected String checkForRedirect(HttpURLConnection connection,
371: String documentBase) throws IOException {
372:
373: if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM
374: || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) {
375:
376: String newURI = (connection.getHeaderField("location"));
377:
378: int index_semikolon = newURI.indexOf(";");
379: int index_question = newURI.indexOf("?");
380:
381: if ((index_semikolon > -1)) {
382: String sessionToken = newURI.substring(
383: index_semikolon + 1,
384: (index_question == -1 ? newURI.length()
385: : index_question));
386: this .copletInstanceData.getPersistentAspectData().put(
387: SESSIONTOKEN, sessionToken);
388: }
389: newURI = resolveURI(newURI, documentBase);
390: return newURI;
391: }
392: return null;
393: }
394:
395: /**
396: * Analyses the XHTML response document for redirects in <meta http-equiv="refresh"> elements.
397: * @param doc The W3C DOM document containing the XHTML response
398: * @param documentBase The current document base (needed for relative redirects)
399: * @return String the redirected URL or null if no redirects are detected.
400: * @throws MalformedURLException if the redirect uri is malformed.
401: */
402: protected String checkForRedirect(Document doc, String documentBase)
403: throws MalformedURLException {
404: Element htmlElement = doc.getDocumentElement();
405: NodeList headList = htmlElement.getElementsByTagName("head");
406: if (headList.getLength() <= 0) {
407: return null;
408: }
409:
410: Element headElement = (Element) headList.item(0);
411: NodeList metaList = headElement.getElementsByTagName("meta");
412: for (int i = 0; i < metaList.getLength(); i++) {
413: Element metaElement = (Element) metaList.item(i);
414: String httpEquiv = metaElement.getAttribute("http-equiv");
415: if ("refresh".equalsIgnoreCase(httpEquiv)) {
416: String content = metaElement.getAttribute("content");
417: if (content != null) {
418: String time = content.substring(0, content
419: .indexOf(';'));
420: try {
421: if (Integer.parseInt(time) > 10) {
422: getLogger()
423: .warn(
424: "Redirects with refresh time longer than 10 seconds ("
425: + time
426: + " seconds) will be ignored!");
427: return null;
428: }
429: } catch (NumberFormatException ex) {
430: getLogger().warn(
431: "Failed to convert refresh time from redirect to integer: "
432: + time);
433: return null;
434: }
435:
436: String newURI = content.substring(content
437: .indexOf('=') + 1);
438:
439: int index_semikolon = newURI.indexOf(";");
440: int index_question = newURI.indexOf("?");
441:
442: if ((index_semikolon > -1)) {
443: String sessionToken = newURI.substring(
444: index_semikolon + 1,
445: (index_question == -1 ? newURI.length()
446: : index_question));
447: this .copletInstanceData
448: .getPersistentAspectData().put(
449: SESSIONTOKEN, sessionToken);
450: }
451: newURI = resolveURI(newURI, documentBase);
452: return newURI;
453: }
454: }
455: }
456: return null;
457: }
458:
459: /**
460: * Reads the HTML document from given connection and returns a correct W3C DOM XHTML document
461: * @param connection hte HttpURLConnection to read from
462: * @return the result as valid W3C DOM XHTML document
463: */
464: protected Document readXML(HttpURLConnection connection)
465: throws SAXException {
466: try {
467: int charEncoding = configuredEncoding;
468:
469: String contentType = connection
470: .getHeaderField("Content-Type");
471: int begin = contentType.indexOf("charset=");
472: int end = -1;
473: if (begin > -1) {
474: begin += "charset=".length();
475: end = contentType.indexOf(';', begin);
476: if (end == -1) {
477: end = contentType.length();
478: }
479: String charset = contentType.substring(begin, end);
480: charEncoding = encodingConstantFromString(charset);
481: }
482:
483: InputStream stream = connection.getInputStream();
484: // Setup an instance of Tidy.
485: Tidy tidy = new Tidy();
486: tidy.setXmlOut(true);
487:
488: tidy.setCharEncoding(charEncoding);
489: tidy.setXHTML(true);
490:
491: //Set Jtidy warnings on-off
492: tidy.setShowWarnings(this .getLogger().isWarnEnabled());
493: //Set Jtidy final result summary on-off
494: tidy.setQuiet(!this .getLogger().isInfoEnabled());
495: //Set Jtidy infos to a String (will be logged) instead of System.out
496: StringWriter stringWriter = new StringWriter();
497: //FIXME ??
498: PrintWriter errorWriter = new PrintWriter(stringWriter);
499: tidy.setErrout(errorWriter);
500: // Extract the document using JTidy and stream it.
501: Document doc = tidy.parseDOM(
502: new BufferedInputStream(stream), null);
503: errorWriter.flush();
504: errorWriter.close();
505: return doc;
506: } catch (Exception ex) {
507: throw new SAXException(ex);
508: }
509: }
510:
511: /**
512: * Helper method to convert the HTTP encoding String to JTidy encoding constants.
513: * @param encoding the HTTP encoding String
514: * @return the corresponding JTidy constant.
515: */
516: private int encodingConstantFromString(String encoding) {
517: if ("ISO8859_1".equalsIgnoreCase(encoding)) {
518: return Configuration.LATIN1;
519: } else if ("UTF-8".equalsIgnoreCase(encoding)) {
520: return Configuration.UTF8;
521: } else {
522: return Configuration.LATIN1;
523: }
524: }
525:
526: /**
527: * Establish the HttpURLConnection to the given uri.
528: * User-Agent, Accept-Language and Encoding headers will be copied from the original
529: * request, if no other headers are specified.
530: * @param request the original request
531: * @param uri the remote uri
532: * @param query the remote query string
533: * @param post true if request method was POST
534: * @return the established HttpURLConnection
535: * @throws IOException on any exception
536: */
537: protected HttpURLConnection connect(Request request, String uri,
538: String query, boolean post) throws IOException {
539:
540: String cookie = (String) copletInstanceData
541: .getAttribute(COOKIE);
542:
543: if (!post) {
544: uri = uri + query;
545: }
546:
547: URL url = new URL(uri);
548:
549: HttpURLConnection connection = (HttpURLConnection) url
550: .openConnection();
551:
552: connection.setInstanceFollowRedirects(false);
553:
554: connection.setRequestMethod(request.getMethod());
555: connection.setRequestProperty("User-Agent",
556: (userAgent != null) ? userAgent : request
557: .getHeader("User-Agent"));
558:
559: connection.setRequestProperty("Accept-Language", request
560: .getHeader("Accept-Language"));
561:
562: if (cookie != null) {
563: connection.setRequestProperty(COOKIE, cookie);
564: }
565:
566: if (post) {
567: connection.setDoOutput(true);
568: connection.setRequestProperty("Content-Type",
569: "application/x-www-form-urlencoded");
570: connection.setRequestProperty("Content-Length", String
571: .valueOf(query.length()));
572: }
573:
574: connection.connect();
575:
576: if (post) {
577: PrintWriter out = new PrintWriter(connection
578: .getOutputStream());
579: out.print(query);
580: out.close();
581: }
582:
583: copletInstanceData.setAttribute(COOKIE, connection
584: .getHeaderField(COOKIE));
585: documentBase = uri.substring(0, uri.lastIndexOf('/') + 1);
586: copletInstanceData.setAttribute(DOCUMENT_BASE, documentBase);
587: return connection;
588: }
589:
590: /**
591: * Resolve the possibly relative uri to an absolue uri based on given document base.
592: * @param uri the uri to resolve
593: * @param documentBase the current document base
594: * @return returns an absolute URI based on document base (e.g. http://mydomain.com/some/file.html)
595: * @throws MalformedURLException if uri or document base is malformed.
596: */
597: public static String resolveURI(String uri, String documentBase)
598: throws MalformedURLException {
599: if (uri == null) {
600: throw new IllegalArgumentException(
601: "URI to be resolved must not be null!");
602: }
603:
604: if (uri.indexOf("://") > -1) {
605: return uri;
606: }
607:
608: if (documentBase == null) {
609: throw new IllegalArgumentException(
610: "Documentbase String must not be null!");
611: }
612:
613: //cut ./ from uri
614: if (uri.startsWith("./")) {
615: uri = uri.substring(2);
616: }
617:
618: URL documentBaseURL = new URL(documentBase);
619:
620: //absolute uri
621: if (uri.startsWith("/")) {
622: return documentBaseURL.getProtocol() + "://"
623: + documentBaseURL.getAuthority() + uri;
624: }
625: return documentBaseURL.toExternalForm() + uri;
626: }
627:
628: public static CopletInstanceData getInstanceData(
629: ServiceManager manager, String copletID, String portalName)
630: throws ProcessingException {
631: PortalService portalService = null;
632: try {
633: portalService = (PortalService) manager
634: .lookup(PortalService.ROLE);
635:
636: ProfileManager profileManager = portalService
637: .getComponentManager().getProfileManager();
638: CopletInstanceData data = profileManager
639: .getCopletInstanceData(copletID);
640: return data;
641: } catch (ServiceException e) {
642: throw new ProcessingException(
643: "Error getting portal service.", e);
644: } finally {
645: manager.release(portalService);
646: }
647: }
648:
649: /**
650: * Method getInstanceData.
651: * @param manager
652: * @param objectModel
653: * @param parameters
654: * @return CopletInstanceData
655: * @throws ProcessingException
656: */
657: public static CopletInstanceData getInstanceData(
658: ServiceManager manager, Map objectModel,
659: Parameters parameters) throws ProcessingException {
660: PortalService portalService = null;
661: try {
662: portalService = (PortalService) manager
663: .lookup(PortalService.ROLE);
664:
665: // determine coplet id
666: String copletId = null;
667: Map context = (Map) objectModel
668: .get(ObjectModelHelper.PARENT_CONTEXT);
669: if (context != null) {
670: copletId = (String) context
671: .get(Constants.COPLET_ID_KEY);
672: if (copletId == null) {
673: throw new ProcessingException(
674: "copletId must be passed as parameter or in the object model within the parent context.");
675: }
676: } else {
677: try {
678: copletId = parameters.getParameter(COPLET_ID_PARAM);
679:
680: } catch (ParameterException e) {
681: throw new ProcessingException(
682: "copletId and portalName must be passed as parameter or in the object model within the parent context.");
683: }
684: }
685: return portalService.getComponentManager()
686: .getProfileManager()
687: .getCopletInstanceData(copletId);
688: } catch (ServiceException e) {
689: throw new ProcessingException(
690: "Error getting portal service.", e);
691: } finally {
692: manager.release(portalService);
693: }
694: }
695: }
|