/* * Copyright 2013 OmniFaces. * * Licensed 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.omnifaces.security.jaspic; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.regex.Pattern.quote; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.bind.DatatypeConverter; /** * An assortment of various utility methods. * * @author Arjan Tijms * */ public final class Utils { private static final String ERROR_UNSUPPORTED_ENCODING = "UTF-8 is apparently not supported on this platform."; private Utils() {} public static boolean notNull(Object... objects) { for (Object object : objects) { if (object == null) { return false; } } return true; } /** * Returns true if the given string is null or is empty. * * @param string The string to be checked on emptiness. * @return True if the given string is null or is empty. */ public static boolean isEmpty(String string) { return string == null || string.isEmpty(); } /** * Returns <code>true</code> if the given array is null or is empty. * * @param array The array to be checked on emptiness. * @return <code>true</code> if the given array is null or is empty. */ public static boolean isEmpty(Object[] array) { return array == null || array.length == 0; } /** * Returns <code>true</code> if the given collection is null or is empty. * * @param collection The collection to be checked on emptiness. * @return <code>true</code> if the given collection is null or is empty. */ public static boolean isEmpty(Collection<?> collection) { return collection == null || collection.isEmpty(); } /** * Returns <code>true</code> if the given object equals one of the given objects. * @param <T> The generic object type. * @param object The object to be checked if it equals one of the given objects. * @param objects The argument list of objects to be tested for equality. * @return <code>true</code> if the given object equals one of the given objects. */ @SafeVarargs public static <T> boolean isOneOf(T object, T... objects) { for (Object other : objects) { if (object == null ? other == null : object.equals(other)) { return true; } } return false; } public static String getBaseURL(HttpServletRequest request) { String url = request.getRequestURL().toString(); return url.substring(0, url.length() - request.getRequestURI().length()) + request.getContextPath(); } public static void redirect(HttpServletResponse response, String location) { try { response.sendRedirect(location); } catch (IOException e) { throw new IllegalStateException(e); } } public static void redirect(HttpServletRequest request, HttpServletResponse response, String location) { try { if (isFacesAjaxRequest(request)) { response.setHeader("Cache-Control", "no-cache,no-store,must-revalidate"); response.setDateHeader("Expires", 0); response.setHeader("Pragma", "no-cache"); // Backwards compatibility for HTTP 1.0. response.setContentType("text/xml"); response.setCharacterEncoding(UTF_8.name()); response.getWriter().printf(FACES_AJAX_REDIRECT_XML, location); } else { response.sendRedirect(location); } } catch (IOException e) { throw new IllegalStateException(e); } } private static final Set<String> FACES_AJAX_HEADERS = unmodifiableSet("partial/ajax", "partial/process"); private static final String FACES_AJAX_REDIRECT_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<partial-response><redirect url=\"%s\"></redirect></partial-response>"; public static boolean isFacesAjaxRequest(HttpServletRequest request) { return FACES_AJAX_HEADERS.contains(request.getHeader("Faces-Request")); } @SuppressWarnings("unchecked") public static <E> Set<E> unmodifiableSet(Object... values) { Set<E> set = new HashSet<>(); for (Object value : values) { if (value instanceof Object[]) { for (Object item : (Object[]) value) { set.add((E) item); } } else if (value instanceof Collection<?>) { for (Object item : (Collection<?>) value) { set.add((E) item); } } else { set.add((E) value); } } return Collections.unmodifiableSet(set); } public static String encodeURL(String string) { if (string == null) { return null; } try { return URLEncoder.encode(string, UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ENCODING, e); } } public static String decodeURL(String string) { if (string == null) { return null; } try { return URLDecoder.decode(string, UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ENCODING, e); } } public static String getSingleParameterFromState(String state, String paramName) { Map<String, List<String>> requestStateParameters = getParameterMapFromState(state); List<String> parameterValues = requestStateParameters.get(paramName); if (!isEmpty(parameterValues)) { return parameterValues.get(0); } return null; } public static Map<String, List<String>> getParameterMapFromState(String state) { return toParameterMap(unserializeURLSafe(state)); } /** * Converts the given request query string to request parameter values map. * @param queryString The request query string. * @return The request query string as request parameter values map. */ public static Map<String, List<String>> toParameterMap(String queryString) { String[] parameters = queryString.split(quote("&")); Map<String, List<String>> parameterMap = new LinkedHashMap<>(parameters.length); for (String parameter : parameters) { if (parameter.contains("=")) { String[] pair = parameter.split(quote("=")); String key = decodeURL(pair[0]); String value = (pair.length > 1 && !isEmpty(pair[1])) ? decodeURL(pair[1]) : ""; List<String> values = parameterMap.get(key); if (values == null) { values = new ArrayList<>(1); parameterMap.put(key, values); } values.add(value); } } return parameterMap; } /** * Converts the given request parameter values map to request query string. * @param parameterMap The request parameter values map. * @return The request parameter values map as request query string. */ public static String toQueryString(Map<String, List<String>> parameterMap) { StringBuilder queryString = new StringBuilder(); for (Entry<String, List<String>> entry : parameterMap.entrySet()) { String name = encodeURL(entry.getKey()); for (String value : entry.getValue()) { if (queryString.length() > 0) { queryString.append("&"); } queryString.append(name).append("=").append(encodeURL(value)); } } return queryString.toString(); } public static String getSingleParameterFromQueryString(String queryString, String paramName) { if (!isEmpty(queryString)) { Map<String, List<String>> requestParameters = toParameterMap(queryString); if (!isEmpty(requestParameters.get(paramName))) { return requestParameters.get(paramName).get(0); } } return null; } /** * Serialize the given string to the short possible unique URL-safe representation. The current implementation will * decode the given string with UTF-8 and then compress it with ZLIB using "best compression" algorithm and then * Base64-encode the resulting bytes without the <code>=</code> padding, whereafter the Base64 characters * <code>+</code> and <code>/</code> are been replaced by respectively <code>-</code> and <code>_</code> to make it * URL-safe (so that no platform-sensitive URL-encoding needs to be done when used in URLs). * @param string The string to be serialized. * @return The serialized URL-safe string, or <code>null</code> when the given string is itself <code>null</code>. */ public static String serializeURLSafe(String string) { if (string == null) { return null; } try { InputStream raw = new ByteArrayInputStream(string.getBytes(UTF_8)); ByteArrayOutputStream deflated = new ByteArrayOutputStream(); stream(raw, new DeflaterOutputStream(deflated, new Deflater(Deflater.BEST_COMPRESSION))); String base64 = DatatypeConverter.printBase64Binary(deflated.toByteArray()); return base64.replace('+', '-').replace('/', '_').replace("=", ""); } catch (IOException e) { // This will occur when ZLIB and/or UTF-8 are not supported, but this is not to be expected these days. throw new RuntimeException(e); } } /** * Unserialize the given serialized URL-safe string. This does the inverse of {@link #serializeURLSafe(String)}. * @param string The serialized URL-safe string to be unserialized. * @return The unserialized string, or <code>null</code> when the given string is by itself <code>null</code>. * @throws IllegalArgumentException When the given serialized URL-safe string is not in valid format as returned by * {@link #serializeURLSafe(String)}. */ public static String unserializeURLSafe(String string) { if (string == null) { return null; } try { String base64 = string.replace('-', '+').replace('_', '/') + "===".substring(0, string.length() % 4); InputStream deflated = new ByteArrayInputStream(DatatypeConverter.parseBase64Binary(base64)); return new String(toByteArray(new InflaterInputStream(deflated)), UTF_8); } catch (UnsupportedEncodingException e) { // This will occur when UTF-8 is not supported, but this is not to be expected these days. throw new RuntimeException(e); } catch (Exception e) { // This will occur when the string is not in valid Base64 or ZLIB format. throw new IllegalArgumentException(e); } } public static long stream(InputStream input, OutputStream output) throws IOException { try (ReadableByteChannel inputChannel = Channels.newChannel(input); WritableByteChannel outputChannel = Channels.newChannel(output)) { ByteBuffer buffer = ByteBuffer.allocateDirect(10240); long size = 0; while (inputChannel.read(buffer) != -1) { buffer.flip(); size += outputChannel.write(buffer); buffer.clear(); } return size; } } /** * Read the given input stream into a byte array. The given input stream will implicitly be closed after streaming, * regardless of whether an exception is been thrown or not. * @param input The input stream. * @return The input stream as a byte array. * @throws IOException When an I/O error occurs. */ public static byte[] toByteArray(InputStream input) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); stream(input, output); return output.toByteArray(); } }