| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.cocoon.portal.transformation; |
| |
| import java.io.BufferedInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.net.HttpURLConnection; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.Enumeration; |
| import java.util.Map; |
| |
| import org.apache.avalon.framework.activity.Disposable; |
| import org.apache.avalon.framework.parameters.ParameterException; |
| import org.apache.avalon.framework.parameters.Parameterizable; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.environment.ObjectModelHelper; |
| import org.apache.cocoon.environment.Request; |
| import org.apache.cocoon.environment.SourceResolver; |
| import org.apache.cocoon.portal.Constants; |
| import org.apache.cocoon.portal.PortalService; |
| import org.apache.cocoon.portal.coplet.CopletData; |
| import org.apache.cocoon.portal.coplet.CopletInstanceData; |
| import org.apache.cocoon.portal.profile.ProfileManager; |
| import org.apache.cocoon.portal.util.InputModuleHelper; |
| import org.apache.cocoon.transformation.AbstractTransformer; |
| import org.apache.cocoon.util.NetUtils; |
| import org.apache.cocoon.xml.XMLUtils; |
| import org.apache.cocoon.xml.dom.DOMStreamer; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NodeList; |
| import org.w3c.tidy.Configuration; |
| import org.w3c.tidy.Tidy; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * This transformer is used to insert the XHTML data from an request |
| * to an external application at the specified element ("envelope-tag" parameter). |
| * Nesessary connection data for the external request like sessionid, cookies, |
| * documentbase, the uri, etc. will be taken from the application coplet instance |
| * data. |
| * @author <a href="mailto:friedrich.klenner@rzb.at">Friedrich Klenner</a> |
| * @author <a href="mailto:gernot.koller@rizit.at">Gernot Koller</a> |
| * |
| * @version CVS $Id$ |
| */ |
| public class ProxyTransformer |
| extends AbstractTransformer |
| implements Serviceable, Disposable, Parameterizable { |
| |
| /** |
| * Parameter for specifying the envelope tag |
| */ |
| public static final String ENVELOPE_TAG_PARAMETER = "envelope-tag"; |
| |
| public static final String PORTALNAME = "cocoon-portal-portalname"; |
| public static final String COPLETID = "cocoon-portal-copletid"; |
| public static final String PROXY_PREFIX = "proxy-"; |
| |
| public static final String COPLET_ID_PARAM = "copletId"; |
| public static final String PORTAL_NAME_PARAM = "portalName"; |
| |
| // Coplet instance data keys |
| public static final String SESSIONTOKEN = "sessiontoken"; |
| public static final String COOKIE = "cookie"; |
| public static final String START_URI = "start-uri"; |
| public static final String LINK = "link"; |
| public static final String DOCUMENT_BASE = "documentbase"; |
| |
| /** |
| * Parameter for specifying the java protocol handler (used for https) |
| */ |
| public static final String PROTOCOL_HANDLER_PARAMETER = "protocol-handler"; |
| |
| /** |
| * The document base uri |
| */ |
| protected String documentBase; |
| |
| /** |
| * The current link to the external application |
| */ |
| protected String link; |
| |
| /** |
| * The default value for the envelope Tag |
| */ |
| protected String defaultEnvelopeTag; |
| |
| /** |
| * This tag will include the external XHMTL |
| */ |
| protected String envelopeTag; |
| |
| /** |
| * The Avalon component manager |
| */ |
| protected ServiceManager manager; |
| |
| /** |
| * The coplet instance data |
| */ |
| protected CopletInstanceData copletInstanceData; |
| |
| /** |
| * The original request to the portal |
| */ |
| protected Request request; |
| |
| /** |
| * The encoding (JTidy constant) if configured |
| */ |
| protected int configuredEncoding; |
| |
| /** |
| * The user agent identification string if confiugured |
| */ |
| protected String userAgent; |
| |
| /** The sitemap parameters */ |
| protected Parameters parameters; |
| |
| /** Helper for resolving input modules. */ |
| protected InputModuleHelper imHelper; |
| |
| /** |
| * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager) |
| */ |
| public void service(ServiceManager manager) throws ServiceException { |
| this.manager = manager; |
| this.imHelper = new InputModuleHelper(manager); |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.activity.Disposable#dispose() |
| */ |
| public void dispose() { |
| if ( this.imHelper != null ) { |
| this.imHelper.dispose(); |
| this.imHelper = null; |
| } |
| } |
| |
| /** |
| * For the proxy transformer the envelope-tag parameter can be specified. |
| * @see org.apache.avalon.framework.parameters.Parameterizable#parameterize(Parameters) |
| */ |
| public void parameterize(Parameters parameters) { |
| this.defaultEnvelopeTag = parameters.getParameter(ENVELOPE_TAG_PARAMETER, null); |
| } |
| |
| /** |
| * @see org.apache.cocoon.sitemap.SitemapModelComponent#setup(SourceResolver, Map, String, Parameters) |
| */ |
| public void setup(SourceResolver resolver, |
| Map objectModel, |
| String src, |
| Parameters parameters) |
| throws ProcessingException, SAXException, IOException { |
| this.parameters = parameters; |
| this.request = ObjectModelHelper.getRequest(objectModel); |
| |
| this.copletInstanceData = getInstanceData(this.manager, objectModel, parameters); |
| |
| final CopletData copletData = this.copletInstanceData.getCopletData(); |
| |
| this.link = (String) this.copletInstanceData.getTemporaryAttribute(LINK); |
| |
| this.documentBase = (String) this.copletInstanceData.getAttribute(DOCUMENT_BASE); |
| |
| if (this.link == null) { |
| final String startURI = (String)copletData.getAttribute(START_URI); |
| this.link = this.imHelper.resolve(startURI); |
| } |
| |
| if (documentBase == null) { |
| this.documentBase = this.link.substring(0, this.link.lastIndexOf('/') + 1); |
| copletInstanceData.setAttribute(DOCUMENT_BASE, this.documentBase); |
| } |
| |
| this.configuredEncoding = encodingConstantFromString((String)copletData.getAttribute("encoding")); |
| this.userAgent = (String)copletData.getAttribute("user-agent"); |
| this.envelopeTag = parameters.getParameter(ENVELOPE_TAG_PARAMETER, this.defaultEnvelopeTag); |
| |
| if (envelopeTag == null) { |
| throw new ProcessingException("Can not initialize ProxyTransformer - sitemap parameter 'envelope-tag' missing"); |
| } |
| } |
| |
| /** |
| * @see org.apache.avalon.excalibur.pool.Recyclable#recycle() |
| */ |
| public void recycle() { |
| super.recycle(); |
| this.envelopeTag = null; |
| this.userAgent = null; |
| this.documentBase = null; |
| this.link = null; |
| this.request = null; |
| this.parameters = null; |
| this.copletInstanceData = null; |
| } |
| |
| /** |
| * @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes) |
| */ |
| public void startElement( |
| String uri, |
| String name, |
| String raw, |
| Attributes attributes) |
| throws SAXException { |
| super.startElement(uri, name, raw, attributes); |
| |
| if (name.equalsIgnoreCase(this.envelopeTag)) { |
| //super.startElement(uri, name, raw, attributes); |
| processRequest(); |
| //super.endElement(uri, name, raw); |
| } |
| } |
| |
| /** |
| * Processes the request to the external application |
| */ |
| protected void processRequest() throws SAXException { |
| try { |
| String remoteURI = null; |
| try { |
| remoteURI = resolveURI(link, documentBase); |
| } catch (MalformedURLException ex) { |
| throw new SAXException(ex); |
| } |
| boolean firstparameter = true; |
| boolean post = ("POST".equals(request.getMethod())); |
| int pos = remoteURI.indexOf('?'); |
| final StringBuffer query = new StringBuffer(); |
| if ( pos != -1 ) { |
| if ( !post ) { |
| query.append('?'); |
| } |
| query.append(remoteURI.substring(pos+1)); |
| firstparameter = true; |
| remoteURI = remoteURI.substring(0, pos); |
| } |
| |
| // append all parameters of the current request, except those where |
| // the name of the request parameter starts with "cocoon-portal-" |
| final Enumeration enumeration = request.getParameterNames(); |
| while (enumeration.hasMoreElements()) { |
| String paramName = (String) enumeration.nextElement(); |
| |
| if (!paramName.startsWith("cocoon-portal-")) { |
| String[] paramValues = request.getParameterValues(paramName); |
| for (int i = 0; i < paramValues.length; i++) { |
| firstparameter = this.appendParameter(query, firstparameter, post, paramName, paramValues[i]); |
| } |
| } |
| } |
| |
| // now append parameters from the sitemap - if any |
| final String[] names = this.parameters.getNames(); |
| for(int i=0; i<names.length; i++) { |
| if ( names[i].startsWith("add:") ) { |
| final String value = this.parameters.getParameter(names[i]); |
| if ( value != null && value.trim().length() > 0 ) { |
| final String pName = names[i].substring(4); |
| firstparameter = this.appendParameter(query, firstparameter, post, pName, value.trim()); |
| } |
| } |
| |
| } |
| |
| Document result = null; |
| try { |
| do { |
| if ( this.getLogger().isDebugEnabled() ) { |
| this.getLogger().debug("Invoking '" + remoteURI + query.toString() +"', post="+post); |
| } |
| HttpURLConnection connection = |
| connect(request, remoteURI, query.toString(), post); |
| remoteURI = checkForRedirect(connection, documentBase); |
| |
| if (remoteURI == null) { |
| result = readXML(connection); |
| remoteURI = checkForRedirect(result, documentBase); |
| } |
| } |
| while (remoteURI != null); |
| } catch (IOException ex) { |
| throw new SAXException( |
| "Failed to retrieve remoteURI " + remoteURI, |
| ex); |
| } |
| |
| XMLUtils.stripDuplicateAttributes(result, null); |
| |
| DOMStreamer streamer = new DOMStreamer(); |
| streamer.setContentHandler(contentHandler); |
| streamer.stream(result.getDocumentElement()); |
| } catch (SAXException se) { |
| throw se; |
| } catch (Exception ex) { |
| throw new SAXException(ex); |
| } |
| } |
| |
| protected boolean appendParameter(StringBuffer buffer, |
| boolean firstparameter, |
| boolean post, |
| String name, |
| String value) |
| throws UnsupportedEncodingException { |
| if (firstparameter) { |
| if (!post) { |
| buffer.append('?'); |
| } |
| firstparameter = false; |
| } else { |
| buffer.append('&'); |
| } |
| |
| buffer.append(NetUtils.encode(name, "utf-8")); |
| buffer.append('='); |
| buffer.append(NetUtils.encode(value, "utf-8")); |
| |
| return firstparameter; |
| } |
| |
| /** |
| * Check the http status code of the http response to detect any redirects. |
| * @param connection The HttpURLConnection |
| * @param documentBase The current documentBase (needed for relative redirects) |
| * @return the redirected URL or null if no redirects are detected. |
| * @throws IOException if exceptions occure while analysing the response |
| */ |
| protected String checkForRedirect( |
| HttpURLConnection connection, |
| String documentBase) |
| throws IOException { |
| |
| if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM |
| || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) { |
| |
| String newURI = (connection.getHeaderField("location")); |
| |
| int index_semikolon = newURI.indexOf(";"); |
| int index_question = newURI.indexOf("?"); |
| |
| if ((index_semikolon > -1)) { |
| String sessionToken = |
| newURI.substring( |
| index_semikolon + 1, |
| (index_question == -1 |
| ? newURI.length() |
| : index_question)); |
| this.copletInstanceData.getPersistentAspectData().put( |
| SESSIONTOKEN, |
| sessionToken); |
| } |
| newURI = resolveURI(newURI, documentBase); |
| return newURI; |
| } |
| return null; |
| } |
| |
| /** |
| * Analyses the XHTML response document for redirects in <meta http-equiv="refresh"> elements. |
| * @param doc The W3C DOM document containing the XHTML response |
| * @param documentBase The current document base (needed for relative redirects) |
| * @return String the redirected URL or null if no redirects are detected. |
| * @throws MalformedURLException if the redirect uri is malformed. |
| */ |
| protected String checkForRedirect(Document doc, String documentBase) |
| throws MalformedURLException { |
| Element htmlElement = doc.getDocumentElement(); |
| NodeList headList = htmlElement.getElementsByTagName("head"); |
| if (headList.getLength() <= 0) { |
| return null; |
| } |
| |
| Element headElement = (Element) headList.item(0); |
| NodeList metaList = headElement.getElementsByTagName("meta"); |
| for (int i = 0; i < metaList.getLength(); i++) { |
| Element metaElement = (Element) metaList.item(i); |
| String httpEquiv = metaElement.getAttribute("http-equiv"); |
| if ("refresh".equalsIgnoreCase(httpEquiv)) { |
| String content = metaElement.getAttribute("content"); |
| if (content != null) { |
| String time = |
| content.substring(0, content.indexOf(';')); |
| try { |
| if (Integer.parseInt(time) > 10) { |
| getLogger().warn( |
| "Redirects with refresh time longer than 10 seconds (" |
| + time |
| + " seconds) will be ignored!"); |
| return null; |
| } |
| } |
| catch (NumberFormatException ex) { |
| getLogger().warn( |
| "Failed to convert refresh time from redirect to integer: " |
| + time); |
| return null; |
| } |
| |
| String newURI = |
| content.substring(content.indexOf('=') + 1); |
| |
| int index_semikolon = newURI.indexOf(";"); |
| int index_question = newURI.indexOf("?"); |
| |
| if ((index_semikolon > -1)) { |
| String sessionToken = |
| newURI.substring( |
| index_semikolon + 1, |
| (index_question == -1 |
| ? newURI.length() |
| : index_question)); |
| this.copletInstanceData.getPersistentAspectData().put( |
| SESSIONTOKEN, |
| sessionToken); |
| } |
| newURI = resolveURI(newURI, documentBase); |
| return newURI; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Reads the HTML document from given connection and returns a correct W3C DOM XHTML document |
| * @param connection hte HttpURLConnection to read from |
| * @return the result as valid W3C DOM XHTML document |
| */ |
| protected Document readXML(HttpURLConnection connection) |
| throws SAXException { |
| try { |
| int charEncoding = configuredEncoding; |
| |
| String contentType = connection.getHeaderField("Content-Type"); |
| int begin = contentType.indexOf("charset="); |
| int end = -1; |
| if (begin > -1) { |
| begin += "charset=".length(); |
| end = contentType.indexOf(';', begin); |
| if (end == -1) { |
| end = contentType.length(); |
| } |
| String charset = contentType.substring(begin, end); |
| charEncoding = encodingConstantFromString(charset); |
| } |
| |
| InputStream stream = connection.getInputStream(); |
| // Setup an instance of Tidy. |
| Tidy tidy = new Tidy(); |
| tidy.setXmlOut(true); |
| |
| tidy.setCharEncoding(charEncoding); |
| tidy.setXHTML(true); |
| |
| //Set Jtidy warnings on-off |
| tidy.setShowWarnings(this.getLogger().isWarnEnabled()); |
| //Set Jtidy final result summary on-off |
| tidy.setQuiet(!this.getLogger().isInfoEnabled()); |
| //Set Jtidy infos to a String (will be logged) instead of System.out |
| StringWriter stringWriter = new StringWriter(); |
| //FIXME ?? |
| PrintWriter errorWriter = new PrintWriter(stringWriter); |
| tidy.setErrout(errorWriter); |
| // Extract the document using JTidy and stream it. |
| Document doc = tidy.parseDOM(new BufferedInputStream(stream), null); |
| errorWriter.flush(); |
| errorWriter.close(); |
| return doc; |
| } catch (Exception ex) { |
| throw new SAXException(ex); |
| } |
| } |
| |
| /** |
| * Helper method to convert the HTTP encoding String to JTidy encoding constants. |
| * @param encoding the HTTP encoding String |
| * @return the corresponding JTidy constant. |
| */ |
| private int encodingConstantFromString(String encoding) { |
| if ("ISO8859_1".equalsIgnoreCase(encoding)) { |
| return Configuration.LATIN1; |
| } |
| else if ("UTF-8".equalsIgnoreCase(encoding)) { |
| return Configuration.UTF8; |
| } |
| else { |
| return Configuration.LATIN1; |
| } |
| } |
| |
| /** |
| * Establish the HttpURLConnection to the given uri. |
| * User-Agent, Accept-Language and Encoding headers will be copied from the original |
| * request, if no other headers are specified. |
| * @param request the original request |
| * @param uri the remote uri |
| * @param query the remote query string |
| * @param post true if request method was POST |
| * @return the established HttpURLConnection |
| * @throws IOException on any exception |
| */ |
| protected HttpURLConnection connect( |
| Request request, |
| String uri, |
| String query, |
| boolean post) |
| throws IOException { |
| |
| String cookie = (String) copletInstanceData.getAttribute(COOKIE); |
| |
| if (!post) { |
| uri = uri + query; |
| } |
| |
| URL url = new URL(uri); |
| |
| HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
| |
| connection.setInstanceFollowRedirects(false); |
| |
| connection.setRequestMethod(request.getMethod()); |
| connection.setRequestProperty( |
| "User-Agent", |
| (userAgent != null) ? userAgent : request.getHeader("User-Agent")); |
| |
| connection.setRequestProperty( |
| "Accept-Language", |
| request.getHeader("Accept-Language")); |
| |
| if (cookie != null) { |
| connection.setRequestProperty(COOKIE, cookie); |
| } |
| |
| if (post) { |
| connection.setDoOutput(true); |
| connection.setRequestProperty( |
| "Content-Type", |
| "application/x-www-form-urlencoded"); |
| connection.setRequestProperty( |
| "Content-Length", |
| String.valueOf(query.length())); |
| } |
| |
| connection.connect(); |
| |
| if (post) { |
| PrintWriter out = new PrintWriter(connection.getOutputStream()); |
| out.print(query); |
| out.close(); |
| } |
| |
| copletInstanceData.setAttribute( |
| COOKIE, |
| connection.getHeaderField(COOKIE)); |
| documentBase = uri.substring(0, uri.lastIndexOf('/') + 1); |
| copletInstanceData.setAttribute(DOCUMENT_BASE, documentBase); |
| return connection; |
| } |
| |
| /** |
| * Resolve the possibly relative uri to an absolue uri based on given document base. |
| * @param uri the uri to resolve |
| * @param documentBase the current document base |
| * @return returns an absolute URI based on document base (e.g. http://mydomain.com/some/file.html) |
| * @throws MalformedURLException if uri or document base is malformed. |
| */ |
| public static String resolveURI(String uri, String documentBase) |
| throws MalformedURLException { |
| if (uri == null) { |
| throw new IllegalArgumentException("URI to be resolved must not be null!"); |
| } |
| |
| if (uri.indexOf("://") > -1) { |
| return uri; |
| } |
| |
| if (documentBase == null) { |
| throw new IllegalArgumentException("Documentbase String must not be null!"); |
| } |
| |
| //cut ./ from uri |
| if (uri.startsWith("./")) { |
| uri = uri.substring(2); |
| } |
| |
| URL documentBaseURL = new URL(documentBase); |
| |
| //absolute uri |
| if (uri.startsWith("/")) { |
| return documentBaseURL.getProtocol() |
| + "://" |
| + documentBaseURL.getAuthority() |
| + uri; |
| } |
| return documentBaseURL.toExternalForm() + uri; |
| } |
| |
| public static CopletInstanceData getInstanceData(ServiceManager manager, |
| String copletID, |
| String portalName) |
| throws ProcessingException { |
| PortalService portalService = null; |
| try { |
| portalService = (PortalService) manager.lookup(PortalService.ROLE); |
| |
| ProfileManager profileManager = portalService.getComponentManager().getProfileManager(); |
| CopletInstanceData data = profileManager.getCopletInstanceData(copletID); |
| return data; |
| } catch (ServiceException e) { |
| throw new ProcessingException("Error getting portal service.", e); |
| } finally { |
| manager.release(portalService); |
| } |
| } |
| |
| /** |
| * Method getInstanceData. |
| * @param manager |
| * @param objectModel |
| * @param parameters |
| * @return CopletInstanceData |
| * @throws ProcessingException |
| */ |
| public static CopletInstanceData getInstanceData(ServiceManager manager, |
| Map objectModel, |
| Parameters parameters) |
| throws ProcessingException { |
| PortalService portalService = null; |
| try { |
| portalService = (PortalService) manager.lookup(PortalService.ROLE); |
| |
| // determine coplet id |
| String copletId = null; |
| Map context = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); |
| if (context != null) { |
| copletId = (String) context.get(Constants.COPLET_ID_KEY); |
| if (copletId == null) { |
| throw new ProcessingException("copletId must be passed as parameter or in the object model within the parent context."); |
| } |
| } else { |
| try { |
| copletId = parameters.getParameter(COPLET_ID_PARAM); |
| |
| } catch (ParameterException e) { |
| throw new ProcessingException("copletId and portalName must be passed as parameter or in the object model within the parent context."); |
| } |
| } |
| return portalService.getComponentManager().getProfileManager().getCopletInstanceData(copletId); |
| } catch (ServiceException e) { |
| throw new ProcessingException("Error getting portal service.", e); |
| } finally { |
| manager.release(portalService); |
| } |
| } |
| } |