/*
* Universal Media Server, for streaming any media to DLNA
* compatible renderers based on the http://www.ps3mediaserver.org.
* Copyright (C) 2012 UMS developers.
*
* This program is a 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; version 2
* of the License only.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Formatter;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JEditorPane;
import javax.swing.JTextPane;
import javax.swing.text.html.HTMLEditorKit;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import static org.apache.commons.lang3.StringUtils.isBlank;
import org.apache.commons.lang3.text.translate.UnicodeUnescaper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
public class StringUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(StringUtil.class);
private static final int[] MULTIPLIER = new int[] {3600, 60, 1};
public static final String SEC_TIME_FORMAT = "%02d:%02d:%02.0f";
public static final String DURATION_TIME_FORMAT = "%02d:%02d:%05.2f";
public static final String NEWLINE_CHARACTER = System.getProperty("line.separator");
/**
* Appends "<<u>tag</u> " to the StringBuilder. This is a typical HTML/DIDL/XML tag opening.
*
* @param sb String to append the tag beginning to.
* @param tag String that represents the tag
*/
public static void openTag(StringBuilder sb, String tag) {
sb.append("<");
sb.append(tag);
}
/**
* Appends the closing symbol > to the StringBuilder. This is a typical HTML/DIDL/XML tag closing.
*
* @param sb String to append the ending character of a tag.
*/
public static void endTag(StringBuilder sb) {
sb.append(">");
}
/**
* Appends "</<u>tag</u>>" to the StringBuilder. This is a typical closing HTML/DIDL/XML tag.
*
* @param sb
* @param tag
*/
public static void closeTag(StringBuilder sb, String tag) {
sb.append("</");
sb.append(tag);
sb.append(">");
}
public static void addAttribute(StringBuilder sb, String attribute, Object value) {
sb.append(' ');
sb.append(attribute);
sb.append("=\"");
sb.append(value);
sb.append("\"");
}
public static void addXMLTagAndAttribute(StringBuilder sb, String tag, Object value) {
sb.append("<");
sb.append(tag);
sb.append(">");
sb.append(value);
sb.append("</");
sb.append(tag);
sb.append(">");
}
/**
* Does double transformations between &<> characters and their XML representation with ampersands.
*
* @param s String to be encoded
* @return Encoded String
*/
public static String encodeXML(String s) {
s = s.replace("&", "&");
s = s.replace("<", "<");
s = s.replace(">", ">");
/* Skip encoding/escaping ' and " for compatibility with some renderers
* This might need to be made into a renderer option if some renderers require them to be encoded
* s = s.replace("\"", """);
* s = s.replace("'", "'");
*/
// The second encoding/escaping of & is not a bug, it's what effectively adds the second layer of encoding/escaping
s = s.replace("&", "&");
return s;
}
/**
* Removes xml character representations.
*
* @param s String to be cleaned
* @return Encoded String
*/
public static String unEncodeXML(String s) {
// Note: ampersand substitution must be first in order to undo double transformations
// TODO: support ' and " if/when required, see encodeXML() above
return s.replace("&", "&").replace("<", "<").replace(">", ">");
}
/**
* Converts a URL string to a more canonical form
*
* @param url String to be converted
* @return Converted String.
*/
public static String convertURLToFileName(String url) {
url = url.replace('/', '\u00b5');
url = url.replace('\\', '\u00b5');
url = url.replace(':', '\u00b5');
url = url.replace('?', '\u00b5');
url = url.replace('*', '\u00b5');
url = url.replace('|', '\u00b5');
url = url.replace('<', '\u00b5');
url = url.replace('>', '\u00b5');
return url;
}
/**
* Parse as double, or if it's not just one number, handles {hour}:{minute}:{seconds}
*
* @param time
* @return
*/
public static double convertStringToTime(String time) throws IllegalArgumentException {
if (isBlank(time)) {
throw new IllegalArgumentException("time String should not be blank.");
}
try {
return Double.parseDouble(time);
} catch (NumberFormatException e) {
String[] arguments = time.split(":");
double sum = 0;
int i = 0;
for (String argument : arguments) {
sum += Double.parseDouble(argument.replace(",", ".")) * MULTIPLIER[i];
i++;
}
return sum;
}
}
/**
* Converts time to string.
*
* @param d time in double.
* @param timeFormat Format string e.g. "%02d:%02d:%02f" or use predefined constants
* SEC_TIME_FORMAT, DURATION_TIME_FORMAT.
*
* @return Converted String.
*/
public static String convertTimeToString(double d, String timeFormat) {
StringBuilder sb = new StringBuilder();
try (Formatter formatter = new Formatter(sb, Locale.US)) {
double s = d % 60;
int h = (int) (d / 3600);
int m = ((int) (d / 60)) % 60;
formatter.format(timeFormat, h, m, s);
}
return sb.toString();
}
/**
* Removes leading zeros up to the nth char of an hh:mm:ss time string,
* normalizing it first if necessary.
*
* @param t time string.
* @param n position to stop checking
*
* @return the Shortened String.
*/
public static String shortTime(String t, int n) {
n = n < 8 ? n : 8;
if (!isBlank(t)) {
if (t.startsWith("NOT_IMPLEMENTED")) {
return t.length() > 15 ? t.substring(15) : " ";
}
int i = t.indexOf('.');
// Throw out the decimal portion, if any
if (i > -1) {
t = t.substring(0, i);
}
int l = t.length();
// Normalize if necessary
if (l < 8) {
t = "00:00:00".substring(0, 8 - l) + t;
} else if (l > 8) {
t = t.substring(l - 8);
}
for (i = 0; i < n; i++) {
if (t.charAt(i) != "00:00:00".charAt(i)) {
break;
}
}
return t.substring(i);
}
return "00:00:00".substring(n);
}
public static boolean isZeroTime(String t) {
return isBlank(t) || "00:00:00.000".contains(t);
}
/**
* A unicode unescaper that translates unicode escapes, e.g. '\u005c', while leaving
* intact any sequences that can't be interpreted as escaped unicode.
*/
public static class LaxUnicodeUnescaper extends UnicodeUnescaper {
@Override
public int translate(CharSequence input, int index, Writer out) throws IOException {
try {
return super.translate(input, index, out);
} catch (IllegalArgumentException e) {
// Leave it alone and continue
}
return 0;
}
}
/**
* Returns the argument string surrounded with quotes if it contains a space,
* otherwise returns the string as is.
*
* @param arg The argument string
* @return The string, optionally in quotes.
*/
public static String quoteArg(String arg) {
if (arg != null && arg.indexOf(' ') > -1) {
return "\"" + arg + "\"";
}
return arg;
}
/**
* Fill a string in a unicode safe way.
*
* @param subString The <code>String</code> to be filled with
* @param count The number of times to repeat the <code>String</code>
* @return The filled string
*/
public static String fillString(String subString, int count) {
StringBuilder sb = new StringBuilder(subString.length() * count);
for (int i = 0; i < count; i++) {
sb.append(subString);
}
return sb.toString();
}
/**
* Fill a string in a unicode safe way provided that the char array contains
* a valid unicode sequence.
*
* @param chars The <code>char[]</code> to be filled with
* @param count The number of times to repeat the <code>char[]</code>
* @return The filled string
*/
public static String fillString(char[] chars, int count) {
StringBuilder sb = new StringBuilder(chars.length * count);
for (int i = 0; i < count; i++) {
sb.append(chars);
}
return sb.toString();
}
/**
* Fill a string in a unicode safe way. 8 bit (< 256) code points
* equals ISO 8859-1 codes.
*
* @param codePoint The unicode code point to be filled with
* @param count The number of times to repeat the unicode code point
* @return The filled string
*/
public static String fillString(int codePoint, int count) {
return fillString(Character.toChars(codePoint), count);
}
/**
* Returns the <code>body</code> of a HTML {@link String} formatted by
* {@link HTMLEditorKit} as typically used by {@link JEditorPane} and
* {@link JTextPane} stripped for tags, newline, indentation and with
* <code><br></code> tags converted to newline.<br>
* <br>
* <strong>Note: This is not a universal or sophisticated HTML stripping
* method, but is purpose built for these circumstances.</strong>
*
* @param html the HTML formatted text as described above
* @return The "deHTMLified" text
*/
public static String stripHTML(String html) {
Pattern pattern = Pattern.compile("<body>(.*)</body>", Pattern.CASE_INSENSITIVE + Pattern.DOTALL);
Matcher matcher = pattern.matcher(html);
if (matcher.find()) {
return matcher.group(1).replaceAll("\n ", "").trim().replaceAll("(?i)<br>", "\n").replaceAll("<.*?>","");
}
throw new IllegalArgumentException("HTML text not as expected, must have <body> section");
}
/**
* Convenience method to check if a {@link String} is not <code>null</code>
* and contains anything other than whitespace.
*
* @param s the {@link String} to evaluate
* @return The verdict
*/
public static boolean hasValue(String s) {
return s != null && !s.trim().isEmpty();
}
/**
* Escapes {@link org.apache.lucene} special characters with backslash
*
* @param s the {@link String} to evaluate
* @return The converted String
*/
@SuppressFBWarnings("SF_SWITCH_NO_DEFAULT")
public static String luceneEscape(final String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
switch (ch) {
case '+':
case '-':
case '&':
case '|':
case '!':
case '(':
case ')':
case '{':
case '}':
case '[':
case ']':
case '^':
case '\"':
case '~':
case '*':
case '?':
case ':':
case '\\':
case '/':
sb.append("\\");
default:
sb.append(ch);
}
}
return sb.toString();
}
/**
* Escapes special characters with backslashes for FFmpeg subtitles
*
* @param s the {@link String} to evaluate
* @return The converted String
*/
public static String ffmpegEscape(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
switch (ch) {
case '\'':
sb.append("\\\\\\'");
break;
case ':':
sb.append("\\\\:");
break;
case '\\':
sb.append("/");
break;
case ']':
case '[':
case ',':
case ';':
sb.append("\\");
default:
sb.append(ch);
}
}
return sb.toString();
}
public static String prettifyXML(String xml, int indentWidth) throws SAXException, ParserConfigurationException, XPathExpressionException, TransformerException {
try {
// Turn XML string into a document
Document xmlDocument =
DocumentBuilderFactory.newInstance().
newDocumentBuilder().
parse(new InputSource(new ByteArrayInputStream(xml.getBytes("utf-8"))));
// Remove whitespaces outside tags
xmlDocument.normalize();
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodeList = (NodeList) xPath.evaluate(
"//text()[normalize-space()='']",
xmlDocument,
XPathConstants.NODESET
);
for (int i = 0; i < nodeList.getLength(); ++i) {
Node node = nodeList.item(i);
node.getParentNode().removeChild(node);
}
// Setup pretty print options
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setAttribute("indent-number", indentWidth);
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
// Return pretty print XML string
StringWriter stringWriter = new StringWriter();
transformer.transform(new DOMSource(xmlDocument), new StreamResult(stringWriter));
return stringWriter.toString();
} catch (IOException e) {
LOGGER.warn("Failed to read XML document, returning the source document: {}", e.getMessage());
LOGGER.trace("", e);
return xml;
}
}
}