package ca.sqlpower.util; import java.beans.PropertyChangeEvent; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.servlet.ServletContext; import org.apache.log4j.Logger; import ca.sqlpower.enterprise.client.SPServerInfo; import ca.sqlpower.object.CleanupExceptions; import ca.sqlpower.object.SPChildEvent; import ca.sqlpower.object.SPListener; import ca.sqlpower.object.SPObject; import ca.sqlpower.sqlobject.SQLObject; import ca.sqlpower.util.UserPrompter.UserPromptOptions; import ca.sqlpower.util.UserPrompter.UserPromptResponse; import ca.sqlpower.util.UserPrompterFactory.UserPromptType; public class SQLPowerUtils { private static final Logger logger = Logger.getLogger(SQLPowerUtils.class); /** * Checks if the two arguments o1 and o2 are equal to each other, either because * both are null, or because o1.equals(o2). * * @param o1 One object or null reference to compare * @param o2 The other object or null reference to compare */ public static boolean areEqual (Object o1, Object o2) { if (o1 == o2) { // this also covers (null == null) return true; } else { if (o1 == null || o2 == null) { return false; } else { // pass through to object equals method return o1.equals(o2); } } } /** * Searches through the tree recursively to find if the spo * is part of the hierarchy. */ public static boolean hierarchyContains(SPObject root, SPObject child) { return SQLPowerUtils.getAncestorList(child).contains(root); } /** * Replaces double quotes, ampersands, and less-than signs with * their character reference equivalents. This makes the returned * string be safe for use as an XML content data or as an attribute value * enclosed in double quotes. From the XML Spec at http://www.w3.org/TR/REC-xml/#sec-predefined-ent: * 4.6 Predefined Entities * "Definition: Entity and character references may both be used to escape the left angle bracket, ampersand, * and other delimiters. A set of general entities (amp, lt, gt, apos, quot) is specified for this purpose...] * All XML processors must recognize these entities whether they are declared or not. For interoperability, * valid XML documents should declare these entities..." * * Also escapes newlines because we need to preserve them in remarks. */ public static String escapeXML(String src) { if (src == null) return ""; StringBuffer sb = new StringBuffer(src.length()+10); // arbitrary amount of extra space char ch; for (int i = 0, n = src.length(); i < n; i++) { ch = src.charAt(i); if (ch == '\'') { sb.append("'"); } else if (ch == '"') { sb.append("""); } else if (ch == '&') { sb.append("&"); } else if (ch == '<') { sb.append("<"); } else if (ch == '>') { sb.append(">"); } else if (ch == 0x03 || ch == 0x1a) { logger.info("Stripping out illegal characters from " + src + " as it will cause the XML to fail."); } else { sb.append(ch); } } return sb.toString(); } public static String unescapeXML(String src) { if (src == null) return ""; StringBuffer sb = new StringBuffer(src.length()+10); // arbitrary amount of extra space char ch; StringBuffer encoding = null; for (int i = 0, n = src.length(); i < n; i++) { ch = src.charAt(i); if (ch == '&') { encoding = new StringBuffer("&"); } else if (encoding != null && ch == ';') { encoding.append(ch); if (encoding.toString().equals("'")) { sb.append('\''); } else if (encoding.toString().equals(""")) { sb.append('"'); } else if (encoding.toString().equals("&")) { sb.append('&'); } else if (encoding.toString().equals("<")) { sb.append('<'); } else if (encoding.toString().equals(">")) { sb.append('>'); } else { logger.info("Could not find an encoding for " + encoding.toString()); } encoding = null; } else if (encoding != null) { encoding.append(ch); } else { sb.append(ch); } // if (ch == '\'') { // sb.append("'"); // } else if (ch == '"') { // sb.append("""); // } else if (ch == '&') { // sb.append("&"); // } else if (ch == '<') { // sb.append("<"); // } else if (ch == '>') { // sb.append(">"); // } else if (ch == 0x03 || ch == 0x1a) { // logger.info("Stripping out illegal characters from " + src + // " as it will cause the XML to fail."); // } else { // sb.append(ch); // } } return sb.toString(); } /** * Replaces double quotes, ampersands, less-than and greater-than signs with * their character reference equivalents. This makes the returned string be * safe for use as an HTML content data. * */ public static String escapeHTML(String src) { if (src == null) return ""; StringBuffer sb = new StringBuffer(src.length()+10); // arbitrary amount of extra space char ch; for (int i = 0, n = src.length(); i < n; i++) { ch = src.charAt(i); if (ch == '"') { sb.append("""); } else if (ch == '&') { sb.append("&"); } else if (ch == '<') { sb.append("<"); } else if (ch == '>') { sb.append(">"); } else if (ch == 0x03 || ch == 0x1a) { logger.info("Stripping out illegal characters from " + src + " as it will cause the XML to fail."); } else { sb.append(ch); } } return sb.toString(); } /** * Escapes newlines because we need to preserve them in remarks and can be used elsewhere. */ public static String escapeNewLines(String src) { if (src == null) return ""; StringBuffer sb = new StringBuffer(src.length()+10); // arbitrary amount of extra space char ch; for (int i = 0, n = src.length(); i < n; i++) { ch = src.charAt(i); sb.append(ch == '\n' ? "&crlf;" : ch); } return sb.toString(); } /** * Unescapes the clrf entities put in by the escapeXML function. */ public static String unEscapeNewLines(String src) { if (src == null) return ""; StringBuffer sb = new StringBuffer(src.length()+10); // arbitrary amount of extra space char ch; for (int i = 0, n = src.length(); i < n; i++) { ch = src.charAt(i); if (ch == '&') { if (src.length() - i > 6 && "&crlf;".equals(src.substring(i,i+6))) { sb.append("\n"); i += 5; } else { sb.append(ch); } } else { sb.append(ch); } } return sb.toString(); } /** * Logs the given property change event at the DEBUG level. The format is: * <pre> * <i>Message</i>: <i>propertyName</i> "<i>oldValue</i>" -> "<i>newValue</i>" * </pre> * * @param logger The logger to log to * @param message The message to prefix the property change information * @param evt The event to print the details of */ public static void logPropertyChange( @Nonnull Logger logger, @Nullable String message, @Nonnull PropertyChangeEvent evt) { if (logger.isDebugEnabled()) { logger.debug(message + ": " + evt.getPropertyName() + " \"" + evt.getOldValue() + "\" -> \"" + evt.getNewValue() + "\""); } } /** * Copies the contents of a given {@link InputStream} into a given * {@link OutputStream}. * * @param source * The {@link InputStream} to copy data from * @param output * The {@link OutputStream} to copy the data to * @return The total number of bytes that got copied. * @throws IOException * If an I/O error occurs */ public static long copyStream(InputStream source, OutputStream output) throws IOException { int next; long total = 0; while ((next = source.read()) != -1) { output.write(next); total++; } output.flush(); return total; } /** * This method will recursively clean up a given object and all of its * descendants. * * @param o * The object to clean up, including its dependencies. * @return A collection of exceptions and errors that occurred during * cleanup if any occurred. */ public static CleanupExceptions cleanupSPObject(SPObject o) { CleanupExceptions exceptions = new CleanupExceptions(); exceptions.add(o.cleanup()); for (SPObject child : o.getChildren()) { exceptions.add(cleanupSPObject(child)); } return exceptions; } /** * This method returns a list of all of the ancestors of the given * {@link SPObject}. The order of the ancestors is such that the highest * ancestor is at the start of the list and the parent of the object itself * is at the end of the list. */ public static List<SPObject> getAncestorList(SPObject o) { List<SPObject> ancestors = new ArrayList<SPObject>(); SPObject parent = o.getParent(); while (parent != null) { ancestors.add(0, parent); parent = parent.getParent(); } return ancestors; } /** * Locates the SPObject inside the root SPObject which has the given * UUID, returning null if the item is not found. Throws ClassCastException * if in item is found, but it is not of the expected type. * * Note: If you are using an ArchitectProject, it is better to use its * getObjectInTree method, since that uses a HashMap, not recursion. * * @param <T> * The expected type of the item * @param uuid * The UUID of the item * @param expectedType * The type of the item with the given UUID. If you are uncertain * what type of object it is, or you do not want a * ClassCastException in case the item is of the wrong type, use * <tt>SPObject.class</tt> for this parameter. * @return The item, or null if no item with the given UUID exists in the * descendent tree rooted at the given root object. */ public static <T extends SPObject> T findByUuid(SPObject root, String uuid, Class<T> expectedType) { return expectedType.cast(findRecursively(root, uuid)); } /** * Performs a preorder traversal of the given {@link SPObject} and its * descendants, returning the first SPObject having the given UUID. * Returns null if no such SPObject exists under startWith. * * Note: If you are using an ArchitectProject, it is better to use its * getObjectInTree method, since that uses a HashMap, not recursion. * * @param startWith * The SPObject to start the search with. * @param uuid * The UUID to search for * @return the first SPObject having the given UUID in a preorder * traversal of startWith and its descendants. Returns null if no * such SPObject exists. */ private static SPObject findRecursively(SPObject startWith, String uuid) { if (startWith == null) { throw new IllegalArgumentException("Cannot search a null object for children with the uuid " + uuid); } if (uuid.equals(startWith.getUUID())) { return startWith; } List<? extends SPObject> children; if (startWith instanceof SQLObject) { children = ((SQLObject) startWith).getChildrenWithoutPopulating(); } else { children = startWith.getChildren(); } for (SPObject child : children) { SPObject found = findRecursively(child, uuid); if (found != null) { return found; } } return null; } public static Map<String, SPObject> buildIdMap(SPObject startWith) { Map<String, SPObject> idMap = new HashMap<String, SPObject>(); if (startWith == null) { throw new IllegalArgumentException("Root object is null"); } idMap.put(startWith.getUUID(), startWith); List<? extends SPObject> children; if (startWith instanceof SQLObject) { children = ((SQLObject) startWith).getChildrenWithoutPopulating(); } else { children = startWith.getChildren(); } for (SPObject child : children) { idMap.putAll(buildIdMap(child)); } return idMap; } /** * Adds the given listeners to the hierarchy of {@link SPObject}s rooted at * <code>root</code>. * * @param root * The object at the top of the subtree to listen to. Must not be * null. * @param spcl * The SQL Power child listener to add to root and all its * SPObject descendants. If you do not want {@link SPChildEvent}s, * you can provide null for this parameter. */ public static void listenToHierarchy(SPObject root, SPListener spcl) { root.addSPListener(spcl); if (root.allowsChildren()) { List<? extends SPObject> children; if (root instanceof SQLObject) { children = ((SQLObject) root).getChildrenWithoutPopulating(); } else { children = root.getChildren(); } for (SPObject wob : children) { listenToHierarchy(wob, spcl); } } } /** * This method is similar to listenToHierarchy but only listens to the * first two levels in the tree, i.e. the listener is not added to the * grand children of the root. See * {@link #lisenToHierachy(MatchMakerListener listener, MatchMakerObject root)} */ public static void listenToShallowHierarchy(SPListener listener, SPObject root) { root.addSPListener(listener); logger.debug("listenToShallowHierarchy: \"" + root.getName() + "\" (" + root.getClass().getName() + ") children: " + root.getChildren()); for (SPObject spo : root.getChildren()) { spo.addSPListener(listener); } } /** * Removes the given listeners from the hierarchy of {@link SPObject}s * rooted at <code>root</code>. * * @param root * The object at the top of the subtree to unlisten to. Must not * be null. * @param spcl * The SQL Power child listener to remove from root and all its * SPObject descendants. If you do not want to unlisten to * {@link SPChildEvent}s, you can provide null for this * parameter. */ public static void unlistenToHierarchy(SPObject root, SPListener spcl) { root.removeSPListener(spcl); if (root.allowsChildren()) { List<? extends SPObject> children; if (root instanceof SQLObject) { children = ((SQLObject) root).getChildrenWithoutPopulating(); } else { children = root.getChildren(); } for (SPObject wob : children) { unlistenToHierarchy(wob, spcl); } } } /** * Prints the subtree rooted at the given {@link SPObject} to the given * output stream. This is only intended for debugging; any machine parsing * of the output of this method is incorrect! * * @param out * the target of the debug information (often System.out) * @param startWith * the root object for the dump */ public static void printSubtree(PrintWriter out, SPObject startWith) { printSubtree(out, startWith, 0); } /** * Recursive subroutine of {@link #printSubtree(PrintWriter, SPObject)}. * * @param out * The print stream to print to * @param startWith * The object to print (and whose children to process * recursively) * @param indentDepth * The amount of indent to print before printing the object * information */ private static void printSubtree(PrintWriter out, SPObject startWith, int indentDepth) { out.printf("%s%s \"%s\" (%s)\n", spaces(indentDepth * 2), startWith.getClass().getSimpleName(), startWith.getName(), startWith.getUUID()); for (SPObject child : startWith.getChildren()) { printSubtree(out, child, indentDepth + 1); } } /** * Creates a string consisting of the desired number of spaces. * * @param n * The number of spaces in the string. * @return A string of length n which consists entirely of spaces. */ private static String spaces(int n) { StringBuilder sb = new StringBuilder(n); for (int i = 0; i < n; i++) { sb.append(" "); } return sb.toString(); } /** * Returns the human-readable summary of the given service info object. * Anywhere a server is referred to within the Wabit, this method should be * used to convert the service info object into the string the user sees. * * @param si * The service info to summarize. * @return The Wabit's canonical human-readable representation of the given * service info. */ public static String serviceInfoSummary(SPServerInfo si) { return si.getName() + " (" + si.getServerAddress() + ":" + si.getPort() + ")"; } /** * This method will display the cleanup errors to the user. If the * user prompter factory given is null the errors will be logged instead. */ public static void displayCleanupErrors(@Nonnull CleanupExceptions cleanupObject, UserPrompterFactory upf) { if (upf != null) { if (!cleanupObject.isCleanupSuccessful()) { StringBuffer message = new StringBuffer(); message.append("The following errors occurred during closing\n"); for (String error : cleanupObject.getErrorMessages()) { message.append(" " + error + "\n"); } for (Exception exception : cleanupObject.getExceptions()) { message.append(" " + exception.getMessage() + "\n"); logger.error("Exception during cleanup", exception); } UserPrompter up = upf.createUserPrompter( message.toString(), UserPromptType.MESSAGE, UserPromptOptions.OK, UserPromptResponse.OK, null, "OK"); up.promptUser(); } } else { logCleanupErrors(cleanupObject); } } /** * Logs the exceptions and errors. This is useful if there is no available * user prompter. */ public static void logCleanupErrors(@Nonnull CleanupExceptions cleanupObject) { for (String error : cleanupObject.getErrorMessages()) { logger.debug("Exception during cleanup, " + error); } for (Exception exception : cleanupObject.getExceptions()) { logger.error("Exception during cleanup", exception); } } /** * Resolves/decodes a path that can be any kind of URI, including a local * file reference. This method recognizes a superset of the path specs that * {@link #resolveConfiguredFilePath(ServletContext, String)} does. * <p> * The syntax for the path argument is documented in detail below: * <br><br> * Information for configuring file/URL locations in this file: * <br><br> * Configuration parameters described as accepting "any location" are * interpreted as follows: * <br> * <ol> * <li>If the value contains "://" then it is taken as an absolute URI, and * is fed to the java.net.URI constructor as-is * <br><br> * NOTE: if you are referencing a local file, you may find option 3, below, * more convenient and reliable than crafting a correct file:// URI * yourself, especially if the file's pathname contains spaces.<br><br></li> * * <li>If the value starts with "webapp:" then it is taken as a location * inside this web application. Note that items in the WEB-INF directory are * accessible, as are the publicly-readable items outside of WEB-INF. * <br><br> * Caveat: This only works if the webapp is deployed in "exploded" form * (served from actual files and not straight out of the .war file) * <br><br> * This format is used for the default value, so the caveat applies by * default.<br><br></li> * * <li>Otherwise, the value is taken as a filesystem location which may be * absolute or relative. The value is fed to the java.io.File constructor * as-is, and is then converted to a URI using File.toURI().</li> * </ol> * <br> * Configuration parameters described as accepting * "local filesystem locations" are interpreted using only steps 2 and 3 * above. It is an error to specify a location that contains the "://" * sequence of characters in it. * * @param context * If the path starts with webapp: this context will be used to * find where the file is located. * @param relativeDir * If the path is a relative one we will look for the path * starting from this directory. If this directory is not * specified then the default execution directory will be used. * @param path * @return */ public static URI resolveConfiguredPath(ServletContext context, File relativeDir, String path) { if (path.contains("://")) { try { return new URI(path); } catch (URISyntaxException e) { throw new IllegalArgumentException( "Configured path " + path + " looks like a URI, but isn't. " + "Check your typing.", e); } } else { return resolveConfiguredFilePath(context, relativeDir, path).toURI(); } } /** * @see #resolveConfiguredPath(ServletContext, File, String) */ public static URI resolveConfiguredPath(ServletContext context, String path) { return resolveConfiguredPath(context, null, path); } /** * Resolves/decodes a path that can be any kind of URI, including a local * file reference. Compared with * {@link #resolveConfiguredPath(ServletContext, String)}, this method * recognizes the subset of path specs that refer to the local filesystem * and do not contain the substring "://" (so file:// URIs are not * recognized by this method). * <p> * The syntax for the path argument is documented in detail on * {@link #resolveConfiguredPath(ServletContext, String)}. * * @param context * If the path starts with webapp: this context will be used to * find where the file is located. * @param relativeDir * If the path is a relative one we will look for the path * starting from this directory. If this directory is not * specified then the default execution directory will be used. * @param path * @return */ public static File resolveConfiguredFilePath(ServletContext context, File relativeDir, String path) { if (path.startsWith("webapp:")) { if (context == null) { throw new IllegalArgumentException( "Path specifications starting with 'webapp:' are only " + "allowed in web applications"); } String contextPath = path.substring("webapp:".length()); String realPath = context.getRealPath(contextPath); if (realPath == null) { throw new IllegalArgumentException( "Webapp path " + path + " could not be resolved. " + "Check that it is valid, and ensure this webapp has " + "been deployed in \"exploded\" mode."); } File file = new File(realPath); if (!file.exists()) { throw new IllegalArgumentException( "Webapp path " + path + " resolved to " + file.getAbsolutePath() + ", which does not exist. Check that the path is valid, " + "and ensure this webapp has been deployed in \"exploded\" mode."); } return file; } else { File file = new File(path); if (relativeDir != null && !file.isAbsolute()) { file = new File(relativeDir, path); } return file; } } /** * This method mirrors the * {@link #resolveConfiguredPath(ServletContext, String)} except it * takes a file that is the existing directory that contains the file we are * resolving. This allows us to translate files normally using the webapp: * file type when we do not have a servlet context. * * @param rootFile * @param path * @return */ public static URI resolveConfiguredPath(File rootFile, String path) { if (path.contains("://") || path.startsWith("file:")) { try { return new URI(path); } catch (URISyntaxException e) { throw new IllegalArgumentException( "Configured path " + path + " looks like a URI, but isn't. " + "Check your typing.", e); } } else { return resolveConfiguredFilePath(rootFile, path).toURI(); } } /** * This method mirrors the * {@link #resolveConfiguredFilePath(ServletContext, String)} except it * takes a file that is the existing directory that contains the file we are * resolving. This allows us to translate files normally using the webapp: * file type when we do not have a servlet context. * * @param rootFile * @param path * @return */ public static File resolveConfiguredFilePath(File rootFile, String path) { if (path.startsWith("webapp:")) { if (rootFile == null) { throw new IllegalArgumentException( "Path specifications starting with 'webapp:' must have a root" + " file specified"); } if (!rootFile.exists()) { throw new IllegalArgumentException("The root directory " + rootFile + " does not exist."); } if (!rootFile.isDirectory()) { throw new IllegalStateException("The root directory " + rootFile + " is not a directory."); } String contextPath = path.substring("webapp:".length()); File file = new File(rootFile, contextPath); if (!file.exists()) { throw new IllegalArgumentException( "Webapp path " + path + " resolved to " + file.getAbsolutePath() + ", which does not exist. Check that the path is valid, " + "and ensure this webapp has been deployed in \"exploded\" mode."); } return file; } else { File file = new File(path); if (rootFile != null && !file.isAbsolute()) { file = new File(rootFile, path); } return file; } } /** * Returns the first ancestor of <tt>so</tt> which is of the given type, or * <tt>null</tt> if <tt>so</tt> doesn't have an ancestor whose class is * <tt>ancestorType</tt>. * * @param so * The object for whose ancestor to look. (Thanks, Winston). * @return The nearest ancestor of type ancestorType, or null if no such * ancestor exists. */ public static <T extends SPObject> T getAncestor(SPObject so, Class<T> ancestorType) { while (so != null) { if (so.getClass().equals(ancestorType) || ancestorType.isAssignableFrom(so.getClass())) return ancestorType.cast(so); so = so.getParent(); } return null; } /** * Follows the chain of exceptions (using the getCause() method) to find the * root cause, which is the exception whose getCause() method returns null. * * @param t The Throwable for which you want to know the root cause. Must not * be null. * @return The ultimate cause of t. This may be t itself. */ public static Throwable rootCause(Throwable t) { while (t.getCause() != null) t = t.getCause(); return t; } /** * Writes a stack trace to a string for user readability. * @param throwable * @return */ public static String exceptionStackToString(final Throwable throwable) { // Details information Throwable t = throwable; StringWriter stringWriter = new StringWriter(); final PrintWriter traceWriter = new PrintWriter(stringWriter); do { t.printStackTrace(traceWriter); if (SQLPowerUtils.rootCause(t) instanceof SQLException) { t = ((SQLException) SQLPowerUtils.rootCause(t)).getNextException(); if (t != null) { traceWriter.println("Next Exception:"); //$NON-NLS-1$ } } else { t = null; } } while (t != null); traceWriter.close(); return stringWriter.toString(); } }