diff --git a/norconex-commons-lang/TODO.txt b/norconex-commons-lang/TODO.txt new file mode 100644 index 00000000..849a4e3e --- /dev/null +++ b/norconex-commons-lang/TODO.txt @@ -0,0 +1,21 @@ +TODO: +============== + +- Upgrade to Commons Configuration 2.x + +- Upgrade to Apache Velocity 2.x when available in Maven. + +- Modify Jar copier to handle cases where snapshot are timestamped instead and + not being considered the latest when they should. Like: + norconex-commons-lang-1.13.0-20170328.184247-17.jar vs + norconex-commons-lang-1.13.0-SNAPSHOT.jar + https://github.com/Norconex/collector-http/issues/331#issuecomment-290196986 + +- Consider splitting Properties by \u001e by default (record separator). + +- Redo DurationUtil to be more flexible (using fluid builder approach. + or check Apache or Java 8 equivalent classes if they can be made as flexible. + Look at: + https://commons.apache.org/proper/commons-lang/javadocs/api-3.5/index.html?org/apache/commons/lang3/time/DurationFormatUtils.html + http://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatterBuilder.html + http://joda-time.sourceforge.net/apidocs/org/joda/time/format/PeriodFormatterBuilder.html diff --git a/norconex-commons-lang/pom.xml b/norconex-commons-lang/pom.xml index 66de7497..993df53b 100644 --- a/norconex-commons-lang/pom.xml +++ b/norconex-commons-lang/pom.xml @@ -1,5 +1,5 @@ + + resources + + zip + + false + + + ${basedir}/src/main/assembly/ + + install.* + scripts/** + + / + + + \ No newline at end of file diff --git a/norconex-commons-lang/src/main/assembly/scripts/copy-jars.bat b/norconex-commons-lang/src/main/assembly/scripts/copy-jars.bat new file mode 100644 index 00000000..6eb35a3a --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/copy-jars.bat @@ -0,0 +1,34 @@ +@echo off +REM Copyright 2017 Norconex Inc. +REM +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +cd %~dp0 + +if [%2]==[] goto usage + +java -Dfile.encoding=UTF8 -cp "./lib/*;../lib/*" com.norconex.commons.lang.jar.JarCopier %* + +goto :eof + +:usage +echo. +echo Usage: %~nx0 source target +echo. +echo Safe copy of one or several jar files, resolving conflicts. +echo. +echo Arguments: +echo source jar file or directory containing jar files to copy +echo target directory where to copy the jars to +echo. + +exit /B 1 diff --git a/norconex-commons-lang/src/main/assembly/scripts/copy-jars.sh b/norconex-commons-lang/src/main/assembly/scripts/copy-jars.sh new file mode 100644 index 00000000..c102ba7b --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/copy-jars.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# 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. +cd $(dirname $0) + +if [ "$#" -ne 2 ]; then + echo "" + echo "Usage: $0 source target" + echo "" + echo "Safe copy of one or several jar files, resolving conflicts." + echo "" + echo "Arguments:" + echo " source jar file or directory containing jar files to copy" + echo " target directory where to copy the jars to" + exit 1 +fi + +java -Dfile.encoding=UTF8 -cp "./lib/*:../lib/*" com.norconex.commons.lang.jar.JarCopier "$@" + diff --git a/norconex-commons-lang/src/main/assembly/scripts/decrypt.bat b/norconex-commons-lang/src/main/assembly/scripts/decrypt.bat new file mode 100644 index 00000000..ab9dfb37 --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/decrypt.bat @@ -0,0 +1,43 @@ +@echo off +REM Copyright 2017 Norconex Inc. +REM +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +cd %~dp0 + +if [%3]==[] goto usage +if [%1] NEQ [-k] if [%1] NEQ [-f] if [%1] NEQ [-e] if [%1] NEQ [-p] goto invalidArg + +java -Dfile.encoding=UTF8 -cp "./lib/*;../lib/*" com.norconex.commons.lang.encrypt.EncryptionUtil decrypt %* + +goto :eof + +:invalidArg +echo Invalid argument: %1 +goto usage + +:usage +echo. +echo Usage: %~nx0 -k^|-f^|-e^|-p key text +echo. +echo Decrypt a string using a custom key. +echo. +echo Arguments: +echo -k key is the encryption key +echo -f key is the file containing the encryption key +echo -e key is the environment variable holding the encryption key +echo -p key is the system property holding the encryption key +echo key the encryption key (or file, or env. variable, etc.) +echo text text to decrypt +echo. + +exit /B 1 diff --git a/norconex-commons-lang/src/main/assembly/scripts/decrypt.sh b/norconex-commons-lang/src/main/assembly/scripts/decrypt.sh new file mode 100644 index 00000000..cf723314 --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/decrypt.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# 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. +cd $(dirname $0) + +argerror=false +if [ "$#" -eq 3 ] && [ "$1" != "-k" ] && [ "$1" != "-f" ] && [ "$1" != "-e" ] && [ "$1" != "-p" ]; then + echo "Invalid argument: $1" + argerror=true +fi +if [ "$#" -ne 3 ]; then + argerror=true +fi +if [ "$argerror" = true ]; then + echo "" + echo "Usage: $0 -k|-f|-e|-p key text" + echo "" + echo "Decrypt a string using a custom key." + echo "" + echo "Arguments:" + echo " -k key is the encryption key" + echo " -f key is the file containing the encryption key" + echo " -e key is the environment variable holding the encryption key" + echo " -p key is the system property holding the encryption key" + echo " key the encryption key (or file, or env. variable, etc.)" + echo " text text to decrypt" + exit 1 +fi + +java -Dfile.encoding=UTF8 -cp "./lib/*:../lib/*" com.norconex.commons.lang.encrypt.EncryptionUtil decrypt "$@" + diff --git a/norconex-commons-lang/src/main/assembly/scripts/encrypt.bat b/norconex-commons-lang/src/main/assembly/scripts/encrypt.bat new file mode 100644 index 00000000..773db453 --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/encrypt.bat @@ -0,0 +1,44 @@ +@echo off +REM Copyright 2017 Norconex Inc. +REM +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +@echo off +cd %~dp0 + +if [%3]==[] goto usage +if [%1] NEQ [-k] if [%1] NEQ [-f] if [%1] NEQ [-e] if [%1] NEQ [-p] goto invalidArg + +java -Dfile.encoding=UTF8 -cp "./lib/*;../lib/*" com.norconex.commons.lang.encrypt.EncryptionUtil encrypt %* + +goto :eof + +:invalidArg +echo Invalid argument: %1 +goto usage + +:usage +echo. +echo Usage: %~nx0 -k^|-f^|-e^|-p key text +echo. +echo Encrypt a string using a custom key. +echo. +echo Arguments: +echo -k key is the encryption key +echo -f key is the file containing the encryption key +echo -e key is the environment variable holding the encryption key +echo -p key is the system property holding the encryption key +echo key the encryption key (or file, or env. variable, etc.) +echo text text to encrypt +echo. + +exit /B 1 diff --git a/norconex-commons-lang/src/main/assembly/scripts/encrypt.sh b/norconex-commons-lang/src/main/assembly/scripts/encrypt.sh new file mode 100644 index 00000000..0252134e --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/encrypt.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# 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. +cd $(dirname $0) + +argerror=false +if [ "$#" -eq 3 ] && [ "$1" != "-k" ] && [ "$1" != "-f" ] && [ "$1" != "-e" ] && [ "$1" != "-p" ]; then + echo "Invalid argument: $1" + argerror=true +fi +if [ "$#" -ne 3 ]; then + argerror=true +fi +if [ "$argerror" = true ]; then + echo "" + echo "Usage: $0 -k|-f|-e|-p key text" + echo "" + echo "Encrypt a string using a custom key." + echo "" + echo "Arguments:" + echo " -k key is the encryption key" + echo " -f key is the file containing the encryption key" + echo " -e key is the environment variable holding the encryption key" + echo " -p key is the system property holding the encryption key" + echo " key the encryption key (or file, or env. variable, etc.)" + echo " text text to encrypt" + exit 1 +fi + +java -Dfile.encoding=UTF8 -cp "./lib/*:../lib/*" com.norconex.commons.lang.encrypt.EncryptionUtil encrypt "$@" + diff --git a/norconex-commons-lang/src/main/assembly/scripts/find-dup-jars.bat b/norconex-commons-lang/src/main/assembly/scripts/find-dup-jars.bat new file mode 100644 index 00000000..abd0879d --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/find-dup-jars.bat @@ -0,0 +1,35 @@ +@echo off +REM Copyright 2017 Norconex Inc. +REM +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +cd %~dp0 + +if [%1]==[] goto usage + +java -Dfile.encoding=UTF8 -cp "./lib/*;../lib/*" com.norconex.commons.lang.jar.JarDuplicateFinder %* + +goto :eof + +:usage +echo. +echo Usage: %~nx0 path [path] [path] [...] +echo. +echo Detect duplicate jars by comparing specified locations. +echo. +echo Arguments: +echo path one or more directories or jar files to check for duplicates +echo. + +exit /B 1 + + diff --git a/norconex-commons-lang/src/main/assembly/scripts/find-dup-jars.sh b/norconex-commons-lang/src/main/assembly/scripts/find-dup-jars.sh new file mode 100644 index 00000000..bde8b63d --- /dev/null +++ b/norconex-commons-lang/src/main/assembly/scripts/find-dup-jars.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# 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. +cd $(dirname $0) + +if [ "$#" -eq 0 ]; then + echo "" + echo "Usage: $0 path [path] [path] [...]" + echo "" + echo "Detect duplicate jars by comparing specified locations." + echo "" + echo "Arguments:" + echo " path one or more directories or jar files to check for duplicates" + exit 1 +fi + +java -Dfile.encoding=UTF8 -cp "./lib/*:../lib/*" com.norconex.commons.lang.jar.JarDuplicateFinder "$@" diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java index 19e48e11..c779ea8b 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java @@ -103,18 +103,6 @@ public static List findSubTypes( } return classes; } - /** - * @deprecated since 1.4.0. Replaced with - * {@link #findSubTypes(List, Class)}. - * @param files directories and/or JARs to scan for classes - * @param superClass the class from which to find subtypes - * @return list of class names - */ - @Deprecated - public static List findImplementors( - List files, Class superClass) { - return findSubTypes(files, superClass); - } /** * Finds the names of all subtypes of the super class for the @@ -148,19 +136,6 @@ public static List findSubTypes( LOG.warn("File not a JAR and not a directory."); return new ArrayList<>(); } - /** - * @deprecated since 1.4.0. Replaced with - * {@link #findSubTypes(File, Class)}. - * @param file directory or JAR to scan for classes - * @param superClass the class from which to find subtypes - * @return list of class names - */ - @Deprecated - public static List findImplementors( - File file, Class superClass) { - return findSubTypes(file, superClass); - } - private static List 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