/** * Copyright (C) 2010 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.util; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileItemFactory; import org.apache.commons.fileupload.disk.DiskFileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.log4j.Logger; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.externalcontext.WebAppListener; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.processor.generator.RequestGenerator; import org.orbeon.oxf.resources.ResourceManagerWrapper; import org.orbeon.oxf.resources.URLFactory; import org.orbeon.oxf.xml.SAXUtils; import org.orbeon.oxf.xml.XMLReceiverAdapter; import org.orbeon.oxf.xml.dom4j.Dom4jUtils; import org.orbeon.oxf.xml.dom4j.LocationData; import javax.servlet.http.HttpServletRequest; import java.io.*; import java.net.*; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class NetUtils { private static final Pattern PATTERN_NO_AMP; public static final int COPY_BUFFER_SIZE = 8192; public static final String STANDARD_PARAMETER_ENCODING = "utf-8"; private static FileItemFactory fileItemFactory; public static final int REQUEST_SCOPE = 0; public static final int SESSION_SCOPE = 1; public static final int APPLICATION_SCOPE = 2; static { final String token = "[^=&]"; PATTERN_NO_AMP = Pattern.compile( "(" + token + "+)=(" + token + "*)(?:&|(?<!&)\\z)" ); } /** * Return true if the document was modified since the given date, based on the If-Modified-Since * header. If the request method was not "GET", or if no valid lastModified value was provided, * consider the document modified. */ public static boolean checkIfModifiedSince(ExternalContext.Request request, long lastModified, Logger logger) { // Do the check only for the GET method if (!"GET".equals(request.getMethod()) || lastModified <= 0) return true; // Check dates final String ifModifiedHeader = StringConversions.getFirstValueFromStringArray(request.getHeaderValuesMap().get("if-modified-since")); if (logger.isDebugEnabled()) logger.debug("Found If-Modified-Since header"); if (ifModifiedHeader != null) { try { long dateTime = DateUtils.parseRFC1123(ifModifiedHeader); if (lastModified <= (dateTime + 1000)) { if (logger.isDebugEnabled()) logger.debug("Sending SC_NOT_MODIFIED response"); return false; } } catch (Exception e) {// used to be ParseException, but NumberFormatException may be thrown as well // Ignore } } return true; } /** * Return a request path info that looks like what one would expect. The path starts with a "/", relative to the * servlet context. If the servlet was included or forwarded to, return the path by which the *current* servlet was * invoked, NOT the path of the calling servlet. * * Request path = servlet path + path info. * * @param request servlet HTTP request * @return path */ public static String getRequestPathInfo(HttpServletRequest request) { // NOTE: Servlet 2.4 spec says: "These attributes [javax.servlet.include.*] are accessible from the included // servlet via the getAttribute method on the request object and their values must be equal to the request URI, // context path, servlet path, path info, and query string of the included servlet, respectively." // NOTE: This is very different from the similarly-named forward attributes, which reflect the values of the // first servlet in the chain! // Get servlet path String servletPath = (String) request.getAttribute("javax.servlet.include.servlet_path"); if (servletPath == null) { servletPath = request.getServletPath(); if (servletPath == null) servletPath = ""; } // Get path info String pathInfo = (String) request.getAttribute("javax.servlet.include.path_info"); if (pathInfo == null) { pathInfo = request.getPathInfo(); if (pathInfo == null) pathInfo = ""; } // Concatenate servlet path and path info, avoiding a double slash String requestPath = servletPath.endsWith("/") && pathInfo.startsWith("/") ? servletPath + pathInfo.substring(1) : servletPath + pathInfo; // Add starting slash if missing if (!requestPath.startsWith("/")) requestPath = "/" + requestPath; return requestPath; } /** * Return the last modification date of the given absolute URL if it is "fast" to do so, i.e. if it is an "oxf:" or * a "file:" protocol. * * @param absoluteURL absolute URL to check * @return last modification date if "fast" or 0 if not fast or if an error occurred */ public static long getLastModifiedIfFast(String absoluteURL) { final long lastModified; if (absoluteURL.startsWith("oxf:") || absoluteURL.startsWith("file:")) { try { lastModified = getLastModified(URLFactory.createURL(absoluteURL)); } catch (IOException e) { throw new OXFException(e); } } else { // Value of 0 for lastModified will cause XFormsResourceServer to set Last-Modified and Expires properly to "now". lastModified = 0; } return lastModified; } /** * Get the last modification date of a URL. * * @return last modified timestamp, null if le 0 */ public static Long getLastModifiedAsLong(URL url) throws IOException { final long connectionLastModified = getLastModified(url); // Zero and negative values often have a special meaning, make sure to normalize here return connectionLastModified <= 0 ? null : connectionLastModified; } /** * Get the last modification date of a URL. * * @return last modified timestamp "as is" */ public static long getLastModified(URL url) throws IOException { if ("file".equals(url.getProtocol())) { // Optimize file: access. Also, this prevents throwing an exception if the file doesn't exist as we try to close the stream below. return new File(URLDecoder.decode(url.getFile(), STANDARD_PARAMETER_ENCODING)).lastModified(); } else { // Use URLConnection final URLConnection urlConnection = url.openConnection(); if (urlConnection instanceof HttpURLConnection) ((HttpURLConnection) urlConnection).setRequestMethod("HEAD"); try { return getLastModified(urlConnection); } finally { final InputStream is = urlConnection.getInputStream(); if (is != null) is.close(); } } } /** * Get the last modification date of an open URLConnection. * * This handles the (broken at some point in the Java libraries) case of the file: protocol. * * @return last modified timestamp "as is" */ public static long getLastModified(URLConnection urlConnection) { try { long lastModified = urlConnection.getLastModified(); if (lastModified == 0 && "file".equals(urlConnection.getURL().getProtocol())) lastModified = new File(URLDecoder.decode(urlConnection.getURL().getFile(), STANDARD_PARAMETER_ENCODING)).lastModified(); return lastModified; } catch (UnsupportedEncodingException e) { // Should not happen as we are using a required encoding throw new OXFException(e); } } /** * Check if an URL is relative to another URL. */ public static boolean relativeURL(URL url1, URL url2) { return ((url1.getProtocol() == null && url2.getProtocol() == null) || url1.getProtocol().equals(url2.getProtocol())) && ((url1.getAuthority() == null && url2.getAuthority() == null) || url1.getAuthority().equals(url2.getAuthority())) && ((url1.getPath() == null && url2.getPath() == null) || url2.getPath().startsWith(url1.getPath())); } public static void copyStream(InputStream is, OutputStream os) throws IOException { int count; final byte[] buffer = new byte[COPY_BUFFER_SIZE]; while ((count = is.read(buffer)) > 0) os.write(buffer, 0, count); } public static void copyStream(Reader reader, Writer writer) throws IOException { int count; final char[] buffer = new char[COPY_BUFFER_SIZE / 2]; while ((count = reader.read(buffer)) > 0) writer.write(buffer, 0, count); } public static String readStreamAsString(Reader reader) throws IOException { final StringBuilderWriter writer = new StringBuilderWriter(); copyStream(reader, writer); return writer.toString(); } public static Map<String, String> getContentTypeParameters(String contentType) { if (contentType == null) return Collections.emptyMap(); // Check whether there may be parameters final int semicolonIndex = contentType.indexOf(";"); if (semicolonIndex == -1) return Collections.emptyMap(); // Tokenize final StringTokenizer st = new StringTokenizer(contentType, ";"); if (!st.hasMoreTokens()) return Collections.emptyMap(); // should not happen as there should be at least the content type st.nextToken(); // No parameters if (!st.hasMoreTokens()) return Collections.emptyMap(); // Parse parameters final Map<String, String> parameters = new HashMap<String, String>(); while (st.hasMoreTokens()) { final String parameter = StringUtils.trimAllToEmpty(st.nextToken()); final int equalIndex = parameter.indexOf('='); if (equalIndex == -1) continue; final String name = StringUtils.trimAllToEmpty(parameter.substring(0, equalIndex)); final String value = StringUtils.trimAllToEmpty(parameter.substring(equalIndex + 1)); parameters.put(name, value); } return parameters; } /** * @param queryString a query string of the form n1=v1&n2=v2&... to decode. May be null. * * @return a Map of String[] indexed by name, an empty Map if the query string was null */ public static Map<String, String[]> decodeQueryString(final CharSequence queryString) { final Map<String, String[]> result = new LinkedHashMap<String, String[]>(); if (queryString != null) { final Matcher matcher = PATTERN_NO_AMP.matcher(queryString); int matcherEnd = 0; while (matcher.find()) { matcherEnd = matcher.end(); try { final String name = URLDecoder.decode(matcher.group(1), NetUtils.STANDARD_PARAMETER_ENCODING); final String value = URLDecoder.decode(matcher.group(2), NetUtils.STANDARD_PARAMETER_ENCODING); StringConversions.addValueToStringArrayMap(result, name, value); } catch (UnsupportedEncodingException e) { // Should not happen as we are using a required encoding throw new OXFException(e); } } if (queryString.length() != matcherEnd) { // There was garbage at the end of the query. throw new OXFException("Malformed URL: " + queryString); } } return result; } private static final Pattern PATTERN_AMP; static { final String token = "[^=&]+"; PATTERN_AMP = Pattern.compile( "(" + token + ")=(" + token + ")?(?:&|&|(?<!&|&)\\z)" ); } // This is a modified copy of decodeQueryString() above. Not sure why we need 2 versions! Try to avoid duplication! public static Map<String, String[]> decodeQueryStringPortlet(final CharSequence queryString) { final Map<String, String[]> result = new LinkedHashMap<String, String[]>(); if (queryString != null) { final Matcher matcher = PATTERN_AMP.matcher(queryString); int matcherEnd = 0; while (matcher.find()) { matcherEnd = matcher.end(); try { String name = URLDecoder.decode(matcher.group(1), STANDARD_PARAMETER_ENCODING); String group2 = matcher.group(2); final String value = group2 != null ? URLDecoder.decode(group2, STANDARD_PARAMETER_ENCODING) : ""; // Handle the case where the source contains &amp; because of double escaping which does occur in // full Ajax updates! if (name.startsWith("amp;")) name = name.substring("amp;".length()); // NOTE: Replace spaces with '+'. This is an artifact of the fact that URLEncoder/URLDecoder // are not fully reversible. StringConversions.addValueToStringArrayMap(result, name, value.replace(' ', '+')); } catch (UnsupportedEncodingException e) { // Should not happen as we are using a required encoding throw new OXFException(e); } } if (queryString.length() != matcherEnd) { // There was garbage at the end of the query. throw new OXFException("Malformed URL: " + queryString); } } return result; } /** * Encode a query string. The input Map contains names indexing Object[]. */ public static String encodeQueryString(Map<String, Object[]> parameters) { final StringBuilder sb = new StringBuilder(100); boolean first = true; try { for (final Map.Entry<String, Object[]> entry : parameters.entrySet()) { for (final Object currentValue : entry.getValue()) { if (currentValue instanceof String) { if (!first) sb.append('&'); sb.append(URLEncoder.encode(entry.getKey(), NetUtils.STANDARD_PARAMETER_ENCODING)); sb.append('='); sb.append(URLEncoder.encode((String) currentValue, NetUtils.STANDARD_PARAMETER_ENCODING)); first = false; } } } } catch (UnsupportedEncodingException e) { // Should not happen as we are using a required encoding throw new OXFException(e); } return sb.toString(); } public static String encodeQueryString2(Map<String, String[]> parameters) { final StringBuilder sb = new StringBuilder(100); boolean first = true; try { for (final Map.Entry<String, String[]> entry : parameters.entrySet()) { for (final Object currentValue : entry.getValue()) { if (!first) sb.append('&'); sb.append(URLEncoder.encode(entry.getKey(), NetUtils.STANDARD_PARAMETER_ENCODING)); sb.append('='); sb.append(URLEncoder.encode((String) currentValue, NetUtils.STANDARD_PARAMETER_ENCODING)); first = false; } } } catch (UnsupportedEncodingException e) { // Should not happen as we are using a required encoding throw new OXFException(e); } return sb.toString(); } /** * Combine a path (possibly with parameters) and a parameters map to form a path info with a query string. */ public static String pathInfoParametersToPathInfoQueryString(String path, Map<String, String[]> parameters) throws IOException { final StringBuilder redirectURL = new StringBuilder(path); if (parameters != null) { boolean first = ! path.contains("?"); for (String name : parameters.keySet()) { final String[] values = parameters.get(name); for (final String currentValue : values) { redirectURL.append(first ? "?" : "&"); redirectURL.append(URLEncoder.encode(name, NetUtils.STANDARD_PARAMETER_ENCODING)); redirectURL.append("="); redirectURL.append(URLEncoder.encode(currentValue, NetUtils.STANDARD_PARAMETER_ENCODING)); first = false; } } } return redirectURL.toString(); } /** * Append a query string to an URL. This adds a '?' or a '&' or nothing, as needed. * * @param urlString existing URL string * @param queryString query string, or null * @return resulting URL */ public static String appendQueryString(String urlString, String queryString) { if (org.apache.commons.lang3.StringUtils.isBlank(queryString)) { return urlString; } else { final StringBuilder updatedActionStringBuilder = new StringBuilder(urlString); updatedActionStringBuilder.append((urlString.indexOf('?') == -1) ? '?' : '&'); updatedActionStringBuilder.append(queryString); return updatedActionStringBuilder.toString(); } } public static String removeQueryString(String urlString) { final int questionIndex = urlString.indexOf('?'); if (questionIndex == -1) return urlString; else return urlString.substring(0, questionIndex); } /** * Check whether a URL starts with a protocol. * * We consider that a protocol consists only of ASCII letters and must be at least two * characters long, to avoid confusion with Windows drive letters. */ public static boolean urlHasProtocol(String urlString) { return getProtocol(urlString) != null; } public static String getProtocol(String urlString) { int colonIndex = urlString.indexOf(":"); // Require at least two characters in a protocol if (colonIndex < 2) return null; // Check that there is a protocol made only of letters for (int i = 0; i < colonIndex; i++) { final char c = urlString.charAt(i); if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) { return null; } } return urlString.substring(0, colonIndex); } /** * Resolve a URI against a base URI. (Be sure to pay attention to the order or parameters.) * * @param href URI to resolve (accept human-readable URI) * @param base URI base (accept human-readable URI) * @return resolved URI */ public static String resolveURI(String href, String base) { final String resolvedURIString; if (base != null) { final URI baseURI; try { baseURI = new URI(encodeHRRI(base, true)); } catch (URISyntaxException e) { throw new OXFException(e); } resolvedURIString = baseURI.resolve(encodeHRRI(href, true)).normalize().toString();// normalize to remove "..", etc. } else { resolvedURIString = encodeHRRI(href, true); } return resolvedURIString; } public static byte[] base64StringToByteArray(String base64String) { return Base64.decode(base64String); } /** * Convert a String in xs:base64Binary to an xs:anyURI. * * NOTE: The implementation creates a temporary file. The Pipeline Context is required so * that the file can be deleted when no longer used. */ public static String base64BinaryToAnyURI(String value, int scope, Logger logger) { // Convert Base64 to binary first final byte[] bytes = base64StringToByteArray(value); return inputStreamToAnyURI(new ByteArrayInputStream(bytes), scope, logger); } /** * Read an InputStream into a byte array. * * @param is InputStream * @return byte array */ public static byte[] inputStreamToByteArray(InputStream is) { try { final ByteArrayOutputStream os = new ByteArrayOutputStream(); copyStream(new BufferedInputStream(is), os); os.close(); return os.toByteArray(); } catch (Exception e) { throw new OXFException(e); } } public static InputStream uriToInputStream(String uri) throws Exception { return new URI(uri).toURL().openStream(); } // NOTE: Used by create-test-data.xpl //@XPathFunction public static String createTemporaryFile(int scope) { return inputStreamToAnyURI(new InputStream() { @Override public int read() { return -1; } }, scope, null); } /** * Convert an InputStream to an xs:anyURI. * * The implementation creates a temporary file. */ public static String inputStreamToAnyURI(InputStream inputStream, int scope, Logger logger) { // Get FileItem final FileItem fileItem = prepareFileItemFromInputStream(inputStream, scope, logger); // Return a file URL final File storeLocation = ((DiskFileItem) fileItem).getStoreLocation(); // Escape "+" because at least in one environment (JBoss 5.1.0 GA on OS X) not escaping the "+" in a file URL causes later incorrect conversion to space return storeLocation.toURI().toString().replace("+", "%2B"); } private static FileItem prepareFileItemFromInputStream(InputStream inputStream, int scope, Logger logger) { // Get FileItem final FileItem fileItem = prepareFileItem(scope, logger); // Write to file OutputStream os = null; try { os = fileItem.getOutputStream(); copyStream(inputStream, os); } catch (IOException e) { throw new OXFException(e); } finally { if (os != null) { try { os.close(); } catch (IOException e) { throw new OXFException(e); } } } // Create file if it doesn't exist (necessary when the file size is 0) final File storeLocation = ((DiskFileItem) fileItem).getStoreLocation(); try { storeLocation.createNewFile(); } catch (IOException e) { throw new OXFException(e); } return fileItem; } /** * Return a FileItem which is going to be automatically destroyed upon destruction of the request, session or * application. */ public static FileItem prepareFileItem(int scope, Logger logger) { // We use the commons file upload utilities to save a file if (fileItemFactory == null) fileItemFactory = new DiskFileItemFactory(0, SystemUtils.getTemporaryDirectory()); final FileItem fileItem = fileItemFactory.createItem("dummy", "dummy", false, null); // Make sure the file is deleted appropriately if (scope == REQUEST_SCOPE) { deleteFileOnRequestEnd(fileItem, logger); } else if (scope == SESSION_SCOPE) { deleteFileOnSessionTermination(fileItem, logger); } else if (scope == APPLICATION_SCOPE) { deleteFileOnApplicationDestroyed(fileItem, logger); } else { throw new OXFException("Invalid context requested: " + scope); } // Return FileItem object return fileItem; } /** * Add listener to fileItem which is going to be automatically destroyed at the end of request * * @param fileItem FileItem */ public static void deleteFileOnRequestEnd(final FileItem fileItem, final Logger logger) { // Make sure the file is deleted at the end of request PipelineContext.get().addContextListener(new PipelineContext.ContextListenerAdapter() { public void contextDestroyed(boolean success) { deleteFileItem(fileItem, REQUEST_SCOPE, logger); } }); } /** * Add listener to fileItem which is going to be automatically destroyed on session destruction * * @param fileItem FileItem */ public static void deleteFileOnSessionTermination(final FileItem fileItem, final Logger logger) { // Try to delete the file on exit and on session termination final ExternalContext externalContext = getExternalContext(); final ExternalContext.Session session = externalContext.getSession(false); if (session != null) { try { session.addListener(new ExternalContext.SessionListener() { public void sessionDestroyed() { deleteFileItem(fileItem, SESSION_SCOPE, logger); } }); } catch (IllegalStateException e) { logger.info("Unable to add session listener: " + e.getMessage()); deleteFileItem(fileItem, SESSION_SCOPE, logger); // remove immediately throw e; } } else if (logger != null) { logger.debug("No existing session found so cannot register temporary file deletion upon session destruction: " + fileItem.getName()); } } /** * Add listener to fileItem which is going to be automatically destroyed when the servlet is destroyed * * @param fileItem FileItem */ public static void deleteFileOnApplicationDestroyed(final FileItem fileItem, final Logger logger) { // Try to delete the file on exit and on session termination final ExternalContext externalContext = getExternalContext(); externalContext.getWebAppContext().addListener(new WebAppListener() { public void webAppDestroyed() { deleteFileItem(fileItem, APPLICATION_SCOPE, logger); } }); } /** * Convert a String in xs:anyURI to an xs:base64Binary. * * The URI has to be a URL. It is read entirely */ public static String anyURIToBase64Binary(String value) { InputStream is = null; try { // Read from URL and convert to Base64 is = URLFactory.createURL(value).openStream(); final StringBuilder sb = new StringBuilder(); SAXUtils.inputStreamToBase64Characters(is, new XMLReceiverAdapter() { public void characters(char ch[], int start, int length) { sb.append(ch, start, length); } }); // Return Base64 String return sb.toString(); } catch (IOException e) { throw new OXFException(e); } finally { if (is != null) { try { is.close(); } catch (IOException e) { throw new OXFException(e); } } } } public static void anyURIToOutputStream(String value, OutputStream outputStream) { InputStream is = null; try { is = URLFactory.createURL(value).openStream(); copyStream(is, outputStream); } catch (IOException e) { throw new OXFException(e); } finally { if (is != null) { try { is.close(); } catch (IOException e) { throw new OXFException(e); } } } } /** * Remove the first path element of a path. Return null if there is only one path element * * E.g. /foo/bar => /bar?a=b * * @param path path to modify * @return modified path or null */ public static String removeFirstPathElement(String path) { final int secondSlashIndex = path.indexOf('/', 1); if (secondSlashIndex == -1) return null; return path.substring(secondSlashIndex); } /** * Return the first path element of a path. If there is only one path element, return the entire path. * * E.g. /foo/bar => /foo * * @param path path to analyze * @return first path element */ public static String getFirstPathElement(String path) { final int secondSlashIndex = path.indexOf('/', 1); if (secondSlashIndex == -1) return path; return path.substring(0, secondSlashIndex); } /** * Encode a Human Readable Resource Identifier to a URI. Leading and trailing spaces are removed first. * * W3C note: https://www.w3.org/TR/leiri/ * * @param uriString URI to encode * @param processSpace whether to process the space character or leave it unchanged * @return encoded URI, or null if uriString was null */ public static String encodeHRRI(String uriString, boolean processSpace) { if (uriString == null) return null; // Note that the XML Schema spec says "Spaces are, in principle, allowed in the ·lexical space· of anyURI, // however, their use is highly discouraged (unless they are encoded by %20).". // We assume that we never want leading or trailing spaces. You can use %20 if you really want this. uriString = StringUtils.trimAllToEmpty(uriString); // We try below to follow the "Human Readable Resource Identifiers" RFC, in draft as of 2007-06-06. // * the control characters #x0 to #x1F and #x7F to #x9F // * space #x20 // * the delimiters "<" #x3C, ">" #x3E, and """ #x22 // * the unwise characters "{" #x7B, "}" #x7D, "|" #x7C, "\" #x5C, "^" #x5E, and "`" #x60 final StringBuilder sb = new StringBuilder(uriString.length() * 2); for (int i = 0; i < uriString.length(); i++) { final char currentChar = uriString.charAt(i); if (currentChar >= 0 && (currentChar <= 0x1f || (processSpace && currentChar == 0x20) || currentChar == 0x22 || currentChar == 0x3c || currentChar == 0x3e || currentChar == 0x5c || currentChar == 0x5e || currentChar == 0x60 || (currentChar >= 0x7b && currentChar <= 0x7d) || (currentChar >= 0x7f && currentChar <= 0x9f))) { sb.append('%'); sb.append(NumberUtils.toHexString((byte) currentChar).toUpperCase()); } else { sb.append(currentChar); } } return sb.toString(); } /** * Get the current external context. * * @return external context if found, null otherwise */ public static ExternalContext getExternalContext() { final PipelineContext pipelineContext = PipelineContext.get(); return (pipelineContext != null) ? (ExternalContext) pipelineContext.getAttribute(PipelineContext.EXTERNAL_CONTEXT) : null; } /** * Get the current session. * * Can return null if the external context is not found or if the session doesn't exist and is not forced. * * @return session if found, null otherwise */ public static ExternalContext.Session getSession(boolean create) { final ExternalContext externalContext = NetUtils.getExternalContext(); return (externalContext != null) ? externalContext.getSession(create) : null; } private static void deleteFile(final File file, final Logger logger) { final boolean success = file.delete(); if (logger != null && logger.isDebugEnabled()) { try { final String message = success ? "deleted temporary file upon session destruction: " : "could not delete temporary file upon session destruction: "; logger.debug(message + file.getCanonicalPath()); } catch (IOException e) { // NOP because as result of getCanonicalPath() and we don't care } } } private static void deleteFileItem(FileItem fileItem, int scope, Logger logger) { fileItem.delete(); if (logger != null && logger.isDebugEnabled() && fileItem instanceof DiskFileItem) { final File storeLocation = ((DiskFileItem) fileItem).getStoreLocation(); if (storeLocation != null) { final String temporaryFileName = storeLocation.getAbsolutePath(); final String scopeString = (scope == REQUEST_SCOPE) ? "request" : (scope == SESSION_SCOPE) ? "session" : "application"; logger.debug("deleting temporary " + scopeString + "-scoped file upon session destruction: " + temporaryFileName); } } } public static File renameAndExpireWithSession(String existingFileURI, final Logger logger) { try { // Assume the file will be deleted with the request so rename it first final String newPath; { final File newFile = File.createTempFile("xforms_upload_", null); newPath = newFile.getCanonicalPath(); newFile.delete(); } final File oldFile = new File(new URI(existingFileURI)); final File newFile = new File(newPath); final boolean success = oldFile.renameTo(newFile); try { final String message = success ? "renamed temporary file" : "could not rename temporary file"; logger.debug(message + " from " + oldFile.getCanonicalPath() + " to " + newFile.getCanonicalPath()); } catch (IOException e) { // NOP } // Mark deletion of the file on exit and on session termination { newFile.deleteOnExit(); final ExternalContext.Session session = getExternalContext().getSession(false); if (session != null) { try { session.addListener(new ExternalContext.SessionListener() { public void sessionDestroyed() { deleteFile(newFile, logger); } }); } catch (IllegalStateException e) { logger.info("Unable to add session listener: " + e.getMessage()); deleteFile(newFile, logger); // remove immediately throw e; } } else { logger.debug("no existing session found so cannot register temporary file deletion upon session destruction: " + newFile.getCanonicalPath()); } } return newFile; } catch (Exception e) { throw new OXFException(e); } } public static void debugLogRequestAsXML(final ExternalContext.Request request) { System.out.println(Dom4jUtils.domToPrettyString(RequestGenerator.readWholeRequestAsDOM4J(request, null))); } public static boolean isSuccessCode(int code) { // Accept any success code (in particular "201 Resource Created") return code >= 200 && code < 300; } public static boolean isRedirectCode(int code) { return (code >= 301 && code <= 303) || code == 307; } /** * Get a File object from either a URL or a path. */ public static File getFile(String configDirectory, String configFile, String configUrl, LocationData locationData, boolean makeDirectories) { return configUrl == null ? getFile(configDirectory, configFile, makeDirectories) : getFile(configUrl, locationData, makeDirectories); } /** * Find the real path of an oxf: or file: URL. */ public static String getRealPath(String configUrl, LocationData locationData) { // Use location data if present so that relative URLs can be supported final URL fullURL = (locationData != null && locationData.file() != null) ? URLFactory.createURL(locationData.file(), configUrl) : URLFactory.createURL(configUrl); final String realPath; if (fullURL.getProtocol().equals("oxf")) { // Get real path to resource path if possible realPath = ResourceManagerWrapper.instance().getRealPath(fullURL.getFile()); if (realPath == null) throw new OXFException("Unable to obtain the real path of the file using the oxf: protocol for URL: " + configUrl); } else if (fullURL.getProtocol().equals("file")) { String host = fullURL.getHost(); realPath = host + (host.length() > 0 ? ":" : "") + fullURL.getFile(); } else { throw new OXFException("Only the file: and oxf: protocols are supported for URL: " + configUrl); } return realPath; } /** * Get a File object for an oxf: or file: URL. */ public static File getFile(String configUrl, LocationData locationData, boolean makeDirectories) { return getFile(null, getRealPath(configUrl, locationData), makeDirectories); } /** * Get a File object from a path. */ public static File getFile(String configDirectory, String configFile, boolean makeDirectories) { final File file; if (configDirectory == null) { // No base directory specified file = new File(configFile); } else { // Base directory specified final File baseDirectory = new File(configDirectory); // Make directories if needed if (makeDirectories) { if (!baseDirectory.exists()) { if (!baseDirectory.mkdirs()) throw new OXFException("Directory '" + baseDirectory + "' could not be created."); } } if (!baseDirectory.isDirectory() || !baseDirectory.canWrite()) throw new OXFException("Directory '" + baseDirectory + "' is not a directory or is not writeable."); file = new File(baseDirectory, configFile); } // Make directories if needed if (makeDirectories) { if (!file.getParentFile().exists()) { if (!file.getParentFile().mkdirs()) throw new OXFException("Directory '" + file.getParentFile() + "' could not be created."); } } return file; } }