/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.cayenne.util;
import org.apache.cayenne.Cayenne;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.di.AdhocObjectFactory;
import org.apache.cayenne.di.spi.DefaultAdhocObjectFactory;
import org.apache.cayenne.di.spi.DefaultClassLoaderManager;
import org.apache.cayenne.reflect.ArcProperty;
import org.apache.cayenne.reflect.AttributeProperty;
import org.apache.cayenne.reflect.PropertyVisitor;
import org.apache.cayenne.reflect.ToManyProperty;
import org.apache.cayenne.reflect.ToOneProperty;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
/**
* Contains various unorganized static utility methods used across Cayenne.
*/
public class Util {
private static final Map<String, String> SPECIAL_CHAR_TO_JAVA_MAPPING = new HashMap<>();
static {
SPECIAL_CHAR_TO_JAVA_MAPPING.put("#", "pound");
}
@Deprecated
private static DefaultAdhocObjectFactory objectFactory;
static {
objectFactory = new DefaultAdhocObjectFactory(null, new DefaultClassLoaderManager());
}
/**
* Converts URL to file. Throws {@link IllegalArgumentException} if the URL
* is not a "file://" URL.
*/
public static File toFile(URL url) throws IllegalArgumentException {
// must convert spaces to %20, or URL->URI conversion may fail
String urlString = url.toExternalForm();
URI uri;
try {
uri = new URI(urlString.replace(" ", "%20"));
} catch (URISyntaxException e) {
throw new IllegalArgumentException("URL " + urlString + " can't be converted to URI", e);
}
return new File(uri);
}
/**
* Reads file contents, returning it as a String, using System default line
* separator.
*/
public static String stringFromFile(File file) throws IOException {
return stringFromFile(file, System.getProperty("line.separator"));
}
/**
* Reads file contents, returning it as a String, joining lines with
* provided separator.
*/
public static String stringFromFile(File file, String joinWith) throws IOException {
StringBuilder buf = new StringBuilder();
try (BufferedReader in = new BufferedReader(new FileReader(file));) {
String line = null;
while ((line = in.readLine()) != null) {
buf.append(line).append(joinWith);
}
}
return buf.toString();
}
/**
* @param objects
* An Iterable of objects that will be converted to Strings and joined together.
* @param separator
* The separator between the strings.
* @return A single string of all the input strings separated by the
* separator.
*/
public static String join(Iterable<?> objects, String separator) {
if (objects == null) {
return "";
}
if (separator == null) {
separator = "";
}
StringBuilder builder = new StringBuilder();
for (Object o : objects) {
if (builder.length() > 0) {
builder.append(separator);
}
String string = o != null ? o.toString() : "";
builder.append(string);
}
return builder.toString();
}
/**
* Replaces all backslashes "\" with forward slashes "/". Convenience method
* to convert path Strings to URI format.
*/
public static String substBackslashes(String string) {
return RegexUtil.substBackslashes(string);
}
/**
* Looks up and returns the root cause of an exception. If none is found,
* returns supplied Throwable object unchanged. If root is found,
* recursively "unwraps" it, and returns the result to the user.
*/
public static Throwable unwindException(Throwable th) {
if (th instanceof SAXException) {
SAXException sax = (SAXException) th;
if (sax.getException() != null) {
return unwindException(sax.getException());
}
} else if (th instanceof SQLException) {
SQLException sql = (SQLException) th;
if (sql.getNextException() != null) {
return unwindException(sql.getNextException());
}
} else if (th.getCause() != null) {
return unwindException(th.getCause());
}
return th;
}
/**
* Compares two objects similar to "Object.equals(Object)". Unlike
* Object.equals(..), this method doesn't throw an exception if any of the
* two objects is null.
*/
public static boolean nullSafeEquals(Object o1, Object o2) {
if (o1 == null) {
return o2 == null;
}
// Arrays must be handled differently since equals() only does
// an "==" for an array and ignores equivalence. If an array, use
// the Jakarta Commons Language component EqualsBuilder to determine
// the types contained in the array and do individual comparisons.
if (o1.getClass().isArray()) {
EqualsBuilder builder = new EqualsBuilder();
builder.append(o1, o2);
return builder.isEquals();
} else { // It is NOT an array, so use regular equals()
return o1.equals(o2);
}
}
/**
* Compares two objects similar to "Comparable.compareTo(Object)". Unlike
* Comparable.compareTo(..), this method doesn't throw an exception if any
* of the two objects is null.
*
* @since 1.1
*/
public static <T> int nullSafeCompare(boolean nullsFirst, Comparable<T> o1, T o2) {
if (o1 == null && o2 == null) {
return 0;
} else if (o1 == null) {
return nullsFirst ? -1 : 1;
} else if (o2 == null) {
return nullsFirst ? 1 : -1;
} else {
return o1.compareTo(o2);
}
}
/**
* Returns true, if the String is null or an empty string.
*/
public static boolean isEmptyString(String string) {
return string == null || string.length() == 0;
}
/**
* Creates Serializable object copy using serialization/deserialization.
*/
@SuppressWarnings("unchecked")
public static <T extends Serializable> T cloneViaSerialization(T object) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream() {
@Override
public byte[] toByteArray() {
return buf;
}
};
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(object);
}
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
return (T) in.readObject();
}
}
/**
* Creates an XMLReader with default feature set. Note that all Cayenne
* internal XML parsers should probably use XMLReader obtained via this
* method for consistency sake, and can customize feature sets as needed.
*/
public static XMLReader createXmlReader() throws SAXException, ParserConfigurationException {
SAXParserFactory spf = SAXParserFactory.newInstance();
// Create a JAXP SAXParser
SAXParser saxParser = spf.newSAXParser();
// Get the encapsulated SAX XMLReader
XMLReader reader = saxParser.getXMLReader();
// set default features
reader.setFeature("http://xml.org/sax/features/namespaces", true);
return reader;
}
/**
* Returns package name for the Java class as a path separated with forward
* slash ("/"). Method is used to lookup resources that are located in
* package subdirectories. For example, a String "a/b/c" will be returned
* for class name "a.b.c.ClassName".
*/
public static String getPackagePath(String className) {
return RegexUtil.getPackagePath(className);
}
/**
* Returns an unqualified class name for the fully qualified name.
*
* @since 3.0
*/
public static String stripPackageName(String className) {
if (className == null || className.length() == 0)
return className;
int lastDot = className.lastIndexOf('.');
if ((-1 == lastDot) || ((className.length() - 1) == lastDot))
return className;
return className.substring(lastDot + 1);
}
/**
* Creates a mutable map out of two arrays with keys and values.
*
* @since 1.2
*/
public static <K, V> Map<K, V> toMap(K[] keys, V[] values) {
int keysSize = (keys != null) ? keys.length : 0;
int valuesSize = (values != null) ? values.length : 0;
if (keysSize == 0 && valuesSize == 0) {
// return mutable map
return new HashMap<>();
}
if (keysSize != valuesSize) {
throw new IllegalArgumentException("The number of keys doesn't match the number of values.");
}
Map<K, V> map = new HashMap<>();
for (int i = 0; i < keysSize; i++) {
map.put(keys[i], values[i]);
}
return map;
}
/**
* Extracts extension from the file name. Dot is not included in the
* returned string.
*/
public static String extractFileExtension(String fileName) {
int dotInd = fileName.lastIndexOf('.');
// if dot is in the first position,
// we are dealing with a hidden file rather than an extension
return (dotInd > 0 && dotInd < fileName.length()) ? fileName.substring(dotInd + 1) : null;
}
/**
* Strips extension from the file name.
*/
public static String stripFileExtension(String fileName) {
int dotInd = fileName.lastIndexOf('.');
// if dot is in the first position,
// we are dealing with a hidden file rather than an extension
return (dotInd > 0) ? fileName.substring(0, dotInd) : fileName;
}
/**
* Strips "\n", "\r\n", "\r" from the argument string, replacing them with a
* provided character.
*
* @since 3.1
*/
public static String stripLineBreaks(String string, char replaceWith) {
if (string == null) {
return null;
}
int len = string.length();
char[] buffer = new char[len];
boolean matched = false;
int j = 0;
for (int i = 0; i < len; i++, j++) {
char c = string.charAt(i);
// skip \n, \r, \r\n
if (c == '\n' || c == '\r') {
matched = true;
// do lookahead
if (i + 1 < len && string.charAt(i + 1) == '\n') {
i++;
}
buffer[j] = replaceWith;
} else {
buffer[j] = c;
}
}
return matched ? new String(buffer, 0, j) : string;
}
/**
* Encodes a string so that it can be used as an attribute value in an XML
* document. Will do conversion of the greater/less signs, quotes and
* ampersands.
*/
public static String encodeXmlAttribute(String string) {
if (string == null) {
return null;
}
int len = string.length();
if (len == 0) {
return string;
}
StringBuilder encoded = new StringBuilder();
for (int i = 0; i < len; i++) {
char c = string.charAt(i);
if (c == '<') {
encoded.append("<");
} else if (c == '\"') {
encoded.append(""");
} else if (c == '>') {
encoded.append(">");
} else if (c == '\'') {
encoded.append("'");
} else if (c == '&') {
encoded.append("&");
} else {
encoded.append(c);
}
}
return encoded.toString();
}
/**
* Trims long strings substituting middle part with "...".
*
* @param str
* String to trim.
* @param maxLength
* maximum allowable length. Must be at least 5, or an
* IllegalArgumentException is thrown.
* @return String
*/
public static String prettyTrim(String str, int maxLength) {
if (maxLength < 5) {
throw new IllegalArgumentException("Algorithm for 'prettyTrim' works only with length >= 5. "
+ "Supplied length is " + maxLength);
}
if (str == null || str.length() <= maxLength) {
return str;
}
// find a section to cut off
int len = maxLength - 3;
int startLen = len / 2;
int endLen = len - startLen;
return str.substring(0, startLen) + "..." + str.substring(str.length() - endLen);
}
/**
* Returns a sorted iterator from an unsorted one. Use this method as a last
* resort, since it is much less efficient then just sorting a collection
* that backs the original iterator.
*/
public static <T> Iterator<T> sortedIterator(Iterator<T> it, Comparator<T> comparator) {
List<T> list = new ArrayList<>();
while (it.hasNext()) {
list.add(it.next());
}
Collections.sort(list, comparator);
return list.iterator();
}
/**
* Builds a hashCode of Collection.
*/
public static int hashCode(Collection<?> c) {
HashCodeBuilder builder = new HashCodeBuilder();
for (Object o : c) {
builder.append(o);
}
return builder.toHashCode();
}
/**
* @since 1.2
*/
public static Pattern sqlPatternToPattern(String pattern, boolean ignoreCase) {
String preprocessed = RegexUtil.sqlPatternToRegex(pattern);
int flag = (ignoreCase) ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : 0;
return Pattern.compile(preprocessed, flag);
}
/**
* Returns true if a Member is accessible via reflection under normal Java
* access controls.
*
* @since 1.2
*/
public static boolean isAccessible(Member member) {
return Modifier.isPublic(member.getModifiers()) && Modifier.isPublic(member.getDeclaringClass().getModifiers());
}
/**
* Creates a Java class, handling regular class names as well as
* single-dimensional arrays and primitive types.
*
* @since 1.2
* @deprecated since 4.0 this method based on statically defined class
* loading algorithm is not going to work in environments like
* OSGi. {@link AdhocObjectFactory} should be used as it can
* provide the environment-specific class loading policy.
*/
@Deprecated
public static Class<?> getJavaClass(String className) throws ClassNotFoundException {
return objectFactory.getJavaClass(className);
}
/**
* Converts names like "ABCD_EFG_123" to Java-style names like "abcdEfg123". If
* <code>capitalize</code> is true, returned name is capitalized (for instance if
* this is a class name).
*
* @since 4.0
*/
// TODO: trace direct users and switch over to ObjectNameGenerator
public static String underscoredToJava(String name, boolean capitalize) {
StringTokenizer st = new StringTokenizer(name, "_");
StringBuilder buf = new StringBuilder();
boolean first = true;
while (st.hasMoreTokens()) {
String token = st.nextToken();
// clear of non-java chars
token = specialCharsToJava(token);
int len = token.length();
if (len == 0) {
continue;
}
// sniff mixed case vs. single case styles
boolean hasLowerCase = false;
boolean hasUpperCase = false;
for (int i = 0; i < len && !(hasUpperCase && hasLowerCase); i++) {
if (Character.isUpperCase(token.charAt(i))) {
hasUpperCase = true;
} else if (Character.isLowerCase(token.charAt(i))) {
hasLowerCase = true;
}
}
// if mixed case, preserve it, if all upper, convert to lower
if (hasUpperCase && !hasLowerCase) {
token = token.toLowerCase();
}
if (first) {
// apply explicit capitalization rules, if this is the first token
first = false;
if (capitalize) {
buf.append(Character.toUpperCase(token.charAt(0)));
} else {
buf.append(Character.toLowerCase(token.charAt(0)));
}
} else {
buf.append(Character.toUpperCase(token.charAt(0)));
}
if (len > 1) {
buf.append(token.substring(1, len));
}
}
return buf.toString();
}
/**
* Replaces special chars with human-readable and Java-id-compatible symbols.
*
* @since 4.0
*/
public static String specialCharsToJava(String string) {
int len = string.length();
if (len == 0) {
return string;
}
StringBuilder buffer = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = string.charAt(i);
if (Character.isJavaIdentifierPart(c)) {
buffer.append(c);
} else {
Object word = SPECIAL_CHAR_TO_JAVA_MAPPING.get(String.valueOf(c));
buffer.append(word != null ? word : "_");
}
}
return buffer.toString();
}
static void setReverse(final Persistent sourceObject, String propertyName, final Persistent targetObject) {
ArcProperty property = (ArcProperty) Cayenne.getClassDescriptor(sourceObject).getProperty(propertyName);
ArcProperty reverseArc = property.getComplimentaryReverseArc();
if (reverseArc != null) {
reverseArc.visit(new PropertyVisitor() {
public boolean visitToMany(ToManyProperty property) {
property.addTargetDirectly(targetObject, sourceObject);
return false;
}
public boolean visitToOne(ToOneProperty property) {
property.setTarget(targetObject, sourceObject, false);
return false;
}
public boolean visitAttribute(AttributeProperty property) {
return false;
}
});
sourceObject.getObjectContext().getGraphManager()
.arcCreated(targetObject.getObjectId(), sourceObject.getObjectId(), reverseArc.getName());
markAsDirty(targetObject);
}
}
static void unsetReverse(final Persistent sourceObject, String propertyName, final Persistent targetObject) {
ArcProperty property = (ArcProperty) Cayenne.getClassDescriptor(sourceObject).getProperty(propertyName);
ArcProperty reverseArc = property.getComplimentaryReverseArc();
if (reverseArc != null) {
reverseArc.visit(new PropertyVisitor() {
public boolean visitToMany(ToManyProperty property) {
property.removeTargetDirectly(targetObject, sourceObject);
return false;
}
public boolean visitToOne(ToOneProperty property) {
property.setTarget(targetObject, null, false);
return false;
}
public boolean visitAttribute(AttributeProperty property) {
return false;
}
});
sourceObject.getObjectContext().getGraphManager()
.arcDeleted(targetObject.getObjectId(), sourceObject.getObjectId(), reverseArc.getName());
markAsDirty(targetObject);
}
}
/**
* Changes object state to MODIFIED if needed, returning true if the change
* has occurred, false if not.
*/
static boolean markAsDirty(Persistent object) {
if (object.getPersistenceState() == PersistenceState.COMMITTED) {
object.setPersistenceState(PersistenceState.MODIFIED);
return true;
}
return false;
}
}