/* * -----------------------------------------------------------------------\ * PerfCake *   * Copyright (C) 2010 - 2016 the original author or authors. *   * 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 org.perfcake.util; import org.perfcake.PerfCakeConst; import org.perfcake.PerfCakeException; import org.perfcake.common.TimestampedRecord; import org.perfcake.debug.PerfCakeDebug; import org.perfcake.util.properties.PropertyGetter; import org.perfcake.util.properties.SystemPropertyGetter; import org.apache.commons.io.IOUtils; import org.apache.commons.math3.stat.regression.SimpleRegression; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Holds useful utility methods used throughout PerfCake. * * @author <a href="mailto:pavel.macik@gmail.com">Pavel Macík</a> * @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a> */ public class Utils { /** * Default name of resource directory. */ public static final File DEFAULT_RESOURCES_DIR = new File("resources"); /** * Default name of plugin directory. */ public static final File DEFAULT_PLUGINS_DIR = new File("lib/plugins"); /** * There should be no instance of a utility class. */ private Utils() { } /** * Replaces all ${<property.name>} placeholders in a string * by respective value of the property named <property.name> using {@link SystemPropertyGetter}. * * @param text * The original string. * @return Filtered string. */ public static String filterProperties(final String text) { final String propertyPattern = "[^\\\\](\\$\\{([^\\$\\{:]+)(:[^\\$\\{:]*)?})"; final String newText = "_" + text; final Matcher matcher = Pattern.compile(propertyPattern).matcher(newText); return filterProperties(newText, matcher, SystemPropertyGetter.INSTANCE).substring(1); } /** * Filters properties in the given string. Consider using {@link org.perfcake.util.StringTemplate} instead. * * @param text * The string to be filtered. * @param matcher * The matcher to find the properties, any user specified matcher can be provided. * @param pg * The {@link org.perfcake.util.properties.PropertyGetter} to provide values of the properties. * @return The filtered text. */ public static String filterProperties(final String text, final Matcher matcher, final PropertyGetter pg) { String filteredString = text; matcher.reset(); while (matcher.find()) { String pValue; final String pName = matcher.group(2); String defaultValue = null; if (matcher.groupCount() == 3 && matcher.group(3) != null) { defaultValue = (matcher.group(3)).substring(1); } pValue = pg.getProperty(pName, defaultValue); if (pValue != null) { filteredString = filteredString.replaceAll(Pattern.quote(matcher.group(1)), Matcher.quoteReplacement(pValue)); } } return filteredString; } /** * Returns a property value. First it looks at system properties using {@link System#getProperty(String)} if the system property does not exist * it looks at environment variables using {@link System#getenv(String)}. If * the variable does not exist the method returns a <code>null</code>. * * @param name * Property name. * @return Property value or <code>null</code>. */ public static String getProperty(final String name) { return getProperty(name, null); } /** * Returns a property value. First it looks at system properties using {@link System#getProperty(String)} if the system property does not exist * it looks at environment variables using {@link System#getenv(String)}. If * the variable does not exist the method returns <code>defautValue</code>. * * @param name * Property name. * @param defaultValue * Default property value. * @return Property value or <code>defaultValue</code>. */ public static String getProperty(final String name, final String defaultValue) { return SystemPropertyGetter.INSTANCE.getProperty(name, defaultValue); } /** * Writes the whole properties map to the given logger at the given level. * * @param logger * The logger to log the properties. * @param level * The level at which to log the properties. * @param properties * The properties to log. */ public static void logProperties(final Logger logger, final Level level, final Properties properties) { logProperties(logger, level, properties, ""); } /** * Writes the whole properties map to the given logger at the given level with the given prefix. * * @param logger * The logger to log the properties. * @param level * The level at which to log the properties. * @param properties * The properties to log. * @param prefix * The prefix to prepend to each log message (used for aligning with spaces, tabs, etc.). */ public static void logProperties(final Logger logger, final Level level, final Properties properties, final String prefix) { if (logger.isEnabled(level)) { for (final Entry<Object, Object> property : properties.entrySet()) { logger.log(level, prefix + property.getKey() + "=" + property.getValue()); } } } /** * Reads URL (file) content into a string. The file content is processed as an UTF-8 encoded text. * * @param url * The file location as an URL. * @return The file contents. * @throws IOException * When it was not possible to read the content. */ public static String readFilteredContent(final URL url) throws IOException { try { return filterProperties(new String(Files.readAllBytes(Paths.get(url.toURI())), getDefaultEncoding())); } catch (URISyntaxException e) { throw new IOException("Invalid URL: " + url, e); } } /** * Reads the file location into a string while filtering properties. The file content is processed as an UTF-8 encoded text. * * @param fileLocation * The file location. * @return The filtered file content. * @throws IOException * When it was not possible to read the content. */ public static String readFilteredContent(final String fileLocation) throws IOException { if (Files.exists(Paths.get(fileLocation))) { return filterProperties(new String(Files.readAllBytes(Paths.get(fileLocation)), getDefaultEncoding())); } else { return readFilteredContent(new URL(fileLocation.matches("^[a-zA-Z0-9-]*://.*") ? fileLocation : "file://" + fileLocation)); } } /** * Reads lines from the given URL as a list of strings. * * @param url * The URL to read the content from. * @return A list of lines in the content in the original order. * @throws IOException * When it was not possible to read the content of the given URL. */ public static List<String> readLines(final URL url) throws IOException { List<String> results = new ArrayList<>(); try (InputStream is = url.openStream(); InputStreamReader isr = new InputStreamReader(is, getDefaultEncoding()); BufferedReader br = new BufferedReader(isr)) { String line; while ((line = br.readLine()) != null) { results.add(line); } } return results; } /** * Reads the lines from the given location. First it tries to read it as a file and then bypassing to {@link #readLines(URL)}. * * @param fileLocation * The file name to read content from. * @return A list of lines in the file in the original order. * @throws IOException * When it was not possible to read the content of the given file. */ public static List<String> readLines(final String fileLocation) throws IOException { if (Files.exists(Paths.get(fileLocation))) { return Files.lines(Paths.get(fileLocation)).collect(Collectors.toList()); } else { return readLines(new URL(fileLocation)); } } /** * Reads the lines from the given file location. The lines are filtered for properties. * * @param fileLocation * The file name to read content from. * @return A list of lines in the file in the original order with filtered content. * @throws IOException * When it was not possible to read the content of the given file. */ public static List<String> readFilteredLines(final String fileLocation) throws IOException { return readLines(fileLocation).stream().map(Utils::filterProperties).collect(Collectors.toList()); } /** * Converts location to URL. If location specifies a protocol, it is immediately converted. Without a protocol specified, output is * file://${<defaultLocationProperty>}/<location><defaultSuffix> using defaultLocation as a default value for the defaultLocationProperty * when the property is undefined. * * @param location * The location of the resource. * @param defaultLocationProperty * The property to read the default location prefix. * @param defaultLocation * The default value for defaultLocationProperty if this property is undefined. * @param defaultSuffix * The default suffix of the location. * @return The URL representing the location. * @throws MalformedURLException * When the location cannot be converted to a URL. */ public static URL locationToUrl(final String location, final String defaultLocationProperty, final String defaultLocation, final String defaultSuffix) throws MalformedURLException { String uri; // if we are looking for a file and there is no path specified, remove the prefix for later automatic directory insertion if (location.startsWith("file://") && !location.substring(7).contains(File.separator)) { uri = location.substring(7); } else { uri = location; } // if there is no protocol specified, try some file locations if (!uri.contains("://")) { final Path p = Paths.get(Utils.getProperty(defaultLocationProperty, defaultLocation), uri + defaultSuffix); uri = p.toUri().toString(); } return new URL(uri); } /** * Converts location to URL with check for the location existence. If location specifies a protocol, it is immediately converted. Without a protocol specified, the following paths * are checked for the existence: * 1. file://location * 2. file://$defaultLocationProperty/location or file://defaultLocation/location (when the property is not set) * 3. file://$defaultLocationProperty/location.suffix or file://defaultLocation/location.suffix (when the property is not set) with all the provided suffixes * If the file was not found, the result is simply file://location * * @param location * The location of the resource. * @param defaultLocationProperty * The property to read the default location prefix. * @param defaultLocation * The default value for defaultLocationProperty if this property is undefined. * @param defaultSuffix * The array of default default suffixes to try when searching for the resource. * @return The URL representing the location. * @throws MalformedURLException * When the location cannot be converted to an URL. */ public static URL locationToUrlWithCheck(final String location, final String defaultLocationProperty, final String defaultLocation, final String... defaultSuffix) throws MalformedURLException { String uri; // if we are looking for a file and there is no path specified, remove the prefix for later automatic directory insertion if (location.startsWith("file://")) { uri = location.substring(7); } else { uri = location; } // if there is no protocol specified, try some file locations if (!uri.contains("://")) { boolean found = false; Path p = Paths.get(uri); if (!Files.exists(p) || Files.isDirectory(p)) { p = Paths.get(Utils.getProperty(defaultLocationProperty, defaultLocation), uri); if (!Files.exists(p) || Files.isDirectory(p)) { if (defaultSuffix != null && defaultSuffix.length > 0) { // boolean found = false; for (final String suffix : defaultSuffix) { p = Paths.get(Utils.getProperty(defaultLocationProperty, defaultLocation), uri + suffix); if (Files.exists(p) && !Files.isDirectory(p)) { found = true; break; } } } } else { found = true; } } else { found = true; } if (found) { uri = p.toUri().toString(); } else { uri = "file://" + uri; } } return new URL(uri); } /** * Determines the default location of resources based on the resourcesDir constant. * * @param locationSuffix * The optional suffix to be added to the path. * @return The location based on the resourcesDir constant. */ public static String determineDefaultLocation(final String locationSuffix) { return DEFAULT_RESOURCES_DIR.getAbsolutePath() + "/" + (locationSuffix == null ? "" : locationSuffix); } /** * Converts camelCaseStringsWithACRONYMS to CAMEL_CASE_STRINGS_WITH_ACRONYMS * * @param camelCase * The camelCase string. * @return The same string in equivalent format for Java enum values. */ public static String camelCaseToEnum(final String camelCase) { final String regex = "([a-z])([A-Z])"; final String replacement = "$1_$2"; return camelCase.replaceAll(regex, replacement).toUpperCase(); } /** * Converts time in milliseconds to H:MM:SS format, where H is unbound. * * @param time * The timestamp in milliseconds. * @return The string representing the timestamp in H:MM:SS format. */ public static String timeToHms(final long time) { final long hours = TimeUnit.MILLISECONDS.toHours(time); final long minutes = TimeUnit.MILLISECONDS.toMinutes(time - TimeUnit.HOURS.toMillis(hours)); final long seconds = TimeUnit.MILLISECONDS.toSeconds(time - TimeUnit.HOURS.toMillis(hours) - TimeUnit.MINUTES.toMillis(minutes)); final StringBuilder sb = new StringBuilder(); sb.append(hours).append(":").append(String.format("%02d", minutes)).append(":").append(String.format("%02d", seconds)); return sb.toString(); } /** * Gets the default encoding. Uses {@link PerfCakeConst#DEFAULT_ENCODING_PROPERTY} system property, if this property is not set, <b>UTF-8</b> is used. * * @return The string representation of default encoding for all read and written files */ public static String getDefaultEncoding() { return Utils.getProperty(PerfCakeConst.DEFAULT_ENCODING_PROPERTY, "UTF-8"); } /** * Computes a linear regression trend of the data set provided. * * @param data * Data on which to compute the trend. * @return The linear regression trend. */ public static double computeRegressionTrend(final Collection<TimestampedRecord<Number>> data) { final SimpleRegression simpleRegression = new SimpleRegression(); final Iterator<TimestampedRecord<Number>> iterator = data.iterator(); TimestampedRecord<Number> currentRecord; while (iterator.hasNext()) { currentRecord = iterator.next(); simpleRegression.addData(currentRecord.getTimestamp(), currentRecord.getValue().doubleValue()); } return simpleRegression.getSlope(); } /** * Sets the property value to the first not-null value from the list. * * @param props * The properties instance. * @param propName * The name of the property to be set. * @param values * The list of possibilities, the first not-null is used to set the property value. */ public static void setFirstNotNullProperty(final Properties props, final String propName, final String... values) { final String notNull = getFirstNotNull(values); if (notNull != null) { props.setProperty(propName, notNull); } } /** * Returns the first not-null string in the provided list. * * @param values * The list of possible values. * @return The first non-null value in the list. */ public static String getFirstNotNull(final String... values) { for (final String value : values) { if (value != null) { return value; } } return null; } /** * Obtains the needed resource with full-path as URL. Works safely on all platforms. * * @param resource * The name of the resource to obtain. * @return The fully qualified resource URL location. * @throws PerfCakeException * In the case of wrong resource name. */ public static URL getResourceAsUrl(final String resource) throws PerfCakeException { try { return Utils.class.getResource(resource).toURI().toURL(); } catch (URISyntaxException | MalformedURLException e) { throw new PerfCakeException(String.format("Cannot obtain resource %s:", resource), e); } } /** * Obtains the needed resource with full-path. Works safely on all platforms. * * @param resource * The name of the resource to obtain. * @return The fully qualified resource location. * @throws PerfCakeException * In the case of wrong resource name. */ public static String getResource(final String resource) throws PerfCakeException { try { return new File(Utils.class.getResource(resource).toURI()).getAbsolutePath(); } catch (final URISyntaxException e) { throw new PerfCakeException(String.format("Cannot obtain resource %s:", resource), e); } } /** * Atomically writes given content to a file. * * @param fileName * The target file name. * @param content * The content to be written. * @throws org.perfcake.PerfCakeException * In the case of s file operations failure. */ public static void writeFileContent(final String fileName, final String content) throws PerfCakeException { writeFileContent(new File(fileName), content); } /** * Atomically writes given content to a file. * * @param file * The target file. * @param content * The content to be written. * @throws org.perfcake.PerfCakeException * In the case of file operations failure. */ public static void writeFileContent(final File file, final String content) throws PerfCakeException { writeFileContent(file.toPath(), content); } /** * Atomically writes given content to a file. * * @param path * The target file path. * @param content * The content to be written. * @throws org.perfcake.PerfCakeException * In the case of file operations failure. */ public static void writeFileContent(final Path path, final String content) throws PerfCakeException { try { final Path workFile = Paths.get(path.toString() + ".work"); Files.write(workFile, content.getBytes(Utils.getDefaultEncoding())); Files.move(workFile, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (final IOException e) { final String message = String.format("Could not write content to the file %s:", path.toString()); throw new PerfCakeException(message, e); } } /** * Takes a resource as a StringTemplate, renders the template using the provided properties and stores it to the given path. * * @param resource * The resource location of a template. * @param target * The target path where to store the rendered template file. * @param properties * The properties to fill into the template. * @throws PerfCakeException * When it is not possible to render the template or store the target file. */ public static void copyTemplateFromResource(final String resource, final Path target, final Properties properties) throws PerfCakeException { Utils.writeFileContent(target, readTemplateFromResource(resource, properties)); } /** * Reads the given resource and processes it as a template. * * @param resource * The resource location of a template. * @param properties * The properties to fill into the template. * @return The filtered content of the template. * @throws PerfCakeException * When it was not possible to read the resource. */ public static String readTemplateFromResource(final String resource, final Properties properties) throws PerfCakeException { try { final StringTemplate template = new StringTemplate(IOUtils.toString(Utils.class.getResourceAsStream(resource), Utils.getDefaultEncoding()), properties); return template.toString(); } catch (final IOException e) { final String message = String.format("Could not render template from resource %s:", resource); throw new PerfCakeException(message, e); } } /** * Reconfigures the logging level of the root logger and all suitable appenders. * * @param level * The desired level. */ public static void setLoggingLevel(final Level level) { final Logger log = LogManager.getLogger(Utils.class); final org.apache.logging.log4j.core.Logger coreLogger = (org.apache.logging.log4j.core.Logger) log; final LoggerContext context = coreLogger.getContext(); context.getConfiguration().getLoggers().get("org.perfcake").setLevel(level); context.updateLoggers(); } /** * Initializes system properties that carry time stamps. */ public static void initTimeStamps() { if (System.getProperty(PerfCakeConst.TIMESTAMP_PROPERTY) == null) { System.setProperty(PerfCakeConst.TIMESTAMP_PROPERTY, String.valueOf(Calendar.getInstance().getTimeInMillis())); } if (System.getProperty(PerfCakeConst.NICE_TIMESTAMP_PROPERTY) == null) { System.setProperty(PerfCakeConst.NICE_TIMESTAMP_PROPERTY, (new SimpleDateFormat("yyyyMMddHHmmss")).format(new Date())); } } /** * Initializes the debug agent when configured. */ public static void initDebugAgent() { if (Boolean.parseBoolean(System.getProperty(PerfCakeConst.DEBUG_PROPERTY))) { PerfCakeDebug.initialize(); } } }