All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.faces.application.resource.ResourceHelper Maven / Gradle / Ivy

The newest version!
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.faces.application.resource;

import static javax.faces.application.ResourceHandler.JSF_SCRIPT_LIBRARY_NAME;
import static javax.faces.application.ResourceHandler.JSF_SCRIPT_RESOURCE_NAME;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ValueExpression;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletResponse;

import com.sun.faces.util.FacesLogger;
import com.sun.faces.util.MessageUtils;
import com.sun.faces.util.Util;

/**
 * 

* Implementations of this class contain the knowledge for finding and serving * web application resources. *

* * @since 2.0 */ public abstract class ResourceHelper { private static final Logger LOGGER = FacesLogger.RESOURCE.getLogger(); /** * This pattern represents a version for a library. * Examples: * 1_1 * 1_11 * 1_11_1 * 1_11_1_2 */ private static final Pattern LIBRARY_VERSION_PATTERN = Pattern.compile("^(\\d+)(_\\d+)+"); /** * This pattern represents a version for a resource. * Examples: * 1_1.jpg * 1_11.323 * 1_11_1.gif * 1_11_1_2.txt * 1_1 * 1_11 * 1_11_1 * 1_11_1_2 * * The extension is optional. */ private static final Pattern RESOURCE_VERSION_PATTERN = Pattern.compile("^((?:\\d+)(?:_\\d+)+)[\\.]?(\\w+)?"); /** * Arbitrary file name to write the compressed bits to. */ private static final String COMPRESSED_CONTENT_FILENAME = "compressed-content"; private static final String[] EL_CONTENT_TYPES = { "text/css", }; static { Arrays.sort(EL_CONTENT_TYPES); } // ---------------------------------------------------------- Public Methods /** * @return the base path in which resources will be stored */ public abstract String getBaseResourcePath(); public abstract String getBaseContractsPath(); protected String getBasePath(String contract) { if(contract == null) { return getBaseResourcePath(); } return getBaseContractsPath() + '/' + contract; } /** *

* If the resource is compressable, return an InputStream to read the * compressed content, otherwise, call {@link #getNonCompressedInputStream(ResourceInfo, javax.faces.context.FacesContext)} * to return the content of the original resource. *

*

* Implementation Note: If any exception occurs trying to return a stream * to the compressed content, log the exception, and instead try to return * a stream to the original content. *

* @param toStream the resource to obtain an InputStream to * @param ctx the {@link javax.faces.context.FacesContext} for the current request * @return an InputStream to the resource, or * null if no resource is found * @throws IOException if an error occurs obtaining the stream */ public InputStream getInputStream(ResourceInfo toStream, FacesContext ctx) throws IOException { // PENDING(edburns): this is a sub-optimal implementation choice // done in the interest of prototyping. It's never a good idea // to do a switch statement based on the type of an object. InputStream in = null; if (toStream instanceof ClientResourceInfo) { ClientResourceInfo resource = (ClientResourceInfo) toStream; in = getInputStreamFromClientInfo(resource, ctx); if (null == in) { ClientResourceInfo resourceWithoutLocalePrefix = new ClientResourceInfo(resource, false); in = getInputStreamFromClientInfo(resourceWithoutLocalePrefix, ctx); if (null != in) { resource.copy(resourceWithoutLocalePrefix); } } } // else { // // PENDING(edburns): get the input stream from the facelet ResourceInfo. // } return in; } private InputStream getInputStreamFromClientInfo(ClientResourceInfo resource, FacesContext ctx) throws IOException { InputStream in = null; if (resource.isCompressable() && clientAcceptsCompression(ctx)) { if (!resource.supportsEL()) { try { String path = resource.getCompressedPath(); in = new BufferedInputStream( new FileInputStream(path + File.separatorChar + COMPRESSED_CONTENT_FILENAME)); } catch (IOException ioe) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, ioe.getMessage(), ioe); } // return null so that the override code will try to serve // the non-compressed content in = null; } } else { byte[] buf = new byte[512]; try (InputStream temp = new BufferedInputStream( new ELEvaluatingInputStream(ctx, resource, getNonCompressedInputStream(resource, ctx))); ByteArrayOutputStream baos = new ByteArrayOutputStream(512); OutputStream out = new GZIPOutputStream(baos);) { // using dynamic compression here for (int read = temp.read(buf); read != -1; read = temp.read(buf)) { out.write(buf, 0, read); } in = new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())); } catch (IOException ioe) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, ioe.getMessage(), ioe); } } } } if (in == null) { if (resource.supportsEL()) { return new BufferedInputStream( new ELEvaluatingInputStream(ctx, resource, getNonCompressedInputStream(resource, ctx))); } else { in = getNonCompressedInputStream(resource, ctx); } } return in; } /** * @param resource the resource to obtain a URL reference to * @param ctx the {@link FacesContext} for the current request * @return a URL to the specified resource, otherwise null * if no resource is found */ public abstract URL getURL(ResourceInfo resource, FacesContext ctx); /** * Search for the specified library/localPrefix combination in an * implementation dependent manner. * * @param libraryName the name of the library * @param localePrefix the logicial identifier for a locale specific library. * if no localePrefix is configured, pass null * @param contract the name of the contract *@param ctx the {@link javax.faces.context.FacesContext} for the current request @return a {@link LibraryInfo} if a matching library based off the inputs * can be found, otherwise returns null */ public abstract LibraryInfo findLibrary(String libraryName, String localePrefix, String contract, FacesContext ctx); /** *

* Search for the specified resource based in the library/localePrefix/resourceName * combination in an implementation dependent manner. *

*

* If the resource is found, and is compressable, call {@link #handleCompression(com.sun.faces.application.resource.ClientResourceInfo)} * to compress the content. *

* * @param library the library this resource should be a part of. If the * the resource that is being searched for isn't part of a library, then * pass null * @param resourceName the name of the resource that is being searched for * @param localePrefix the logicial identifier for a locale specific library. * if no localePrefix is configured, pass null * @param compressable true if the resource can be compressed * @param ctx the {@link javax.faces.context.FacesContext} for the current request * @return a {@link ResourceInfo} if a matching resource based off the inputs * can be found, otherwise returns null */ public abstract ResourceInfo findResource(LibraryInfo library, String resourceName, String localePrefix, boolean compressable, FacesContext ctx); /** *

* The default implementation of this method will call through to * {@link ResourceHelper#getURL(ResourceInfo, javax.faces.context.FacesContext)} * and leverage the URL to obtain the date information of the resource and * return the value of URLConnection.getLastModified() *

* @param resource the resource in question * @param ctx the {@link FacesContext} for the current request * @return the date of the resource in milliseconds (since epoch), * or 0 if the date cannot be determined */ public long getLastModified(ResourceInfo resource, FacesContext ctx) { URL url = getURL(resource, ctx); // resource may have been deleted. if (url == null) { return 0; } return Util.getLastModified(url); } // ------------------------------------------------------- Protected Methods /** * If a {@link ResourceInfo} is not compressable, {@link #getInputStream(ResourceInfo, javax.faces.context.FacesContext)} * will call this method to return a stream to the actual resource. * * @param info the resource to obtain an InputStream to * @param ctx the {@link FacesContext} for the current request * @return an InputStream to the resource * @throws IOException if an error occurs obtaining the stream */ protected abstract InputStream getNonCompressedInputStream(ResourceInfo info, FacesContext ctx) throws IOException; /** *

* Given a collection of path names: *

*
     *   1.1, scripts, images, 1.2
     * 
*

* this method will pick out the directories that represent a library or * resource version and return the latest version found, if any. *

* * @param resourcePaths a collection of paths (consisting of single path * elements) * @param isResource true if the version being looked up * is for a reource, otherwise, pass false if the version * is a library version * @return the latest version or if no version can be detected, otherwise * this method returns null */ protected VersionInfo getVersion(Collection resourcePaths, boolean isResource) { List versionedPaths = new ArrayList<>(resourcePaths.size()); for (String p : resourcePaths) { VersionInfo vp = getVersion(p, isResource); if (vp != null) { versionedPaths.add(vp); } } VersionInfo version = null; if (!versionedPaths.isEmpty()) { Collections.sort(versionedPaths); version = versionedPaths.get(versionedPaths.size() - 1); } return version; } /** * Utility method to compress the content of the original resource * to the temporary directory specified by {@link com.sun.faces.application.resource.ClientResourceInfo#getCompressedPath()}. * * @param info the resource to be compressed * @return true if compression succeeded and the compressed * result is smaller than the original content, otherwise false * @throws IOException if any error occur reading/writing */ protected boolean compressContent(ClientResourceInfo info) throws IOException { InputStream source = null; OutputStream dest = null; try { URL url = info.getHelper() .getURL(info, FacesContext.getCurrentInstance()); URLConnection conn = url.openConnection(); conn.setUseCaches(false); conn.connect(); source = conn.getInputStream(); byte[] buf = new byte[512]; ByteArrayOutputStream baos = new ByteArrayOutputStream(); dest = new GZIPOutputStream(baos); int totalRead = 0; for (int len = source.read(buf); len != -1; len = source.read(buf)) { dest.write(buf, 0, len); totalRead += len; } dest.flush(); try { dest.close(); } catch (IOException ioe) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "Closing stream", ioe); } } if (baos.size() < totalRead) { String outputFile = info.getCompressedPath() + File.separatorChar + COMPRESSED_CONTENT_FILENAME; dest = new FileOutputStream(outputFile); dest.write(baos.toByteArray()); dest.flush(); return true; } return false; } finally { if (source != null) { try { source.close(); } catch (IOException ioe) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "Closing stream", ioe); } } } if (dest != null) { try { dest.close(); } catch (IOException ioe) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "Closing stream", ioe); } } } } } /** *

* This method attempt to verify that the user agent can accept a gzip * encoded response by interrogating the Accept-Encoding * requester header. If it is determined safe to send a gzip encoded * response, send the Content-Encoding header with a value * of gzip.

* *

* See * Utility method to peform the necessary actions to compress content. *

* *

* Implmentation Note: If an exception occurs while compressing the content, * log the IOException and rebuilt the {@link ResourceInfo} as non-compressable. *

* * @param resource the resource to compression * @return the ResourceInfo after compression is complete. If compression * was successful, this should be the same instance. If compression was * not successful, it will be a different instance than what was passed. */ protected ClientResourceInfo handleCompression(ClientResourceInfo resource) { try { if (!resource.supportsEL() && !compressContent(resource)) { resource = rebuildAsNonCompressed(resource); } } catch (IOException ioe) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, ioe.getMessage(), ioe); } resource = rebuildAsNonCompressed(resource); } return resource; } protected boolean resourceSupportsEL(String resourceName, String libraryName, FacesContext ctx) { ExternalContext extContext = ctx.getExternalContext(); String contentType = extContext.getMimeType(resourceName); boolean result = (contentType != null && (Arrays.binarySearch(EL_CONTENT_TYPES, contentType) >= 0)) || (null != resourceName && null != libraryName && JSF_SCRIPT_LIBRARY_NAME.equals(libraryName) && JSF_SCRIPT_RESOURCE_NAME.equals(resourceName)); return result; } /** * @param s input String * @return the String without a leading slash if it has one. */ protected String trimLeadingSlash(String s) { if (s.charAt(0) == '/') { return s.substring(1); } else { return s; } } // --------------------------------------------------------- Private Methods private ClientResourceInfo rebuildAsNonCompressed(ClientResourceInfo resource) { LibraryInfo library = resource.getLibraryInfo(); ClientResourceInfo ret; if (library != null) { ret = new ClientResourceInfo(resource.library, resource.contract, resource.name, resource.version, false, resource.supportsEL, resource.isDevStage, resource.cacheTimestamp); } else { ret = new ClientResourceInfo(resource.contract, resource.name, resource.version, resource.localePrefix, this, false, resource.supportsEL, resource.isDevStage, resource.cacheTimestamp); } return ret; } /** * @param pathElement the path element to verify * @param isResource true if the version being looked up * is for a reource, otherwise, pass false if the version * is a library version * @return true if this path element represents a version * (i.e. matches {@link #LIBRARY_VERSION_PATTERN}), otherwise * returns false */ private VersionInfo getVersion(String pathElement, boolean isResource) { Map appMap = FacesContext.getCurrentInstance().getExternalContext().getApplicationMap(); String[] pathElements = Util.split(appMap, pathElement, "/"); String path = pathElements[pathElements.length - 1]; String extension = null; if (isResource) { Matcher m = RESOURCE_VERSION_PATTERN.matcher(path); return ((m.matches()) ? new VersionInfo(m.group(1), m.group(2)) : null); } else { return ((LIBRARY_VERSION_PATTERN.matcher(path).matches()) ? new VersionInfo(path, extension) : null); } } // ---------------------------------------------------------- Nested Classes private static final class ELEvaluatingInputStream extends InputStream { // Premature optimization is the root of all evil. Blah blah. private List buf = new ArrayList<>(1024); private boolean failedExpressionTest = false; private boolean writingExpression = false; private InputStream inner; private ClientResourceInfo info; private FacesContext ctx; private boolean expressionEvaluated; private boolean endOfStreamReached; // ---------------------------------------------------- Constructors public ELEvaluatingInputStream(FacesContext ctx, ClientResourceInfo info, InputStream inner) { this.inner = inner; this.info = info; this.ctx = ctx; } // ------------------------------------------------ Methods from InputStream @Override public int read() throws IOException { if (null == inner) { return -1; } int i; char c; if (failedExpressionTest) { i = nextRead; nextRead = -1; failedExpressionTest = false; } else if (writingExpression) { if (0 < buf.size()) { i = buf.remove(0); } else { writingExpression = false; i = inner.read(); } } else { // Read a character. i = inner.read(); c = (char) i; // If it *might* be an expression... if (c == '#') { // read another character. i = inner.read(); c = (char) i; // If it's '{', assume we have an expression. if (c == '{') { // read it into the buffer, and evaluate it into the // same buffer. readExpressionIntoBufferAndEvaluateIntoBuffer(); // set the flag so that we need to return content // from the buffer. writingExpression = true; // Make sure to swallow the '{'. i = this.read(); } else { // It's not an expression, we need to return '#', i = '#'; // then return whatever we just read, on the // *next* read; nextRead = c; failedExpressionTest = true; } } } if (i == -1) { endOfStreamReached = true; } return i; } private int nextRead = -1; private void readExpressionIntoBufferAndEvaluateIntoBuffer() throws IOException { int i; char c; do { i = inner.read(); c = (char) i; if (c == '}') { evaluateExpressionIntoBuffer(); } else { buf.add(i); } } while (c != '}' && i != -1); } /* * At this point, we know that getBuf() returns a List * that contains the bytes of the expression. * Turn it into a String, turn the String into a ValueExpression, * evaluate it, store the toString() of it in * expressionResult; */ private void evaluateExpressionIntoBuffer() { char chars[] = new char[buf.size()]; for (int i = 0, len = buf.size(); i < len; i++) { chars[i] = (char) (int) buf.get(i); } String expressionBody = new String(chars); int colon; // If this expression contains a ":" if (-1 != (colon = expressionBody.indexOf(":"))) { // Make sure it contains only one ":" if (!isPropertyValid(expressionBody)) { String message = MessageUtils .getExceptionMessageString(MessageUtils.INVALID_RESOURCE_FORMAT_COLON_ERROR, expressionBody); throw new ELException(message); } Map appMap = FacesContext.getCurrentInstance().getExternalContext().getApplicationMap(); String[] parts = Util.split(appMap, expressionBody, ":"); if (null == parts[0] || null == parts[1]) { String message = MessageUtils .getExceptionMessageString(MessageUtils.INVALID_RESOURCE_FORMAT_NO_LIBRARY_NAME_ERROR, expressionBody); throw new ELException(message); } try { int mark = parts[0].indexOf("[") + 2; char quoteMark = parts[0].charAt(mark - 1); parts[0] = parts[0].substring(mark, colon); if (parts[0].equals("this")) { LibraryInfo libInfo = info.getLibraryInfo(); if (null != libInfo) { parts[0] = libInfo.getName(); } else if (null != info.getContract()) { parts[0] = info.getContract(); } else { throw new NullPointerException("Resource expression is not a library or resource library contract"); } mark = parts[1].indexOf("]") - 1; parts[1] = parts[1].substring(0, mark); expressionBody = "resource[" + quoteMark + parts[0] + ":" + parts[1] + quoteMark + "]"; } } catch (Exception e) { String message = MessageUtils .getExceptionMessageString(MessageUtils.INVALID_RESOURCE_FORMAT_ERROR, expressionBody); throw new ELException(message); } } ELContext elContext = ctx.getELContext(); expressionEvaluated = true; ValueExpression ve = ctx.getApplication().getExpressionFactory(). createValueExpression(elContext, "#{" + expressionBody + "}", String.class); Object value = ve.getValue(elContext); String expressionResult = ((value != null) ? value.toString() : ""); buf.clear(); for (int i = 0, len = expressionResult.length(); i < len; i++) { buf.add((int) expressionResult.charAt(i)); } } @Override public void close() throws IOException { if (endOfStreamReached && !expressionEvaluated) { info.disableEL(); } inner.close(); super.close(); } private boolean isPropertyValid(String property) { int idx = property.indexOf(':'); return (property.indexOf(':', idx + 1) == -1); } } // END ELEvaluatingInputStream }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy