aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main/java/net/sourceforge/subsonic/util
diff options
context:
space:
mode:
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/util')
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java71
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java186
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java54
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java537
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java127
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java328
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 = {
+ {"&", "&amp;"},
+ {"<", "&lt;"},
+ {">", "&gt;"},
+ {"'", "&#39;"},
+ {"\"", "&#34;"},
+ };
+
+ 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>
+ * &lt;foo&gt;
+ * &lt;bar&gt;
+ * &lt;zonk&gt;42&lt;/zonk&gt;
+ * &lt;/bar&gt;
+ * &lt;/foo&gt;
+ * </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>&lt;?xml version="1.0" encoding="UTF-8"?&gt;</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();
+ }
+ }
+
+}