diff options
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/util')
6 files changed, 1303 insertions, 0 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java new file mode 100644 index 00000000..fb240d5f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java @@ -0,0 +1,71 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import java.util.*; + +/** + * Simple implementation of a bounded list. If the maximum size is reached, adding a new element will + * remove the first element in the list. + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2005/05/09 20:01:25 $ + */ +public class BoundedList<E> extends LinkedList<E> { + private int maxSize; + + /** + * Creates a new bounded list with the given maximum size. + * @param maxSize The maximum number of elements the list may hold. + */ + public BoundedList(int maxSize) { + this.maxSize = maxSize; + } + + /** + * Adds an element to the tail of the list. If the list is full, the first element is removed. + * @param e The element to add. + * @return Always <code>true</code>. + */ + public boolean add(E e) { + if (isFull()) { + removeFirst(); + } + return super.add(e); + } + + /** + * Adds an element to the head of list. If the list is full, the last element is removed. + * @param e The element to add. + */ + public void addFirst(E e) { + if (isFull()) { + removeLast(); + } + super.addFirst(e); + } + + /** + * Returns whether the list if full. + * @return Whether the list is full. + */ + private boolean isFull() { + return size() == maxSize; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java new file mode 100644 index 00000000..e91758ef --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java @@ -0,0 +1,186 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import java.io.Closeable; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.Arrays; + +import net.sourceforge.subsonic.Logger; + +/** + * Miscellaneous file utility methods. + * + * @author Sindre Mehus + */ +public final class FileUtil { + + private static final Logger LOG = Logger.getLogger(FileUtil.class); + + /** + * Disallow external instantiation. + */ + private FileUtil() { + } + + public static boolean isFile(final File file) { + return timed(new FileTask<Boolean>("isFile", file) { + @Override + public Boolean execute() { + return file.isFile(); + } + }); + } + + public static boolean isDirectory(final File file) { + return timed(new FileTask<Boolean>("isDirectory", file) { + @Override + public Boolean execute() { + return file.isDirectory(); + } + }); + } + + public static boolean exists(final File file) { + return timed(new FileTask<Boolean>("exists", file) { + @Override + public Boolean execute() { + return file.exists(); + } + }); + } + + public static long lastModified(final File file) { + return timed(new FileTask<Long>("lastModified", file) { + @Override + public Long execute() { + return file.lastModified(); + } + }); + } + + public static long length(final File file) { + return timed(new FileTask<Long>("length", file) { + @Override + public Long execute() { + return file.length(); + } + }); + } + + /** + * Similar to {@link File#listFiles()}, but never returns null. + * Instead a warning is logged, and an empty array is returned. + */ + public static File[] listFiles(final File dir) { + File[] files = timed(new FileTask<File[]>("listFiles", dir) { + @Override + public File[] execute() { + return dir.listFiles(); + } + }); + + if (files == null) { + LOG.warn("Failed to list children for " + dir.getPath()); + return new File[0]; + } + return files; + } + + /** + * Similar to {@link File#listFiles(FilenameFilter)}, but never returns null. + * Instead a warning is logged, and an empty array is returned. + */ + public static File[] listFiles(final File dir, final FilenameFilter filter, boolean sort) { + File[] files = timed(new FileTask<File[]>("listFiles2", dir) { + @Override + public File[] execute() { + return dir.listFiles(filter); + } + }); + if (files == null) { + LOG.warn("Failed to list children for " + dir.getPath()); + return new File[0]; + } + if (sort) { + Arrays.sort(files); + } + return files; + } + + /** + * Returns a short path for the given file. The path consists of the name + * of the parent directory and the given file. + */ + public static String getShortPath(File file) { + if (file == null) { + return null; + } + File parent = file.getParentFile(); + if (parent == null) { + return file.getName(); + } + return parent.getName() + File.separator + file.getName(); + } + + /** + * Closes the "closable", ignoring any excepetions. + * + * @param closeable The Closable to close, may be {@code null}. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + // Ignored + } + } + } + + private static <T> T timed(FileTask<T> task) { +// long t0 = System.nanoTime(); +// try { + return task.execute(); +// } finally { +// long t1 = System.nanoTime(); +// LOG.debug((t1 - t0) / 1000L + " microsec, " + task); +// } + } + + private abstract static class FileTask<T> { + + private final String name; + private final File file; + + public FileTask(String name, File file) { + this.name = name; + this.file = file; + } + + public abstract T execute(); + + @Override + public String toString() { + return name + ", " + file; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java new file mode 100644 index 00000000..7edecaa2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java @@ -0,0 +1,54 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Pair<S, T> implements Serializable { + + private S first; + private T second; + + public Pair() { + } + + public Pair(S first, T second) { + this.first = first; + this.second = second; + } + + public S getFirst() { + return first; + } + + public void setFirst(S first) { + this.first = first; + } + + public T getSecond() { + return second; + } + + public void setSecond(T second) { + this.second = second; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java new file mode 100644 index 00000000..ebad9fbf --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java @@ -0,0 +1,537 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.security.MessageDigest; +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.math.LongRange; + +/** + * Miscellaneous string utility methods. + * + * @author Sindre Mehus + */ +public final class StringUtil { + + public static final String ENCODING_LATIN = "ISO-8859-1"; + public static final String ENCODING_UTF8 = "UTF-8"; + private static final DateFormat ISO_8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + private static final String[][] HTML_SUBSTITUTIONS = { + {"&", "&"}, + {"<", "<"}, + {">", ">"}, + {"'", "'"}, + {"\"", """}, + }; + + private static final String[][] MIME_TYPES = { + {"mp3", "audio/mpeg"}, + {"ogg", "audio/ogg"}, + {"oga", "audio/ogg"}, + {"ogx", "application/ogg"}, + {"aac", "audio/mp4"}, + {"m4a", "audio/mp4"}, + {"flac", "audio/flac"}, + {"wav", "audio/x-wav"}, + {"wma", "audio/x-ms-wma"}, + {"ape", "audio/x-monkeys-audio"}, + {"mpc", "audio/x-musepack"}, + {"shn", "audio/x-shn"}, + + {"flv", "video/x-flv"}, + {"avi", "video/avi"}, + {"mpg", "video/mpeg"}, + {"mpeg", "video/mpeg"}, + {"mp4", "video/mp4"}, + {"m4v", "video/x-m4v"}, + {"mkv", "video/x-matroska"}, + {"mov", "video/quicktime"}, + {"wmv", "video/x-ms-wmv"}, + {"ogv", "video/ogg"}, + {"divx", "video/divx"}, + {"m2ts", "video/MP2T"}, + + {"gif", "image/gif"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"png", "image/png"}, + {"bmp", "image/bmp"}, + }; + + private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "|"}; + + /** + * Disallow external instantiation. + */ + private StringUtil() { + } + + /** + * Returns the specified string converted to a format suitable for + * HTML. All single-quote, double-quote, greater-than, less-than and + * ampersand characters are replaces with their corresponding HTML + * Character Entity code. + * + * @param s the string to convert + * @return the converted string + */ + public static String toHtml(String s) { + if (s == null) { + return null; + } + for (String[] substitution : HTML_SUBSTITUTIONS) { + if (s.contains(substitution[0])) { + s = s.replaceAll(substitution[0], substitution[1]); + } + } + return s; + } + + + /** + * Formats the given date to a ISO-8601 date/time format, and UTC timezone. + * <p/> + * The returned date uses the following format: 2007-12-17T14:57:17 + * + * @param date The date to format + * @return The corresponding ISO-8601 formatted string. + */ + public static String toISO8601(Date date) { + if (date == null) { + return null; + } + + synchronized (ISO_8601_DATE_FORMAT) { + return ISO_8601_DATE_FORMAT.format(date); + } + } + + /** + * Removes the suffix (the substring after the last dot) of the given string. The dot is + * also removed. + * + * @param s The string in question, e.g., "foo.mp3". + * @return The string without the suffix, e.g., "foo". + */ + public static String removeSuffix(String s) { + int index = s.lastIndexOf('.'); + return index == -1 ? s : s.substring(0, index); + } + + /** + * Returns the proper MIME type for the given suffix. + * + * @param suffix The suffix, e.g., "mp3" or ".mp3". + * @return The corresponding MIME type, e.g., "audio/mpeg". If no MIME type is found, + * <code>application/octet-stream</code> is returned. + */ + public static String getMimeType(String suffix) { + for (String[] map : MIME_TYPES) { + if (map[0].equalsIgnoreCase(suffix) || ('.' + map[0]).equalsIgnoreCase(suffix)) { + return map[1]; + } + } + return "application/octet-stream"; + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + * <ul> + * <li><code>format(918)</code> returns <em>"918 B"</em>.</li> + * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li> + * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li> + * </ul> + * This method assumes that 1 KB is 1024 bytes. + * + * @param byteCount The number of bytes. + * @param locale The locale used for formatting. + * @return The formatted string. + */ + public static synchronized String formatBytes(long byteCount, Locale locale) { + + // More than 1 GB? + if (byteCount >= 1024 * 1024 * 1024) { + NumberFormat gigaByteFormat = new DecimalFormat("0.00 GB", new DecimalFormatSymbols(locale)); + return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024)); + } + + // More than 1 MB? + if (byteCount >= 1024 * 1024) { + NumberFormat megaByteFormat = new DecimalFormat("0.0 MB", new DecimalFormatSymbols(locale)); + return megaByteFormat.format((double) byteCount / (1024 * 1024)); + } + + // More than 1 KB? + if (byteCount >= 1024) { + NumberFormat kiloByteFormat = new DecimalFormat("0 KB", new DecimalFormatSymbols(locale)); + return kiloByteFormat.format((double) byteCount / 1024); + } + + return byteCount + " B"; + } + + /** + * Formats a duration with minutes and seconds, e.g., "93:45" + */ + public static String formatDuration(int seconds) { + int minutes = seconds / 60; + int secs = seconds % 60; + + StringBuilder builder = new StringBuilder(6); + builder.append(minutes).append(":"); + if (secs < 10) { + builder.append("0"); + } + builder.append(secs); + return builder.toString(); + } + + /** + * Splits the input string. White space is interpreted as separator token. Double quotes + * are interpreted as grouping operator. <br/> + * For instance, the input <code>"u2 rem "greatest hits""</code> will return an array with + * three elements: <code>{"u2", "rem", "greatest hits"}</code> + * + * @param input The input string. + * @return Array of elements. + */ + public static String[] split(String input) { + if (input == null) { + return new String[0]; + } + + Pattern pattern = Pattern.compile("\".*?\"|\\S+"); + Matcher matcher = pattern.matcher(input); + + List<String> result = new ArrayList<String>(); + while (matcher.find()) { + String element = matcher.group(); + if (element.startsWith("\"") && element.endsWith("\"") && element.length() > 1) { + element = element.substring(1, element.length() - 1); + } + result.add(element); + } + + return result.toArray(new String[result.size()]); + } + + /** + * Reads lines from the given input stream. All lines are trimmed. Empty lines and lines starting + * with "#" are skipped. The input stream is always closed by this method. + * + * @param in The input stream to read from. + * @return Array of lines. + * @throws IOException If an I/O error occurs. + */ + public static String[] readLines(InputStream in) throws IOException { + BufferedReader reader = null; + + try { + reader = new BufferedReader(new InputStreamReader(in)); + List<String> result = new ArrayList<String>(); + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + line = line.trim(); + if (!line.startsWith("#") && line.length() > 0) { + result.add(line); + } + } + return result.toArray(new String[result.size()]); + + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(reader); + } + } + + /** + * Converts the given string of whitespace-separated integers to an <code>int</code> array. + * + * @param s String consisting of integers separated by whitespace. + * @return The corresponding array of ints. + * @throws NumberFormatException If string contains non-parseable text. + */ + public static int[] parseInts(String s) { + if (s == null) { + return new int[0]; + } + + String[] strings = StringUtils.split(s); + int[] ints = new int[strings.length]; + for (int i = 0; i < strings.length; i++) { + ints[i] = Integer.parseInt(strings[i]); + } + return ints; + } + + /** + * Change protocol from "https" to "http" for the given URL. The port number is also changed, + * but not if the given URL is already "http". + * + * @param url The original URL. + * @param port The port number to use, for instance 443. + * @return The transformed URL. + * @throws MalformedURLException If the original URL is invalid. + */ + public static String toHttpUrl(String url, int port) throws MalformedURLException { + URL u = new URL(url); + if ("https".equals(u.getProtocol())) { + return new URL("http", u.getHost(), port, u.getFile()).toString(); + } + return url; + } + + /** + * Determines whether a is equal to b, taking null into account. + * + * @return Whether a and b are equal, or both null. + */ + public static boolean isEqual(Object a, Object b) { + return a == null ? b == null : a.equals(b); + } + + /** + * Parses a locale from the given string. + * + * @param s The locale string. Should be formatted as per the documentation in {@link Locale#toString()}. + * @return The locale. + */ + public static Locale parseLocale(String s) { + if (s == null) { + return null; + } + + String[] elements = s.split("_"); + + if (elements.length == 0) { + return new Locale(s, "", ""); + } + if (elements.length == 1) { + return new Locale(elements[0], "", ""); + } + if (elements.length == 2) { + return new Locale(elements[0], elements[1], ""); + } + return new Locale(elements[0], elements[1], elements[2]); + } + + /** + * URL-encodes the input value using UTF-8. + */ + public static String urlEncode(String s) { + try { + return URLEncoder.encode(s, StringUtil.ENCODING_UTF8); + } catch (UnsupportedEncodingException x) { + throw new RuntimeException(x); + } + } + + /** + * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes. + * + * @param s The string to encode. + * @return The encoded string. + */ + public static String utf8HexEncode(String s) { + if (s == null) { + return null; + } + byte[] utf8; + try { + utf8 = s.getBytes(ENCODING_UTF8); + } catch (UnsupportedEncodingException x) { + throw new RuntimeException(x); + } + return String.valueOf(Hex.encodeHex(utf8)); + } + + /** + * Decodes the given string by using the hexadecimal representation of its UTF-8 bytes. + * + * @param s The string to decode. + * @return The decoded string. + * @throws Exception If an error occurs. + */ + public static String utf8HexDecode(String s) throws Exception { + if (s == null) { + return null; + } + return new String(Hex.decodeHex(s.toCharArray()), ENCODING_UTF8); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param s Data to digest. + * @return MD5 digest as a hex string. + */ + public static String md5Hex(String s) { + if (s == null) { + return null; + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return new String(Hex.encodeHex(md5.digest(s.getBytes(ENCODING_UTF8)))); + } catch (Exception x) { + throw new RuntimeException(x.getMessage(), x); + } + } + + /** + * Returns the file part of an URL. For instance: + * <p/> + * <code> + * getUrlFile("http://archive.ncsa.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer.html") + * </code> + * <p/> + * will return "url-primer.html". + * + * @param url The URL in question. + * @return The file part, or <code>null</code> if no file can be resolved. + */ + public static String getUrlFile(String url) { + try { + String path = new URL(url).getPath(); + if (StringUtils.isBlank(path) || path.endsWith("/")) { + return null; + } + + File file = new File(path); + String filename = file.getName(); + if (StringUtils.isBlank(filename)) { + return null; + } + return filename; + + } catch (MalformedURLException x) { + return null; + } + } + + /** + * Rewrites the URL by changing the protocol, host and port. + * + * @param urlToRewrite The URL to rewrite. + * @param urlWithProtocolHostAndPort Use protocol, host and port from this URL. + * @return The rewritten URL, or an unchanged URL if either argument is not a proper URL. + */ + public static String rewriteUrl(String urlToRewrite, String urlWithProtocolHostAndPort) { + if (urlToRewrite == null) { + return null; + } + + try { + URL urlA = new URL(urlToRewrite); + URL urlB = new URL(urlWithProtocolHostAndPort); + + URL result = new URL(urlB.getProtocol(), urlB.getHost(), urlB.getPort(), urlA.getFile()); + return result.toExternalForm(); + } catch (MalformedURLException x) { + return urlToRewrite; + } + } + + /** + * Makes a given filename safe by replacing special characters like slashes ("/" and "\") + * with dashes ("-"). + * + * @param filename The filename in question. + * @return The filename with special characters replaced by underscores. + */ + public static String fileSystemSafe(String filename) { + for (String s : FILE_SYSTEM_UNSAFE) { + filename = filename.replace(s, "-"); + } + return filename; + } + + /** + * Parses the given string as a HTTP header byte range. See chapter 14.36.1 in RFC 2068 + * for details. + * <p/> + * Only a subset of the allowed syntaxes are supported. Only ranges which specify first-byte-pos + * are supported. The last-byte-pos is optional. + * + * @param range The range from the HTTP header, for instance "bytes=0-499" or "bytes=500-" + * @return A range object (using inclusive values). If the last-byte-pos is not given, the end of + * the returned range is {@link Long#MAX_VALUE}. The method returns <code>null</code> if the syntax + * of the given range is not supported. + */ + public static LongRange parseRange(String range) { + if (range == null) { + return null; + } + + Pattern pattern = Pattern.compile("bytes=(\\d+)-(\\d*)"); + Matcher matcher = pattern.matcher(range); + + if (matcher.matches()) { + String firstString = matcher.group(1); + String lastString = StringUtils.trimToNull(matcher.group(2)); + + long first = Long.parseLong(firstString); + long last = lastString == null ? Long.MAX_VALUE : Long.parseLong(lastString); + + if (first > last) { + return null; + } + + return new LongRange(first, last); + } + return null; + } + + public static String removeMarkup(String s) { + if (s == null) { + return null; + } + return s.replaceAll("<.*?>", ""); + } + + public static String getRESTProtocolVersion() { + // TODO: Read from xsd. + return "1.8.0"; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java new file mode 100644 index 00000000..ec7175a2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java @@ -0,0 +1,127 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import net.sourceforge.subsonic.Logger; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Inet4Address; +import java.util.Enumeration; +import java.util.Random; + +/** + * Miscellaneous general utility methods. + * + * @author Sindre Mehus + */ +public final class Util { + + private static final Logger LOG = Logger.getLogger(Util.class); + private static final Random RANDOM = new Random(System.currentTimeMillis()); + + /** + * Disallow external instantiation. + */ + private Util() { + } + + public static String getDefaultMusicFolder() { + String def = isWindows() ? "c:\\music" : "/var/music"; + return System.getProperty("subsonic.defaultMusicFolder", def); + } + + public static String getDefaultPodcastFolder() { + String def = isWindows() ? "c:\\music\\Podcast" : "/var/music/Podcast"; + return System.getProperty("subsonic.defaultPodcastFolder", def); + } + + public static String getDefaultPlaylistFolder() { + String def = isWindows() ? "c:\\playlists" : "/var/playlists"; + return System.getProperty("subsonic.defaultPlaylistFolder", def); + } + + public static boolean isWindows() { + return System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows"); + } + + public static boolean isWindowsInstall() { + return "true".equals(System.getProperty("subsonic.windowsInstall")); + } + + /** + * Similar to {@link ServletResponse#setContentLength(int)}, but this + * method supports lengths bigger than 2GB. + * <p/> + * See http://blogger.ziesemer.com/2008/03/suns-version-of-640k-2gb.html + * + * @param response The HTTP response. + * @param length The content length. + */ + public static void setContentLength(HttpServletResponse response, long length) { + if (length <= Integer.MAX_VALUE) { + response.setContentLength((int) length); + } else { + response.setHeader("Content-Length", String.valueOf(length)); + } + } + + /** + * Returns the local IP address. + * @return The local IP, or the loopback address (127.0.0.1) if not found. + */ + public static String getLocalIpAddress() { + try { + + // Try the simple way first. + InetAddress address = InetAddress.getLocalHost(); + if (!address.isLoopbackAddress()) { + return address.getHostAddress(); + } + + // Iterate through all network interfaces, looking for a suitable IP. + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + Enumeration<InetAddress> addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) { + return addr.getHostAddress(); + } + } + } + + } catch (Throwable x) { + LOG.warn("Failed to resolve local IP address.", x); + } + + return "127.0.0.1"; + } + + public static int randomInt(int min, int max) { + if (min >= max) { + return 0; + } + return min + RANDOM.nextInt(max - min); + + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java new file mode 100644 index 00000000..a572ac0f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java @@ -0,0 +1,328 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import org.apache.commons.lang.StringEscapeUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.XML; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Stack; + + +/** + * Simplifies building of XML documents. + * <p/> + * <b>Example:</b><br/> + * The following code: + * <pre> + * XMLBuilder builder = XMLBuilder.createXMLBuilder(); + * builder.add("foo").add("bar"); + * builder.add("zonk", 42); + * builder.end().end(); + * System.out.println(builder.toString()); + * </pre> + * produces the following XML: + * <pre> + * <foo> + * <bar> + * <zonk>42</zonk> + * </bar> + * </foo> + * </pre> + * This class is <em>not</em> thread safe. + * <p/> + * Also supports JSON and JSONP formats. + * + * @author Sindre Mehus + */ +public class XMLBuilder { + + private static final String INDENTATION = " "; + private static final String NEWLINE = "\n"; + + private final Writer writer = new StringWriter(); + private final Stack<String> elementStack = new Stack<String>(); + private final boolean json; + private final String jsonpCallback; + + public static XMLBuilder createXMLBuilder() { + return new XMLBuilder(false, null); + } + + public static XMLBuilder createJSONBuilder() { + return new XMLBuilder(true, null); + } + + public static XMLBuilder createJSONPBuilder(String callback) { + return new XMLBuilder(true, callback); + } + + /** + * Creates a new instance. + * + * @param json Whether to produce JSON rather than XML. + * @param jsonpCallback Name of javascript callback for JSONP. + */ + private XMLBuilder(boolean json, String jsonpCallback) { + this.json = json; + this.jsonpCallback = jsonpCallback; + } + + /** + * Adds an XML preamble, with the given encoding. The preamble will typically + * look like this: + * <p/> + * <code><?xml version="1.0" encoding="UTF-8"?></code> + * + * @param encoding The encoding to put in the preamble. + * @return A reference to this object. + */ + public XMLBuilder preamble(String encoding) throws IOException { + writer.write("<?xml version=\"1.0\" encoding=\""); + writer.write(encoding); + writer.write("\"?>"); + newline(); + return this; + } + + /** + * Adds an element with the given name and a single attribute. + * + * @param element The element name. + * @param attributeKey The attributes key. + * @param attributeValue The attributes value. + * @param close Whether to close the element. + * @return A reference to this object. + */ + public XMLBuilder add(String element, String attributeKey, Object attributeValue, boolean close) throws IOException { + return add(element, close, new Attribute(attributeKey, attributeValue)); + } + + /** + * Adds an element with the given name and attributes. + * + * @param element The element name. + * @param close Whether to close the element. + * @param attributes The element attributes. + * @return A reference to this object. + */ + public XMLBuilder add(String element, boolean close, Attribute... attributes) throws IOException { + return add(element, Arrays.asList(attributes), close); + } + + /** + * Adds an element with the given name and attributes. + * + * @param element The element name. + * @param attributes The element attributes. + * @param close Whether to close the element. + * @return A reference to this object. + */ + public XMLBuilder add(String element, Iterable<Attribute> attributes, boolean close) throws IOException { + return add(element, attributes, null, close); + } + + /** + * Adds an element with the given name, attributes and character data. + * + * @param element The element name. + * @param attributes The element attributes. + * @param text The character data. + * @param close Whether to close the element. + * @return A reference to this object. + */ + public XMLBuilder add(String element, Iterable<Attribute> attributes, String text, boolean close) throws IOException { + indent(); + elementStack.push(element); + writer.write('<'); + writer.write(element); + + if (attributes == null) { + attributes = Collections.emptyList(); + } + + Iterator<Attribute> iterator = attributes.iterator(); + + if (iterator.hasNext()) { + writer.write(' '); + } + while (iterator.hasNext()) { + Attribute attribute = iterator.next(); + attribute.append(writer); + if (iterator.hasNext()) { + writer.write(' '); + } + } + + if (close && text == null) { + elementStack.pop(); + writer.write("/>"); + } else { + writer.write('>'); + } + + if (text != null) { + writer.write(text); + + if (close) { + elementStack.pop(); + writer.write("</"); + writer.write(element); + writer.write('>'); + } + } + + newline(); + return this; + } + + /** + * Closes the current element. + * + * @return A reference to this object. + * @throws IllegalStateException If there are no unclosed elements. + */ + public XMLBuilder end() throws IllegalStateException, IOException { + if (elementStack.isEmpty()) { + throw new IllegalStateException("There are no unclosed elements."); + } + + String element = elementStack.pop(); + indent(); + writer.write("</"); + writer.write(element); + writer.write('>'); + newline(); + return this; + } + + /** + * Closes all unclosed elements. + * + * @return A reference to this object. + */ + public XMLBuilder endAll() throws IOException { + while (!elementStack.isEmpty()) { + end(); + } + return this; + } + + /** + * Returns the XML document as a string. + */ + @Override + public String toString() { + String xml = writer.toString(); + if (!json) { + return xml; + } + try { + JSONObject jsonObject = XML.toJSONObject(xml); + + if (jsonpCallback != null) { + return jsonpCallback + "(" + jsonObject.toString(1) + ");"; + } + + return jsonObject.toString(1); + } catch (JSONException x) { + throw new RuntimeException("Failed to convert from XML to JSON.", x); + } + } + + private void indent() throws IOException { + int depth = elementStack.size(); + for (int i = 0; i < depth; i++) { + writer.write(INDENTATION); + } + } + + private void newline() throws IOException { + writer.write(NEWLINE); + } + + /** + * An XML element attribute. + */ + public static class Attribute { + + private final String key; + private final Object value; + + public Attribute(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + private void append(Writer writer) throws IOException { + if (key != null && value != null) { + writer.write(key); + writer.write("=\""); + writer.write(StringEscapeUtils.escapeXml(value.toString())); + writer.write("\""); + } + } + } + + /** + * A set of attributes. + */ + public static class AttributeSet implements Iterable<Attribute> { + + private final Map<String, Attribute> attributes = new LinkedHashMap<String, Attribute>(); + + public void add(Attribute attribute) { + attributes.put(attribute.getKey(), attribute); + } + + public void add(String key, Object value) { + if (key != null && value != null) { + add(new Attribute(key, value)); + } + } + + public void addAll(Iterable<Attribute> attributes) { + for (Attribute attribute : attributes) { + add(attribute); + } + } + + public Iterator<Attribute> iterator() { + return attributes.values().iterator(); + } + } + +} |