findSubTypesFromDirectory(
File dir, Class> superClass) {
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java
index 42eeb746..1747c641 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2016 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
import java.io.StringWriter;
import java.util.List;
import java.util.Properties;
+import java.util.regex.Pattern;
import org.apache.commons.configuration.XMLConfiguration;
import org.apache.commons.io.FilenameUtils;
@@ -176,19 +177,22 @@ public XMLConfiguration loadXML(File configFile, File variables) {
if (!configFile.exists()) {
return null;
}
- XMLConfiguration xml = new XMLConfiguration();
- ConfigurationUtil.disableDelimiterParsing(xml);
- Reader reader = new StringReader(loadString(configFile, variables));
try {
- xml.load(reader);
- reader.close();
+ String xml = loadString(configFile, variables);
+ // clean-up extra duplicate declaration tags due to template
+ // includes/imports that could break parsing.
+ // Keep first |<\\!DOCTYPE.*?>)",
+ Pattern.MULTILINE).matcher(xml).replaceAll("");
+ return XMLConfigurationUtil.newXMLConfiguration(
+ new StringReader(xml));
} catch (Exception e) {
throw new ConfigurationException(
"Cannot load configuration file: \"" + configFile + "\". "
+ "Probably a misconfiguration or the configuration XML "
+ "is not well-formed.", e);
}
- return xml;
}
/**
@@ -237,19 +241,6 @@ public String loadString(File configFile, File variables) {
return sw.toString();
}
- /**
- * This load method will return an Apache XML Configuration without
- * any Velocity parsing, variable substitution or Velocity directives.
- * @param in input stream
- * @return XMLConfiguration
- * @deprecated Since 1.5.0. Use
- * {@link ConfigurationUtil#newXMLConfiguration(Reader)}
- */
- @Deprecated
- public static XMLConfiguration loadXML(Reader in) {
- return ConfigurationUtil.newXMLConfiguration(in);
- }
-
private File getVariablesFile(String fullpath, String baseName) {
File vars = new File(fullpath + baseName + EXTENSION_PROPERTIES);
if (isVariableFile(vars, EXTENSION_PROPERTIES)) {
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationUtil.java
index 680e787b..6d004539 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationUtil.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationUtil.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2016 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,31 +14,19 @@
*/
package com.norconex.commons.lang.config;
-import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.OutputStreamWriter;
import java.io.Reader;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.List;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.XMLConfiguration;
-import org.apache.commons.lang3.CharEncoding;
-import org.apache.log4j.LogManager;
-import org.apache.log4j.Logger;
-
-import com.norconex.commons.lang.file.FileUtil;
/**
* Utility methods when dealing with configuration files.
* @author Pascal Essiembre
+ * @deprecated Since 1.13.0, use {@link XMLConfigurationUtil}
*/
+@Deprecated
public final class ConfigurationUtil {
- private static final Logger LOG =
- LogManager.getLogger(ConfigurationUtil.class);
private ConfigurationUtil() {
super();
@@ -50,9 +38,7 @@ private ConfigurationUtil() {
* @param xml XML configuration
*/
public static void disableDelimiterParsing(XMLConfiguration xml) {
- xml.setListDelimiter('\0');
- xml.setDelimiterParsingDisabled(true);
- xml.setAttributeSplittingDisabled(true);
+ XMLConfigurationUtil.disableDelimiterParsing(xml);
}
@@ -66,9 +52,7 @@ public static void disableDelimiterParsing(XMLConfiguration xml) {
*/
public static XMLConfiguration newXMLConfiguration(
HierarchicalConfiguration c) {
- XMLConfiguration xml = new XMLConfiguration(c);
- disableDelimiterParsing(xml);
- return xml;
+ return XMLConfigurationUtil.newXMLConfiguration(c);
}
/**
* This load method will return an Apache XML Configuration from
@@ -87,14 +71,7 @@ public static XMLConfiguration newXMLConfiguration(
* @since 1.5.0
*/
public static XMLConfiguration newXMLConfiguration(Reader in) {
- XMLConfiguration xml = new XMLConfiguration();
- disableDelimiterParsing(xml);
- try {
- xml.load(in);
- } catch (org.apache.commons.configuration.ConfigurationException e) {
- throw new ConfigurationException("Cannot load XMLConfiguration", e);
- }
- return xml;
+ return XMLConfigurationUtil.newXMLConfiguration(in);
}
@@ -112,7 +89,7 @@ public static XMLConfiguration newXMLConfiguration(Reader in) {
*/
public static T newInstance(
HierarchicalConfiguration node) {
- return newInstance(node, null, true);
+ return XMLConfigurationUtil.newInstance(node);
}
/**
@@ -126,6 +103,7 @@ public static T newInstance(
* @param node the node representing the class to instantiate.
* @param supportXMLConfigurable automatically populates the object from XML
* if it is implementing {@link IXMLConfigurable}.
+ * Since 1.13.0, this flag is always considered true
.
* @param the type of the return value
* @return a new object.
* @throws ConfigurationException if instance cannot be created/populated
@@ -133,7 +111,7 @@ public static T newInstance(
public static T newInstance(
HierarchicalConfiguration node,
boolean supportXMLConfigurable) {
- return newInstance(node, null, supportXMLConfigurable);
+ return XMLConfigurationUtil.newInstance(node);
}
@@ -153,7 +131,7 @@ public static T newInstance(
*/
public static T newInstance(
HierarchicalConfiguration node, T defaultObject) {
- return newInstance(node, defaultObject, true);
+ return XMLConfigurationUtil.newInstance(node, defaultObject);
}
/**
@@ -169,47 +147,15 @@ public static T newInstance(
* returns this default object.
* @param supportXMLConfigurable automatically populates the object from XML
* if it is implementing {@link IXMLConfigurable}.
+ * Since 1.13.0, this flag is always considered true
.
* @param the type of the return value
* @return a new object.
* @throws ConfigurationException if instance cannot be created/populated
*/
- @SuppressWarnings("unchecked")
public static T newInstance(
HierarchicalConfiguration node, T defaultObject,
boolean supportXMLConfigurable) {
- T obj;
- String clazz;
- if (node == null) {
- return defaultObject;
- }
- clazz = node.getString("[@class]", null);
- if (clazz != null) {
- try {
- obj = (T) Class.forName(clazz).newInstance();
- } catch (Exception e) {
- throw new ConfigurationException(
- "This class could not be instantiated: \""
- + clazz + "\".", e);
- }
- } else {
- LOG.debug("A configuration entry was found without class "
- + "reference where one could have been provided; "
- + "using default value:" + defaultObject);
- obj = defaultObject;
- }
- if (obj == null) {
- return defaultObject;
- }
- if (obj instanceof IXMLConfigurable && supportXMLConfigurable) {
- try {
- ((IXMLConfigurable) obj).loadFromXML(newReader(node));
- } catch (IOException e) {
- throw new ConfigurationException(
- "Could not load new instance from XML \""
- + clazz + "\".", e);
- }
- }
- return obj;
+ return XMLConfigurationUtil.newInstance(node, defaultObject);
}
/**
@@ -234,7 +180,7 @@ public static T newInstance(
*/
public static T newInstance(
HierarchicalConfiguration node, String key) {
- return newInstance(node, key, null, true, true);
+ return XMLConfigurationUtil.newInstance(node, key);
}
/**
* Creates a new instance of the class represented by the "class"
@@ -255,6 +201,7 @@ public static T newInstance(
* @param key sub-node name/hierarchical path
* @param supportXMLConfigurable automatically populates the object from XML
* if it is implementing {@link IXMLConfigurable}.
+ * Since 1.13.0, this flag is always considered true
.
* @param the type of the return value
* @return a new object.
* @throws ConfigurationException if instance cannot be created/populated
@@ -262,7 +209,7 @@ public static T newInstance(
public static T newInstance(
HierarchicalConfiguration node, String key,
boolean supportXMLConfigurable) {
- return newInstance(node, key, null, supportXMLConfigurable, true);
+ return XMLConfigurationUtil.newInstance(node, key);
}
/**
* Creates a new instance of the class represented by the "class"
@@ -288,7 +235,7 @@ public static T newInstance(
public static T newInstance(
HierarchicalConfiguration node, String key,
T defaultObject) {
- return newInstance(node, key, defaultObject, true, false);
+ return XMLConfigurationUtil.newInstance(node, key, defaultObject);
}
/**
* Creates a new instance of the class represented by the "class"
@@ -311,61 +258,15 @@ public static T newInstance(
* @param key sub-node name/hierarchical path
* @param supportXMLConfigurable automatically populates the object from XML
* if it is implementing {@link IXMLConfigurable}.
+ * Since 1.13.0, this flag is always considered true
.
* @param the type of the return value
* @return a new object.
*/
public static T newInstance(
HierarchicalConfiguration node, String key,
T defaultObject, boolean supportXMLConfigurable) {
- return newInstance(node, key, defaultObject,
- supportXMLConfigurable, false);
- }
- private static T newInstance(
- HierarchicalConfiguration node, String key,
- T defaultObject, boolean supportXMLConfigurable,
- boolean canThrowException) {
- if (node == null) {
- return defaultObject;
- }
-
- try {
- if (key == null && defaultObject == null) {
- return ConfigurationUtil.newInstance(
- node, (T) null, supportXMLConfigurable);
- }
- HierarchicalConfiguration subconfig =
- safeConfigurationAt(node, key);
- return ConfigurationUtil.newInstance(
- subconfig, defaultObject, supportXMLConfigurable);
- } catch (Exception e) {
- if (canThrowException) {
- if (e instanceof ConfigurationException) {
- throw (ConfigurationException) e;
- } else {
- throw new ConfigurationException(
- "Could not instantiate object from configuration "
- + "for \"" + node.getRoot().getName()
- + " -> " + key + "\".", e);
- }
- } else {
- if (e instanceof ConfigurationException
- && e.getCause() != null
- && e.getCause() instanceof ClassNotFoundException) {
- LOG.error("You declared a class that does not exists "
- + "for \"" + node.getRoot().getName()
- + " -> " + key + "\". "
- + "Check for typos in your XML and make sure that "
- + "class is part of your Java classpath.", e);
- } else{
- LOG.debug("Could not instantiate object from configuration "
- + "for \"" + node.getRoot().getName()
- + " -> " + key + "\".", e);
- }
- }
- return defaultObject;
- }
+ return XMLConfigurationUtil.newInstance(node, key, defaultObject);
}
-
/**
* Creates a new {@link Reader} from a {@link XMLConfiguration}.
* Do not forget to close the reader instance when you are done with it.
@@ -376,23 +277,7 @@ private static T newInstance(
*/
public static Reader newReader(HierarchicalConfiguration node)
throws IOException {
- XMLConfiguration xml;
- if (node instanceof XMLConfiguration) {
- xml = (XMLConfiguration) node;
- } else {
- xml = new XMLConfiguration(node);
- disableDelimiterParsing(xml);
- }
- StringWriter w = new StringWriter();
- try {
- xml.save(w);
- } catch (org.apache.commons.configuration.ConfigurationException e) {
- throw new ConfigurationException(
- "Could transform XML node to reader.", e);
- }
- StringReader r = new StringReader(w.toString());
- w.close();
- return r;
+ return XMLConfigurationUtil.newReader(node);
}
/**
@@ -407,16 +292,7 @@ public static Reader newReader(HierarchicalConfiguration node)
*/
public static XMLConfiguration getXmlAt(
HierarchicalConfiguration node, String key) {
- if (node == null) {
- return null;
- }
- HierarchicalConfiguration sub = safeConfigurationAt(node, key);
- if (sub == null) {
- return null;
- }
- XMLConfiguration xml = new XMLConfiguration(sub);
- disableDelimiterParsing(xml);
- return xml;
+ return XMLConfigurationUtil.getXmlAt(node, key);
}
/**
@@ -429,43 +305,6 @@ public static XMLConfiguration getXmlAt(
*/
public static void assertWriteRead(IXMLConfigurable xmlConfiurable)
throws IOException {
-
- File tempFile = File.createTempFile("XMLConfigurableTester", ".xml");
-
- // Write
- Writer out = new OutputStreamWriter(
- new FileOutputStream(tempFile), CharEncoding.UTF_8);
- try {
- xmlConfiurable.saveToXML(out);
- } finally {
- out.close();
- }
-
- // Read
- XMLConfiguration xml = new ConfigurationLoader().loadXML(tempFile);
- IXMLConfigurable readConfigurable =
- (IXMLConfigurable) ConfigurationUtil.newInstance(xml);
-
- FileUtil.delete(tempFile);
-
- if (!xmlConfiurable.equals(readConfigurable)) {
- LOG.error("BEFORE: " + xmlConfiurable);
- LOG.error(" AFTER: " + readConfigurable);
- throw new ConfigurationException(
- "Saved and loaded XML are not the same.");
- }
+ XMLConfigurationUtil.assertWriteRead(xmlConfiurable);
}
-
- // This method is because the regular configuration at MUST have 1
- // entry or will fail, and the containsKey(String) method is not reliable
- // since it expects a value (body text) or returns false.
- private static HierarchicalConfiguration safeConfigurationAt(
- HierarchicalConfiguration node, String key) {
- List subs = node.configurationsAt(key);
- if (subs != null && !subs.isEmpty()) {
- return subs.get(0);
- }
- return null;
- }
-
}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/XMLConfigurationUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/XMLConfigurationUtil.java
new file mode 100644
index 00000000..ff8de5d8
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/XMLConfigurationUtil.java
@@ -0,0 +1,667 @@
+/* Copyright 2010-2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.List;
+
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.apache.commons.configuration.tree.ConfigurationNode;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import com.norconex.commons.lang.time.DurationParser;
+import com.norconex.commons.lang.time.DurationParserException;
+import com.norconex.commons.lang.xml.ClasspathResourceResolver;
+
+//TODO Add some of the convenience methods found in Collector/Crawler Loader?
+
+//TODO consider checking for a "disable=false|true" and setting it on
+// a method if this method exists, and/or do not load if set to true.
+
+/**
+ * Utility methods when dealing with XML configuration files.
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public final class XMLConfigurationUtil {
+ private static final Logger LOG =
+ LogManager.getLogger(XMLConfigurationUtil.class);
+
+ public static final String W3C_XML_SCHEMA_NS_URI_1_1 =
+ "http://www.w3.org/XML/XMLSchema/v1.1";
+
+ private XMLConfigurationUtil() {
+ super();
+ }
+
+
+ /**
+ * Disables delimiter parsing for both attributes and elements.
+ * @param xml XML configuration
+ */
+ public static void disableDelimiterParsing(XMLConfiguration xml) {
+ xml.setListDelimiter('\0');
+ xml.setDelimiterParsingDisabled(true);
+ xml.setAttributeSplittingDisabled(true);
+ }
+
+
+ /**
+ * This load method will return an Apache XML Configuration from
+ * from a {@link HierarchicalConfiguration}, with delimiter parsing
+ * disabled.
+ * @param c hierarchical configuration
+ * @return XMLConfiguration
+ * @since 1.5.0
+ */
+ public static XMLConfiguration newXMLConfiguration(
+ HierarchicalConfiguration c) {
+ XMLConfiguration xml = new XMLConfiguration(c);
+ disableDelimiterParsing(xml);
+ return xml;
+ }
+ /**
+ * This load method will return an Apache XML Configuration from
+ * from a reader, with delimiter parsing disabled.
+ * Note: Leading and trailing white spaces are not preserved by
+ * default.
+ * To preserve them, add xml:space="preserve"
+ * to your tag, like this:
+ *
+ *
+ * <mytag xml:space="preserve"> </mytag>
+ *
+ * The above example will preserve the white space in the tag's body.
+ * @param in input stream
+ * @return XMLConfiguration
+ * @since 1.5.0
+ */
+ public static XMLConfiguration newXMLConfiguration(Reader in) {
+ XMLConfiguration xml = new XMLConfiguration();
+ disableDelimiterParsing(xml);
+ try {
+ xml.load(in);
+ } catch (org.apache.commons.configuration.ConfigurationException e) {
+ throw new ConfigurationException("Cannot load XMLConfiguration", e);
+ }
+ return xml;
+ }
+
+ /**
+ *
Creates a new instance of the class represented by the "class"
+ * attribute on the supplied XML.
+ * The class must have an empty constructor.
+ * If the class is an instance of {@link IXMLConfigurable}, the object
+ * created will be automatically populated by invoking the
+ * {@link IXMLConfigurable#loadFromXML(Reader)} method,
+ * passing it the node XML automatically populated.
+ * The reader is expected to hold an XML, which will
+ * first be converted to an {@link XMLConfiguration}. This method has the
+ * same effect as invoking:
+ *
+ *
+ * XMLConfigurationUtil.newInstance(
+ * XMLConfigurationUtil.newXMLConfiguration(reader));
+ *
+ *
+ * @param reader the XML representing the class to instantiate.
+ * @param the type of the return value
+ * @return a new object.
+ * @throws ConfigurationException if instance cannot be created/populated
+ */
+ public static T newInstance(Reader reader) {
+ return XMLConfigurationUtil.newInstance(
+ XMLConfigurationUtil.newXMLConfiguration(reader));
+ }
+
+ /**
+ * Creates a new instance of the class represented by the "class" attribute
+ * on the given node. The class must have an empty constructor.
+ * If the class is an instance of {@link IXMLConfigurable}, the object
+ * created will be automatically populated by invoking the
+ * {@link IXMLConfigurable#loadFromXML(Reader)} method,
+ * passing it the node XML automatically populated.
+ * @param node the node representing the class to instantiate.
+ * @param the type of the return value
+ * @return a new object.
+ * @throws ConfigurationException if instance cannot be created/populated
+ */
+ public static T newInstance(
+ HierarchicalConfiguration node) {
+ return newInstance(node, (String) null);
+ }
+
+
+ /**
+ * Creates a new instance of the class represented by the "class" attribute
+ * on the given node. The class must have an empty constructor.
+ * If the class is an instance of {@link IXMLConfigurable}, the object
+ * created will be automatically populated by invoking the
+ * {@link IXMLConfigurable#loadFromXML(Reader)} method,
+ * passing it the node XML automatically populated.
+ * @param node the node representing the class to instantiate.
+ * @param defaultObject if returned object is null or undefined,
+ * returns this default object.
+ * @param the type of the return value
+ * @return a new object.
+ * @throws ConfigurationException if instance cannot be created/populated
+ */
+ @SuppressWarnings("unchecked")
+ public static T newInstance(
+ HierarchicalConfiguration node, T defaultObject) {
+ T obj;
+ String clazz;
+ if (node == null) {
+ return defaultObject;
+ }
+ clazz = node.getString("[@class]", null);
+ if (clazz != null) {
+ try {
+ obj = (T) Class.forName(clazz).newInstance();
+ } catch (Exception e) {
+ throw new ConfigurationException(
+ "This class could not be instantiated: \""
+ + clazz + "\".", e);
+ }
+ } else {
+ LOG.debug("A configuration entry was found without class "
+ + "reference where one could have been provided; "
+ + "using default value:" + defaultObject);
+ obj = defaultObject;
+ }
+ if (obj == null) {
+ return defaultObject;
+ }
+ if (obj instanceof IXMLConfigurable) {
+ loadFromXML((IXMLConfigurable) obj, node);
+ }
+ return obj;
+ }
+
+ /**
+ * Creates a new instance of the class represented by the "class"
+ * attribute
+ * on the sub-node of the node argument, matching the key provided.
+ * The class must have an empty constructor.
+ * If the class is an instance of {@link IXMLConfigurable}, the object
+ * created will be automatically populated by invoking the
+ * {@link IXMLConfigurable#loadFromXML(Reader)} method,
+ * passing it the node XML automatically populated.
+ *
+ * Since 1.6.0, this method should throw a
+ * {@link ConfigurationException} upon error. Use a method
+ * with a default value argument to avoid throwing exceptions.
+ *
+ * @param node the node representing the class to instantiate.
+ * @param key sub-node name/hierarchical path
+ * @param the type of the return value
+ * @return a new object.
+ * @throws ConfigurationException if instance cannot be created/populated
+ */
+ public static T newInstance(
+ HierarchicalConfiguration node, String key) {
+ return newInstance(node, key, (T) null, true);
+ }
+ /**
+ * Creates a new instance of the class represented by the "class"
+ * attribute
+ * on the sub-node of the node argument, matching the key provided.
+ * The class must have an empty constructor.
+ * If the class is an instance of {@link IXMLConfigurable}, the object
+ * created will be automatically populated by invoking the
+ * {@link IXMLConfigurable#loadFromXML(Reader)} method,
+ * passing it the node XML automatically populated.
+ *
+ * This method should not throw exception upon errors, but will return
+ * the default value instead (even if null). Use a method without
+ * a default value argument to get exception on errors.
+ *
+ * @param node the node representing the class to instantiate.
+ * @param defaultObject if returned object is null or undefined,
+ * returns this default object.
+ * @param key sub-node name/hierarchical path
+ * @param the type of the return value
+ * @return a new object.
+ */
+ public static T newInstance(
+ HierarchicalConfiguration node, String key, T defaultObject) {
+ return newInstance(node, key, defaultObject, false);
+ }
+ private static T newInstance(
+ HierarchicalConfiguration node, String key,
+ T defaultObject, boolean canThrowException) {
+ if (node == null) {
+ return defaultObject;
+ }
+
+ try {
+ if (key == null && defaultObject == null) {
+ return newInstance(node, (T) null);
+ }
+ HierarchicalConfiguration subconfig =
+ safeConfigurationAt(node, key);
+ return newInstance(subconfig, defaultObject);
+ } catch (Exception e) {
+ handleException(node.getRootNode(), key, e, canThrowException);
+ return defaultObject;
+ }
+ }
+ private static void handleException(
+ ConfigurationNode rootNode, String key,
+ Exception e, boolean canThrowException) {
+
+ // Throw exception
+ if (canThrowException) {
+ if (e instanceof ConfigurationException) {
+ throw (ConfigurationException) e;
+ } else {
+ throw new ConfigurationException(
+ "Could not instantiate object from configuration "
+ + "for \"" + rootNode.getName()
+ + " -> " + key + "\".", e);
+ }
+ }
+
+ // Log exception
+ if (e instanceof ConfigurationException
+ && e.getCause() != null) {
+ if (e.getCause() instanceof ClassNotFoundException) {
+ LOG.error("You declared a class that does not exists "
+ + "for \"" + rootNode.getName()
+ + " -> " + key + "\". Check for typos in your "
+ + "XML and make sure that "
+ + "class is part of your Java classpath.", e);
+ } else if (e.getCause() instanceof SAXParseException) {
+ String systemId = ((SAXParseException )
+ e.getCause()).getSystemId();
+ if (StringUtils.endsWith(systemId, ".xsd")) {
+ LOG.error("XML Schema parsing error for \""
+ + rootNode.getName()
+ + " -> " + key + "\". Schema: " + systemId, e);
+ } else {
+ LOG.error("XML parsing error for \""
+ + rootNode.getName()
+ + " -> " + key + "\".", e);
+ }
+ }
+ } else{
+ LOG.debug("Could not instantiate object from configuration "
+ + "for \"" + rootNode.getName()
+ + " -> " + key + "\".", e);
+ }
+ }
+
+ /**
+ * Creates a new {@link Reader} from a {@link XMLConfiguration}.
+ * Do not forget to close the reader instance when you are done with it.
+ * @param node the xml configuration to convert to a reader instance.
+ * @return reader
+ * @throws ConfigurationException cannot read configuration
+ * @throws IOException cannot read configuration
+ */
+ public static Reader newReader(HierarchicalConfiguration node)
+ throws IOException {
+ XMLConfiguration xml;
+ if (node instanceof XMLConfiguration) {
+ xml = (XMLConfiguration) node;
+ } else {
+ xml = new XMLConfiguration(node);
+ disableDelimiterParsing(xml);
+ }
+ StringWriter w = new StringWriter();
+ try {
+ xml.save(w);
+ } catch (org.apache.commons.configuration.ConfigurationException e) {
+ throw new ConfigurationException(
+ "Could transform XML node to reader.", e);
+ }
+ StringReader r = new StringReader(w.toString());
+ w.close();
+ return r;
+ }
+
+
+
+ /**
+ * For classes implementing {@link IXMLConfigurable}, validates XML against
+ * a class XSD schema and logs any error/warnings.
+ * The schema expected to be found at the same classpath location and have
+ * the same name as the class, but with the ".xsd" extension.
+ * @param clazz the class to validate the XML for
+ * @param node the XML to validate
+ * @return the number of errors/warnings
+ */
+ public static int validate(Class> clazz, HierarchicalConfiguration node) {
+ return doValidate(clazz, node);
+ }
+ /**
+ * For classes implementing {@link IXMLConfigurable}, validates XML against
+ * a class XSD schema and logs any error/warnings.
+ * The schema expected to be found at the same classpath location and have
+ * the same name as the class, but with the ".xsd" extension.
+ * @param clazz the class to validate the XML for
+ * @param xml the XML to validate
+ * @return the number of errors/warnings
+ */
+ public static int validate(Class> clazz, Reader xml) {
+ return doValidate(clazz, xml);
+ }
+ private static int doValidate(Class> clazz, Object source) {
+ // Only validate if IXMLConfigurable
+ if (clazz == null || !IXMLConfigurable.class.isAssignableFrom(clazz)) {
+ return 0;
+ }
+
+ // Only validate if .xsd file exist in classpath for class
+ String xsdResource = ClassUtils.getSimpleName(clazz) + ".xsd";
+ LOG.debug("Class to validate: " + ClassUtils.getSimpleName(clazz));
+ if (clazz.getResource(xsdResource) == null) {
+ LOG.debug("Resource not found for validation: " + xsdResource);
+ return 0;
+ }
+
+ // Go ahead: validate
+ SchemaFactory schemaFactory =
+ SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI_1_1);
+ schemaFactory.setResourceResolver(new ClasspathResourceResolver(clazz));
+
+ Reader reader = null;
+ try (InputStream xsdStream = clazz.getResourceAsStream(xsdResource)) {
+ if (source instanceof Reader) {
+ reader = (Reader) source;
+ } else {
+ reader = newReader((HierarchicalConfiguration) source);
+ }
+ Schema schema = schemaFactory.newSchema(
+ new StreamSource(xsdStream, getXSDResourcePath(clazz)));
+ Validator validator = schema.newValidator();
+ LogErrorHandler seh = new LogErrorHandler(clazz);
+ validator.setErrorHandler(seh);
+ validator.validate(new StreamSource(reader));
+ return seh.errorCount;
+ } catch (SAXException | IOException e) {
+ throw new ConfigurationException(
+ "Could not validate class: " + clazz, e);
+ } finally {
+ IOUtils.closeQuietly(reader);
+ }
+ }
+
+ /**
+ * Loads XML into the given object, performing validation first.
+ * Except for validation, it is the same as calling
+ * {@link IXMLConfigurable#loadFromXML(Reader)} on an object.
+ * @param obj object to have loaded
+ * @param reader xml reader
+ */
+ public static void loadFromXML(IXMLConfigurable obj, Reader reader) {
+ if (obj == null || reader == null) {
+ return;
+ }
+ loadFromXML(obj, newXMLConfiguration(reader));
+ }
+ /**
+ * Loads XML into the given object, performing validation first.
+ * @param obj object to have loaded
+ * @param node XML node to have loaded
+ */
+ public static void loadFromXML(
+ IXMLConfigurable obj, HierarchicalConfiguration node) {
+ if (obj == null || node == null) {
+ return;
+ }
+ try {
+ validate(obj.getClass(), node);
+ obj.loadFromXML(newReader(node));
+ } catch (IOException e) {
+ throw new ConfigurationException(
+ "Could not load new instance from XML \""
+ + obj.getClass() + "\".", e);
+ }
+ }
+
+ /**
+ * This method is the same as
+ * {@link HierarchicalConfiguration#configurationAt(String)}, except that
+ * it first checks if the key exists before attempting to retrieve it,
+ * and returns null
on missing keys instead of an
+ * IllegalArgumentException
+ * @param node the tree to extract a sub tree from
+ * @param key the key that selects the sub tree
+ * @return a XML configuration that contains this sub tree
+ */
+ public static XMLConfiguration getXmlAt(
+ HierarchicalConfiguration node, String key) {
+ if (node == null) {
+ return null;
+ }
+ HierarchicalConfiguration sub = safeConfigurationAt(node, key);
+ if (sub == null) {
+ return null;
+ }
+ XMLConfiguration xml = new XMLConfiguration(sub);
+ disableDelimiterParsing(xml);
+ return xml;
+ }
+
+ /**
+ * Convenience class for testing that a {@link IXMLConfigurable} instance
+ * can be written, and read into an new instance that is equal as per
+ * {@link #equals(Object)}.
+ * @param xmlConfiurable the instance to test if it writes/read properly
+ * @throws IOException Cannot read/write
+ * @throws ConfigurationException Cannot load configuration
+ */
+ public static void assertWriteRead(IXMLConfigurable xmlConfiurable)
+ throws IOException {
+
+ // Write
+ StringWriter out = new StringWriter();
+ try {
+ xmlConfiurable.saveToXML(out);
+ } finally {
+ out.close();
+ }
+
+ // Read
+ XMLConfiguration xml = newXMLConfiguration(
+ new StringReader(out.toString()));
+ IXMLConfigurable readConfigurable =
+ (IXMLConfigurable) newInstance(xml);
+
+ if (!xmlConfiurable.equals(readConfigurable)) {
+ LOG.error("BEFORE: " + xmlConfiurable);
+ LOG.error(" AFTER: " + readConfigurable);
+ throw new ConfigurationException(
+ "Saved and loaded XML are not the same.");
+ }
+ }
+
+ /**
+ * Gets a duration which can be a numerical value or a textual
+ * representation of a duration as per {@link DurationParser}.
+ * If the duration does not exists for the given key or is blank,
+ * the default value is returned.
+ * If the key value is found but there are parsing errors, a
+ * {@link DurationParserException} will be thrown.
+ * @param xml xml configuration
+ * @param key key to the element/attribute containing the duration
+ * @param defaultValue default duration
+ * @return duration in milliseconds
+ * @since 1.13.0
+ */
+ public static long getDuration(
+ HierarchicalConfiguration xml, String key, long defaultValue) {
+ String duration = xml.getString(key, null);
+ if (StringUtils.isBlank(duration)) {
+ return defaultValue;
+ }
+ return DurationParser.parse(duration);
+ }
+
+ /**
+ * Gets a comma-separated-value string as a String array, trimming values
+ * and removing any blank entries.
+ * Commas can have any spaces before or after.
+ * Since {@link #newXMLConfiguration(Reader)} disables delimiter parsing,
+ * this method is an useful alternative to
+ * {@link HierarchicalConfiguration#getStringArray(String)}.
+ * @param xml xml configuration
+ * @param key key to the element/attribute containing the CSV string
+ * @return string array (or null)
+ * @since 1.13.0
+ */
+ public static String[] getCSVStringArray(
+ HierarchicalConfiguration xml, String key) {
+ return getCSVStringArray(xml, key, null);
+ }
+ /**
+ * Gets a comma-separated-value string as a String array, trimming values
+ * and removing any blank entries.
+ * Commas can have any spaces before or after.
+ * Since {@link #newXMLConfiguration(Reader)} disables delimiter parsing,
+ * this method is an useful alternative to
+ * {@link HierarchicalConfiguration#getStringArray(String)}.
+ * @param xml xml configuration
+ * @param key key to the element/attribute containing the CSV string
+ * @param defaultValues default values if the split returns null
+ * or an empty array
+ * @return string array (or null)
+ * @since 1.13.0
+ */
+ public static String[] getCSVStringArray(
+ HierarchicalConfiguration xml, String key, String[] defaultValues) {
+ String[] values = splitCSV(xml.getString(key, null));
+ if (ArrayUtils.isEmpty(values)) {
+ return defaultValues;
+ }
+ return values;
+ }
+
+ /**
+ * Gets a comma-separated-value string as an int array, removing any
+ * blank entries.
+ * Commas can have any spaces before or after.
+ * Invalid integers will log an error and assign zero instead.
+ * @param xml xml configuration
+ * @param key key to the element/attribute containing the CSV string
+ * @return int array (or null)
+ * @since 1.13.0
+ */
+ public static int[] getCSVIntArray(
+ HierarchicalConfiguration xml, String key) {
+ return getCSVIntArray(xml, key, null);
+ }
+ /**
+ * Gets a comma-separated-value string as an int array, removing any
+ * blank entries.
+ * Commas can have any spaces before or after.
+ * Invalid integers will log an error and assign zero instead.
+ * @param xml xml configuration
+ * @param key key to the element/attribute containing the CSV string
+ * @param defaultValues default values if the split returns null
+ * or an empty array
+ * @return int array (or null)
+ * @since 1.13.0
+ */
+ public static int[] getCSVIntArray(
+ HierarchicalConfiguration xml, String key, int[] defaultValues) {
+ String[] strings = splitCSV(xml.getString(key, null));
+ if (ArrayUtils.isEmpty(strings)) {
+ return defaultValues;
+ }
+ int[] ints = new int[strings.length];
+ for (int i = 0; i < strings.length; i++) {
+ try {
+ ints[i] = Integer.parseInt(strings[i]);
+ } catch (NumberFormatException e) {
+ LOG.error("Invalid integer: " + strings[i], e);
+ }
+ }
+ return ints;
+ }
+
+ // CVS Split: trim + remove blank entries
+ private static String[] splitCSV(String str) {
+ if (str == null) {
+ return null;
+ }
+ return str.trim().split("(\\s*,\\s*)+");
+ }
+
+ private static String getXSDResourcePath(Class> clazz) {
+ if (clazz == null) {
+ return null;
+ }
+ return "/" + clazz.getCanonicalName().replace('.', '/') + ".xsd";
+ }
+
+ // This method is because the regular configurationAt MUST have 1
+ // entry or will fail, and the containsKey(String) method is not reliable
+ // since it expects a value (body text) or returns false.
+ private static HierarchicalConfiguration safeConfigurationAt(
+ HierarchicalConfiguration node, String key) {
+ List subs = node.configurationsAt(key);
+ if (subs != null && !subs.isEmpty()) {
+ return subs.get(0);
+ }
+ return null;
+ }
+
+ private static class LogErrorHandler implements ErrorHandler {
+ private int errorCount = 0;
+ private final Class> clazz;
+ public LogErrorHandler(Class> clazz) {
+ super();
+ this.clazz = clazz;
+ }
+ @Override
+ public void warning(SAXParseException e) throws SAXException {
+ errorCount++;
+ LOG.warn(msg(e));
+ }
+ @Override
+ public void error(SAXParseException e) throws SAXException {
+ errorCount++;
+ LOG.error(msg(e));
+ }
+ @Override
+ public void fatalError(SAXParseException e) throws SAXException {
+ errorCount++;
+ LOG.fatal(msg(e));
+ }
+ private String msg(SAXParseException e) {
+ return "(XML) " + clazz.getSimpleName() + ": " + e.getMessage();
+ }
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java
index f9747a79..df321c5e 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java
@@ -1,4 +1,4 @@
-/* Copyright 2015-2016 Norconex Inc.
+/* Copyright 2015-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,6 +50,10 @@
*
* <above_command> encrypt -f "/path/to/key.txt" "Encrypt this text"
*
+ *
+ * As of 1.13.0, you can also use the encrypt.[sh|bat]
and
+ * decrypt.[sh|bat]
files distributed with this library.
+ *
*
* @author Pascal Essiembre
* @since 1.9.0
@@ -96,7 +100,7 @@ public static void main(String[] args) {
}
private static void printUsage() {
PrintStream out = System.out;
- out.println(" encrypt|decrypt -k|-f|-e key text");
+ out.println(" encrypt|decrypt -k|-f|-e|-p key text");
out.println();
out.println("Where:");
out.println(" encrypt encrypt the text with the given key");
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/ExecException.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/ExecException.java
new file mode 100644
index 00000000..c3513793
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/ExecException.java
@@ -0,0 +1,49 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+/**
+ * An exception thrown by {@link ExecUtil}.
+ * @author Pascal Essiembre
+ * @see ExecUtil
+ * @since 1.13.0
+ */
+public class ExecException extends RuntimeException {
+
+ private static final long serialVersionUID = -1219238586367858298L;
+
+ /**
+ * @see Exception#Exception(String)
+ * @param message exception message
+ */
+ public ExecException(final String message) {
+ super(message);
+ }
+ /**
+ * @see Exception#Exception(Throwable)
+ * @param cause exception cause
+ */
+ public ExecException(final Throwable cause) {
+ super(cause);
+ }
+ /**
+ * @see Exception#Exception(String, Throwable)
+ * @param message exception message
+ * @param cause exception cause
+ */
+ public ExecException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/ExecUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/ExecUtil.java
new file mode 100644
index 00000000..cc248296
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/ExecUtil.java
@@ -0,0 +1,252 @@
+/* Copyright 2010-2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.commons.io.IOUtils;
+
+import com.norconex.commons.lang.io.IInputStreamListener;
+import com.norconex.commons.lang.io.InputStreamConsumer;
+
+/**
+ * Utility methods related to process execution. Checked exceptions
+ * are wrapped in a runtime {@link ExecException}.
+ * @author Pascal Essiembre
+ * @since 1.13.0 (previously part of
+ * JEF API 4.0).
+ */
+public final class ExecUtil {
+
+ /** Identifier for standard output. */
+ public static final String STDOUT = "STDOUT";
+ /** Identifier for standard error. */
+ public static final String STDERR = "STDERR";
+
+ /**
+ * Constructor.
+ */
+ private ExecUtil() {
+ super();
+ }
+
+ /**
+ * Watches a running process. This method will wait until the process
+ * as finished executing before returning with its exit value.
+ * It ensures the process does not hang on some platform by making use
+ * of the {@link InputStreamConsumer} to read its error and output stream.
+ * @param process the process to watch
+ * @return process exit value
+ */
+ public static int watchProcess(Process process) {
+ return watchProcess(process,
+ new IInputStreamListener[] {}, new IInputStreamListener[] {});
+ }
+ /**
+ * Watches a running process. This method will wait until the process
+ * as finished executing before returning with its exit value.
+ * It ensures the process does not hang on some platform by making use
+ * of the {@link InputStreamConsumer} to read its error and output stream.
+ * The listener will be notified every time an error or output line
+ * gets written by the process.
+ * The listener line type will either be "STDERR" or "STDOUT".
+ * @param process the process to watch
+ * @param listener the listener to use for both "STDERR" or "STDOUT".
+ * @return process exit value
+ */
+ public static int watchProcess(
+ Process process,
+ IInputStreamListener listener) {
+ return watchProcess(process,
+ new IInputStreamListener[] {listener},
+ new IInputStreamListener[] {listener});
+ }
+ /**
+ * Watches a running process. This method will wait until the process
+ * as finished executing before returning with its exit value.
+ * It ensures the process does not hang on some platform by making use
+ * of the {@link InputStreamConsumer} to read its error and output stream.
+ * The listener will be notified every time an error or output line
+ * gets written by the process.
+ * The listener line type will either be "STDERR" or "STDOUT".
+ * @param process the process to watch
+ * @param listeners the listeners to use for both "STDERR" or "STDOUT".
+ * @return process exit value
+ */
+ public static int watchProcess(
+ Process process,
+ IInputStreamListener[] listeners) {
+ return watchProcess(process, listeners, listeners);
+ }
+
+ /**
+ * Watches a running process. This method will wait until the process
+ * as finished executing before returning with its exit value.
+ * It ensures the process does not hang on some platform by making use
+ * of the {@link InputStreamConsumer} to read its error and output stream.
+ * The listener will be notified every time an error or output line
+ * gets written by the process.
+ * The listener line type will either be "STDERR" or "STDOUT".
+ * @param process the process to watch
+ * @param outputListener the process output listener
+ * @param errorListener the process error listener
+ * @return process exit value
+ * @throws InterruptedException problem while waiting for process to finish
+ */
+ public static int watchProcess(
+ Process process,
+ IInputStreamListener outputListener,
+ IInputStreamListener errorListener) throws InterruptedException {
+ return watchProcess(process,
+ new IInputStreamListener[] {outputListener},
+ new IInputStreamListener[] {errorListener});
+ }
+ /**
+ * Watches a running process. This method will wait until the process
+ * as finished executing before returning with its exit value.
+ * It ensures the process does not hang on some platform by making use
+ * of the {@link InputStreamConsumer} to read its error and output stream.
+ * The listeners will be notified every time an error or output line
+ * gets written by the process.
+ * The listener line type will either be "STDERR" or "STDOUT".
+ * @param process the process to watch
+ * @param outputListeners the process output listeners
+ * @param errorListeners the process error listeners
+ * @return process exit value
+ */
+ public static int watchProcess(
+ Process process,
+ IInputStreamListener[] outputListeners,
+ IInputStreamListener[] errorListeners) {
+ return watchProcess(process, null, outputListeners, errorListeners);
+ }
+ /**
+ * Watches a running process while sending data to its STDIN.
+ * This method will wait until the process
+ * as finished executing before returning with its exit value.
+ * It ensures the process does not hang on some platform by making use
+ * of the {@link InputStreamConsumer} to read its error and output stream.
+ * The listeners will be notified every time an error or output line
+ * gets written by the process.
+ * The listener line type will either be "STDERR" or "STDOUT".
+ * @param process the process to watch
+ * @param input input sent to process STDIN
+ * @param outputListeners the process output listeners
+ * @param errorListeners the process error listeners
+ * @return process exit value
+ */
+ public static int watchProcess(
+ Process process,
+ InputStream input,
+ IInputStreamListener[] outputListeners,
+ IInputStreamListener[] errorListeners) {
+ watchProcessAsync(process, input, outputListeners, errorListeners);
+ try {
+ return process.waitFor();
+ } catch (InterruptedException e) {
+ throw new ExecException("Process was interrupted.", e);
+ }
+ }
+
+
+ /**
+ * Watches process output. This method is the same as
+ * {@link #watchProcess(
+ * Process, IInputStreamListener, IInputStreamListener)}
+ * with the exception of not waiting for the process to complete before
+ * returning.
+ * @param process the process on which to watch outputs
+ * @param outputListener the process output listeners
+ * @param errorListener the process error listeners
+ */
+ public static void watchProcessAsync(
+ Process process,
+ IInputStreamListener outputListener,
+ IInputStreamListener errorListener) {
+ watchProcessAsync(process,
+ new IInputStreamListener[] {outputListener},
+ new IInputStreamListener[] {errorListener});
+ }
+
+
+ /**
+ * Watches process output. This method is the same as
+ * {@link #watchProcess(
+ * Process, IInputStreamListener[], IInputStreamListener[])}
+ * with the exception of not waiting for the process to complete before
+ * returning.
+ * @param process the process on which to watch outputs
+ * @param outputListeners the process output listeners
+ * @param errorListeners the process error listeners
+ */
+ public static void watchProcessAsync(
+ Process process,
+ IInputStreamListener[] outputListeners,
+ IInputStreamListener[] errorListeners) {
+ watchProcessAsync(process, null, outputListeners, errorListeners);
+ }
+
+ /**
+ * Watches process output while sending data to its STDIN.
+ * This method is the same as
+ * {@link #watchProcess(Process, InputStream,
+ * IInputStreamListener[], IInputStreamListener[])}
+ * with the exception of not waiting for the process to complete before
+ * returning.
+ * @param process the process on which to watch outputs
+ * @param input input sent to process STDIN
+ * @param outputListeners the process output listeners
+ * @param errorListeners the process error listeners
+ */
+ public static void watchProcessAsync(
+ final Process process,
+ final InputStream input,
+ final IInputStreamListener[] outputListeners,
+ final IInputStreamListener[] errorListeners) {
+ // listen for output
+ InputStreamConsumer output = new InputStreamConsumer(
+ process.getInputStream(), STDOUT, outputListeners);
+ output.start();
+
+ // listen for error
+ InputStreamConsumer error = new InputStreamConsumer(
+ process.getErrorStream(), STDERR, errorListeners);
+ error.start();
+
+ // send input
+ if (input != null) {
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ try (OutputStream stdin = process.getOutputStream()) {
+ IOUtils.copy(input, stdin);
+ } catch (IOException e) {
+ throw new ExecException(
+ "Error sending input stream to proces.", e);
+ }
+ }
+ };
+ t.start();
+ try{
+ t.join();
+ } catch(InterruptedException e) {
+ throw new ExecException(
+ "Process interrupted while sending input stream.", e);
+ }
+ }
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/IExceptionFilter.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/IExceptionFilter.java
new file mode 100644
index 00000000..dc96da4f
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/IExceptionFilter.java
@@ -0,0 +1,35 @@
+/* Copyright 2010-2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+/**
+ * Filter for limiting the exceptions to be eligible for retry.
+ * @author Pascal Essiembre
+ * @see Retrier
+ * @since 1.13.0 (previously part of
+ * JEF API 4.0).
+ */
+public interface IExceptionFilter {
+
+ /**
+ * Filters an exception. Runtime exceptions can be of any type,
+ * but checked exceptions are always wrapped
+ * in a {@link RetriableException}.
+ * @param e the exception to filter
+ * @return true
if the exception should trigger a retry,
+ * false
to abort execution
+ */
+ boolean retry(Exception e);
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/IRetriable.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/IRetriable.java
new file mode 100644
index 00000000..5de65ce7
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/IRetriable.java
@@ -0,0 +1,34 @@
+/* Copyright 2010-2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+/**
+ * Upon failure, the run
method will get
+ * re-executed by a {@link Retrier} until successful or fails according
+ * to the {@link Retrier} conditions.
+ * @param type of optional return value
+ * @author Pascal Essiembre
+ * @see Retrier
+ * @since 1.13.0 (previously part of
+ * JEF API 4.0).
+ */
+public interface IRetriable {
+ /**
+ * Code to be executed until successful (no exception thrown).
+ * @throws RetriableException any exception
+ * @return optional return value
+ */
+ T execute() throws RetriableException;
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/RetriableException.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/RetriableException.java
new file mode 100644
index 00000000..05d545c8
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/RetriableException.java
@@ -0,0 +1,72 @@
+/* Copyright 2010-2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * Exception thrown when {@link Retrier} failed to execute a
+ * {@link IRetriable} instance. In cases there were multiple failed attempts,
+ * {@link #getCause()} will return the last exception captured.
+ * You can get all exceptions captured by {@link Retrier}
+ * with {@link #getAllCauses()} (up to a maximum specified by {@link Retrier}).
+ * @author Pascal Essiembre
+ * @see Retrier
+ * @since 1.13.0 (previously part of
+ * JEF API 4.0).
+ */
+public class RetriableException extends Exception {
+
+ private static final long serialVersionUID = 5236102272021889018L;
+
+ private final Throwable[] causes;
+
+ /**
+ * Constructor.
+ * @param message exception message
+ */
+ /*default*/ RetriableException(final String message) {
+ super(message);
+ this.causes = null;
+ }
+ /**
+ * Constructor.
+ * @param causes exception causes
+ */
+ /*default*/ RetriableException(final Throwable... causes) {
+ super(getLastCause(causes));
+ this.causes = causes;
+ }
+ /**
+ * @param message exception message
+ * @param causes exception causes
+ */
+ /*default*/ RetriableException(
+ final String message, final Throwable... causes) {
+ super(message, getLastCause(causes));
+ this.causes = causes;
+ }
+
+ public synchronized Throwable[] getAllCauses() {
+ return ArrayUtils.clone(causes);
+ }
+
+ private static Throwable getLastCause(Throwable[] causes) {
+ if (ArrayUtils.isEmpty(causes)) {
+ return null;
+ }
+ return causes[causes.length -1];
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/Retrier.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/Retrier.java
new file mode 100644
index 00000000..074f42bc
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/Retrier.java
@@ -0,0 +1,192 @@
+/* Copyright 2010-2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+import org.apache.commons.collections4.queue.CircularFifoQueue;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+import com.norconex.commons.lang.Sleeper;
+
+/**
+ * This class is responsible for executing {@link IRetriable}
+ * instances. Upon reaching the maximum number of retries allowed, it
+ * will throw a {@link RetriableException}, wrapping the last exceptions
+ * encountered, if any, to a configurable maximum.
+ * @author Pascal Essiembre
+ * @since 1.13.0 (previously "RetriableExecutor" part of
+ * JEF API 4.0).
+ */
+public class Retrier {
+
+ private static final Logger LOG = LogManager.getLogger(Retrier.class);
+
+ /** Default maximum number of retries. */
+ public static final int DEFAULT_MAX_RETRIES = 10;
+ /** Default wait time (milliseconds) before making a new attempt. */
+ public static final long DEFAULT_RETRY_DELAY = 0;
+ /** Default maximum number of exception causes kept. */
+ public static final int DEFAULT_MAX_CAUSES_KEPT = 10;
+
+ private static final Exception[] EMPTY_EXCEPTIONS = new Exception[] {};
+
+ private int maxRetries = DEFAULT_MAX_RETRIES;
+ private long retryDelay = DEFAULT_RETRY_DELAY;
+ private IExceptionFilter exceptionFilter;
+ private int maxCauses = DEFAULT_MAX_CAUSES_KEPT;
+
+ /**
+ * Creates a new instance with the default maximum retries and default
+ * retry delay (no delay).
+ */
+ public Retrier() {
+ super();
+ }
+ /**
+ * Creates a new instance with the default retry delay (no delay).
+ * @param maxRetries maximum number of execution retries
+ */
+ public Retrier(int maxRetries) {
+ this.maxRetries = maxRetries;
+ }
+ /**
+ * Creates a new instance which will retry execution
+ * only if the exception thrown by an attempt is accepted by
+ * the {@link IExceptionFilter} (up to maxRetries
).
+ * Uses the default maximum retries and default retry delay.
+ * @param exceptionFilter exception filter
+ */
+ public Retrier(IExceptionFilter exceptionFilter) {
+ this.exceptionFilter = exceptionFilter;
+ }
+ /**
+ * Creates a new instance which will retry execution
+ * only if the exception thrown by an attempt is accepted by
+ * the {@link IExceptionFilter} (up to maxRetries
).
+ * @param exceptionFilter exception filter
+ * @param maxRetries maximum number of retries
+ */
+ public Retrier(IExceptionFilter exceptionFilter, int maxRetries) {
+ super();
+ this.maxRetries = maxRetries;
+ this.exceptionFilter = exceptionFilter;
+ }
+
+ /**
+ * Gets the maximum number of retries (the initial run does not count as
+ * a retry).
+ * @return maximum number of retries
+ */
+ public int getMaxRetries() {
+ return maxRetries;
+ }
+ /**
+ * Sets the maximum number of retries (the initial run does not count as
+ * a retry).
+ * @param maxRetries maximum number of retries
+ * @return this instance
+ */
+ public Retrier setMaxRetries(int maxRetries) {
+ this.maxRetries = maxRetries;
+ return this;
+ }
+ /**
+ * Gets the delay in milliseconds before attempting to execute again.
+ * @return delay in milliseconds
+ */
+ public long getRetryDelay() {
+ return retryDelay;
+ }
+ /**
+ * Sets the delay in milliseconds before attempting to execute again.
+ * @param retryDelay delay in milliseconds
+ * @return this instance
+ */
+ public Retrier setRetryDelay(long retryDelay) {
+ this.retryDelay = retryDelay;
+ return this;
+ }
+ /**
+ * Sets an exception filter that limits the exceptions eligible for retry.
+ * @return exception filter
+ */
+ public IExceptionFilter getExceptionFilter() {
+ return exceptionFilter;
+ }
+ /**
+ * Sets an exception filter that limits the exceptions eligible for retry.
+ * @param exceptionFilter exception filter
+ * @return this instance
+ */
+ public Retrier setExceptionFilter(IExceptionFilter exceptionFilter) {
+ this.exceptionFilter = exceptionFilter;
+ return this;
+ }
+ /**
+ * Gets the maximum number of exception causes to keep when all attempts
+ * were made and a {@link RetriableException} is thrown.
+ * @return max number of causes
+ */
+ public int getMaxCauses() {
+ return maxCauses;
+ }
+ /**
+ * Sets the maximum number of exception causes to keep when all attempts
+ * were made and a {@link RetriableException} is thrown.
+ * @param maxCauses max number of causes
+ * @return this instance
+ */
+ public Retrier setMaxCauses(int maxCauses) {
+ this.maxCauses = maxCauses;
+ return this;
+ }
+ /**
+ * Runs the {@link IRetriable} instance. This method is not thread safe.
+ * @param retriable the code to run
+ * @param type of optional return value
+ * @return execution output if any, or null
+ * @throws RetriableException wrapper around last exception encountered
+ * or exception thrown when max rerun attempts is reached.
+ */
+ public T execute(IRetriable retriable) throws RetriableException {
+ int attemptCount = 0;
+ CircularFifoQueue exceptions =
+ new CircularFifoQueue<>(maxCauses);
+ while (attemptCount <= maxRetries) {
+ try {
+ return retriable.execute();
+ } catch (Exception e) {
+ exceptions.add(e);
+ if (exceptionFilter != null && !exceptionFilter.retry(e)) {
+ throw new RetriableException(
+ "Encountered an exception preventing "
+ + "execution retry.",
+ exceptions.toArray(EMPTY_EXCEPTIONS));
+ }
+ }
+ attemptCount++;
+ if (attemptCount < maxRetries) {
+ LOG.warn("Execution failed, retrying ("
+ + attemptCount + " of " + maxRetries
+ + " maximum retries).",
+ exceptions.get(exceptions.size() -1));
+ Sleeper.sleepMillis(retryDelay);
+ }
+ }
+ throw new RetriableException(
+ "Execution failed, maximum number of retries reached.",
+ exceptions.toArray(EMPTY_EXCEPTIONS));
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/SystemCommand.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/SystemCommand.java
new file mode 100644
index 00000000..61ebdff3
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/SystemCommand.java
@@ -0,0 +1,508 @@
+/* Copyright 2010-2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
+import org.apache.commons.lang3.text.translate.LookupTranslator;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+import com.norconex.commons.lang.io.IInputStreamListener;
+import com.norconex.commons.lang.io.InputStreamLineListener;
+
+/**
+ * Represents a program to be executed by the underlying system
+ * (on the "command line"). This class attempts to be system-independent,
+ * which means given an executable path should be sufficient to run
+ * programs on any systems (e.g. it handles prefixing an executable with OS
+ * specific commands as well as preventing process hanging on some OS when
+ * there is nowhere to display the output).
+ *
+ * @author Pascal Essiembre
+ * @since 1.13.0 (previously part of
+ * JEF API 4.0).
+ */
+public class SystemCommand {
+
+ private static final Logger LOG = LogManager.getLogger(SystemCommand.class);
+
+ private static final String[] CMD_PREFIXES_WIN_LEGACY =
+ new String[] { "command.com", "/C" };
+ private static final String[] CMD_PREFIXES_WIN_CURRENT =
+ new String[] { "cmd.exe", "/C" };
+
+ private static final IInputStreamListener[] EMPTY_LISTENERS =
+ new IInputStreamListener[] {};
+
+ private final String[] command;
+ private final File workdir;
+ // Null means inherit from those of java process
+ private Map environmentVariables = null;
+
+ private final List errorListeners =
+ Collections.synchronizedList(new ArrayList());
+ private final List outputListeners =
+ Collections.synchronizedList(new ArrayList());
+
+ private Process process;
+
+ /**
+ * Creates a command for which the execution will be in the working
+ * directory of the current process. If more than one command values
+ * are passed, the first element of the array
+ * is the command and subsequent elements are arguments.
+ * If your command or arguments contain spaces, they will be escaped
+ * according to your operating sytem (surrounding with double-quotes on
+ * Windows and backslash on other operating systems).
+ * @param command the command to run
+ */
+ public SystemCommand(String... command) {
+ this(null, command);
+ }
+
+ /**
+ * Creates a command. If more than one command values
+ * are passed, the first element of the array
+ * is the command and subsequent elements are arguments.
+ * If your command or arguments contain spaces, they will be escaped
+ * according to your operating sytem (surrounding with double-quotes on
+ * Windows and backslash on other operating systems).
+ * @param command the command to run
+ * @param workdir command working directory.
+ */
+ public SystemCommand(File workdir, String... command) {
+ super();
+ this.command = command;
+ this.workdir = workdir;
+ }
+
+
+ /**
+ * Gets the command to be run.
+ * @return the command
+ */
+ public String[] getCommand() {
+ return ArrayUtils.clone(command);
+ }
+
+ /**
+ * Gets the command working directory.
+ * @return command working directory.
+ */
+ public File getWorkdir() {
+ return workdir;
+ }
+
+ /**
+ * Adds an error (STDERR) listener to this system command.
+ * @param listener command error listener
+ */
+ public void addErrorListener(
+ final IInputStreamListener listener) {
+ synchronized (errorListeners) {
+ errorListeners.add(0, listener);
+ }
+ }
+ /**
+ * Removes an error (STDERR) listener.
+ * @param listener command error listener
+ */
+ public void removeErrorListener(
+ final IInputStreamListener listener) {
+ synchronized (errorListeners) {
+ errorListeners.remove(listener);
+ }
+ }
+ /**
+ * Adds an output (STDOUT) listener to this system command.
+ * @param listener command output listener
+ */
+ public void addOutputListener(
+ final IInputStreamListener listener) {
+ synchronized (outputListeners) {
+ outputListeners.add(0, listener);
+ }
+ }
+ /**
+ * Removes an output (STDOUT) listener.
+ * @param listener command output listener
+ */
+ public void removeOutputListener(
+ final IInputStreamListener listener) {
+ synchronized (outputListeners) {
+ outputListeners.remove(listener);
+ }
+ }
+ /**
+ * Gets environment variables.
+ * @return environment variables
+ */
+ public Map getEnvironmentVariables() {
+ return environmentVariables;
+ }
+ /**
+ * Sets environment variables. Set to null
(default) for the
+ * command to inherit the environment of the current process.
+ * @param environmentVariables environment variables
+ */
+ public void setEnvironmentVariables(
+ Map environmentVariables) {
+ this.environmentVariables = environmentVariables;
+ }
+
+ /**
+ * Returns whether the command is currently running.
+ * @return true
if running
+ */
+ public boolean isRunning() {
+ if (process == null) {
+ return false;
+ }
+ try {
+ process.exitValue();
+ return false;
+ } catch (IllegalThreadStateException e) {
+ return true;
+ }
+ }
+
+ /**
+ * Aborts the running command. If the command is not currently running,
+ * aborting it will have no effect.
+ */
+ public void abort() {
+ if (process != null) {
+ process.destroy();
+ }
+ }
+
+ /**
+ * Executes this system command and returns only when the underlying
+ * process stopped running.
+ * @return process exit value
+ * @throws SystemCommandException problem executing command
+ */
+ public int execute() throws SystemCommandException {
+ return execute(false);
+ }
+
+ /**
+ * Executes this system command. When run in the background,
+ * this method does not wait for the process to complete before returning.
+ * In such case the status code should always be 0 unless it terminated
+ * abruptly (may not reflect the process termination status).
+ * When NOT run in the background, this method waits and returns
+ * only when the underlying process stopped running.
+ * Alternatively, to run a command asynchronously, you can wrap it in
+ * its own thread.
+ * @param runInBackground true
to runs the system command in
+ * background.
+ * @return process exit value
+ * @throws SystemCommandException problem executing command
+ * @throws IllegalStateException when command is already running
+ */
+ public int execute(boolean runInBackground) throws SystemCommandException {
+ return execute(null, runInBackground);
+ }
+
+ /**
+ * Executes this system command with the given input and returns only when
+ * the underlying process stopped running.
+ * @param input process input (fed to STDIN)
+ * @return process exit value
+ * @throws SystemCommandException problem executing command
+ */
+ public int execute(InputStream input) throws SystemCommandException {
+ return execute(input, false);
+ }
+
+ /**
+ * Executes this system command with the given input. When run in the
+ * background, this method does not wait for the process to complete before
+ * returning.
+ * In such case the status code should always be 0 unless it terminated
+ * abruptly (may not reflect the process termination status).
+ * When NOT run in the background, this method waits and returns
+ * only when the underlying process stopped running.
+ * Alternatively, to run a command asynchronously, you can wrap it in
+ * its own thread.
+ * @param input process input (fed to STDIN)
+ * @param runInBackground true
to runs the system command in
+ * background.
+ * @return process exit value
+ * @throws SystemCommandException problem executing command
+ * @throws IllegalStateException when command is already running
+ */
+ public int execute(final InputStream input, boolean runInBackground)
+ throws SystemCommandException {
+ if (isRunning()) {
+ throw new IllegalStateException(
+ "Command is already running: " + toString());
+ }
+ String[] cleanCommand = getCleanCommand();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Executing command: "
+ + StringUtils.join(cleanCommand, " "));
+ }
+ try {
+ process = Runtime.getRuntime().exec(
+ cleanCommand, environmentArray(), workdir);
+ } catch (IOException e) {
+ throw new SystemCommandException("Could not execute command: "
+ + toString(), e);
+ }
+
+ IInputStreamListener[] outListeners =
+ outputListeners.toArray(EMPTY_LISTENERS);
+
+ IInputStreamListener[] errListeners =
+ errorListeners.toArray(EMPTY_LISTENERS);
+ ErrorTracker errorTracker = new ErrorTracker();
+ errListeners = ArrayUtils.add(errListeners, errorTracker);
+
+ int exitValue = 0;
+ if (runInBackground) {
+ ExecUtil.watchProcessAsync(
+ process, input, outListeners, errListeners);
+ try {
+ // Check in case the process terminated abruptly.
+ exitValue = process.exitValue();
+ } catch (IllegalThreadStateException e) {
+ // Do nothing
+ }
+ } else {
+ exitValue = ExecUtil.watchProcess(
+ process, input, outListeners, errListeners);
+ }
+
+ if (exitValue != 0) {
+ LOG.error("Command returned with exit value " + process.exitValue()
+ + " (command properly escaped?). Command: "
+ + StringUtils.join(cleanCommand, " ") + " Error: \""
+ + errorTracker.b.toString() + "\"");
+ }
+ process = null;
+ return exitValue;
+ }
+
+ /**
+ * Returns the command to be executed.
+ */
+ @Override
+ public String toString() {
+ return StringUtils.join(command, " ");
+ }
+
+ private String[] environmentArray() {
+ if (environmentVariables == null) {
+ return null;
+ }
+ List envs = new ArrayList<>();
+ for (Entry entry : environmentVariables.entrySet()) {
+ envs.add(entry.getKey() + "=" + entry.getValue());
+ }
+ return envs.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
+ }
+
+ private String[] getCleanCommand() throws SystemCommandException {
+ List cmd =
+ new ArrayList<>(Arrays.asList(ArrayUtils.nullToEmpty(command)));
+ if (cmd.isEmpty()) {
+ throw new SystemCommandException("No command specified.");
+ }
+
+ escape(cmd);
+ wrapCommand(cmd);
+ return cmd.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
+ }
+
+ // Removes OS prefixes from a command since they will be re-added
+ // and can affect processing logic if not
+ private void removePrefixes(List cmd, String[] prefixes) {
+ if (ArrayUtils.isEmpty(prefixes) || cmd.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < prefixes.length; i++) {
+ String prefix = prefixes[i];
+ if (cmd.size() > i && prefix.equalsIgnoreCase(cmd.get(0))) {
+ cmd.remove(0);
+ } else {
+ return;
+ }
+ }
+ }
+
+ /**
+ * Escapes spaces in each parts of the command as well as special
+ * characters in some operating systems, if they are not already
+ * escaped. Escapes according to operating system escape mechanism.
+ * If there is only one part to the command (size-one list), it
+ * can be the entire command with arguments passed as one string. In such
+ * case, there are little ways to tell if spaces are part of an argument
+ * or separates two arguments, so no escaping is performed.
+ * @param command the command to escape.
+ */
+ public static void escape(List command) {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ escapeWindows(command);
+ } else {
+ escapeNonWindows(command);
+ }
+ }
+
+ /**
+ * Escapes spaces in each parts of the command as well as special
+ * characters in some operating systems, if they are not already
+ * escaped. Escapes according to operating system escape mechanism.
+ * If there is only one part to the command (size-one array), it
+ * can be the entire command with arguments passed as one string. In such
+ * case, there are little ways to tell if spaces are part of an argument
+ * or separates two arguments, so no escaping is performed.
+ * @param command the command to escape.
+ * @return escaped command
+ */
+ public static String[] escape(String... command) {
+ List list = new ArrayList(Arrays.asList(command));
+ escape(list);
+ return list.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
+ }
+
+
+ // With windows, using cmd.exe /C requires putting arguments with
+ // spaces in quotes, and then everything after cmd.exe /C in quotes
+ // as well. See:
+ // http://stackoverflow.com/questions/6376113/how-to-use-spaces-in-cmd
+ private static void escapeWindows(List cmd) {
+ // If only 1 arg, it could be the command plus args together so there
+ // is no way to tell if spaces should be escaped, so we assume they
+ // were properly escaped to begin with.
+ if (cmd.size() == 1) {
+ //TODO attempt to escape arguments properly checking if
+ //an argument starts with a driver letter and/or checking
+ //if a file exists as we combine the arguments until a file
+ //is found? If found, escape the sequence.
+ return;
+ }
+
+ List newCmd = new ArrayList();
+ for (String arg : cmd) {
+ if (StringUtils.contains(arg, ' ')
+ && !arg.matches("^\\s*\".*\"\\s*$")) {
+ newCmd.add('"' + arg + '"');
+ } else {
+ newCmd.add(arg);
+ }
+ }
+ cmd.clear();
+ cmd.addAll(newCmd);
+ }
+
+ // Escape spaces with a backslash if not already escaped
+ private static void escapeNonWindows(List cmd) {
+ // If only 1 arg, it could be the command plus args together so there
+ // is no way to tell if spaces should be escaped, so we assume they
+ // were properly escaped to begin with and we break it up by
+ // non-escaped spaces or the OS will not think the single string
+ // is one command (as opposed to command + args) and can fail.
+ if (cmd.size() == 1) {
+ String[] parts = cmd.get(0).split("(? cmd) {
+ if (SystemUtils.OS_NAME == null || !SystemUtils.IS_OS_WINDOWS) {
+ return;
+ }
+ String[] prefixes;
+ if (SystemUtils.IS_OS_WINDOWS_95
+ || SystemUtils.IS_OS_WINDOWS_98
+ || SystemUtils.IS_OS_WINDOWS_ME) {
+ prefixes = CMD_PREFIXES_WIN_LEGACY;
+ } else {
+ // NT, 2000, XP and up
+ prefixes = CMD_PREFIXES_WIN_CURRENT;
+ }
+ removePrefixes(cmd, prefixes);
+ String wrappedCmd = "\"" + StringUtils.join(cmd, " ") + "\"";
+ cmd.clear();
+ cmd.addAll(Arrays.asList(prefixes));
+ cmd.add(wrappedCmd);
+ }
+
+ private class ErrorTracker extends InputStreamLineListener {
+ private final StringBuilder b = new StringBuilder();
+ @Override
+ protected void lineStreamed(String type, String line) {
+ if (b.length() > 0) {
+ b.append('\n');
+ }
+ b.append(line);
+ }
+ }
+
+ //TODO remove the following when Apache Commons Lang 3.6 is out, which
+ // will contain StringEscapeUtils#escapeShell(String)
+ private static final CharSequenceTranslator ESCAPE_XSI =
+ new LookupTranslator(
+ new String[][] {
+ {"|", "\\|"},
+ {"&", "\\&"},
+ {";", "\\;"},
+ {"<", "\\<"},
+ {">", "\\>"},
+ {"(", "\\("},
+ {")", "\\)"},
+ {"$", "\\$"},
+ {"`", "\\`"},
+ {"\\", "\\\\"},
+ {"\"", "\\\""},
+ {"'", "\\'"},
+ {" ", "\\ "},
+ {"\t", "\\\t"},
+ {"\r\n", ""},
+ {"\n", ""},
+ {"*", "\\*"},
+ {"?", "\\?"},
+ {"[", "\\["},
+ {"#", "\\#"},
+ {"~", "\\~"},
+ {"=", "\\="},
+ {"%", "\\%"},
+ });
+ private static String escapeShell(String input) {
+ return ESCAPE_XSI.translate(input);
+ }
+}
\ No newline at end of file
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/SystemCommandException.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/SystemCommandException.java
new file mode 100644
index 00000000..3f0ef3f4
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/SystemCommandException.java
@@ -0,0 +1,49 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+/**
+ * An exception thrown by a executing {@link SystemCommand}.
+ * @author Pascal Essiembre
+ * @see SystemCommand
+ * @since 1.13.0
+ */
+public class SystemCommandException extends Exception {
+
+ private static final long serialVersionUID = 5236102272021889018L;
+
+ /**
+ * @see Exception#Exception(String)
+ * @param message exception message
+ */
+ public SystemCommandException(final String message) {
+ super(message);
+ }
+ /**
+ * @see Exception#Exception(Throwable)
+ * @param cause exception cause
+ */
+ public SystemCommandException(final Throwable cause) {
+ super(cause);
+ }
+ /**
+ * @see Exception#Exception(String, Throwable)
+ * @param message exception message
+ * @param cause exception cause
+ */
+ public SystemCommandException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IFileVisitor.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/package-info.java
similarity index 54%
rename from norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IFileVisitor.java
rename to norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/package-info.java
index a9661878..edcc00df 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IFileVisitor.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/exec/package-info.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2014 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -12,22 +12,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.norconex.commons.lang.io;
-
-import java.io.File;
-
/**
- * Visitor to be used with FileUtil.visit*
methods.
- * @see FileUtil
- * @author Pascal Essiembre
- * @deprecated Since 1.4.0, use
- * {@link com.norconex.commons.lang.file.IFileVisitor}
+ * Utility classes related to process/code execution.
*/
-@Deprecated
-public interface IFileVisitor {
- /**
- * Visits a file or directory.
- * @param file the file or directory being visited
- */
- void visit(File file);
-}
+package com.norconex.commons.lang.exec;
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/file/FileUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/file/FileUtil.java
index 1900f505..e031df52 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/file/FileUtil.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/file/FileUtil.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2016 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,7 +28,6 @@
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.commons.lang3.time.DateFormatUtils;
@@ -133,8 +132,7 @@ public static void moveFileToDir(File sourceFile, File targetDir)
}
String fileName = sourceFile.getName();
- File targetFile = new File(targetDir.getAbsolutePath()
- + SystemUtils.PATH_SEPARATOR + fileName);
+ File targetFile = new File(targetDir, fileName);
moveFile(sourceFile, targetFile);
}
@@ -223,16 +221,6 @@ public static void delete(File file) throws IOException {
}
}
- /**
- * @deprecated renamed to {@link #delete(File)}.
- * @param file file or directory to delete
- * @throws IOException cannot delete file.
- */
- @Deprecated
- public static void deleteFile(File file) throws IOException {
- delete(file);
- }
-
/**
* Deletes all directories that are empty from a given parent directory.
* @param parentDir the directory where to start looking for empty
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/FileMonitor.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/FileMonitor.java
deleted file mode 100644
index 4c222f3a..00000000
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/FileMonitor.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/* Copyright 2010-2016 Norconex Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.norconex.commons.lang.io;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.net.URL;
-import java.util.Hashtable;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * Class monitoring a {@link File} for changes and notifying all registered
- * {@link IFileChangeListener}.
- *
- * @author Pascal Essiembre
- * @since 1.3.0
- * @deprecated Since 1.4.0, use
- * {@link com.norconex.commons.lang.file.FileMonitor}
- */
-@Deprecated
-public final class FileMonitor {
-
- private static final FileMonitor INSTANCE = new FileMonitor();
-
- private Timer timer;
-
- private Map timerEntries;
-
- /**
- * Constructor.
- */
- private FileMonitor() {
- timer = new Timer(true);
- timerEntries = new Hashtable();
- }
-
- /**
- * Gets the file monitor instance.
- *
- * @return file monitor instance
- */
- public static FileMonitor getInstance() {
- return INSTANCE;
- }
-
- /**
- * Adds a monitored file with a {@link IFileChangeListener}.
- *
- * @param listener listener to notify when the file changed.
- * @param fileName name of the file to monitor.
- * @param period polling period in milliseconds.
- * @throws FileNotFoundException error with the file
- */
- public void addFileChangeListener(IFileChangeListener listener,
- String fileName, long period) throws FileNotFoundException {
- addFileChangeListener(listener, new File(fileName), period);
- }
-
- /**
- * Adds a monitored file with a FileChangeListener.
- *
- * @param listener listener to notify when the file changed.
- * @param file the file to monitor.
- * @param period polling period in milliseconds.
- * @throws FileNotFoundException error with the file
- */
- public void addFileChangeListener(IFileChangeListener listener, File file,
- long period) throws FileNotFoundException {
- removeFileChangeListener(listener, file);
- FileMonitorTask task = new FileMonitorTask(listener, file);
- timerEntries.put(file.toString() + listener.hashCode(), task);
- timer.schedule(task, period, period);
- }
-
- /**
- * Remove the listener from the notification list.
- *
- * @param listener the listener to be removed.
- * @param fileName name of the file for which to remove the listener
- */
- public void removeFileChangeListener(IFileChangeListener listener,
- String fileName) {
- removeFileChangeListener(listener, new File(fileName));
- }
-
- /**
- * Remove the listener from the notification list.
- *
- * @param listener the listener to be removed.
- * @param file the file for which to remove the listener
- */
- public void removeFileChangeListener(
- IFileChangeListener listener, File file) {
- FileMonitorTask task = timerEntries.remove(file.toString()
- + listener.hashCode());
- if (task != null) {
- task.cancel();
- }
- }
-
- /**
- * Fires notification that a file changed.
- *
- * @param listener
- * file change listener
- * @param file
- * the file that changed
- */
- protected void fireFileChangeEvent(
- IFileChangeListener listener, File file) {
- listener.fileChanged(file);
- }
-
- /**
- * File monitoring task.
- */
- class FileMonitorTask extends TimerTask {
- private IFileChangeListener listener;
- private File monitoredFile;
- private long lastModified;
-
- public FileMonitorTask(IFileChangeListener listener, File file)
- throws FileNotFoundException {
- this.listener = listener;
- this.lastModified = 0;
- monitoredFile = file;
- // but is it on CLASSPATH?
- if (!monitoredFile.exists()) {
- URL fileURL = listener.getClass().getClassLoader()
- .getResource(file.toString());
- if (fileURL != null) {
- monitoredFile = new File(fileURL.getFile());
- } else {
- throw new FileNotFoundException("File Not Found: " + file);
- }
- }
- this.lastModified = monitoredFile.lastModified();
- }
-
- @Override
- public void run() {
- long fileLastModified = monitoredFile.lastModified();
- if (fileLastModified != this.lastModified) {
- this.lastModified = fileLastModified;
- fireFileChangeEvent(this.listener, monitoredFile);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/FileUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/FileUtil.java
deleted file mode 100644
index d91b4e5e..00000000
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/FileUtil.java
+++ /dev/null
@@ -1,571 +0,0 @@
-/* Copyright 2010-2016 Norconex Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.norconex.commons.lang.io;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.Date;
-import java.util.LinkedList;
-
-import org.apache.commons.io.Charsets;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
-
-/**
- * Utility methods when dealing with files and directories.
- * @author Pascal Essiembre
- * @deprecated Since 1.4.0, use {@link com.norconex.commons.lang.file.FileUtil}
- */
-@Deprecated
-public final class FileUtil {
-
- private FileUtil() {
- super();
- }
-
- /**
- * Converts any String to a valid file-system file name representation. The
- * valid file name is constructed so it can be written to virtually any
- * operating system.
- * Use {@link #fromSafeFileName(String)} to get back the original name.
- * @param unsafeFileName the file name to make safe.
- * @return valid file name
- */
- public static String toSafeFileName(String unsafeFileName) {
- return com.norconex.commons.lang.file.FileUtil.toSafeFileName(
- unsafeFileName);
- }
- /**
- * Converts a "safe" file name originally created with
- * {@link #toSafeFileName(String)} into its original string.
- * @param safeFileName the file name to convert to its origianl form
- * @return original string
- */
- public static String fromSafeFileName(String safeFileName) {
- return com.norconex.commons.lang.file.FileUtil.fromSafeFileName(
- safeFileName);
- }
-
-
- /**
- * Moves a file to a directory. Like {@link #moveFile(File, File)}:
- *
- * - If the target directory does not exists, it creates it first.
- * - If the target file already exists, it deletes it first.
- * - If target file deletion does not work, it will try 10 times,
- * waiting 1 second between each try to give a chance to whatever
- * OS lock on the file to go.
- * - It throws a IOException if the move failed (as opposed to fail
- * silently).
- *
- * @param sourceFile source file to move
- * @param targetDir target destination
- * @throws IOException cannot move file.
- */
- public static void moveFileToDir(File sourceFile, File targetDir)
- throws IOException {
- com.norconex.commons.lang.file.FileUtil.moveFileToDir(
- sourceFile, targetDir);
- }
-
- /**
- * Moves a file to a new file location. This method is different from the
- * {@link File#renameTo(File)} method in such that:
- *
- * - If the target file already exists, it deletes it first.
- * - If target file deletion does not work, it will try 10 times,
- * waiting 1 second between each try to give a chance to whatever
- * OS lock on the file to go.
- * - It throws a IOException if the move failed (as opposed to fail
- * silently).
- *
- * @param sourceFile source file to move
- * @param targetFile target destination
- * @throws IOException cannot move file.
- */
- public static void moveFile(File sourceFile, File targetFile)
- throws IOException {
- com.norconex.commons.lang.file.FileUtil.moveFile(
- sourceFile, targetFile);
- }
-
- /**
- * Deletes a file or a directory recursively. This method does:
- *
- * - If file or directory deletion does not work, it will try 10 times,
- * waiting 1 second between each try to give a chance to whatever
- * OS lock on the file to go.
- * - It throws a IOException if the delete failed (as opposed to fail
- * silently).
- * - If file is
null
or does not exist, nothing happens.
- *
- * @param file file or directory to delete
- * @throws IOException cannot delete file.
- */
- public static void deleteFile(File file) throws IOException {
- com.norconex.commons.lang.file.FileUtil.delete(file);
- }
-
- /**
- * Deletes all directories that are empty from a given parent directory.
- * @param parentDir the directory where to start looking for empty
- * directories
- * @return the number of deleted directories
- */
- public static int deleteEmptyDirs(File parentDir) {
- return com.norconex.commons.lang.file.FileUtil.deleteEmptyDirs(
- parentDir);
- }
-
- /**
- * Deletes all directories that are empty and are older
- * than the given date. If the date is null
, all empty
- * directories will be deleted, regardless of their date.
- * @param parentDir the directory where to start looking for empty
- * directories
- * @param date the date to compare empty directories against
- * @return the number of deleted directories
- * @since 1.3.0
- */
- public static int deleteEmptyDirs(File parentDir, final Date date) {
- return com.norconex.commons.lang.file.FileUtil.deleteEmptyDirs(
- parentDir, date);
- }
-
- /**
- * Create all parent directories for a file if they do not exists.
- * If they exist already, this method does nothing. This method assumes
- * the last segment is a file or will be a file.
- * @param file the file to create parent directories for
- * @return The newly created parent directory
- * @throws IOException if somethign went wrong creating the parent
- * directories
- */
- public static File createDirsForFile(File file) throws IOException {
- return com.norconex.commons.lang.file.FileUtil.createDirsForFile(file);
- }
-
- /**
- * Visits all files and directories under a directory.
- * @param dir the directory
- * @param visitor the visitor
- */
- public static void visitAllDirsAndFiles(File dir, IFileVisitor visitor) {
- visitAllDirsAndFiles(dir, visitor, null);
- }
- /**
- * Visits all files and directories under a directory.
- * @param dir the directory
- * @param visitor the visitor
- * @param filter an optional filter to restrict the files being visited
- */
- public static void visitAllDirsAndFiles(
- File dir, IFileVisitor visitor, FileFilter filter) {
- visitor.visit(dir);
- if (!dir.exists()) {
- return;
- } else if (dir.isDirectory()) {
- File[] children = dir.listFiles(filter);
- if (children != null) {
- for (int i=0; i lines = new LinkedList<>();
- BufferedReader reader = new BufferedReader(
- new InputStreamReader(new FileInputStream(file), encoding));
-
- int remainingLinesToRead = numberOfLinesToRead;
- String line = StringUtils.EMPTY;
- while(line != null && remainingLinesToRead-- > 0){
- line = reader.readLine();
- if (!stripBlankLines || StringUtils.isNotBlank(line)) {
- if (filter != null && filter.accept(line)) {
- lines.addFirst(line);
- } else {
- remainingLinesToRead++;
- }
- } else {
- remainingLinesToRead++;
- }
- }
- reader.close();
- return lines.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
- }
-
- /**
- * Returns the specified number of lines starting from the end
- * of a text file.
- * @param file the file to read lines from
- * @param numberOfLinesToRead the number of lines to read
- * @return array of file lines
- * @throws IOException i/o problem
- */
- public static String[] tail(File file, int numberOfLinesToRead)
- throws IOException {
- return tail(file, Charsets.ISO_8859_1.toString(), numberOfLinesToRead);
- }
-
- /**
- * Returns the specified number of lines starting from the end
- * of a text file.
- * @param file the file to read lines from
- * @param encoding the file encoding
- * @param numberOfLinesToRead the number of lines to read
- * @return array of file lines
- * @throws IOException i/o problem
- */
- public static String[] tail(File file, String encoding,
- int numberOfLinesToRead) throws IOException {
- return tail(file, encoding, numberOfLinesToRead, true);
- }
-
- /**
- * Returns the specified number of lines starting from the end
- * of a text file.
- * @param file the file to read lines from
- * @param encoding the file encoding
- * @param numberOfLinesToRead the number of lines to read
- * @param stripBlankLines whether to return blank lines or not
- * @return array of file lines
- * @throws IOException i/o problem
- */
- public static String[] tail(File file, String encoding,
- int numberOfLinesToRead, boolean stripBlankLines)
- throws IOException {
- return tail(file, encoding, numberOfLinesToRead, stripBlankLines, null);
- }
-
- /**
- * Returns the specified number of lines starting from the end
- * of a text file.
- * @param file the file to read lines from
- * @param encoding the file encoding
- * @param numberOfLinesToRead the number of lines to read
- * @param stripBlankLines whether to return blank lines or not
- * @param filter InputStream filter
- * @return array of file lines
- * @throws IOException i/o problem
- */
- public static String[] tail(File file, String encoding,
- final int numberOfLinesToRead, boolean stripBlankLines,
- IInputStreamFilter filter)
- throws IOException {
- assertFile(file);
- assertNumOfLinesToRead(numberOfLinesToRead);
- LinkedList lines = new LinkedList<>();
- BufferedReader reader = new BufferedReader(new InputStreamReader(
- new ReverseFileInputStream(file), encoding));
- int remainingLinesToRead = numberOfLinesToRead;
- String line = StringUtils.EMPTY;
- while(line != null && remainingLinesToRead-- > 0){
- line = StringUtils.defaultString(reader.readLine());
- char[] chars = line.toCharArray();
- for (int j = 0, k = chars.length - 1; j < k; j++, k--) {
- char temp = chars[j];
- chars[j] = chars[k];
- chars[k] = temp;
- }
- String newLine = new String(chars);
- if (!stripBlankLines || StringUtils.isNotBlank(line)) {
- if (filter != null && filter.accept(newLine)) {
- lines.addFirst(newLine);
- } else {
- remainingLinesToRead++;
- }
- } else {
- remainingLinesToRead++;
- }
- }
- reader.close();
- return lines.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
- }
-
-
- /**
- * Creates (if not already existing) a series of directories reflecting
- * the current date, up to the day unit, under a given parent directory.
- * For example, a date of 2000-12-31 will create the following directory
- * structure:
- *
- * /<parentDir>/2000/12/31/
- *
- * @param parentDir the parent directory where to create date directories
- * @return the directory representing the full path created
- * @throws IOException if the parent directory is not valid
- */
- public static File createDateDirs(File parentDir) throws IOException {
- return com.norconex.commons.lang.file.FileUtil.createDateDirs(
- parentDir);
- }
- /**
- * Creates (if not already existing) a series of directories reflecting
- * a date, up to the day unit, under a given parent directory. For example,
- * a date of 2000-12-31 will create the following directory structure:
- *
- * /<parentDir>/2000/12/31/
- *
- * @param parentDir the parent directory where to create date directories
- * @param date the date to create directories from
- * @return the directory representing the full path created
- * @throws IOException if the parent directory is not valid
- */
- public static File createDateDirs(File parentDir, Date date)
- throws IOException {
- return com.norconex.commons.lang.file.FileUtil.createDateDirs(
- parentDir, date);
- }
-
- /**
- * Creates (if not already existing) a series of directories reflecting
- * the current date and time, up to the seconds, under a given parent
- * directory. For example, a date of 2000-12-31T13:34:12 will create the
- * following directory structure:
- *
- * /<parentDir>/2000/12/31/13/34/12/
- *
- * @param parentDir the parent directory where to create date directories
- * @return the directory representing the full path created
- * @throws IOException if the parent directory is not valid
- */
- public static File createDateTimeDirs(File parentDir) throws IOException {
- return com.norconex.commons.lang.file.FileUtil.createDateTimeDirs(
- parentDir);
- }
- /**
- * Creates (if not already existing) a series of directories reflecting
- * a date and time, up to the seconds, under a given parent directory.
- * For example,
- * a date of 2000-12-31T13:34:12 will create the following directory
- * structure:
- *
- * /<parentDir>/2000/12/31/13/34/12/
- *
- * @param parentDir the parent directory where to create date directories
- * @param dateTime the date to create directories from
- * @return the directory representing the full path created
- * @throws IOException if the parent directory is not valid
- */
- public static File createDateTimeDirs(File parentDir, Date dateTime)
- throws IOException {
- return com.norconex.commons.lang.file.FileUtil.createDateTimeDirs(
- parentDir, dateTime);
- }
-
- private static void assertNumOfLinesToRead(int num) {
- if (num <= 0) {
- throw new IllegalArgumentException(
- "Not a valid number to read: " + num);
- }
- }
-
- private static void assertFile(File file) throws IOException {
- if (file == null || !file.exists()
- || !file.isFile() || !file.canRead()) {
- throw new IOException("Not a valid file: " + file);
- }
- }
-}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IInputStreamListener.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IInputStreamListener.java
new file mode 100644
index 00000000..164409e6
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IInputStreamListener.java
@@ -0,0 +1,44 @@
+/* Copyright 2010-2014 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.io;
+
+/**
+ *
+ * Listener that is being notified every time a chunk of bytes is
+ * processed from a given input stream.
+ *
+ *
+ * Since 1.13.0, use {@link InputStreamLineListener} to listen to each line
+ * being streamed from text files.
+ *
+ *
+ * Should not be considered thread safe.
+ *
+ * @author Pascal Essiembre
+ * @see InputStreamConsumer
+ * @see InputStreamLineListener
+ */
+public interface IInputStreamListener {
+
+ /**
+ * Invoked when a chunk of bytes is streamed.
+ * @param type type of what is being streamed, as defined by the class
+ * using this listener
+ * @param bytes chunk of bytes streamed
+ * @param length length of valid bytes to read or -1 if no more bytes to
+ * read
+ */
+ void streamed(String type, byte[] bytes, int length);
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IStreamListener.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IStreamListener.java
index 41cf70c6..c3bad009 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IStreamListener.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IStreamListener.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2014 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,9 @@
* given stream.
* @author Pascal Essiembre
* @see StreamGobbler
+ * @deprecated As of 1.13.0, use {@link IInputStreamListener} instead.
*/
+@Deprecated
public interface IStreamListener {
/**
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/InputStreamConsumer.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/InputStreamConsumer.java
new file mode 100644
index 00000000..e6e36306
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/InputStreamConsumer.java
@@ -0,0 +1,225 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * A stream consumer will read all it can from a stream in its own thread.
+ * This is often required by some processes/operating systems in order to
+ * prevent application freeze. For example, this is a way to capture the
+ * STDOUT and STDERR from a process. This class also allows to "listen" to
+ * what is being read. Listeners should not be considered thread-safe.
+ * If you share a listener between threads, make sure to
+ * have unique types to identify each one or the content being streamed
+ * from different threads can easily be mixed up in the order sent and is
+ * likely not a desired behavior.
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public class InputStreamConsumer extends Thread {
+
+ public static final int DEFAULT_CHUNK_SIZE = 1024;
+ private final List listeners =
+ Collections.synchronizedList(new ArrayList());
+ /** The input stream we are reading. */
+ private final InputStream input;
+ private final String type;
+ private final int chunkSize;
+
+ /**
+ * Constructor.
+ * @param input input stream
+ */
+ public InputStreamConsumer(InputStream input) {
+ this(input, null);
+ }
+ /**
+ * Constructor.
+ * @param input input stream
+ * @param type an optional way to identify each portion being read to
+ * stream listeners. Useful when listeners are shared.
+ * Can be null
.
+ * @param listeners input stream listeners
+ */
+ public InputStreamConsumer(
+ InputStream input, String type,
+ IInputStreamListener... listeners) {
+ this(input, DEFAULT_CHUNK_SIZE, type, listeners);
+ }
+ /**
+ * Constructor.
+ * @param input input stream
+ * @param chunkSize how many bytes to read at once before (will also
+ * be the maximum byte array size sent to listeners)
+ * @param type an optional way to identify each portion being read to
+ * stream listeners. Useful when listeners are shared.
+ * Can be null
.
+ * @param listeners input stream listeners
+ */
+ public InputStreamConsumer(
+ InputStream input, int chunkSize, String type,
+ IInputStreamListener... listeners) {
+ super("StreamConsumer" + (type == null ? "": "-" + type));
+ this.input = input;
+ this.type = type;
+ this.chunkSize = chunkSize;
+ if (!ArrayUtils.isEmpty(listeners)) {
+ this.listeners.addAll(0, Arrays.asList(listeners));
+ }
+ }
+
+ @Override
+ public void run() {
+ beforeStreaming();
+ try {
+ byte[] buffer = new byte[chunkSize];
+ int length;
+ while ((length = input.read(buffer)) != -1) {
+ fireStreamed(buffer, length);
+ }
+ fireStreamed(ArrayUtils.EMPTY_BYTE_ARRAY, -1);
+ } catch (IOException e) {
+ throw new StreamException("Problem consuming input stream.", e);
+ }
+ afterStreaming();
+ }
+ /**
+ * Returns stream listeners.
+ * @return the listeners
+ */
+ public IInputStreamListener[] getStreamListeners() {
+ return listeners.toArray(new IInputStreamListener[] {});
+ }
+ /**
+ * Gets the stream type or null
if no type was set.
+ * @return the type
+ */
+ public String getType() {
+ return type;
+ }
+ /**
+ * Invoked just before steaming begins, in a new thread.
+ * Default implementation does nothing. This method is for implementors.
+ */
+ protected void beforeStreaming() {
+ // do nothing (for subclasses)
+ }
+ /**
+ * Invoked just after steaming ended, before the thread dies.
+ * Default implementation does nothing. This method is for implementors.
+ */
+ protected void afterStreaming() {
+ // do nothing (for subclasses)
+ }
+
+ /**
+ * Starts this consumer thread and wait for it to complete before returning.
+ * @throws StreamException if streaming is interrupted while waiting
+ */
+ public synchronized void startAndWait() {
+ start();
+ try {
+ join();
+ } catch (InterruptedException e) {
+ throw new StreamException("Streaming interrupted.", e);
+ }
+ }
+
+ private synchronized void fireStreamed(byte[] bytes, int length) {
+ for (IInputStreamListener listener : listeners) {
+ listener.streamed(type, bytes, length);
+ }
+ }
+
+ /**
+ * Convenience method for creasing a consumer instance and starting it.
+ * @param input input stream to consume.
+ */
+ public static void consume(InputStream input) {
+ consume(input, null);
+ }
+ /**
+ * Convenience method for creasing a consumer instance and starting it.
+ * @param input input stream
+ * @param type an optional way to identify each portion being read to
+ * stream listeners. Useful when listeners are shared.
+ * Can be null
.
+ * @param listeners input stream listeners
+ */
+ public static void consume(InputStream input, String type,
+ IInputStreamListener... listeners) {
+ consume(input,DEFAULT_CHUNK_SIZE, type);
+ }
+ /**
+ * Convenience method for creasing a consumer instance, starting it,
+ * and waiting for it to complete.
+ * @param input input stream
+ * @param chunkSize how many bytes to read at once before (will also
+ * be the maximum byte array size sent to listeners)
+ * @param type an optional way to identify each portion being read to
+ * stream listeners. Useful when listeners are shared.
+ * Can be null
.
+ * @param listeners input stream listeners
+ */
+ public static void consume(InputStream input, int chunkSize, String type,
+ IInputStreamListener... listeners) {
+ new InputStreamConsumer(input, chunkSize, type, listeners).start();
+ }
+ /**
+ * Convenience method for creasing a consumer instance, starting it,
+ * and waiting for it to complete.
+ * @param input input stream to consume.
+ */
+ public static void consumeAndWait(InputStream input) {
+ consumeAndWait(input, null);
+ }
+ /**
+ * Convenience method for creasing a consumer instance and starting it.
+ * @param input input stream
+ * @param type an optional way to identify each portion being read to
+ * stream listeners. Useful when listeners are shared.
+ * Can be null
.
+ * @param listeners input stream listeners
+ */
+ public static void consumeAndWait(InputStream input, String type,
+ IInputStreamListener... listeners) {
+ consumeAndWait(input,DEFAULT_CHUNK_SIZE, type);
+ }
+ /**
+ * Convenience method for creasing a consumer instance, starting it,
+ * and waiting for it to complete.
+ * @param input input stream
+ * @param chunkSize how many bytes to read at once before (will also
+ * be the maximum byte array size sent to listeners)
+ * @param type an optional way to identify each portion being read to
+ * stream listeners. Useful when listeners are shared.
+ * Can be null
.
+ * @param listeners input stream listeners
+ */
+ public static void consumeAndWait(
+ InputStream input, int chunkSize, String type,
+ IInputStreamListener... listeners) {
+ new InputStreamConsumer(
+ input, chunkSize, type, listeners).startAndWait();
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/InputStreamLineListener.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/InputStreamLineListener.java
new file mode 100644
index 00000000..fa3cf51f
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/InputStreamLineListener.java
@@ -0,0 +1,144 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.io;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.Charsets;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Listener that is being notified every time a line is processed from a
+ * given stream. Not thread-safe. Use a new instance for each thread or at
+ * a minimum, make sure to give a unique type argument to
+ * each {@link InputStreamConsumer} to prevent lines content being mixed up.
+ * @author Pascal Essiembre
+ * @see InputStreamConsumer
+ */
+public abstract class InputStreamLineListener implements IInputStreamListener {
+
+ private static final byte NL = (byte) '\n';
+ private static final byte CR = (byte) '\r';
+ private static final byte RESET = (byte) '\0';
+
+ private final Map buffers = new HashMap<>();
+
+ private final Charset charset;
+
+ /**
+ * Creates a line listener with UTF-8 character encoding.
+ */
+ public InputStreamLineListener() {
+ this(StandardCharsets.UTF_8);
+ }
+ /**
+ * Creates a line listener with supplied character encoding
+ * (defaults to UTF-8 if null).
+ * @param charset character encoding
+ */
+ public InputStreamLineListener(Charset charset) {
+ super();
+ if (charset == null) {
+ this.charset = StandardCharsets.UTF_8;
+ } else {
+ this.charset = charset;
+ }
+ }
+ /**
+ * Creates a line listener with supplied character encoding
+ * (defaults to UTF-8 if null).
+ * @param charset character encoding
+ */
+ public InputStreamLineListener(String charset) {
+ super();
+ if (charset == null) {
+ this.charset = StandardCharsets.UTF_8;
+ } else {
+ this.charset = Charsets.toCharset(charset);
+ }
+ }
+
+ @Override
+ public void streamed(String type, byte[] bytes, int length) {
+ Buffer buffer = getBuffer(type);
+ if (length == -1) {
+ if (!buffer.lastEmpty) {
+ flushBuffer(type, buffer);
+ }
+ return;
+ }
+
+ for (int i = 0; i < length; i++) {
+ byte b = bytes[i];
+ if (isEOL(b)) {
+ if (isEOL(buffer.lastEolByte) && b != buffer.lastEolByte) {
+ buffer.lastEolByte = RESET;
+ } else {
+ flushBuffer(type, buffer);
+ buffer.lastEolByte = b;
+ }
+ } else {
+ buffer.lastEolByte = RESET;
+ buffer.baos.write(b);
+ buffer.lastEmpty = false;
+ }
+ }
+ }
+
+ /**
+ * Invoked when a line is streamed.
+ * @param type type of line, as defined by the class using the listener
+ * @param line line processed
+ */
+ protected abstract void lineStreamed(String type, String line);
+
+ private boolean isEOL(byte b) {
+ return b == NL || b == CR;
+ }
+
+ private void flushBuffer(String type, Buffer buffer) {
+ try {
+ lineStreamed(type, buffer.baos.toString(charset.toString()));
+ buffer.baos.reset();
+ buffer.lastEmpty = true;
+ } catch (UnsupportedEncodingException e) {
+ throw new StreamException("Unsupported charset: " + charset, e);
+ }
+ }
+
+ private synchronized Buffer getBuffer(String type) {
+ String key = type;
+ if (key == null) {
+ key = StringUtils.EMPTY;
+ }
+
+ Buffer buf = buffers.get(key);
+ if (buf == null) {
+ buf = new Buffer();
+ buffers.put(key, buf);
+ }
+ return buf;
+ }
+
+ class Buffer {
+ private boolean lastEmpty = true;
+ private byte lastEolByte = RESET;
+ private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/StreamGobbler.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/StreamGobbler.java
index 31cfaa0f..113c998b 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/StreamGobbler.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/StreamGobbler.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2014 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,8 +34,9 @@
* application freeze. For example, this is a way to capture the STDOUT and
* STDERR from a process.
* @author Pascal Essiembre
+ * @deprecated As of 1.13.0, use {@link InputStreamConsumer} instead.
*/
-@SuppressWarnings("nls")
+@Deprecated
public class StreamGobbler extends Thread {
private static final Logger LOG =
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarCopier.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarCopier.java
index 52b56596..5770f752 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarCopier.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarCopier.java
@@ -1,4 +1,4 @@
-/* Copyright 2016 Norconex Inc.
+/* Copyright 2016-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -317,7 +317,7 @@ private int getDuplicatesHandlingUserGlobalChoice() {
info(" 4) Copy source Jar regardless of target Jar");
info(" (may overwrite or cause mixed versions).");
info("");
- info(" 5) Let me chose for each files.");
+ info(" 5) Let me choose for each files.");
while (true) {
info("");
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicateFinder.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicateFinder.java
index 2504653a..4485ec38 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicateFinder.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicateFinder.java
@@ -1,4 +1,4 @@
-/* Copyright 2016 Norconex Inc.
+/* Copyright 2016-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.AbstractSetValuedMap;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
@@ -101,7 +102,7 @@ public static List findJarDuplicates(File... jarPaths) {
public static void main(String[] args) {
List dups = JarDuplicateFinder.findJarDuplicates(args);
System.out.println("Found " + dups.size()
- + " Jar(s) having ore more duplicates.");
+ + " Jar(s) having one or more duplicates.");
for (JarDuplicates jarDuplicates : dups) {
System.out.println();
System.out.println(
@@ -113,7 +114,9 @@ public static void main(String[] args) {
} else {
System.out.print(" ");
}
- System.out.println(jarFile.getPath());
+ System.out.println(jarFile.getPath() + " ["
+ + DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT
+ .format(jarFile.getLastModified()) + "]");
}
}
}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicates.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicates.java
index ce10a843..5fd6f494 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicates.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarDuplicates.java
@@ -1,4 +1,4 @@
-/* Copyright 2016 Norconex Inc.
+/* Copyright 2016-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -69,9 +69,9 @@ public boolean hasVersionConflict() {
if (ArrayUtils.isEmpty(jarFiles)) {
return false;
}
- String version = jarFiles[0].getVersion();
+ JarFile firstJar = jarFiles[0];
for (JarFile jarFile : jarFiles) {
- if (!Objects.equals(version, jarFile.getVersion())) {
+ if (!firstJar.isSameVersionAndTime(jarFile)) {
return true;
}
}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarFile.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarFile.java
index 4c5f33d6..8b356c18 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarFile.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/jar/JarFile.java
@@ -1,4 +1,4 @@
-/* Copyright 2016 Norconex Inc.
+/* Copyright 2016-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -70,7 +70,8 @@ public JarFile(File jarFile) {
this.baseName = fullName;
this.version = null;
}
- this.comparableVersion = new Version(this.version);
+ this.comparableVersion = new Version(
+ this.version, this.path.lastModified());
}
public File getPath() {
@@ -112,6 +113,32 @@ public int hashCode() {
.toHashCode();
}
+ /**
+ * Gets whether this Jar has the same name and version as
+ * the provided jar.
+ * @param jarFile jar file
+ * @return true
if the jar names and versions are the same.
+ * @since 1.13.0
+ */
+ public boolean isSameVersion(JarFile jarFile) {
+ if (jarFile == null) {
+ return false;
+ }
+ return fullName.equals(jarFile.fullName);
+ }
+ /**
+ * Gets whether this Jar has the same name and version as
+ * the provided jar, as well as the same last modified date.
+ * @param jarFile jar file
+ * @return true
if the jar names, versions and last modified
+ * dates are the same.
+ * @since 1.13.0
+ */
+ public boolean isSameVersionAndTime(JarFile jarFile) {
+ return isSameVersion(jarFile)
+ && path.lastModified() == jarFile.path.lastModified();
+ }
+
@Override
public int compareTo(JarFile o) {
int result = comparableVersion.compareTo(o.comparableVersion);
@@ -122,10 +149,13 @@ public int compareTo(JarFile o) {
}
private class Version implements Comparable {
- Object[] segments;
+ private Object[] segments;
+ private long lastModified;
+
- public Version(String version) {
+ public Version(String version, long lastModified) {
super();
+ this.lastModified = lastModified;
if (version == null) {
segments = new Object[0];
} else {
@@ -203,8 +233,8 @@ public int compareTo(Version o) {
}
}
}
- // it is a tie... weird this should not happen
- return 0;
+ // it is a tie... compare last modified
+ return Long.compare(lastModified, o.lastModified) * -1;
}
}
}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/log/CountingConsoleAppender.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/log/CountingConsoleAppender.java
new file mode 100644
index 00000000..5c5548b0
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/log/CountingConsoleAppender.java
@@ -0,0 +1,151 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Layout;
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ * A console appender that keeps track of how many events of each matching level
+ * were logged.
+ * Contrary to {@link ConsoleAppender}, invoking the empty constructor
+ * on this class will set a default layout ({@link #DEFAULT_LAYOUT}).
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public class CountingConsoleAppender extends ConsoleAppender {
+
+ public static final Layout DEFAULT_LAYOUT = new PatternLayout("%-5p %m%n");
+ private final Map counters = new HashMap<>();
+ private final Map, Logger> loggers = new HashMap<>();
+
+ public CountingConsoleAppender() {
+ this(DEFAULT_LAYOUT);
+ }
+
+ public CountingConsoleAppender(Layout layout, String target) {
+ super(layout, target);
+ }
+
+ public CountingConsoleAppender(Layout layout) {
+ super(layout);
+ }
+ @Override
+ public void append(LoggingEvent event) {
+ super.append(event);
+ getCounter(event.getLevel()).incrementAndGet();
+ }
+
+ /**
+ * Whether this appender counted any log events.
+ * @return true
if an event was logged
+ */
+ public boolean isEmpty() {
+ return counters.isEmpty();
+ }
+
+ /**
+ * Gets the number of events logged for the given log level.
+ * @param level log level
+ * @return number of events logged
+ */
+ public int getCount(Level level) {
+ return getCounter(level).get();
+ }
+
+ /**
+ * Gets the number of events logged for all log levels.
+ * @return number of events logged
+ */
+ public int getCount() {
+ int count = 0;
+ for (AtomicInteger i : counters.values()) {
+ count += i.get();
+ }
+ return count;
+ }
+
+ /**
+ * Resets all counts to zero.
+ */
+ @Override
+ public synchronized void reset() {
+ super.reset();
+ if (counters != null) {
+ counters.clear();
+ }
+ }
+
+ /**
+ * Starts counting log events for a class by creating a logger for that
+ * class and appending itself to it. If a logger was already created
+ * for the class, it will be reused (but the passed log level will be set
+ * on it).
+ * @param clazz class to count log events for
+ * @param logLevel minimum log level to track
+ */
+ public synchronized void startCountingFor(
+ Class> clazz, Level logLevel) {
+ Logger logger = loggers.get(clazz);
+ if (logger == null) {
+ logger = LogManager.getLogger(clazz);
+ loggers.put(clazz, logger);
+ }
+ if (!logger.isAttached(this)) {
+ logger.addAppender(this);
+ }
+ logger.setLevel(logLevel);
+ }
+ /**
+ * Stops counting log events for a class by removing this appender
+ * from the logger previously created for the supplied class.
+ * This method has no effect if {@link #startCountingFor(Class, Level)}
+ * was not previously invoked with the same class.
+ * @param clazz class to stop counting log events for
+ */
+ public synchronized void stopCountingFor(Class> clazz) {
+ Logger logger = loggers.get(clazz);
+ if (logger != null) {
+ logger.removeAppender(this);
+ }
+ }
+
+ private synchronized AtomicInteger getCounter(Level level) {
+ AtomicInteger i = counters.get(level);
+ if (i == null) {
+ i = new AtomicInteger();
+ counters.put(level, i);
+ }
+ return i;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
+ .append(counters)
+ .toString();
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IFileChangeListener.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/log/package-info.java
similarity index 52%
rename from norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IFileChangeListener.java
rename to norconex-commons-lang/src/main/java/com/norconex/commons/lang/log/package-info.java
index a73d230f..d912c68d 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/io/IFileChangeListener.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/log/package-info.java
@@ -1,33 +1,18 @@
-/* Copyright 2010-2014 Norconex Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.norconex.commons.lang.io;
-
-import java.io.File;
-
-/**
- * Listener for file changes, to be used with a {@link FileMonitor}.
- * @author Pascal Essiembre
- * @since 1.3.0
- * @deprecated Since 1.4.0, use
- * {@link com.norconex.commons.lang.file.IFileChangeListener}
- */
-@Deprecated
-public interface IFileChangeListener {
- /**
- * Invoked when a file changes.
- * @param file changed file.
- */
- void fileChanged(File file);
-}
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Utility classes related to logging.
+ */
+package com.norconex.commons.lang.log;
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/map/Properties.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/map/Properties.java
index 676c9e20..1a4ad09f 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/map/Properties.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/map/Properties.java
@@ -70,6 +70,9 @@
public class Properties extends ObservableMap>
implements Serializable {
+ //TODO have getXXXRegex() methods to return all keys or values matching
+ // a regular expression
+
//TODO remove support for case sensitivity and provide a utility
//class that does it instead on any string-key maps?
// OR, store it in a case sensitive way instead of keeping
@@ -1290,14 +1293,14 @@ public List put(String key, List values) {
return remove(key);
}
List nullSafeValues = new ArrayList<>(values.size());
- for (String value : nullSafeValues) {
+ for (String value : values) {
if (value == null) {
nullSafeValues.add(StringUtils.EMPTY);
} else {
nullSafeValues.add(value);
}
}
- return super.put(caseResolvedKey(key), values);
+ return super.put(caseResolvedKey(key), nullSafeValues);
}
/*
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParser.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParser.java
new file mode 100644
index 00000000..44724b1e
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParser.java
@@ -0,0 +1,235 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.time;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.Priority;
+
+import com.norconex.commons.lang.map.Properties;
+
+/**
+ *
+ * Parse a textual (English) representation of a duration and converts it into
+ * a long
millisecond value.
+ *
+ *
+ * If the string is made of digits only, it is assumed to be a millisecond
+ * and the value will remain the same.
+ *
+ *
+ * The duration unit can be written in single character or full words.
+ * For instance, "months" can be represented as uppercase "M", "month",
+ * or "months". Some abbreviations are also accepted (e.g., "mo", "mos",
+ * "mth", "mths"). Here is a list you can rely on:
+ *
+ *
+ * - y,yr,yrs,year,years
+ * - M,mo,mos,mth,mths,month,months
+ * - w,wk,wks,week,weeks
+ * - d,day,days
+ * - h,hs,hrs,hour,hours
+ * - m,min,minute,minutes
+ * - s,sec,second,seconds
+ * - S,ms,msec,millis,millisecond,milliseconds
+ *
+ *
+ * Single-character representation are case sensitive. Other terms are not.
+ * No distinction is made between plural and singular.
+ * Numeric values can be integers or decimals numbers (e.g., 2.5 months).
+ * One month duration uses the average of 30.44 days per month. A numeric
+ * value must be followed by a time unit. Other terms or characters
+ * are ignored.
+ *
+ * Examples:
+ *
+ * All of the following will be parsed properly:
+ *
+ *
+ * - 3 hours, 30 minute, and 30 seconds
+ * - 6h10m23s
+ * - 2.5hrs
+ * - 10y9 months, 8 d, 7hrs, 6 minute, and 5.5 Seconds
+ *
+ *
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public final class DurationParser {
+
+ private static final Logger LOG =
+ LogManager.getLogger(DurationParser.class);
+
+ private static final Properties UNIT_LABELS = new Properties();
+ static {
+ UNIT_LABELS.setMultiValueDelimiter(",");
+ try {
+ UNIT_LABELS.load(DurationParser.class.getResourceAsStream(
+ ClassUtils.getShortCanonicalName(DurationParser.class)
+ + ".properties"), ",");
+ } catch (IOException e) {
+ throw new DurationParserException(
+ "Coult not initialize DurationParser.", e);
+ }
+ }
+
+ private enum Unit{
+ millisecond(1),
+ second(millisecond.ms() * 1000),
+ minute(second.ms() * 60),
+ hour(minute.ms() * 60),
+ day(hour.ms() * 24),
+ week(day.ms() * 7),
+ month((double) (((float) day.ms()) * 30.44f)),
+ year(month.ms() * 12);
+ private double ms;
+ Unit(double ms) {
+ this.ms = ms;
+ }
+ private double ms() {
+ return ms;
+ }
+ };
+
+ private static final Pattern PATTERN =
+ Pattern.compile("(\\d+([\\.,]\\d+){0,1})(\\D+)");
+
+ public DurationParser() {
+ super();
+ }
+
+ /**
+ * Parses an English representation of a duration and converts it
+ * to milliseconds.
+ * If the value cannot be parsed, a {@link DurationParserException}
+ * is thrown.
+ * @param duration the duration to parse
+ * @return milliseconds
+ */
+ public static long parse(String duration) {
+ return parse(duration, -1, true);
+ }
+ /**
+ * Parses an English representation of a duration and converts it
+ * to milliseconds.
+ * If the value cannot be parsed, the default value is returned
+ * (no exception is thrown).
+ * @param duration the duration to parse
+ * @param defaultValue default value
+ * @return milliseconds
+ */
+ public static long parse(String duration, long defaultValue) {
+ return parse(duration, defaultValue, false);
+ }
+
+ private static long parse(
+ String duration, long defaultValue, boolean throwException) {
+
+ if (StringUtils.isBlank(duration)) {
+ parseError(throwException, Level.DEBUG, "Blank duration value.");
+ return defaultValue;
+ }
+
+ // If only digits, consider milliseconds and convert to long
+ if (NumberUtils.isDigits(duration.trim())) {
+ return NumberUtils.toLong(duration);
+ }
+
+ // There must be at least one digit
+ if (!duration.matches(".*\\d+.*")) {
+ parseError(throwException, Level.ERROR,
+ "Could not parse duration: \""
+ + duration+ "\". No number.");
+ return defaultValue;
+ }
+
+ // Else parse the string
+ Matcher m = PATTERN.matcher(duration);
+ long ms = 0;
+ while (m.find()) {
+ String numGroup = m.group(1);
+ String unitGroup = m.group(3).trim();
+
+ String num = numGroup.replace(',', '.');
+ if (!NumberUtils.isParsable(num)) {
+ parseError(throwException, Level.ERROR,
+ "Could not parse duration: \"" + duration
+ + "\". Invalid duration value: " + numGroup);
+ return defaultValue;
+ }
+ float val = NumberUtils.toFloat(num, -1);
+ if (val == -1) {
+ parseError(throwException, Level.ERROR,
+ "Could not parse duration: \"" + duration
+ + "\". Invalid duration value: " + numGroup);
+ return defaultValue;
+ }
+
+ String unitStr = unitGroup.replaceFirst("^(\\w+)(.*)", "$1");
+ Unit unit = getUnit(unitStr);
+ if (unit == null) {
+ parseError(throwException, Level.ERROR,
+ "Could not parse duration: \"" + duration
+ + "\". Unknown unit: \"" + unitStr + "\".");
+ return defaultValue;
+ }
+ ms += unit.ms * val;
+ }
+ return ms;
+ }
+
+ private static void parseError(
+ boolean throwException, Priority logLevel, String message) {
+ if (throwException) {
+ throw new DurationParserException(message);
+ }
+ LOG.log(logLevel, message);
+ }
+
+ private static synchronized Unit getUnit(String label) {
+ if (StringUtils.isBlank(label)) {
+ return null;
+ }
+
+ // if unit label is 1 char, compare to first item, case sensitive
+ if (label.length() == 1) {
+ for (String key : UNIT_LABELS.keySet()) {
+ if (UNIT_LABELS.getString(key).equals(label)) {
+ return Unit.valueOf(key);
+ }
+ }
+ // if more than 1 char, compare ignoring case
+ } else {
+ for (Entry> e : UNIT_LABELS.entrySet()) {
+ for (String value : e.getValue()) {
+ if (value.equalsIgnoreCase(label)) {
+ return Unit.valueOf(e.getKey());
+ }
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParser.properties b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParser.properties
new file mode 100644
index 00000000..a0d469ce
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParser.properties
@@ -0,0 +1,30 @@
+#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# Copyright 2010-2014 Norconex Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+day = d,day,days
+
+hour = h,hs,hrs,hour,hours
+
+millisecond = S,ms,msec,millis,millisecond,milliseconds
+
+minute = m,min,minute,minutes
+
+month = M,mo,mos,mth,mths,month,months
+
+second = s,sec,second,seconds
+
+week = w,wk,wks,week,weeks
+
+year = y,yr,yrs,year,years
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParserException.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParserException.java
new file mode 100644
index 00000000..dffa043b
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/time/DurationParserException.java
@@ -0,0 +1,59 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.time;
+
+/**
+ * Runtime exception when a {@link DurationParser} could not parse a string
+ * value.
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public class DurationParserException extends RuntimeException {
+
+ private static final long serialVersionUID = 8484839654375152232L;
+
+ /**
+ * Constructor.
+ */
+ public DurationParserException() {
+ super();
+ }
+
+ /**
+ * Constructor.
+ * @param message exception message
+ */
+ public DurationParserException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor.
+ * @param cause exception root cause
+ */
+ public DurationParserException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructor.
+ * @param message exception message
+ * @param cause exception root cause
+ */
+ public DurationParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/URLNormalizer.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/URLNormalizer.java
index d0ad5601..666b739a 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/URLNormalizer.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/URLNormalizer.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2016 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -764,6 +764,24 @@ public URLNormalizer removeSessionIds() {
return this;
}
+ /**
+ * Removes trailing hash character ("#").
+ * http://www.example.com/path# →
+ * http://www.example.com/path
+ *
+ * This only removes the hash character if it is the last character.
+ * To remove an entire URL fragment, use {@link #removeFragment()}.
+ *
+ * @return this instance
+ * @since 1.13.0
+ */
+ public URLNormalizer removeTrailingHash() {
+ if (url.endsWith("#") && StringUtils.countMatches(url, "#") == 1) {
+ url = StringUtils.removeEnd(url, "#");
+ }
+ return this;
+ }
+
/**
* Returns the normalized URL as string.
* @return URL
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ClasspathInput.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ClasspathInput.java
new file mode 100644
index 00000000..1469c1d0
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ClasspathInput.java
@@ -0,0 +1,142 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.xml;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.w3c.dom.ls.LSInput;
+
+/**
+ * Load XML Schema resources input from Classpath.
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public class ClasspathInput implements LSInput {
+
+ private static final Logger LOG =
+ LogManager.getLogger(ClasspathInput.class);
+
+ private String publicId;
+ private String systemId;
+ private BufferedInputStream inputStream;
+
+ public ClasspathInput(String publicId, String sysId, InputStream input) {
+ this.publicId = publicId;
+ this.systemId = sysId;
+ this.inputStream = new BufferedInputStream(input);
+ }
+
+ @Override
+ public String getPublicId() {
+ return publicId;
+ }
+
+ @Override
+ public void setPublicId(String publicId) {
+ this.publicId = publicId;
+ }
+
+ @Override
+ public String getBaseURI() {
+ return null;
+ }
+
+ @Override
+ public InputStream getByteStream() {
+ return null;
+ }
+
+ @Override
+ public boolean getCertifiedText() {
+ return false;
+ }
+
+ @Override
+ public Reader getCharacterStream() {
+ return null;
+ }
+
+ @Override
+ public String getEncoding() {
+ return null;
+ }
+
+ @Override
+ public String getStringData() {
+ synchronized (inputStream) {
+ try {
+ byte[] input = new byte[inputStream.available()];
+ inputStream.read(input);
+ return new String(input);
+ } catch (IOException e) {
+ LOG.error("Could not get string data.", e);
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void setBaseURI(String baseURI) {
+ //NOOP
+ }
+
+ @Override
+ public void setByteStream(InputStream byteStream) {
+ //NOOP
+ }
+
+ @Override
+ public void setCertifiedText(boolean certifiedText) {
+ //NOOP
+ }
+
+ @Override
+ public void setCharacterStream(Reader characterStream) {
+ //NOOP
+ }
+
+ @Override
+ public void setEncoding(String encoding) {
+ //NOOP
+ }
+
+ @Override
+ public void setStringData(String stringData) {
+ //NOOP
+ }
+
+ @Override
+ public String getSystemId() {
+ return systemId;
+ }
+
+ @Override
+ public void setSystemId(String systemId) {
+ this.systemId = systemId;
+ }
+
+ public BufferedInputStream getInputStream() {
+ return inputStream;
+ }
+
+ public void setInputStream(BufferedInputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+}
\ No newline at end of file
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ClasspathResourceResolver.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ClasspathResourceResolver.java
new file mode 100644
index 00000000..dbbf9f82
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ClasspathResourceResolver.java
@@ -0,0 +1,113 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.xml;
+
+import java.io.InputStream;
+
+import javax.xml.validation.SchemaFactory;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.w3c.dom.ls.LSInput;
+import org.w3c.dom.ls.LSResourceResolver;
+
+/**
+ *
+ * Resolves XML Schema (XSD) include directives by looking for the
+ * specified resource on the Classpath.
+ *
+ *
+ * To use, set this resolver on your {@link SchemaFactory}, like this:
+ *
+ *
+ * SchemaFactory schemaFactory =
+ * SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ * schemaFactory.setResourceResolver(
+ * new ClasspathResourceResolver(MyClass.class));
+ *
+ *
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public class ClasspathResourceResolver implements LSResourceResolver {
+
+ private static final Logger LOG =
+ LogManager.getLogger(ClasspathResourceResolver.class);
+
+ private final String rootPath;
+
+ public ClasspathResourceResolver() {
+ this((String) null);
+ }
+
+ /**
+ * Resolves the resource relative to the given class.
+ * @param relativeTo class to use as base for resolution
+ */
+ public ClasspathResourceResolver(Class> relativeTo) {
+ this(getPackageResourcePathFromClass(relativeTo));
+ }
+ /**
+ * Resolves the resource relative to the given package path.
+ * @param relativeTo package path to use as base for resolution
+ */
+ public ClasspathResourceResolver(String relativeTo) {
+ super();
+ this.rootPath = relativeTo;
+ }
+
+ @Override
+ public LSInput resolveResource(String type, String namespaceURI,
+ String publicId, String systemId, String baseURI) {
+ String path = rootPath;
+ if (baseURI != null) {
+ path = getPackageResourcePathFromBaseURI(baseURI);
+ }
+ String r = getResourcePath(path, systemId);
+ InputStream resourceAsStream = getClass().getResourceAsStream(r);
+ if (resourceAsStream == null) {
+ LOG.error("Resource not found: " + r
+ + " (baseURI: " + baseURI + "; systemId: " + systemId);
+ }
+ return new ClasspathInput(publicId, r, resourceAsStream);
+ }
+
+ private String getResourcePath(String path, String systemId) {
+ if (systemId.startsWith("/")) {
+ // Absolute path, no need to resolve
+ return systemId;
+ }
+ int upCount = StringUtils.countMatches(systemId, "../");
+ String newPath = path;
+ for (int i = 0; i < upCount; i++) {
+ newPath = newPath.replaceFirst("(.*/)(.*/)$", "$1");
+ }
+ return newPath + systemId.replaceFirst("^(../)+", "");
+ }
+ private static String getPackageResourcePathFromClass(Class> klass) {
+ if (klass == null) {
+ return StringUtils.EMPTY;
+ }
+ return "/" + klass.getPackage().getName().replace('.', '/') + "/";
+
+ }
+ private String getPackageResourcePathFromBaseURI(String baseURI) {
+ if (baseURI == null) {
+ return StringUtils.EMPTY;
+ }
+ return baseURI.replaceFirst("^(file://)*(.*/).*", "$2");
+ }
+}
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/EnhancedXMLStreamWriter.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/EnhancedXMLStreamWriter.java
index 93760faf..8d2d4a8f 100644
--- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/EnhancedXMLStreamWriter.java
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/EnhancedXMLStreamWriter.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2016 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,9 @@
/**
*
* A version of {@link XMLStreamWriter} that adds convenience methods
- * for adding simple elements and typed attributes.
+ * for adding simple elements and typed attributes, as well as offering
+ * pretty-printing. Can be used on its own or as a wrapper to an existing
+ * XMLStreamWriter
instance.
*
*
* @author Pascal Essiembre
@@ -41,28 +43,72 @@ public class EnhancedXMLStreamWriter implements XMLStreamWriter {
LogManager.getLogger(EnhancedXMLStreamWriter.class);
private final XMLStreamWriter writer;
+ //TODO consider constructor with new EnhancedXMLStreamWriterConfig instead
private final boolean writeBlanks;
+ // -1 = no indent, 0 = new lines only, 1+ = new lines + num of spaces,
+ private final int indent;
+ private int depth = 0;
+ private boolean indentEnd = false;
public EnhancedXMLStreamWriter(Writer out) throws XMLStreamException {
this(out, false);
}
public EnhancedXMLStreamWriter(Writer out, boolean writeBlanks)
throws XMLStreamException {
+ this(out, writeBlanks, -1);
+ }
+ /**
+ * Creates a new xml stream writer.
+ * @param out writer used to write XML
+ * @param writeBlanks true
to write attributes/elements
+ * with no values
+ * @param indent how many spaces to use for indentation (-1=no indent;
+ * 0=newline only; 1+=number of spaces after newline)
+ * @throws XMLStreamException problem creating XML stream writer
+ * @since 1.13.0
+ */
+ public EnhancedXMLStreamWriter(Writer out, boolean writeBlanks, int indent)
+ throws XMLStreamException {
super();
XMLOutputFactory factory = createXMLOutputFactory();
writer = factory.createXMLStreamWriter(out);
this.writeBlanks = writeBlanks;
+ this.indent = indent;
}
public EnhancedXMLStreamWriter(XMLStreamWriter xmlStreamWriter) {
this(xmlStreamWriter, false);
}
public EnhancedXMLStreamWriter(
XMLStreamWriter xmlStreamWriter, boolean writeBlanks) {
+ this(xmlStreamWriter, writeBlanks, -1);
+ }
+ /**
+ * Creates a new xml stream writer.
+ * @param xmlStreamWriter wrapped stream writer
+ * @param writeBlanks true
to write attributes/elements
+ * with no values
+ * @param indent how many spaces to use for indentation (-1=no indent;
+ * 0=newline only; 1+=number of spaces after newline)
+ * @since 1.13.0
+ */
+ public EnhancedXMLStreamWriter(
+ XMLStreamWriter xmlStreamWriter, boolean writeBlanks, int indent) {
super();
this.writer = xmlStreamWriter;
this.writeBlanks = writeBlanks;
+ this.indent = indent;
}
+ private void indent() throws XMLStreamException {
+ indentEnd = true;
+ if (indent > -1) {
+ writer.writeCharacters("\n");
+ if (indent > 0) {
+ writer.writeCharacters(StringUtils.repeat(' ', depth * indent));
+ }
+ }
+ }
+
private static XMLOutputFactory createXMLOutputFactory() {
XMLOutputFactory factory = XMLOutputFactory.newFactory();
// If using Woodstox factory, disable structure validation
@@ -167,11 +213,11 @@ private void writeElementObject(String localName, Object value)
throws XMLStreamException {
String strValue = Objects.toString(value, null);
if (StringUtils.isNotBlank(strValue)) {
- writer.writeStartElement(localName);
- writer.writeCharacters(strValue);
- writer.writeEndElement();
+ writeStartElement(localName);
+ writeCharacters(strValue);
+ writeEndElement();
} else if (writeBlanks) {
- writer.writeEmptyElement(localName);
+ writeEmptyElement(localName);
}
}
@@ -179,40 +225,54 @@ private void writeElementObject(String localName, Object value)
@Override
public void writeStartElement(String localName) throws XMLStreamException {
+ indent();
+ depth++;
writer.writeStartElement(localName);
}
@Override
public void writeStartElement(String namespaceURI, String localName)
throws XMLStreamException {
+ indent();
+ depth++;
writer.writeStartElement(namespaceURI, localName);
}
@Override
public void writeStartElement(String prefix, String localName,
String namespaceURI) throws XMLStreamException {
+ indent();
+ depth++;
writer.writeStartElement(prefix, localName, namespaceURI);
}
@Override
public void writeEmptyElement(String namespaceURI, String localName)
throws XMLStreamException {
+ indent();
writer.writeEmptyElement(namespaceURI, localName);
}
@Override
public void writeEmptyElement(String prefix, String localName,
String namespaceURI) throws XMLStreamException {
+ indent();
writer.writeEmptyElement(prefix, localName, namespaceURI);
}
@Override
public void writeEmptyElement(String localName) throws XMLStreamException {
+ indent();
writer.writeEmptyElement(localName);
}
@Override
public void writeEndElement() throws XMLStreamException {
+ depth--;
+ if (indentEnd) {
+ indent();
+ }
+ indentEnd = true;
writer.writeEndElement();
}
@@ -263,18 +323,21 @@ public void writeDefaultNamespace(String namespaceURI)
@Override
public void writeComment(String data) throws XMLStreamException {
+ indent();
writer.writeComment(data);
}
@Override
public void writeProcessingInstruction(String target)
throws XMLStreamException {
+ indent();
writer.writeProcessingInstruction(target);
}
@Override
public void writeProcessingInstruction(String target, String data)
throws XMLStreamException {
+ indent();
writer.writeProcessingInstruction(target, data);
}
@@ -285,6 +348,7 @@ public void writeCData(String data) throws XMLStreamException {
@Override
public void writeDTD(String dtd) throws XMLStreamException {
+ indent();
writer.writeDTD(dtd);
}
@@ -311,7 +375,26 @@ public void writeStartDocument(String encoding, String version)
@Override
public void writeCharacters(String text) throws XMLStreamException {
- writer.writeCharacters(text);
+ if (indent < 0) {
+ writer.writeCharacters(text);
+ return;
+ }
+
+ // We are indeting...
+ if (StringUtils.isNotBlank(text)) {
+ String[] lines = text.split("\n");
+ if (lines.length == 1) {
+ writer.writeCharacters(lines[0]);
+ indentEnd = false;
+ } else {
+ for (String line : lines) {
+ indent();
+ writer.writeCharacters(line);
+ }
+ }
+ } else {
+ indentEnd = false;
+ }
}
@Override
diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ListErrorHandler.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ListErrorHandler.java
new file mode 100644
index 00000000..4691b854
--- /dev/null
+++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/xml/ListErrorHandler.java
@@ -0,0 +1,90 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.xml;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXParseException;
+
+/**
+ * SAX {@link ErrorHandler} which stores SAX Exceptions that can later
+ * be retrieved as {@link List}. Does not throw exceptions.
+ * @author Pascal Essiembre
+ * @since 1.13.0
+ */
+public class ListErrorHandler implements ErrorHandler {
+
+ private final List errors = new ArrayList<>();
+ private final List warnings = new ArrayList<>();
+ private final List fatalErrors = new ArrayList<>();
+
+ @Override
+ public void error(SAXParseException exception) {
+ errors.add(exception);
+ }
+ @Override
+ public void fatalError(SAXParseException exception) {
+ fatalErrors.add(exception);
+ }
+ @Override
+ public void warning(SAXParseException exception) {
+ warnings.add(exception);
+ }
+
+ public List getErrors() {
+ return errors;
+ }
+ public List getErrorMessages() {
+ return getMessages(errors);
+ }
+ public List getFatalErrors() {
+ return fatalErrors;
+ }
+ public List getFatalErrorMessages() {
+ return getMessages(fatalErrors);
+ }
+ public List getWarnings() {
+ return warnings;
+ }
+ public List getWarningMessages() {
+ return getMessages(warnings);
+ }
+ public List getAll() {
+ List all = new ArrayList<>();
+ all.addAll(fatalErrors);
+ all.addAll(errors);
+ all.addAll(warnings);
+ return all;
+ }
+ public List getAllMessages() {
+ return getMessages(getAll());
+ }
+ public boolean isEmpty() {
+ return errors.isEmpty() && fatalErrors.isEmpty() && warnings.isEmpty();
+ }
+ public int size() {
+ return errors.size() + fatalErrors.size() + warnings.size();
+ }
+
+ private List getMessages(List exceptions) {
+ List msgs = new ArrayList<>();
+ for (SAXParseException e : exceptions) {
+ msgs.add(e.getLocalizedMessage());
+ }
+ return msgs;
+ }
+}
diff --git a/norconex-commons-lang/src/site/markdown/download.md.vm b/norconex-commons-lang/src/site/markdown/download.md.vm
index 53b3190c..b53c2f5c 100644
--- a/norconex-commons-lang/src/site/markdown/download.md.vm
+++ b/norconex-commons-lang/src/site/markdown/download.md.vm
@@ -30,6 +30,10 @@ $h2 Binaries
**Older Releases**
+ * [1.12.3]($nexusPath/1.12.2/norconex-commons-lang-1.12.3.zip)
+ [[Release Notes](changes-report.html#a1.12.3)]
+ * [1.12.2]($nexusPath/1.12.2/norconex-commons-lang-1.12.2.zip)
+ [[Release Notes](changes-report.html#a1.12.2)]
* [1.12.1]($nexusPath/1.12.1/norconex-commons-lang-1.12.1.zip)
[[Release Notes](changes-report.html#a1.12.1)]
* [1.12.0]($nexusPath/1.12.0/norconex-commons-lang-1.12.0.zip)
diff --git a/norconex-commons-lang/src/site/markdown/index.md.vm b/norconex-commons-lang/src/site/markdown/index.md.vm
index bca54efc..506be955 100644
--- a/norconex-commons-lang/src/site/markdown/index.md.vm
+++ b/norconex-commons-lang/src/site/markdown/index.md.vm
@@ -115,6 +115,7 @@ Other classes not fitting anyhere. Includes:
* Long unique ID generator
* Simple text encryption/decryption
* Jar copy version conflict resolution
+ * Simplifies system command execution
Classes: [com.norconex.commons.lang.*](./apidocs/overview-summary.html)
\ No newline at end of file
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/ExternalApp.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/ExternalApp.java
new file mode 100644
index 00000000..3f906ae8
--- /dev/null
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/ExternalApp.java
@@ -0,0 +1,222 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.taskdefs.Java;
+import org.apache.tools.ant.types.Path;
+
+/**
+ * Sample external app that reverse word order in lines.
+ * Also prints specific environment variables to STDOUT or STDERR.
+ * @author Pascal Essiembre
+ */
+public class ExternalApp {
+
+ public static final String TYPE_INFILE_OUTFILE = "infile-outfile";
+ public static final String TYPE_INFILE_STDOUT = "infile-stdout";
+ public static final String TYPE_STDIN_OUTFILE = "stdin-outfile";
+ public static final String TYPE_STDIN_STDOUT = "stdin-stdout";
+
+ public static final String ENV_STDOUT_BEFORE = "stdout_before";
+ public static final String ENV_STDOUT_AFTER = "stdout_after";
+ public static final String ENV_STDERR_BEFORE = "stderr_before";
+ public static final String ENV_STDERR_AFTER = "stderr_after";
+
+ // reverse the word order in each lines
+ public static void main(String[] args) throws IOException {
+ if (args.length < 1) {
+ System.err.println(
+ "Expected arguments: [infile] [outfile]");
+ System.err.println("Where: is one of:");
+ System.err.println(" " + TYPE_INFILE_OUTFILE);
+ System.err.println(" " + TYPE_INFILE_STDOUT);
+ System.err.println(" " + TYPE_STDIN_OUTFILE);
+ System.err.println(" " + TYPE_STDIN_STDOUT);
+ System.exit(-1);
+ }
+
+ String type = args[0];
+ int fileArgIndex = 1;
+ File inFile = null;
+ File outFile = null;
+ if (type.contains("infile")) {
+ inFile = new File(args[fileArgIndex]);
+ fileArgIndex++;
+ }
+ if (type.contains("outfile")) {
+ outFile = new File(args[fileArgIndex]);
+ }
+
+ printEnvToStdout(ENV_STDOUT_BEFORE);
+ printEnvToStderr(ENV_STDERR_BEFORE);
+ OutputStream output = getOutputStream(outFile);
+ try (InputStream input = getInputStream(inFile)) {
+ List lines =
+ IOUtils.readLines(input, StandardCharsets.UTF_8);
+ for (String line : lines) {
+ String[] words = line.split(" ");
+ ArrayUtils.reverse(words);
+ output.write(StringUtils.join(words, " ").getBytes());
+ output.write('\n');
+ output.flush();
+ }
+ }
+
+ printEnvToStdout(ENV_STDOUT_AFTER);
+ printEnvToStderr(ENV_STDERR_AFTER);
+ if (output != System.out) {
+ output.close();
+ }
+ }
+
+
+ private static void printEnvToStdout(String varName) {
+ String var = System.getenv(varName);
+ if (StringUtils.isNotBlank(var)) {
+ System.out.println(var);
+ }
+ }
+ private static void printEnvToStderr(String varName) {
+ String var = System.getenv(varName);
+ if (StringUtils.isNotBlank(var)) {
+ System.err.println(var);
+ }
+ }
+
+ private static InputStream getInputStream(File inFile)
+ throws FileNotFoundException {
+ if (inFile != null) {
+ return new FileInputStream(inFile);
+ }
+ return System.in;
+ }
+ private static OutputStream getOutputStream(File outFile)
+ throws FileNotFoundException {
+ if (outFile != null) {
+ return new FileOutputStream(outFile);
+ }
+ return System.out;
+ }
+
+ public static SystemCommand newSystemCommand(String type, File... files) {
+ return new SystemCommand(newCommandLine(type, files));
+ }
+
+ public static String newCommandLine(String type, File... files) {
+ Project project = new Project();
+ project.init();
+ try {
+ Java javaTask = new Java();
+ javaTask.setTaskName("runjava");
+ javaTask.setProject(project);
+ javaTask.setFork(true);
+ javaTask.setFailonerror(true);
+ javaTask.setClassname(ExternalApp.class.getName());
+ javaTask.setClasspath(
+ new Path(project, SystemUtils.JAVA_CLASS_PATH));
+ String args = type;
+ if (files != null) {
+ for (File file : files) {
+ args += " \"" + file.getAbsolutePath() + "\"";
+ }
+ }
+ javaTask.getCommandLine().createArgument().setLine(args);
+
+ String[] cmdArray = javaTask.getCommandLine().getCommandline();
+ cmdArray = SystemCommand.escape(cmdArray);
+
+ String cmd = StringUtils.join(cmdArray, " ");
+ cmd = fixCommand(cmd);
+ return cmd;
+ } catch (BuildException e) {
+ throw e;
+ }
+ }
+
+ // Fix the command as necessary.
+ // Shorten the command by eliminating items we do not need
+ // from classpath and using shorter command aliases. This is necessary
+ // to prevent keep only necessary to prevent command line length limitation
+ // on windows ("The command line is too long.").
+ private static String fixCommand(String command) {
+ String cmd = command;
+ cmd = cmd.replaceFirst(" -classpath ", " -cp ");
+
+ String cp = cmd.replaceFirst(".*\\s+-cp\\s+(.*)\\s+"
+ + ExternalApp.class.getName() + ".*", "$1");
+ boolean isQuoted = false;
+ if (cp.matches("^\".*\"$")) {
+ isQuoted = true;
+ cp = StringUtils.strip(cp, "\"");
+ }
+ StringBuilder b = new StringBuilder();
+ Matcher m = Pattern.compile(".*?([;:]|$)").matcher(cp);
+ while (m.find()) {
+ String path = m.group();
+ if (keepPath(path)) {
+ b.append(path);
+ }
+ }
+ cp = b.toString();
+ cp = StringUtils.stripEnd(cp, ":;");
+ cp = cp.replace("\\", "\\\\");
+ if (isQuoted) {
+ cp = "\"" + cp + "\"";
+ }
+ cmd = cmd.replaceFirst("(.*\\s+-cp\\s+)(.*)(\\s+"
+ + ExternalApp.class.getName() + ".*)", "$1" + cp + "$3");
+ return cmd;
+ }
+
+ private static final String[] KEEPERS = new String[] {
+ "norconex-importer",
+ "norconex-commons-lang",
+ "junit",
+ "commons-io",
+ "commons-lang3",
+ "log4j",
+ "ant",
+ };
+ private static boolean keepPath(String path) {
+ if (StringUtils.isBlank(path)) {
+ return false;
+ }
+ for (String keeper : KEEPERS) {
+ if (path.contains(keeper)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/RetrierTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/RetrierTest.java
new file mode 100644
index 00000000..93f4c822
--- /dev/null
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/RetrierTest.java
@@ -0,0 +1,71 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RetrierTest {
+
+ @Test
+ public void testRetrierMaxRetryReached() {
+ final MutableInt count = new MutableInt();
+ try {
+ new Retrier(20).setMaxCauses(5).execute(new IRetriable() {
+ @Override
+ public Void execute() throws RetriableException {
+ count.increment();
+ throw new RuntimeException(count.toString());
+ }
+ });
+ } catch (RetriableException e) {
+ // initial run + 20 retries == 21
+ Assert.assertEquals(21, count.intValue());
+ // Only keeps 5 causes
+ Assert.assertEquals(5, e.getAllCauses().length);
+ Assert.assertEquals("17", e.getAllCauses()[0].getMessage());
+ Assert.assertEquals("21", e.getCause().getMessage());
+ }
+ }
+
+ @Test
+ public void testRetrierExceptionFilter() {
+ final MutableInt count = new MutableInt();
+ try {
+ new Retrier(new IExceptionFilter() {
+ @Override
+ public boolean retry(Exception e) {
+ return "retryMe".equals(e.getMessage());
+ }
+ }, 20).execute(new IRetriable() {
+ @Override
+ public Void execute() throws RetriableException {
+ count.increment();
+ if (count.intValue() < 7) {
+ throw new RuntimeException("retryMe");
+ }
+ throw new RuntimeException("failMe");
+ }
+ });
+ } catch (RetriableException e) {
+ // Failed for real after 7 attempts
+ Assert.assertEquals(7, count.intValue());
+ Assert.assertEquals(7, e.getAllCauses().length);
+ Assert.assertEquals("retryMe", e.getAllCauses()[0].getMessage());
+ Assert.assertEquals("failMe", e.getCause().getMessage());
+ }
+ }
+}
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/SystemCommandTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/SystemCommandTest.java
new file mode 100644
index 00000000..52bfc8a4
--- /dev/null
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/exec/SystemCommandTest.java
@@ -0,0 +1,171 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.exec;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.norconex.commons.lang.io.InputStreamLineListener;
+
+public class SystemCommandTest {
+
+ public static final String IN_FILE_PATH = "/exec/sample-input.txt";
+ public static final String EXPECTED_OUT_FILE_PATH =
+ "/exec/expected-output.txt";
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void testInFileOutFile() throws IOException, SystemCommandException {
+ File inFile = inputAsFile();
+ File outFile = newTempFile();
+
+ SystemCommand cmd = ExternalApp.newSystemCommand(
+ ExternalApp.TYPE_INFILE_OUTFILE, inFile, outFile);
+ ExternalAppListener l = addEnvAndListener(cmd);
+ cmd.execute();
+
+ Assert.assertEquals(expectedOutputAsString(), fileAsString(outFile));
+ Assert.assertTrue("Listener missed some output.", l.capturedThemAll());
+
+ }
+
+ @Test
+ public void testInFileStdout() throws IOException, SystemCommandException {
+ File inFile = inputAsFile();
+
+ SystemCommand cmd = ExternalApp.newSystemCommand(
+ ExternalApp.TYPE_INFILE_STDOUT, inFile);
+ ExternalAppListener l = addEnvAndListener(cmd);
+ cmd.execute();
+ Assert.assertEquals(expectedOutputAsString(), l.getStdoutContent());
+ Assert.assertTrue("Listener missed some output.", l.capturedThemAll());
+ }
+
+ @Test
+ public void testStdinOutFile() throws IOException, SystemCommandException {
+ InputStream input = inputAsStream();
+ File outFile = newTempFile();
+
+ SystemCommand cmd = ExternalApp.newSystemCommand(
+ ExternalApp.TYPE_STDIN_OUTFILE, outFile);
+ ExternalAppListener l = addEnvAndListener(cmd);
+ cmd.execute(input);
+ input.close();
+ Assert.assertEquals(expectedOutputAsString(), fileAsString(outFile));
+ Assert.assertTrue("Listener missed some output.", l.capturedThemAll());
+ }
+
+ @Test
+ public void testStdinStdout() throws IOException, SystemCommandException {
+ InputStream input = inputAsStream();
+
+ SystemCommand cmd = ExternalApp.newSystemCommand(
+ ExternalApp.TYPE_STDIN_STDOUT);
+ ExternalAppListener l = addEnvAndListener(cmd);
+ cmd.execute(input);
+ input.close();
+ Assert.assertEquals(expectedOutputAsString(), l.getStdoutContent());
+ Assert.assertTrue("Listener missed some output.", l.capturedThemAll());
+ }
+
+ private File inputAsFile() throws IOException {
+ File inFile = newTempFile();
+ FileUtils.copyInputStreamToFile(
+ getClass().getResourceAsStream(IN_FILE_PATH), inFile);
+ return inFile;
+ }
+ private InputStream inputAsStream() throws IOException {
+ return getClass().getResourceAsStream(IN_FILE_PATH);
+ }
+ private String fileAsString(File file) throws IOException {
+ return FileUtils.readFileToString(file, StandardCharsets.UTF_8).trim();
+ }
+ private String expectedOutputAsString() throws IOException {
+ return IOUtils.toString(getClass().getResourceAsStream(
+ EXPECTED_OUT_FILE_PATH), StandardCharsets.UTF_8);
+ }
+ private File newTempFile() throws IOException {
+ File file = tempFolder.newFile();
+ if (!file.exists()) {
+ // Just making sure it exists
+ FileUtils.touch(file);
+ }
+ return file;
+ }
+
+ private ExternalAppListener addEnvAndListener(SystemCommand cmd) {
+ Map envs = new HashMap<>();
+ envs.put(ExternalApp.ENV_STDOUT_BEFORE, ExternalApp.ENV_STDOUT_BEFORE);
+ envs.put(ExternalApp.ENV_STDOUT_AFTER, ExternalApp.ENV_STDOUT_AFTER);
+ envs.put(ExternalApp.ENV_STDERR_BEFORE, ExternalApp.ENV_STDERR_BEFORE);
+ envs.put(ExternalApp.ENV_STDERR_AFTER, ExternalApp.ENV_STDERR_AFTER);
+ cmd.setEnvironmentVariables(envs);
+
+ ExternalAppListener l = new ExternalAppListener();
+ cmd.addErrorListener(l);
+ cmd.addOutputListener(l);
+ return l;
+ }
+
+ class ExternalAppListener extends InputStreamLineListener {
+ private boolean stdoutBefore = false;
+ private boolean stdoutAfter = false;
+ private boolean stderrBefore = false;
+ private boolean stderrAfter = false;
+ private StringBuilder b = new StringBuilder();
+
+ @Override
+ public void lineStreamed(String type, String line) {
+ if ("STDOUT".equals(type)) {
+ if (ExternalApp.ENV_STDOUT_BEFORE.equals(line)) {
+ stdoutBefore = true;
+ } else if (ExternalApp.ENV_STDOUT_AFTER.equals(line)) {
+ stdoutAfter = true;
+ } else {
+ if (b.length() > 0) {
+ b.append('\n');
+ }
+ b.append(line);
+ }
+ } else if ("STDERR".equals(type)) {
+ if (ExternalApp.ENV_STDERR_BEFORE.equals(line)) {
+ stderrBefore = true;
+ } else if (ExternalApp.ENV_STDERR_AFTER.equals(line)) {
+ stderrAfter = true;
+ }
+ }
+ }
+ public boolean capturedThemAll() {
+ return stdoutBefore && stdoutAfter && stderrBefore && stderrAfter;
+ }
+ public String getStdoutContent() {
+ return b.toString();
+ }
+ }
+
+}
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/ByteArrayOutputStreamTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/ByteArrayOutputStreamTest.java
index da32ecf1..1fca1990 100644
--- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/ByteArrayOutputStreamTest.java
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/ByteArrayOutputStreamTest.java
@@ -1,4 +1,4 @@
-/* Copyright 2015 Norconex Inc.
+/* Copyright 2015-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,12 +17,7 @@
import java.io.IOException;
import org.apache.commons.lang3.CharEncoding;
-import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.Level;
-import org.apache.log4j.Logger;
-import org.apache.log4j.PatternLayout;
import org.junit.Assert;
-import org.junit.Before;
import org.junit.Test;
/**
@@ -30,15 +25,15 @@
*/
public class ByteArrayOutputStreamTest {
- @Before
- public void before() {
- Logger logger = Logger.getRootLogger();
- logger.setLevel(Level.DEBUG);
- logger.setAdditivity(false);
- logger.addAppender(new ConsoleAppender(
- new PatternLayout("%-5p [%C{1}] %m%n"),
- ConsoleAppender.SYSTEM_OUT));
- }
+// @Before
+// public void before() {
+// Logger logger = Logger.getRootLogger();
+// logger.setLevel(Level.DEBUG);
+// logger.setAdditivity(false);
+// logger.addAppender(new ConsoleAppender(
+// new PatternLayout("%-5p [%C{1}] %m%n"),
+// ConsoleAppender.SYSTEM_OUT));
+// }
@Test
public void testByteArrayOutputStream() throws IOException {
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/CachedInputStreamTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/CachedInputStreamTest.java
index e76a96b4..ed41ac56 100644
--- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/CachedInputStreamTest.java
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/CachedInputStreamTest.java
@@ -1,4 +1,4 @@
-/* Copyright 2014-2015 Norconex Inc.
+/* Copyright 2014-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,12 +21,7 @@
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.NullInputStream;
import org.apache.commons.lang3.CharEncoding;
-import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.Level;
-import org.apache.log4j.Logger;
-import org.apache.log4j.PatternLayout;
import org.junit.Assert;
-import org.junit.Before;
import org.junit.Test;
/**
@@ -34,15 +29,15 @@
*/
public class CachedInputStreamTest {
- @Before
- public void before() {
- Logger logger = Logger.getRootLogger();
- logger.setLevel(Level.DEBUG);
- logger.setAdditivity(false);
- logger.addAppender(new ConsoleAppender(
- new PatternLayout("%-5p [%C{1}] %m%n"),
- ConsoleAppender.SYSTEM_OUT));
- }
+// @Before
+// public void before() {
+// Logger logger = Logger.getRootLogger();
+// logger.setLevel(Level.DEBUG);
+// logger.setAdditivity(false);
+// logger.addAppender(new ConsoleAppender(
+// new PatternLayout("%-5p [%C{1}] %m%n"),
+// ConsoleAppender.SYSTEM_OUT));
+// }
@Test
public void testContentMatchMemCache() throws IOException {
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/TextReaderTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/TextReaderTest.java
index 9a721a32..cb06d4ee 100644
--- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/TextReaderTest.java
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/io/TextReaderTest.java
@@ -32,11 +32,12 @@ public class TextReaderTest {
public void testSentenceBreaks() throws IOException {
TextReader reader = getTextReader("funkyParagraphBreaks.txt", 60);
+ @SuppressWarnings("unused")
String text = null;
int count = 0;
while ((text = reader.readText()) != null) {
count++;
- System.out.println("CHUNK #" + count + " = " + text);
+// System.out.println("CHUNK #" + count + " = " + text);
}
reader.close();
Assert.assertEquals("Wrong number of sentences", 10, count);
@@ -48,11 +49,12 @@ public void testSentenceBreaks() throws IOException {
public void testParagraphBreaks() throws IOException {
TextReader reader = getTextReader("funkyParagraphBreaks.txt", 100);
+ @SuppressWarnings("unused")
String text = null;
int count = 0;
while ((text = reader.readText()) != null) {
count++;
- System.out.println("CHUNK #" + count + " = " + text);
+// System.out.println("CHUNK #" + count + " = " + text);
}
reader.close();
Assert.assertEquals("Wrong number of text chunks returned", 5, count);
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/map/PropertiesTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/map/PropertiesTest.java
index 4ea91e46..fefe140b 100644
--- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/map/PropertiesTest.java
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/map/PropertiesTest.java
@@ -117,4 +117,28 @@ public void testPutAll() throws Exception {
assertEquals(Arrays.asList("1", "2"), props2.get("KEY"));
}
+
+ @Test
+ public void testPut() throws Exception {
+ List list = Arrays.asList("1", null, "2", "");
+
+ // Case insensitive
+ Properties props1 = new Properties(true);
+ props1.put("key", list);
+
+ assertEquals(1, props1.keySet().size());
+ assertEquals(4, props1.get("kEy").size());
+ assertEquals(Arrays.asList("1", "", "2", ""), props1.get("kEy"));
+
+
+ // Case sensitive
+ Properties props2 = new Properties(false);
+ props2.put("key", list);
+
+ assertEquals(1, props2.keySet().size());
+ assertEquals(null, props2.get("kEy"));
+ assertEquals(Arrays.asList("1", "", "2", ""), props2.get("key"));
+
+ }
+
}
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/time/DurationParserTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/time/DurationParserTest.java
new file mode 100644
index 00000000..472f4494
--- /dev/null
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/time/DurationParserTest.java
@@ -0,0 +1,39 @@
+/* Copyright 2017 Norconex Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.norconex.commons.lang.time;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DurationParserTest {
+
+ private static final long SECOND = 1000;
+ private static final long MINUTE = 60 * SECOND;
+ private static final long HOUR = 60 * MINUTE;
+ private static final long DAY = 24 * HOUR;
+ private static final long TEST_DURATION =
+ 54 * DAY + 18 * HOUR + 1 * MINUTE + 23 * SECOND;
+
+ @Test
+ public void testDurationParser() {
+ Assert.assertEquals(TEST_DURATION, DurationParser.parse("54d18h1m23s"));
+ Assert.assertEquals(TEST_DURATION, DurationParser.parse(
+ "54 days, 18 hours, 1 minute, and 23 seconds"));
+ Assert.assertEquals(TEST_DURATION, DurationParser.parse(
+ "54 days, 18 hours, 1 minute, and 23 seconds"));
+ Assert.assertEquals(TEST_DURATION, DurationParser.parse(
+ "54days,18 hrs1min23 s"));
+ }
+}
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java
index ac18c1cc..a9c0e852 100644
--- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java
@@ -1,4 +1,4 @@
-/* Copyright 2015-2016 Norconex Inc.
+/* Copyright 2015-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,7 @@
import static org.junit.Assert.assertEquals;
-import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.Level;
-import org.apache.log4j.Logger;
-import org.apache.log4j.PatternLayout;
import org.junit.After;
-import org.junit.Before;
import org.junit.Test;
public class HttpURLTest {
@@ -32,15 +27,15 @@ public class HttpURLTest {
private String t;
- @Before
- public void before() {
- Logger logger = Logger.getRootLogger();
- logger.setLevel(Level.DEBUG);
- logger.setAdditivity(false);
- logger.addAppender(new ConsoleAppender(
- new PatternLayout("%-5p [%C{1}] %m%n"),
- ConsoleAppender.SYSTEM_OUT));
- }
+// @Before
+// public void before() {
+// Logger logger = Logger.getRootLogger();
+// logger.setLevel(Level.DEBUG);
+// logger.setAdditivity(false);
+// logger.addAppender(new ConsoleAppender(
+// new PatternLayout("%-5p [%C{1}] %m%n"),
+// ConsoleAppender.SYSTEM_OUT));
+// }
@After
public void tearDown() throws Exception {
@@ -144,4 +139,11 @@ public void testNonHttpProtocolWithPort() {
t = "ftp://ftp.example.com:20/dir";
assertEquals(t, new HttpURL(s).toString());
}
+
+ @Test
+ public void testInvalidURL() {
+ s = "http://www.example.com/\"path\"";
+ t = "http://www.example.com/%22path%22";
+ assertEquals(t, new HttpURL(s).toString());
+ }
}
\ No newline at end of file
diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java
index 1028d4a5..27d9f0f2 100644
--- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java
+++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java
@@ -1,4 +1,4 @@
-/* Copyright 2010-2015 Norconex Inc.
+/* Copyright 2010-2017 Norconex Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,13 +20,8 @@
import java.util.Map;
import java.util.Map.Entry;
-import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.Level;
-import org.apache.log4j.Logger;
-import org.apache.log4j.PatternLayout;
import org.junit.After;
import org.junit.Assert;
-import org.junit.Before;
import org.junit.Test;
public class URLNormalizerTest {
@@ -34,17 +29,6 @@ public class URLNormalizerTest {
private String s;
private String t;
-
- @Before
- public void before() {
- Logger logger = Logger.getRootLogger();
- logger.setLevel(Level.DEBUG);
- logger.setAdditivity(false);
- logger.addAppender(new ConsoleAppender(
- new PatternLayout("%-5p [%C{1}] %m%n"),
- ConsoleAppender.SYSTEM_OUT));
- }
-
@After
public void tearDown() throws Exception {
s = null;
@@ -250,6 +234,19 @@ public void testRemoveTrailingSlash() {
t = "http://www.example.com";
assertEquals(t, n(s).removeTrailingSlash().toString());
}
+
+ @Test
+ public void testRemoveTrailingHash() {
+ s = "http://www.example.com/blah#";
+ t = "http://www.example.com/blah";
+ assertEquals(t, n(s).removeTrailingHash().toString());
+ s = "http://www.example.com/blah#whatever";
+ t = "http://www.example.com/blah#whatever";
+ assertEquals(t, n(s).removeTrailingHash().toString());
+ s = "http://www.example.com";
+ t = "http://www.example.com";
+ assertEquals(t, n(s).removeTrailingHash().toString());
+ }
@Test
public void testRemoveDotSegments() {
diff --git a/norconex-commons-lang/src/test/resources/exec/expected-output.txt b/norconex-commons-lang/src/test/resources/exec/expected-output.txt
new file mode 100644
index 00000000..90cc9cdd
--- /dev/null
+++ b/norconex-commons-lang/src/test/resources/exec/expected-output.txt
@@ -0,0 +1,4 @@
+line. first the is This
+line. second the is Here
+
+third. skipping after line, fourth The
\ No newline at end of file
diff --git a/norconex-commons-lang/src/test/resources/exec/sample-input.txt b/norconex-commons-lang/src/test/resources/exec/sample-input.txt
new file mode 100644
index 00000000..03bb07e8
--- /dev/null
+++ b/norconex-commons-lang/src/test/resources/exec/sample-input.txt
@@ -0,0 +1,4 @@
+This is the first line.
+Here is the second line.
+
+The fourth line, after skipping third.
\ No newline at end of file
diff --git a/norconex-commons-lang/src/test/resources/log4j.properties b/norconex-commons-lang/src/test/resources/log4j.properties
new file mode 100644
index 00000000..647a6ee4
--- /dev/null
+++ b/norconex-commons-lang/src/test/resources/log4j.properties
@@ -0,0 +1,10 @@
+log4j.debug=false
+
+log4j.rootLogger=INFO,Stdout
+
+log4j.logger.com.norconex.commons.lang=INFO
+log4j.logger.org.apache=WARN
+
+log4j.appender.Stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n