Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.jmeter.save; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.jmeter.reporters.ResultCollectorHelper; import org.apache.jmeter.samplers.SampleEvent; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.testelement.TestElement; import org.apache.jmeter.util.JMeterUtils; import org.apache.jmeter.util.NameUpdater; import org.apache.jorphan.collections.HashTree; import org.apache.jorphan.logging.LoggingManager; import org.apache.jorphan.util.JMeterError; import org.apache.jorphan.util.JOrphanUtils; import org.apache.log.Logger; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.DataHolder; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; import com.thoughtworks.xstream.io.xml.XppDriver; import com.thoughtworks.xstream.mapper.CannotResolveClassException; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; /** * Handles setting up XStream serialisation. * The class reads alias definitions from saveservice.properties. * */ public class SaveService { private static final Logger log = LoggingManager.getLoggerForClass(); // Names of DataHolder entries for JTL processing public static final String SAMPLE_EVENT_OBJECT = "SampleEvent"; // $NON-NLS-1$ public static final String RESULTCOLLECTOR_HELPER_OBJECT = "ResultCollectorHelper"; // $NON-NLS-1$ // Names of DataHolder entries for JMX processing public static final String TEST_CLASS_NAME = "TestClassName"; // $NON-NLS-1$ private static final class XStreamWrapper extends XStream { private XStreamWrapper(ReflectionProvider reflectionProvider) { super(reflectionProvider); } // Override wrapMapper in order to insert the Wrapper in the chain @Override protected MapperWrapper wrapMapper(MapperWrapper next) { // Provide our own aliasing using strings rather than classes return new MapperWrapper(next) { // Translate alias to classname and then delegate to wrapped class @Override public Class<?> realClass(String alias) { String fullName = aliasToClass(alias); if (fullName != null) { fullName = NameUpdater.getCurrentName(fullName); } return super.realClass(fullName == null ? alias : fullName); } // Translate to alias and then delegate to wrapped class @Override public String serializedClass(@SuppressWarnings("rawtypes") // superclass does not use types Class type) { if (type == null) { return super.serializedClass(null); // was type, but that caused FindBugs warning } String alias = classToAlias(type.getName()); return alias == null ? super.serializedClass(type) : alias; } }; } } private static final XStream JMXSAVER = new XStreamWrapper(new PureJavaReflectionProvider()); private static final XStream JTLSAVER = new XStreamWrapper(new PureJavaReflectionProvider()); static { JTLSAVER.setMode(XStream.NO_REFERENCES); // This is needed to stop XStream keeping copies of each class } // The XML header, with placeholder for encoding, since that is controlled by property private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"<ph>\"?>"; // $NON-NLS-1$ // Default file name private static final String SAVESERVICE_PROPERTIES_FILE = "/bin/saveservice.properties"; // $NON-NLS-1$ // Property name used to define file name private static final String SAVESERVICE_PROPERTIES = "saveservice_properties"; // $NON-NLS-1$ // Define file format versions private static final String VERSION_2_2 = "2.2"; // $NON-NLS-1$ // Holds the mappings from the saveservice properties file // Key: alias Entry: full class name // There may be multiple aliases which map to the same class private static final Properties aliasToClass = new Properties(); // Holds the reverse mappings // Key: full class name Entry: primary alias private static final Properties classToAlias = new Properties(); // Version information for test plan header // This is written to JMX files by ScriptWrapperConverter // Also to JTL files by ResultCollector private static final String VERSION = "1.2"; // $NON-NLS-1$ // This is written to JMX files by ScriptWrapperConverter private static String propertiesVersion = "";// read from properties file; written to JMX files // Must match _version property value in saveservice.properties // used to ensure saveservice.properties and SaveService are updated simultaneously static final String PROPVERSION = "2.9";// Expected version $NON-NLS-1$ // Internal information only private static String fileVersion = ""; // computed from saveservice.properties file// $NON-NLS-1$ // Must match the sha1 checksum of the file saveservice.properties (without newline character), // used to ensure saveservice.properties and SaveService are updated simultaneously static final String FILEVERSION = "2e0ec2b2360e52cd5de4e0f20fa51c1809f6895c"; // Expected value $NON-NLS-1$ private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$ static { log.info("Testplan (JMX) version: " + VERSION_2_2 + ". Testlog (JTL) version: " + VERSION_2_2); initProps(); checkVersions(); } // Helper method to simplify alias creation from properties private static void makeAlias(String aliasList, String clazz) { String[] aliases = aliasList.split(","); // Can have multiple aliases for same target classname String alias = aliases[0]; for (String a : aliases) { Object old = aliasToClass.setProperty(a, clazz); if (old != null) { log.error("Duplicate class detected for " + alias + ": " + clazz + " & " + old); } } Object oldval = classToAlias.setProperty(clazz, alias); if (oldval != null) { log.error("Duplicate alias detected for " + clazz + ": " + alias + " & " + oldval); } } public static Properties loadProperties() throws IOException { Properties nameMap = new Properties(); try (FileInputStream fis = new FileInputStream(JMeterUtils.getJMeterHome() + JMeterUtils.getPropDefault(SAVESERVICE_PROPERTIES, SAVESERVICE_PROPERTIES_FILE))) { nameMap.load(fis); } return nameMap; } private static String getChecksumForPropertiesFile() throws NoSuchAlgorithmException, IOException { MessageDigest md = MessageDigest.getInstance("SHA1"); try (FileReader fileReader = new FileReader(JMeterUtils.getJMeterHome() + JMeterUtils.getPropDefault(SAVESERVICE_PROPERTIES, SAVESERVICE_PROPERTIES_FILE)); BufferedReader reader = new BufferedReader(fileReader)) { String line = null; while ((line = reader.readLine()) != null) { md.update(line.getBytes()); } } return JOrphanUtils.baToHexString(md.digest()); } private static void initProps() { // Load the alias properties try { fileVersion = getChecksumForPropertiesFile(); } catch (IOException | NoSuchAlgorithmException e) { log.fatalError("Can't compute checksum for saveservice properties file", e); throw new JMeterError("JMeter requires the checksum of saveservice properties file to continue", e); } try { Properties nameMap = loadProperties(); // now create the aliases for (Map.Entry<Object, Object> me : nameMap.entrySet()) { String key = (String) me.getKey(); String val = (String) me.getValue(); if (!key.startsWith("_")) { // $NON-NLS-1$ makeAlias(key, val); } else { // process special keys if (key.equalsIgnoreCase("_version")) { // $NON-NLS-1$ propertiesVersion = val; log.info("Using SaveService properties version " + propertiesVersion); } else if (key.equalsIgnoreCase("_file_version")) { // $NON-NLS-1$ log.info("SaveService properties file version is now computed by a checksum," + "the property _file_version is not used anymore and can be removed."); } else if (key.equalsIgnoreCase("_file_encoding")) { // $NON-NLS-1$ fileEncoding = val; log.info("Using SaveService properties file encoding " + fileEncoding); } else { key = key.substring(1);// Remove the leading "_" try { final String trimmedValue = val.trim(); if (trimmedValue.equals("collection") // $NON-NLS-1$ || trimmedValue.equals("mapping")) { // $NON-NLS-1$ registerConverter(key, JMXSAVER, true); registerConverter(key, JTLSAVER, true); } else { registerConverter(key, JMXSAVER, false); registerConverter(key, JTLSAVER, false); } } catch (IllegalAccessException | InstantiationException | ClassNotFoundException | IllegalArgumentException | SecurityException | InvocationTargetException | NoSuchMethodException e1) { log.warn("Can't register a converter: " + key, e1); } } } } } catch (IOException e) { log.fatalError("Bad saveservice properties file", e); throw new JMeterError("JMeter requires the saveservice properties file to continue"); } } /** * Register converter. * @param key * @param jmxsaver * @param useMapper * * @throws InstantiationException * @throws IllegalAccessException * @throws InvocationTargetException * @throws NoSuchMethodException * @throws ClassNotFoundException */ private static void registerConverter(String key, XStream jmxsaver, boolean useMapper) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException { if (useMapper) { jmxsaver.registerConverter((Converter) Class.forName(key).getConstructor(new Class[] { Mapper.class }) .newInstance(new Object[] { jmxsaver.getMapper() })); } else { jmxsaver.registerConverter((Converter) Class.forName(key).newInstance()); } } // For converters to use public static String aliasToClass(String s) { String r = aliasToClass.getProperty(s); return r == null ? s : r; } // For converters to use public static String classToAlias(String s) { String r = classToAlias.getProperty(s); return r == null ? s : r; } // Called by Save function public static void saveTree(HashTree tree, OutputStream out) throws IOException { // Get the OutputWriter to use OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out); writeXmlHeader(outputStreamWriter); // Use deprecated method, to avoid duplicating code ScriptWrapper wrapper = new ScriptWrapper(); wrapper.testPlan = tree; JMXSAVER.toXML(wrapper, outputStreamWriter); outputStreamWriter.write('\n');// Ensure terminated properly outputStreamWriter.close(); } // Used by Test code public static void saveElement(Object el, OutputStream out) throws IOException { // Get the OutputWriter to use OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out); writeXmlHeader(outputStreamWriter); // Use deprecated method, to avoid duplicating code JMXSAVER.toXML(el, outputStreamWriter); outputStreamWriter.close(); } // Used by Test code public static Object loadElement(InputStream in) throws IOException { // Get the InputReader to use InputStreamReader inputStreamReader = getInputStreamReader(in); // Use deprecated method, to avoid duplicating code Object element = JMXSAVER.fromXML(inputStreamReader); inputStreamReader.close(); return element; } /** * Save a sampleResult to an XML output file using XStream. * * @param evt sampleResult wrapped in a sampleEvent * @param writer output stream which must be created using {@link #getFileEncoding(String)} * @throws IOException when writing data to output fails */ // Used by ResultCollector.sampleOccurred(SampleEvent event) public synchronized static void saveSampleResult(SampleEvent evt, Writer writer) throws IOException { DataHolder dh = JTLSAVER.newDataHolder(); dh.put(SAMPLE_EVENT_OBJECT, evt); // This is effectively the same as saver.toXML(Object, Writer) except we get to provide the DataHolder // Don't know why there is no method for this in the XStream class try { JTLSAVER.marshal(evt.getResult(), new XppDriver().createWriter(writer), dh); } catch (RuntimeException e) { throw new IllegalArgumentException( "Failed marshalling:" + (evt.getResult() != null ? showDebuggingInfo(evt.getResult()) : "null"), e); } writer.write('\n'); } /** * * @param result SampleResult * @return String debugging information */ private static String showDebuggingInfo(SampleResult result) { try { return "class:" + result.getClass() + ",content:" + ToStringBuilder.reflectionToString(result); } catch (Exception e) { return "Exception occured creating debug from event, message:" + e.getMessage(); } } /** * @param elem test element * @param writer output stream which must be created using {@link #getFileEncoding(String)} * @throws IOException when writing data to output fails */ // Used by ResultCollector#recordStats() public synchronized static void saveTestElement(TestElement elem, Writer writer) throws IOException { JMXSAVER.toXML(elem, writer); // TODO should this be JTLSAVER? Only seems to be called by MonitorHealthVisualzer writer.write('\n'); } private static boolean versionsOK = true; // private static void checkVersion(Class clazz, String expected) { // // String actual = "*NONE*"; // $NON-NLS-1$ // try { // actual = (String) clazz.getMethod("getVersion", null).invoke(null, null); // actual = extractVersion(actual); // } catch (Exception ignored) { // // Not needed // } // if (0 != actual.compareTo(expected)) { // versionsOK = false; // log.warn("Version mismatch: expected '" + expected + "' found '" + actual + "' in " + clazz.getName()); // } // } // Routines for TestSaveService static String getPropertyVersion() { return SaveService.propertiesVersion; } static String getFileVersion() { return SaveService.fileVersion; } // Allow test code to check for spurious class references static List<String> checkClasses() { final ClassLoader classLoader = SaveService.class.getClassLoader(); List<String> missingClasses = new ArrayList<>(); //boolean OK = true; for (Object clazz : classToAlias.keySet()) { String name = (String) clazz; if (!NameUpdater.isMapped(name)) {// don't bother checking class is present if it is to be updated try { Class.forName(name, false, classLoader); } catch (ClassNotFoundException e) { log.error( "Unexpected entry in saveservice.properties; class does not exist and is not upgraded: " + name); missingClasses.add(name); } } } return missingClasses; } static boolean checkVersions() { versionsOK = true; // Disable converter version checks as they are more of a nuisance than helpful // checkVersion(BooleanPropertyConverter.class, "493779"); // $NON-NLS-1$ // checkVersion(HashTreeConverter.class, "514283"); // $NON-NLS-1$ // checkVersion(IntegerPropertyConverter.class, "493779"); // $NON-NLS-1$ // checkVersion(LongPropertyConverter.class, "493779"); // $NON-NLS-1$ // checkVersion(MultiPropertyConverter.class, "514283"); // $NON-NLS-1$ // checkVersion(SampleResultConverter.class, "571992"); // $NON-NLS-1$ // // // Not built until later, so need to use this method: // try { // checkVersion( // Class.forName("org.apache.jmeter.protocol.http.util.HTTPResultConverter"), // $NON-NLS-1$ // "514283"); // $NON-NLS-1$ // } catch (ClassNotFoundException e) { // versionsOK = false; // log.warn(e.getLocalizedMessage()); // } // checkVersion(StringPropertyConverter.class, "493779"); // $NON-NLS-1$ // checkVersion(TestElementConverter.class, "549987"); // $NON-NLS-1$ // checkVersion(TestElementPropertyConverter.class, "549987"); // $NON-NLS-1$ // checkVersion(ScriptWrapperConverter.class, "514283"); // $NON-NLS-1$ // checkVersion(TestResultWrapperConverter.class, "514283"); // $NON-NLS-1$ // checkVersion(SampleSaveConfigurationConverter.class,"549936"); // $NON-NLS-1$ if (!PROPVERSION.equalsIgnoreCase(propertiesVersion)) { log.warn("Bad _version - expected " + PROPVERSION + ", found " + propertiesVersion + "."); } // if (!FILEVERSION.equalsIgnoreCase(fileVersion)) { // log.warn("Bad _file_version - expected " + FILEVERSION + ", found " + fileVersion +"."); // } if (versionsOK) { log.info("All converter versions present and correct"); } return versionsOK; } /** * Read results from JTL file. * * @param reader of the file * @param resultCollectorHelper helper class to enable TestResultWrapperConverter to deliver the samples * @throws IOException if an I/O error occurs */ public static void loadTestResults(InputStream reader, ResultCollectorHelper resultCollectorHelper) throws IOException { // Get the InputReader to use InputStreamReader inputStreamReader = getInputStreamReader(reader); DataHolder dh = JTLSAVER.newDataHolder(); dh.put(RESULTCOLLECTOR_HELPER_OBJECT, resultCollectorHelper); // Allow TestResultWrapper to feed back the samples // This is effectively the same as saver.fromXML(InputStream) except we get to provide the DataHolder // Don't know why there is no method for this in the XStream class JTLSAVER.unmarshal(new XppDriver().createReader(reader), null, dh); inputStreamReader.close(); } /** * Load a Test tree (JMX file) * @param reader the JMX file as an {@link InputStream} * @return the loaded tree or null if an error occurs * @throws IOException if there is a problem reading the file or processing it * @deprecated use {@link SaveService}{@link #loadTree(File)} */ @Deprecated public static HashTree loadTree(InputStream reader) throws IOException { try { return readTree(reader, null); } catch (IllegalArgumentException e) { log.error("Problem loading XML, message:" + e.getMessage(), e); return null; } finally { JOrphanUtils.closeQuietly(reader); } } /** * Load a Test tree (JMX file) * @param file the JMX file * @return the loaded tree * @throws IOException if there is a problem reading the file or processing it */ public static HashTree loadTree(File file) throws IOException { log.info("Loading file: " + file); try (InputStream reader = new FileInputStream(file)) { return readTree(reader, file); } } /** * * @param reader {@link InputStream} * @param file the JMX file used only for debug, can be null * @return the loaded tree * @throws IOException if there is a problem reading the file or processing it */ private static HashTree readTree(InputStream reader, File file) throws IOException { if (!reader.markSupported()) { reader = new BufferedInputStream(reader); } reader.mark(Integer.MAX_VALUE); ScriptWrapper wrapper = null; try { // Get the InputReader to use InputStreamReader inputStreamReader = getInputStreamReader(reader); wrapper = (ScriptWrapper) JMXSAVER.fromXML(inputStreamReader); inputStreamReader.close(); if (wrapper == null) { log.error("Problem loading XML: see above."); return null; } return wrapper.testPlan; } catch (CannotResolveClassException e) { if (file != null) { throw new IllegalArgumentException("Problem loading XML from:'" + file.getAbsolutePath() + "', cannot determine class for element: " + e, e); } else { throw new IllegalArgumentException("Problem loading XML, cannot determine class for element: " + e, e); } } catch (ConversionException | NoClassDefFoundError e) { if (file != null) { throw new IllegalArgumentException( "Problem loading XML from:'" + file.getAbsolutePath() + "', missing class " + e, e); } else { throw new IllegalArgumentException("Problem loading XML, missing class " + e, e); } } } private static InputStreamReader getInputStreamReader(InputStream inStream) { // Check if we have a encoding to use from properties Charset charset = getFileEncodingCharset(); if (charset != null) { return new InputStreamReader(inStream, charset); } else { // We use the default character set encoding of the JRE return new InputStreamReader(inStream); } } private static OutputStreamWriter getOutputStreamWriter(OutputStream outStream) { // Check if we have a encoding to use from properties Charset charset = getFileEncodingCharset(); if (charset != null) { return new OutputStreamWriter(outStream, charset); } else { // We use the default character set encoding of the JRE return new OutputStreamWriter(outStream); } } /** * Returns the file Encoding specified in saveservice.properties or the default * @param dflt value to return if file encoding was not provided * * @return file encoding or default */ // Used by ResultCollector when creating output files public static String getFileEncoding(String dflt) { if (fileEncoding != null && fileEncoding.length() > 0) { return fileEncoding; } else { return dflt; } } private static Charset getFileEncodingCharset() { // Check if we have a encoding to use from properties if (fileEncoding != null && fileEncoding.length() > 0) { return Charset.forName(fileEncoding); } else { // We use the default character set encoding of the JRE return null; } } private static void writeXmlHeader(OutputStreamWriter writer) throws IOException { // Write XML header if we have the charset to use for encoding Charset charset = getFileEncodingCharset(); if (charset != null) { // We do not use getEncoding method of Writer, since that returns // the historical name String header = XML_HEADER.replaceAll("<ph>", charset.name()); writer.write(header); writer.write('\n'); } } // Normal output // ---- Debugging information ---- // required-type : org.apache.jorphan.collections.ListedHashTree // cause-message : WebServiceSampler : WebServiceSampler // class : org.apache.jmeter.save.ScriptWrapper // message : WebServiceSampler : WebServiceSampler // line number : 929 // path : /jmeterTestPlan/hashTree/hashTree/hashTree[4]/hashTree[5]/WebServiceSampler // cause-exception : com.thoughtworks.xstream.alias.CannotResolveClassException // ------------------------------- /** * Simplify getMessage() output from XStream ConversionException * @param ce - ConversionException to analyse * @return string with details of error */ public static String CEtoString(ConversionException ce) { String msg = "XStream ConversionException at line: " + ce.get("line number") + "\n" + ce.get("message") + "\nPerhaps a missing jar? See log file."; return msg; } public static String getPropertiesVersion() { return propertiesVersion; } public static String getVERSION() { return VERSION; } }