/* This file is part of Airsonic. Airsonic 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. Airsonic 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 Airsonic. If not, see . Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.util; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.apache.commons.lang.StringUtils; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.*; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; /** * Miscellaneous string utility methods. * * @author Sindre Mehus */ public final class StringUtil { public static final String ENCODING_UTF8 = "UTF-8"; private static final Pattern SPLIT_PATTERN = Pattern.compile("\"([^\"]*)\"|(\\S+)"); private static final String[][] MIME_TYPES = { {"mp3", "audio/mpeg"}, {"ogg", "audio/ogg"}, {"oga", "audio/ogg"}, {"opus", "audio/ogg"}, {"ogx", "application/ogg"}, {"aac", "audio/mp4"}, {"m4a", "audio/mp4"}, {"m4b", "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"}, {"ts", "video/MP2T"}, {"webm", "video/webm"}, {"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 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, * application/octet-stream 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"; } public static String getMimeType(String suffix, boolean sonos) { String result = getMimeType(suffix); // Sonos doesn't work with "audio/mp4" but needs "audio/aac" for ALAC and AAC (in MP4 container) return sonos && "audio/mp4".equals(result) ? "audio/aac" : result; } public static String getSuffix(String mimeType) { for (String[] map : MIME_TYPES) { if (map[1].equalsIgnoreCase(mimeType)) { return map[0]; } } return null; } /** * Converts a byte-count to a formatted string suitable for display to the user. * For instance: * * 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 TB? if (byteCount >= 1024L * 1024 * 1024 * 1024) { NumberFormat teraByteFormat = new DecimalFormat("0.00 TB", new DecimalFormatSymbols(locale)); return teraByteFormat.format(byteCount / ((double) 1024 * 1024 * 1024 * 1024)); } // More than 1 GB? if (byteCount >= 1024L * 1024 * 1024) { NumberFormat gigaByteFormat = new DecimalFormat("0.00 GB", new DecimalFormatSymbols(locale)); return gigaByteFormat.format(byteCount / ((double) 1024 * 1024 * 1024)); } // More than 1 MB? if (byteCount >= 1024L * 1024) { NumberFormat megaByteFormat = new DecimalFormat("0.0 MB", new DecimalFormatSymbols(locale)); return megaByteFormat.format(byteCount / ((double) 1024 * 1024)); } // More than 1 KB? if (byteCount >= 1024L) { 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., "4:34" or "93:45" */ public static String formatDurationMSS(int seconds) { if (seconds < 0) { throw new IllegalArgumentException("seconds must be >= 0"); } return String.format("%d:%02d", seconds / 60, seconds % 60); } /** * Formats a duration with H:MM:SS, e.g., "1:33:45" */ public static String formatDurationHMMSS(int seconds) { int hours = seconds / 3600; seconds -= hours * 3600; return String.format("%d:%s%s", hours, seconds < 600 ? "0" : "", formatDurationMSS(seconds)); } /** * Formats a duration to M:SS or H:MM:SS */ public static String formatDuration(int seconds) { if (seconds >= 3600) { return formatDurationHMMSS(seconds); } return formatDurationMSS(seconds); } /** * Splits the input string. White space is interpreted as separator token. Double quotes * are interpreted as grouping operator.
* For instance, the input "u2 rem "greatest hits"" will return an array with * three elements: {"u2", "rem", "greatest hits"} * * @param input The input string. * @return Array of elements. */ public static String[] split(String input) { if (input == null) { return new String[0]; } List result = new ArrayList<>(); Matcher m = SPLIT_PATTERN.matcher(input); while (m.find()) { if (m.group(1) != null) { result.add(m.group(1)); // quoted string } else { result.add(m.group(2)); // unquoted string } } 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 { try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { List result = new ArrayList(); for (String line = reader.readLine(); line != null; line = reader.readLine()) { line = line.trim(); if (!line.startsWith("#") && !line.isEmpty()) { result.add(line); } } return result.toArray(new String[result.size()]); } finally { FileUtil.closeQuietly(in); } } /** * Converts the given string of whitespace-separated integers to an int 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]; } return Stream.of(StringUtils.split(s)) .mapToInt(Integer::parseInt) .toArray(); } /** * 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; } List elements = new ArrayList<>(Arrays.asList(s.split("_", 3))); while (elements.size() < 3) { elements.add(""); } return new Locale(elements.get(0), elements.get(1), elements.get(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); } } /** * URL-decodes the input value using UTF-8. */ public static String urlDecode(String s) { try { return URLDecoder.decode(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; utf8 = s.getBytes(StandardCharsets.UTF_8); 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 DecoderException If an error occurs. */ public static String utf8HexDecode(String s) throws DecoderException { if (s == null) { return null; } return new String(Hex.decodeHex(s.toCharArray()), StandardCharsets.UTF_8); } /** * Returns the file part of an URL. For instance: *

* * getUrlFile("http://archive.ncsa.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer.html") * *

* will return "url-primer.html". * * @param url The URL in question. * @return The file part, or null 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; } } /** * 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; } public static String removeMarkup(String s) { if (s == null) { return null; } return s.replaceAll("<.*?>", ""); } }