/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2013 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.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 com.sun.faces.util.Util.getFacesMapping; import static com.sun.faces.util.Util.getFirstWildCardMappingToFacesServlet; import static com.sun.faces.util.Util.getLastModified; import static com.sun.faces.util.Util.isExactMapped; import static com.sun.faces.util.Util.isPrefixMapped; import static com.sun.faces.util.Util.isResourceExactMappedToFacesServlet; import static java.util.Collections.emptyMap; import static java.util.Locale.US; import static java.util.logging.Level.FINEST; import static javax.faces.application.ProjectStage.Development; import static javax.faces.application.ProjectStage.Production; import static javax.faces.application.ResourceHandler.JSF_SCRIPT_LIBRARY_NAME; import static javax.faces.application.ResourceHandler.JSF_SCRIPT_RESOURCE_NAME; import static javax.faces.application.ResourceHandler.RESOURCE_IDENTIFIER; import java.io.Externalizable; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInput; import java.io.ObjectOutput; import java.net.URL; import java.net.URLConnection; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import javax.faces.application.ProjectStage; import javax.faces.application.Resource; import javax.faces.application.ResourceHandler; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.servlet.http.HttpServletRequest; import com.sun.faces.application.ApplicationAssociate; import com.sun.faces.util.FacesLogger; /** * Default implementation of {@link javax.faces.application.Resource}. * The ResourceImpl instance itself has the same lifespan as the * request, however, the ResourceInfo instances that back this object * are cached by the ResourceManager to reduce the time spent scanning * for resources. */ public class ResourceImpl extends Resource implements Externalizable { // Log instance for this class private static final Logger LOGGER = FacesLogger.APPLICATION.getLogger(); /* HTTP Date format required by the HTTP/1.1 RFC */ private static final String RFC1123_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss zzz"; private static final String IF_MODIFIED_SINCE = "If-Modified-Since"; private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); /* The meta data on the resource */ private transient ResourceInfo resourceInfo; /* * Response headers that need to be added by the ResourceManager * implementation. */ private transient Map<String,String> responseHeaders; /** * Time when this application was started. This is used to generate * expiration headers. */ private long initialTime; /** * Lifespan of this resource for caching purposes. */ private long maxAge; // ------------------------------------------------------------ Constructors /** * Necessary for serialization. */ @SuppressWarnings({"UnusedDeclaration"}) public ResourceImpl() { } /** * Creates a new instance of ResourceBase */ public ResourceImpl(ResourceInfo resourceInfo, String contentType, long initialTime, long maxAge) { this.resourceInfo = resourceInfo; super.setResourceName(resourceInfo.getName()); super.setLibraryName(resourceInfo.getLibraryInfo() != null ? resourceInfo.getLibraryInfo().getName() : null); super.setContentType(contentType); this.initialTime = initialTime; this.maxAge = maxAge; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ResourceImpl resource = (ResourceImpl) o; return resourceInfo.equals(resource.resourceInfo); } @Override public int hashCode() { return resourceInfo.hashCode(); } // --------------------------------------------------- Methods from Resource /** * @see javax.faces.application.Resource#getInputStream() */ @Override public InputStream getInputStream() throws IOException { initResourceInfo(); return resourceInfo.getHelper().getInputStream(resourceInfo, FacesContext.getCurrentInstance()); } /** * @see javax.faces.application.Resource#getURL() */ @Override public URL getURL() { return resourceInfo.getHelper().getURL(resourceInfo, FacesContext.getCurrentInstance()); } /** * <p> * Implementation note. Any values added to getResponseHeaders() * will only be visible across multiple calls to this method when * servicing a resource request (i.e. {@link ResourceHandler#isResourceRequest(javax.faces.context.FacesContext)} * returns <code>true</code>). If we're not servicing a resource request, * an empty Map will be returned and the values added are effectively thrown * away. * </p> * * @see javax.faces.application.Resource#getResponseHeaders() */ @Override public Map<String, String> getResponseHeaders() { if (isResourceRequest()) { if (responseHeaders == null) { responseHeaders = new HashMap<>(6, 1.0f); } long expiresTime; if (FacesContext.getCurrentInstance().isProjectStage(Development)) { expiresTime = new Date().getTime(); } else { expiresTime = new Date().getTime() + maxAge; } SimpleDateFormat format = new SimpleDateFormat(RFC1123_DATE_PATTERN, US); format.setTimeZone(GMT); responseHeaders.put("Expires", format.format(new Date(expiresTime))); URL url = getURL(); InputStream in = null; try { URLConnection conn = url.openConnection(); conn.setUseCaches(false); conn.connect(); in = conn.getInputStream(); long lastModified = getLastModified(url); long contentLength = conn.getContentLength(); if (lastModified == 0) { lastModified = initialTime; } responseHeaders.put("Last-Modified", format.format(new Date(lastModified))); if (lastModified != 0 && contentLength != -1) { responseHeaders.put("ETag", "W/\"" + contentLength + '-' + lastModified + '"'); } } catch (IOException ioe) { if (LOGGER.isLoggable(FINEST)) { LOGGER.log(FINEST, "Closing stream", ioe); } } finally { if (in != null) { try { in.close(); } catch (IOException ioe) { if (LOGGER.isLoggable(FINEST)) { LOGGER.log(FINEST, "Closing stream", ioe); } } } } return responseHeaders; } else { return emptyMap(); } } /** * @see javax.faces.application.Resource#getRequestPath() */ @Override public String getRequestPath() { FacesContext context = FacesContext.getCurrentInstance(); String facesServletMapping = getFacesMapping(context); String uri = null; // Check for exact mapping first if (isExactMapped(facesServletMapping)) { String resource = RESOURCE_IDENTIFIER + '/' + getResourceName(); // Check if the FacesServlet is exact mapped to the resource if (isResourceExactMappedToFacesServlet(context.getExternalContext(), resource)) { uri = facesServletMapping + resource; } else { // No exact mapping for the requested resource, see if Facelets servlet is mapped to // e.g. /faces/* or *.xhtml and take that mapping String mapping = getFirstWildCardMappingToFacesServlet(context.getExternalContext()); if (mapping == null) { // If there are only exact mappings and the resource is not exact mapped, // we can't serve this resource throw new IllegalStateException( "No suitable mapping for FacesServlet found. To serve resources " + "FacesServlet should have at least one prefix or suffix mapping." ); } facesServletMapping = mapping.replace("*", ""); } } if (uri == null) { // If it is extension mapped if (isPrefixMapped(facesServletMapping)) { uri = facesServletMapping + RESOURCE_IDENTIFIER + '/' + getResourceName(); } else { uri = RESOURCE_IDENTIFIER + '/' + getResourceName() + facesServletMapping; } } boolean queryStarted = false; if (getLibraryName() != null) { queryStarted = true; uri += "?ln=" + getLibraryName(); } String version = ""; initResourceInfo(); if (resourceInfo.getLibraryInfo() != null && resourceInfo.getLibraryInfo().getVersion() != null) { version += resourceInfo.getLibraryInfo().getVersion().toString(); } if (resourceInfo.getVersion() != null) { version += resourceInfo.getVersion().toString(); } if (version.length() > 0) { uri += ((queryStarted) ? "&v=" : "?v=") + version; queryStarted = true; } String localePrefix = resourceInfo.getLocalePrefix(); if (localePrefix != null) { uri += ((queryStarted) ? "&loc=" : "?loc=") + localePrefix; queryStarted = true; } String contract = resourceInfo.getContract(); if (contract != null) { uri += ((queryStarted) ? "&con=" : "?con=") + contract; queryStarted = true; } if (JSF_SCRIPT_RESOURCE_NAME.equals(getResourceName()) && JSF_SCRIPT_LIBRARY_NAME.equals(getLibraryName())) { ProjectStage stage = context.getApplication().getProjectStage(); switch (stage) { case Development: uri += ((queryStarted) ? "&stage=Development" : "?stage=Development" ); break; case SystemTest: uri += ((queryStarted) ? "&stage=SystemTest" : "?stage=SystemTest" ); break; case UnitTest: uri += ((queryStarted) ? "&stage=UnitTest" : "?stage=UnitTest" ); break; default: assert(stage.equals(Production)); } } uri = context.getApplication().getViewHandler() .getResourceURL(context, uri); return uri; } /** * @see javax.faces.application.Resource#userAgentNeedsUpdate(javax.faces.context.FacesContext) */ @Override public boolean userAgentNeedsUpdate(FacesContext context) { // 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. if (resourceInfo instanceof FaceletResourceInfo) { return true; } // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html // 14.25 If-Modified-Since // if the requested variant has not been modified since the time // specified in this field, an entity will not be returned from the // server; instead, a 304 (not modified) response will be returned // without any message-body. // A date which is later than the server's current time is // invalid. Map<String,String> requestHeaders = context.getExternalContext().getRequestHeaderMap(); if (requestHeaders.containsKey(IF_MODIFIED_SINCE)) { initResourceInfo(); /* * Make sure that we strip the milliseconds out of what comes back * from the getLastModified call for a resource as the * 'If-Modified-Since' header does not use milliseconds. */ long lastModifiedOfResource = (((ClientResourceInfo)resourceInfo).getLastModified(context) / 1000) * 1000; long lastModifiedHeader = getIfModifiedHeader(context.getExternalContext()); if (0 == lastModifiedOfResource) { long startupTime = ApplicationAssociate.getInstance(context.getExternalContext()).getTimeOfInstantiation(); return startupTime > lastModifiedHeader; } else { return lastModifiedOfResource > lastModifiedHeader; } } return true; } // --------------------------------------------------------- Private Methods /* * This method should only be called if the 'If-Modified-Since' header * is present in the request header map. */ private long getIfModifiedHeader(ExternalContext extcontext) { Object request = extcontext.getRequest(); if (request instanceof HttpServletRequest) { // try to use the container where we can. V3 for instance // has a FastHttpDateFormat format/parse implementation // which is more than likely more performant than SimpleDateFormat // (otherwise, why would it be there?). return ((HttpServletRequest) request).getDateHeader(IF_MODIFIED_SINCE); } else { SimpleDateFormat format = new SimpleDateFormat(RFC1123_DATE_PATTERN, Locale.US); try { Date ifModifiedSinceDate = format.parse(extcontext.getRequestHeaderMap().get(IF_MODIFIED_SINCE)); return ifModifiedSinceDate.getTime(); } catch (ParseException ex) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, "jsf.application.resource.invalid_if_modified_since_header", new Object[]{ extcontext.getRequestHeaderMap().get(IF_MODIFIED_SINCE) }); if (ex != null) { LOGGER.log(Level.WARNING, "", ex); } } return -1; } } } // --------------------------------------------- Methods from Externalizable @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(getResourceName()); out.writeObject(getLibraryName()); out.writeObject(getContentType()); out.writeLong(initialTime); out.writeLong(maxAge); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { setResourceName((String) in.readObject()); setLibraryName((String) in.readObject()); setContentType((String) in.readObject()); initialTime = in.readLong(); maxAge = in.readLong(); } private void initResourceInfo(){ if (resourceInfo != null) { return; } ResourceManager manager = ApplicationAssociate.getInstance(FacesContext.getCurrentInstance().getExternalContext()).getResourceManager(); resourceInfo = manager.findResource(getLibraryName(), getResourceName(), getContentType(), FacesContext.getCurrentInstance()); } // --------------------------------------------------------- Private Methods private boolean isResourceRequest() { FacesContext ctx = FacesContext.getCurrentInstance(); return ctx.getApplication().getResourceHandler().isResourceRequest(ctx); } }