/*
* Copyright 2017 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.util;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableMap;
import static java.util.logging.Level.FINE;
import static java.util.regex.Pattern.quote;
import static org.omnifaces.util.Reflection.toClassOrNull;
import static org.omnifaces.util.Servlets.getSubmittedFileName;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import javax.servlet.http.Part;
import javax.xml.bind.DatatypeConverter;
/**
* <p>
* Collection of general utility methods that do not fit in one of the more specific classes.
*
* <h3>This class is not listed in showcase! Should I use it?</h3>
* <p>
* This class is indeed intented for internal usage only. We won't add methods here on user request. We only add methods
* here once we encounter non-DRY code in OmniFaces codebase. The methods may be renamed/changed without notice.
* <p>
* We don't stop you from using it if you think you find it useful, but you'd really better pick e.g. Google Guava or
* perhaps the good 'ol Apache Commons. This Utils class exists because OmniFaces intends to be free of 3rd party
* dependencies.
*
* @author Arjan Tijms
* @author Bauke Scholtz
*/
public final class Utils {
// Constants ------------------------------------------------------------------------------------------------------
private static final Logger logger = Logger.getLogger(Utils.class.getName());
private static final int DEFAULT_STREAM_BUFFER_SIZE = 10240;
private static final String PATTERN_RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz";
private static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone("GMT");
private static final Pattern PATTERN_ISO639_ISO3166_LOCALE = Pattern.compile("[a-z]{2,3}(_[A-Z]{2})?");
private static final int BASE64_SEGMENT_LENGTH = 4;
private static final int UNICODE_3_BYTES = 0xfff;
private static final int UNICODE_2_BYTES = 0xff;
private static final int UNICODE_1_BYTE = 0xf;
private static final int UNICODE_END_PRINTABLE_ASCII = 0x7f;
private static final int UNICODE_BEGIN_PRINTABLE_ASCII = 0x20;
private static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = createPrimitiveDefaults();
private static final String ERROR_UNSUPPORTED_ENCODING = "UTF-8 is apparently not supported on this platform.";
// Constructors ---------------------------------------------------------------------------------------------------
private Utils() {
// Hide constructor.
}
// Initialization -------------------------------------------------------------------------------------------------
private static Map<Class<?>, Object> createPrimitiveDefaults() {
Map<Class<?>, Object> primitiveDefaults = new HashMap<>();
primitiveDefaults.put(boolean.class, false);
primitiveDefaults.put(byte.class, (byte) 0);
primitiveDefaults.put(short.class, (short) 0);
primitiveDefaults.put(char.class, (char) 0);
primitiveDefaults.put(int.class, 0);
primitiveDefaults.put(long.class, (long) 0);
primitiveDefaults.put(float.class, (float) 0);
primitiveDefaults.put(double.class, (double) 0);
return unmodifiableMap(primitiveDefaults);
}
// Lang -----------------------------------------------------------------------------------------------------------
/**
* Returns <code>true</code> if the given string is null or is empty.
* @param string The string to be checked on emptiness.
* @return <code>true</code> 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 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 map is null or is empty.
* @param map The map to be checked on emptiness.
* @return <code>true</code> if the given map is null or is empty.
*/
public static boolean isEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}
/**
* Returns <code>true</code> if the given part is null or is empty.
* @param part The part to be checked on emptiness.
* @return <code>true</code> if the given part is null or is empty.
* @since 2.6
*/
public static boolean isEmpty(Part part) {
return part == null || (isEmpty(getSubmittedFileName(part)) && part.getSize() <= 0);
}
/**
* Returns <code>true</code> if the given object is null or an empty array or has an empty toString() result.
* @param value The value to be checked on emptiness.
* @return <code>true</code> if the given object is null or an empty array or has an empty toString() result.
*/
public static boolean isEmpty(Object value) {
if (value == null) {
return true;
}
else if (value instanceof String) {
return isEmpty((String) value);
}
else if (value instanceof Collection) {
return isEmpty((Collection<?>) value);
}
else if (value instanceof Map) {
return isEmpty((Map<?, ?>) value);
}
else if (value instanceof Part) {
return isEmpty((Part) value);
}
else if (value.getClass().isArray()) {
return Array.getLength(value) == 0;
}
else {
return value.toString() == null || value.toString().isEmpty();
}
}
/**
* Returns <code>true</code> if at least one value is empty.
* @param values the values to be checked on emptiness
* @return <code>true</code> if any value is empty and <code>false</code> if no values are empty
* @since 1.8
*/
public static boolean isAnyEmpty(Object... values) {
for (Object value : values) {
if (isEmpty(value)) {
return true;
}
}
return false;
}
/**
* Returns <code>true</code> if the given string is null or is empty or contains whitespace only. In addition to
* {@link #isEmpty(String)}, this thus also returns <code>true</code> when <code>string.trim().isEmpty()</code>
* returns <code>true</code>.
* @param string The string to be checked on blankness.
* @return True if the given string is null or is empty or contains whitespace only.
* @since 1.5
*/
public static boolean isBlank(String string) {
return isEmpty(string) || string.trim().isEmpty();
}
/**
* Returns <code>true</code> if the given string is parseable as a number. I.e. it is not null, nor blank and contains solely
* digits. I.e., it won't throw a <code>NumberFormatException</code> when parsing as <code>Long</code>.
* @param string The string to be checked as number.
* @return <code>true</code> if the given string is parseable as a number.
* @since 1.5.
*/
public static boolean isNumber(String string) {
try {
// Performance tests taught that this approach is in general faster than regex or char-by-char checking.
return Long.valueOf(string) != null;
}
catch (Exception ignore) {
logger.log(FINE, "Ignoring thrown exception; the sole intent is to return false instead.", ignore);
return false;
}
}
/**
* Returns <code>true</code> if the given string is parseable as a decimal. I.e. it is not null, nor blank and contains solely
* digits. I.e., it won't throw a <code>NumberFormatException</code> when parsing as <code>Double</code>.
* @param string The string to be checked as decimal.
* @return <code>true</code> if the given string is parseable as a decimal.
* @since 1.5.
*/
public static boolean isDecimal(String string) {
try {
// Performance tests taught that this approach is in general faster than regex or char-by-char checking.
return Double.valueOf(string) != null;
}
catch (Exception ignore) {
logger.log(FINE, "Ignoring thrown exception; the sole intent is to return false instead.", ignore);
return false;
}
}
/**
* Returns the first non-<code>null</code> object of the argument list, or <code>null</code> if there is no such
* element.
* @param <T> The generic object type.
* @param objects The argument list of objects to be tested for non-<code>null</code>.
* @return The first non-<code>null</code> object of the argument list, or <code>null</code> if there is no such
* element.
*/
@SafeVarargs
public static <T> T coalesce(T... objects) {
for (T object : objects) {
if (object != null) {
return object;
}
}
return null;
}
/**
* 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 (Objects.equals(object, other)) {
return true;
}
}
return false;
}
/**
* Returns <code>true</code> if the given string starts with one of the given prefixes.
* @param string The object to be checked if it starts with one of the given prefixes.
* @param prefixes The argument list of prefixes to be checked
* @return <code>true</code> if the given string starts with one of the given prefixes.
* @since 1.4
*/
public static boolean startsWithOneOf(String string, String... prefixes) {
for (String prefix : prefixes) {
if (string.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* Returns <code>true</code> if an instance of the given class could also be an instance of one of the given classes.
* @param cls The class to be checked if it could also be an instance of one of the given classes.
* @param classes The argument list of classes to be tested.
* @return <code>true</code> if the given class could also be an instance of one of the given classes.
* @since 2.0
*/
public static boolean isOneInstanceOf(Class<?> cls, Class<?>... classes) {
for (Class<?> other : classes) {
if (cls == null ? other == null : other.isAssignableFrom(cls)) {
return true;
}
}
return false;
}
/**
* Returns <code>true</code> if the given class has at least one of the given annotations.
* @param cls The class to be checked if it has at least one of the given annotations.
* @param annotations The argument list of annotations to be tested on the given class.
* @return <code>true</code> if the given clazz would be an instance of one of the given clazzes.
* @since 2.0
*/
@SafeVarargs
public static boolean isOneAnnotationPresent(Class<?> cls, Class<? extends Annotation>... annotations) {
for (Class<? extends Annotation> annotation : annotations) {
if (cls.isAnnotationPresent(annotation)) {
return true;
}
}
return false;
}
/**
* Returns the default value of the given class, covering primitives.
* E.g. if given class is <code>int.class</code>, then it will return <code>0</code>. Autoboxing will do the rest.
* Non-primitives and <code>void.class</code> will return <code>null</code>.
* @param cls The class to obtain the default value for.
* @return The default value of the given class, covering primitives.
* @since 2.4
*/
public static Object getDefaultValue(Class<?> cls) {
return cls.isPrimitive() ? PRIMITIVE_DEFAULTS.get(cls) : null;
}
// I/O ------------------------------------------------------------------------------------------------------------
/**
* Stream the given input to the given output via NIO {@link Channels} and a directly allocated NIO
* {@link ByteBuffer}. Both the input and output streams will implicitly be closed after streaming,
* regardless of whether an exception is been thrown or not.
* @param input The input stream.
* @param output The output stream.
* @return The length of the written bytes.
* @throws IOException When an I/O error occurs.
*/
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(DEFAULT_STREAM_BUFFER_SIZE);
long size = 0;
while (inputChannel.read(buffer) != -1) {
buffer.flip();
size += outputChannel.write(buffer);
buffer.clear();
}
return size;
}
}
/**
* Stream a specified range of the given file to the given output via NIO {@link Channels} and a directly allocated
* NIO {@link ByteBuffer}. The output stream will only implicitly be closed after streaming when the specified range
* represents the whole file, regardless of whether an exception is been thrown or not.
* @param file The file.
* @param output The output stream.
* @param start The start position (offset).
* @param length The (intented) length of written bytes.
* @return The (actual) length of the written bytes. This may be smaller when the given length is too large.
* @throws IOException When an I/O error occurs.
* @since 2.2
*/
public static long stream(File file, OutputStream output, long start, long length) throws IOException {
if (start == 0 && length >= file.length()) {
return stream(new FileInputStream(file), output);
}
try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(file.toPath(), StandardOpenOption.READ)) {
WritableByteChannel outputChannel = Channels.newChannel(output);
ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE);
long size = 0;
while (fileChannel.read(buffer, start + size) != -1) {
buffer.flip();
if (size + buffer.limit() > length) {
buffer.limit((int) (length - size));
}
size += outputChannel.write(buffer);
if (size >= length) {
break;
}
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.
* @since 2.0
*/
public static byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
stream(input, output);
return output.toByteArray();
}
/**
* Check if the given resource is not <code>null</code> and then close it, whereby any caught {@link IOException}
* is been returned instead of thrown, so that the caller can if necessary handle (log) or just ignore it without
* the need to put another try-catch.
* @param resource The closeable resource to be closed.
* @return The caught {@link IOException}, or <code>null</code> if none is been thrown.
*/
public static IOException close(Closeable resource) {
if (resource != null) {
try {
resource.close();
}
catch (IOException e) {
return e;
}
}
return null;
}
/**
* Returns <code>true</code> if the given object is serializable.
* @param object The object to be tested.
* @return <code>true</code> if the given object is serializable.
* @since 2.4
*/
public static boolean isSerializable(Object object) {
try (ObjectOutputStream output = new ObjectOutputStream(new NullOutputStream())) {
output.writeObject(object);
return true;
}
catch (IOException ignore) {
logger.log(FINE, "Ignoring thrown exception; the sole intent is to return false instead.", ignore);
return false;
}
}
private static final class NullOutputStream extends OutputStream {
@Override
public void write(int b) throws IOException {
// NOOP.
}
@Override
public void write(byte[] b) throws IOException {
// NOOP.
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
// NOOP.
}
}
// Collections ----------------------------------------------------------------------------------------------------
/**
* Creates an unmodifiable set based on the given values. If one of the values is an instance of an array or a
* collection, then each of its values will also be merged into the set. Nested arrays or collections will result
* in a {@link ClassCastException}.
* @param <E> The expected set element type.
* @param values The values to create an unmodifiable set for.
* @return An unmodifiable set based on the given values.
* @throws ClassCastException When one of the values or one of the arrays or collections is of wrong type.
* @since 1.1
*/
@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);
}
/**
* Converts an iterable into a list.
* <p>
* This method makes NO guarantee to whether changes to the source iterable are
* reflected in the returned list or not. For instance if the given iterable
* already is a list, it's returned directly.
*
* @param <E> The generic iterable element type.
* @param iterable The iterable to be converted.
* @return The list representation of the given iterable, possibly the same instance as that iterable.
* @since 1.5
*/
public static <E> List<E> iterableToList(Iterable<E> iterable) {
if (iterable instanceof List) {
return (List<E>) iterable;
}
else if (iterable instanceof Collection) {
return new ArrayList<>((Collection<E>) iterable);
}
else {
List<E> list = new ArrayList<>();
Iterator<E> iterator = iterable.iterator();
while (iterator.hasNext()) {
list.add(iterator.next());
}
return list;
}
}
/**
* Converts comma separated values in a string into a list with those values.
* <p>
* E.g. a string with "foo, bar, kaz" will be converted into a <code>List</code>
* with values:
* <ul>
* <li>"foo"</li>
* <li>"bar"</li>
* <li>"kaz"</li>
* </ul>
*
* Note that whitespace will be stripped. Empty entries are not supported. This method defaults to
* using a comma (<code>","</code>) as delimiter. See {@link Utils#csvToList(String, String)} for when
* a different delimiter is needed.
*
* @param values string with comma separated values
* @return a list with all values encountered in the <code>values</code> argument, can be the empty list.
* @since 1.4
*/
public static List<String> csvToList(String values) {
return csvToList(values, ",");
}
/**
* Converts comma separated values in a string into a list with those values.
* <p>
* E.g. a string with "foo, bar, kaz" will be converted into a <code>List</code>
* with values:
* <ul>
* <li>"foo"</li>
* <li>"bar"</li>
* <li>"kaz"</li>
* </ul>
*
* Note that whitespace will be stripped. Empty entries are not supported.
*
* @param values string with comma separated values
* @param delimiter the delimiter used to separate the actual values in the <code>values</code> parameter.
* @return a list with all values encountered in the <code>values</code> argument, can be the empty list.
* @since 1.4
*/
public static List<String> csvToList(String values, String delimiter) {
if (isEmpty(values)) {
return emptyList();
}
List<String> list = new ArrayList<>();
for (String value : values.split(quote(delimiter))) {
String trimmedValue = value.trim();
if (!isEmpty(trimmedValue)) {
list.add(trimmedValue);
}
}
return list;
}
/**
* Returns a new map that contains the reverse of the given map.
* <p>
* The reverse of a map means that every value X becomes a key X' with as corresponding
* value Y' the key Y that was originally associated with the value X.
*
* @param <T> The generic map key/value type.
* @param source the map that is to be reversed
* @return the reverse of the given map
*/
public static <T> Map<T, T> reverse(Map<T, T> source) {
Map<T, T> target = new HashMap<>();
for (Entry<T, T> entry : source.entrySet()) {
target.put(entry.getValue(), entry.getKey());
}
return target;
}
/**
* Checks if the given collection contains an object with the given class name.
*
* @param objects collection of objects to check
* @param className name of the class to be checked for
* @return true if the collection contains at least one object with the given class name, false otherwise
* @since 1.6
*/
public static boolean containsByClassName(Collection<?> objects, String className) {
Class<?> cls = toClassOrNull(className);
for (Object object : objects) {
if (object.getClass() == cls) {
return true;
}
}
return false;
}
// Dates ----------------------------------------------------------------------------------------------------------
/**
* Formats the given {@link Date} to a string in RFC1123 format. This format is used in HTTP headers and in
* JavaScript <code>Date</code> constructor.
* @param date The <code>Date</code> to be formatted to a string in RFC1123 format.
* @return The formatted string.
* @since 1.2
*/
public static String formatRFC1123(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);
sdf.setTimeZone(TIMEZONE_GMT);
return sdf.format(date);
}
/**
* Parses the given string in RFC1123 format to a {@link Date} object.
* @param string The string in RFC1123 format to be parsed to a <code>Date</code> object.
* @return The parsed <code>Date</code>.
* @throws ParseException When the given string is not in RFC1123 format.
* @since 1.2
*/
public static Date parseRFC1123(String string) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);
return sdf.parse(string);
}
// Locale ---------------------------------------------------------------------------------------------------------
/**
* Parses the given object representing the locale to a {@link Locale} object.
* If it is <code>null</code>, then return <code>null</code>.
* Else if it is already an instance of <code>Locale</code>, then just return it.
* Else if it is in pattern ISO 639 alpha-2/3, optionally followed by "_" and ISO 3166-1 alpha-2 country code, then
* split the language/country and construct a new <code>Locale</code> with it.
* Else parse it via {@link Locale#forLanguageTag(String)} and return it.
* @param locale The object representing the locale.
* @return The parsed <code>Locale</code>.
* @since 2.3
*/
public static Locale parseLocale(Object locale) {
if (locale == null) {
return null;
}
else if (locale instanceof Locale) {
return (Locale) locale;
}
else {
String localeString = locale.toString();
if (PATTERN_ISO639_ISO3166_LOCALE.matcher(localeString).matches()) {
String[] languageAndCountry = localeString.split("_");
String language = languageAndCountry[0];
String country = languageAndCountry.length > 1 ? languageAndCountry[1] : "";
return new Locale(language, country);
}
else {
return Locale.forLanguageTag(localeString);
}
}
}
// Encoding/decoding ----------------------------------------------------------------------------------------------
/**
* 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>.
* @since 1.2
*/
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 UnsupportedOperationException(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)}.
* @since 1.2
*/
public static String unserializeURLSafe(String string) {
if (string == null) {
return null;
}
try {
String base64 = string.replace('-', '+').replace('_', '/') + "===".substring(0, string.length() % BASE64_SEGMENT_LENGTH);
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 UnsupportedOperationException(e);
}
catch (Exception e) {
// This will occur when the string is not in valid Base64 or ZLIB format.
throw new IllegalArgumentException(e);
}
}
/**
* URL-encode the given string using UTF-8.
* @param string The string to be URL-encoded using UTF-8.
* @return The given string, URL-encoded using UTF-8, or <code>null</code> if <code>null</code> was given.
* @throws UnsupportedOperationException When this platform does not support UTF-8.
* @since 1.4
*/
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);
}
}
/**
* URL-decode the given string using UTF-8.
* @param string The string to be URL-decode using UTF-8.
* @return The given string, URL-decode using UTF-8, or <code>null</code> if <code>null</code> was given.
* @throws UnsupportedOperationException When this platform does not support UTF-8.
* @since 1.4
*/
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);
}
}
/**
* URI-encode the given string using UTF-8. URIs (paths and filenames) have different encoding rules as compared to
* URL query string parameters. {@link URLEncoder} is actually only for www (HTML) form based query string parameter
* values (as used when a webbrowser submits a HTML form). URI encoding has a lot in common with URL encoding, but
* the space has to be %20 and some chars doesn't necessarily need to be encoded.
* @param string The string to be URI-encoded using UTF-8.
* @return The given string, URI-encoded using UTF-8, or <code>null</code> if <code>null</code> was given.
* @throws UnsupportedOperationException When this platform does not support UTF-8.
* @since 2.4
*/
public static String encodeURI(String string) {
if (string == null) {
return null;
}
return encodeURL(string)
.replace("+", "%20")
.replace("%21", "!")
.replace("%27", "'")
.replace("%28", "(")
.replace("%29", ")")
.replace("%7E", "~");
}
// Escaping/unescaping --------------------------------------------------------------------------------------------
/**
* Escapes the given string according the JavaScript code rules. This escapes among others the special characters,
* the whitespace, the quotes and the unicode characters. Useful whenever you want to use a Java string variable as
* a JavaScript string variable.
* @param string The string to be escaped according the JavaScript code rules.
* @param escapeSingleQuote Whether to escape single quotes as well or not. Set to <code>false</code> if you want
* to escape it for usage in JSON.
* @return The escaped string according the JavaScript code rules.
*/
public static String escapeJS(String string, boolean escapeSingleQuote) {
if (string == null) {
return null;
}
StringBuilder builder = new StringBuilder(string.length());
for (char c : string.toCharArray()) {
if (c > UNICODE_3_BYTES) {
builder.append("\\u").append(Integer.toHexString(c));
}
else if (c > UNICODE_2_BYTES) {
builder.append("\\u0").append(Integer.toHexString(c));
}
else if (c > UNICODE_END_PRINTABLE_ASCII) {
builder.append("\\u00").append(Integer.toHexString(c));
}
else if (c < UNICODE_BEGIN_PRINTABLE_ASCII) {
escapeJSControlCharacter(builder, c);
}
else {
escapeJSASCIICharacter(builder, c, escapeSingleQuote);
}
}
return builder.toString();
}
private static void escapeJSControlCharacter(StringBuilder builder, char c) {
switch (c) {
case '\b':
builder.append('\\').append('b');
break;
case '\n':
builder.append('\\').append('n');
break;
case '\t':
builder.append('\\').append('t');
break;
case '\f':
builder.append('\\').append('f');
break;
case '\r':
builder.append('\\').append('r');
break;
default:
if (c > UNICODE_1_BYTE) {
builder.append("\\u00").append(Integer.toHexString(c));
}
else {
builder.append("\\u000").append(Integer.toHexString(c));
}
break;
}
}
private static void escapeJSASCIICharacter(StringBuilder builder, char c, boolean escapeSingleQuote) {
switch (c) {
case '\'':
if (escapeSingleQuote) {
builder.append('\\');
}
builder.append('\'');
break;
case '"':
builder.append('\\').append('"');
break;
case '\\':
builder.append('\\').append('\\');
break;
case '/':
builder.append('\\').append('/');
break;
default:
builder.append(c);
break;
}
}
}