001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.save;
020:
021: import java.io.BufferedInputStream;
022: import java.io.FileInputStream;
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.io.InputStreamReader;
026: import java.io.OutputStreamWriter;
027: import java.io.OutputStream;
028: import java.io.Reader;
029: import java.io.Writer;
030: import java.lang.reflect.InvocationTargetException;
031: import java.util.Iterator;
032: import java.util.Map;
033: import java.util.Properties;
034:
035: import java.nio.charset.Charset;
036:
037: import org.apache.jmeter.samplers.SampleEvent;
038: import org.apache.jmeter.testelement.TestElement;
039: import org.apache.jmeter.util.JMeterUtils;
040: import org.apache.jorphan.collections.HashTree;
041: import org.apache.jorphan.logging.LoggingManager;
042: import org.apache.jorphan.util.JMeterError;
043: import org.apache.jorphan.util.JOrphanUtils;
044: import org.apache.log.Logger;
045:
046: import com.thoughtworks.xstream.XStream;
047: import com.thoughtworks.xstream.io.xml.XppDriver;
048: import com.thoughtworks.xstream.mapper.CannotResolveClassException;
049: import com.thoughtworks.xstream.mapper.Mapper;
050: import com.thoughtworks.xstream.mapper.MapperWrapper;
051: import com.thoughtworks.xstream.converters.ConversionException;
052: import com.thoughtworks.xstream.converters.Converter;
053: import com.thoughtworks.xstream.converters.DataHolder;
054: import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
055:
056: /**
057: * Handles setting up XStream serialisation.
058: * The class reads alias definitions from saveservice.properties.
059: *
060: */
061: public class SaveService {
062:
063: private static final Logger log = LoggingManager
064: .getLoggerForClass();
065:
066: public static final String SAMPLE_EVENT_OBJECT = "SampleEvent"; // $NON-NLS-1$
067:
068: private static final XStream saver = new XStream(
069: new PureJavaReflectionProvider()) {
070: // Override wrapMapper in order to insert the Wrapper in the chain
071: protected MapperWrapper wrapMapper(MapperWrapper next) {
072: // Provide our own aliasing using strings rather than classes
073: return new MapperWrapper(next) {
074: // Translate alias to classname and then delegate to wrapped class
075: public Class realClass(String alias) {
076: String fullName = aliasToClass(alias);
077: return super .realClass(fullName == null ? alias
078: : fullName);
079: }
080:
081: // Translate to alias and then delegate to wrapped class
082: public String serializedClass(Class type) {
083: if (type == null) {
084: return super .serializedClass(null); // was type, but that caused FindBugs warning
085: }
086: String alias = classToAlias(type.getName());
087: return alias == null ? super .serializedClass(type)
088: : alias;
089: }
090: };
091: }
092: };
093:
094: // The XML header, with placeholder for encoding, since that is controlled by property
095: private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"<ph>\"?>"; // $NON-NLS-1$
096:
097: // Default file name
098: private static final String SAVESERVICE_PROPERTIES_FILE = "/bin/saveservice.properties"; // $NON-NLS-1$
099:
100: // Property name used to define file name
101: private static final String SAVESERVICE_PROPERTIES = "saveservice_properties"; // $NON-NLS-1$
102:
103: // Define file format property names
104: private static final String FILE_FORMAT = "file_format"; // $NON-NLS-1$
105: private static final String FILE_FORMAT_TESTPLAN = "file_format.testplan"; // $NON-NLS-1$
106: private static final String FILE_FORMAT_TESTLOG = "file_format.testlog"; // $NON-NLS-1$
107:
108: // Define file format versions
109: private static final String VERSION_2_0 = "2.0"; // $NON-NLS-1$
110: //NOT USED private static final String VERSION_2_1 = "2.1"; // $NON-NLS-1$
111: private static final String VERSION_2_2 = "2.2"; // $NON-NLS-1$
112:
113: // Default to overall format, and then to version 2.2
114: public static final String TESTPLAN_FORMAT = JMeterUtils
115: .getPropDefault(FILE_FORMAT_TESTPLAN, JMeterUtils
116: .getPropDefault(FILE_FORMAT, VERSION_2_2));
117:
118: public static final String TESTLOG_FORMAT = JMeterUtils
119: .getPropDefault(FILE_FORMAT_TESTLOG, JMeterUtils
120: .getPropDefault(FILE_FORMAT, VERSION_2_2));
121:
122: private static final boolean IS_TESTPLAN_FORMAT_20 = VERSION_2_0
123: .equals(TESTPLAN_FORMAT);
124:
125: private static final boolean IS_TESTLOG_FORMAT_20 = VERSION_2_0
126: .equals(TESTLOG_FORMAT);
127:
128: private static final boolean IS_TESTPLAN_FORMAT_22 = VERSION_2_2
129: .equals(TESTPLAN_FORMAT);
130:
131: // Holds the mappings from the saveservice properties file
132: private static final Properties aliasToClass = new Properties();
133:
134: // Holds the reverse mappings
135: private static final Properties classToAlias = new Properties();
136:
137: // Version information for test plan header
138: // This is written to JMX files by ScriptWrapperConverter
139: // Also to JTL files by ResultCollector
140: private static final String VERSION = "1.2"; // $NON-NLS-1$
141:
142: // This is written to JMX files by ScriptWrapperConverter
143: private static String propertiesVersion = "";// read from properties file; written to JMX files
144: private static final String PROPVERSION = "2.0";// Expected version $NON-NLS-1$
145:
146: // Internal information only
147: private static String fileVersion = ""; // read from properties file// $NON-NLS-1$
148: private static final String FILEVERSION = "594567"; // Expected value $NON-NLS-1$
149: private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$
150:
151: static {
152: log.info("Testplan (JMX) version: " + TESTPLAN_FORMAT
153: + ". Testlog (JTL) version: " + TESTLOG_FORMAT);
154: initProps();
155: checkVersions();
156: }
157:
158: // Helper method to simplify alias creation from properties
159: private static void makeAlias(String alias, String clazz) {
160: aliasToClass.setProperty(alias, clazz);
161: Object oldval = classToAlias.setProperty(clazz, alias);
162: if (oldval != null) {
163: log.error("Duplicate alias detected for " + clazz + ": "
164: + alias + " & " + oldval);
165: }
166: }
167:
168: public static Properties loadProperties() throws IOException {
169: Properties nameMap = new Properties();
170: FileInputStream fis = null;
171: try {
172: fis = new FileInputStream(JMeterUtils.getJMeterHome()
173: + JMeterUtils.getPropDefault(
174: SAVESERVICE_PROPERTIES,
175: SAVESERVICE_PROPERTIES_FILE));
176: nameMap.load(fis);
177: } finally {
178: JOrphanUtils.closeQuietly(fis);
179: }
180: return nameMap;
181: }
182:
183: private static void initProps() {
184: // Load the alias properties
185: try {
186: Properties nameMap = loadProperties();
187: // now create the aliases
188: Iterator it = nameMap.entrySet().iterator();
189: while (it.hasNext()) {
190: Map.Entry me = (Map.Entry) it.next();
191: String key = (String) me.getKey();
192: String val = (String) me.getValue();
193: if (!key.startsWith("_")) {
194: makeAlias(key, val);
195: } else {
196: // process special keys
197: if (key.equalsIgnoreCase("_version")) { // $NON-NLS-1$
198: propertiesVersion = val;
199: log
200: .info("Using SaveService properties version "
201: + propertiesVersion);
202: } else if (key.equalsIgnoreCase("_file_version")) { // $NON-NLS-1$
203: fileVersion = extractVersion(val);
204: log
205: .info("Using SaveService properties file version "
206: + fileVersion);
207: } else if (key.equalsIgnoreCase("_file_encoding")) { // $NON-NLS-1$
208: fileEncoding = val;
209: log
210: .info("Using SaveService properties file encoding "
211: + fileEncoding);
212: } else {
213: key = key.substring(1);// Remove the leading "_"
214: try {
215: if (val.trim().equals("collection")) { // $NON-NLS-1$
216: saver
217: .registerConverter((Converter) Class
218: .forName(key)
219: .getConstructor(
220: new Class[] { Mapper.class })
221: .newInstance(
222: new Object[] { saver
223: .getMapper() }));
224: } else if (val.trim().equals("mapping")) { // $NON-NLS-1$
225: saver
226: .registerConverter((Converter) Class
227: .forName(key)
228: .getConstructor(
229: new Class[] { Mapper.class })
230: .newInstance(
231: new Object[] { saver
232: .getMapper() }));
233: } else {
234: saver
235: .registerConverter((Converter) Class
236: .forName(key)
237: .newInstance());
238: }
239: } catch (IllegalAccessException e1) {
240: log.warn("Can't register a converter: "
241: + key, e1);
242: } catch (InstantiationException e1) {
243: log.warn("Can't register a converter: "
244: + key, e1);
245: } catch (ClassNotFoundException e1) {
246: log.warn("Can't register a converter: "
247: + key, e1);
248: } catch (IllegalArgumentException e1) {
249: log.warn("Can't register a converter: "
250: + key, e1);
251: } catch (SecurityException e1) {
252: log.warn("Can't register a converter: "
253: + key, e1);
254: } catch (InvocationTargetException e1) {
255: log.warn("Can't register a converter: "
256: + key, e1);
257: } catch (NoSuchMethodException e1) {
258: log.warn("Can't register a converter: "
259: + key, e1);
260: }
261: }
262: }
263: }
264: } catch (IOException e) {
265: log.fatalError("Bad saveservice properties file", e);
266: throw new JMeterError(
267: "JMeter requires the saveservice properties file to continue");
268: }
269: }
270:
271: // For converters to use
272: public static String aliasToClass(String s) {
273: String r = aliasToClass.getProperty(s);
274: return r == null ? s : r;
275: }
276:
277: // For converters to use
278: public static String classToAlias(String s) {
279: String r = classToAlias.getProperty(s);
280: return r == null ? s : r;
281: }
282:
283: // Called by Save function
284: public static void saveTree(HashTree tree, OutputStream out)
285: throws IOException {
286: // Get the OutputWriter to use
287: OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out);
288: writeXmlHeader(outputStreamWriter);
289: // Use deprecated method, to avoid duplicating code
290: ScriptWrapper wrapper = new ScriptWrapper();
291: wrapper.testPlan = tree;
292: saver.toXML(wrapper, outputStreamWriter);
293: outputStreamWriter.write('\n');// Ensure terminated properly
294: outputStreamWriter.close();
295: }
296:
297: // Used by Test code
298: public static void saveElement(Object el, OutputStream out)
299: throws IOException {
300: // Get the OutputWriter to use
301: OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out);
302: writeXmlHeader(outputStreamWriter);
303: // Use deprecated method, to avoid duplicating code
304: saver.toXML(el, outputStreamWriter);
305: outputStreamWriter.close();
306: }
307:
308: // Used by Test code
309: public static Object loadElement(InputStream in) throws IOException {
310: // Get the InputReader to use
311: InputStreamReader inputStreamReader = getInputStreamReader(in);
312: // Use deprecated method, to avoid duplicating code
313: Object element = saver.fromXML(inputStreamReader);
314: inputStreamReader.close();
315: return element;
316: }
317:
318: /**
319: * Save a sampleResult to an XML output file using XStream.
320: *
321: * @param evt sampleResult wrapped in a sampleEvent
322: * @param writer output stream which must be created using {@link #getFileEncoding(String)}
323: */
324: // Used by ResultCollector#recordResult()
325: public synchronized static void saveSampleResult(SampleEvent evt,
326: Writer writer) throws IOException {
327: DataHolder dh = saver.newDataHolder();
328: dh.put(SAMPLE_EVENT_OBJECT, evt);
329: // This is effectively the same as saver.toXML(Object, Writer) except we get to provide the DataHolder
330: // Don't know why there is no method for this in the XStream class
331: saver.marshal(evt.getResult(), new XppDriver()
332: .createWriter(writer), dh);
333: writer.write('\n');
334: }
335:
336: /**
337: * @param elem test element
338: * @param writer output stream which must be created using {@link #getFileEncoding(String)}
339: */
340: // Used by ResultCollector#recordStats()
341: public synchronized static void saveTestElement(TestElement elem,
342: Writer writer) throws IOException {
343: saver.toXML(elem, writer);
344: writer.write('\n');
345: }
346:
347: private static boolean versionsOK = true;
348:
349: // Extract version digits from String of the form #Revision: n.mm #
350: // (where # is actually $ above)
351: private static final String REVPFX = "$Revision: ";
352: private static final String REVSFX = " $"; // $NON-NLS-1$
353:
354: private static String extractVersion(String rev) {
355: if (rev.length() > REVPFX.length() + REVSFX.length()) {
356: return rev.substring(REVPFX.length(), rev.length()
357: - REVSFX.length());
358: }
359: return rev;
360: }
361:
362: // private static void checkVersion(Class clazz, String expected) {
363: //
364: // String actual = "*NONE*"; // $NON-NLS-1$
365: // try {
366: // actual = (String) clazz.getMethod("getVersion", null).invoke(null, null);
367: // actual = extractVersion(actual);
368: // } catch (Exception ignored) {
369: // // Not needed
370: // }
371: // if (0 != actual.compareTo(expected)) {
372: // versionsOK = false;
373: // log.warn("Version mismatch: expected '" + expected + "' found '" + actual + "' in " + clazz.getName());
374: // }
375: // }
376:
377: // Routines for TestSaveService
378: static boolean checkPropertyVersion() {
379: return SaveService.PROPVERSION
380: .equals(SaveService.propertiesVersion);
381: }
382:
383: static boolean checkFileVersion() {
384: return SaveService.FILEVERSION.equals(SaveService.fileVersion);
385: }
386:
387: static boolean checkVersions() {
388: versionsOK = true;
389: // Disable converter version checks as they are more of a nuisance than helpful
390: // checkVersion(BooleanPropertyConverter.class, "493779"); // $NON-NLS-1$
391: // checkVersion(HashTreeConverter.class, "514283"); // $NON-NLS-1$
392: // checkVersion(IntegerPropertyConverter.class, "493779"); // $NON-NLS-1$
393: // checkVersion(LongPropertyConverter.class, "493779"); // $NON-NLS-1$
394: // checkVersion(MultiPropertyConverter.class, "514283"); // $NON-NLS-1$
395: // checkVersion(SampleResultConverter.class, "571992"); // $NON-NLS-1$
396: //
397: // // Not built until later, so need to use this method:
398: // try {
399: // checkVersion(
400: // Class.forName("org.apache.jmeter.protocol.http.util.HTTPResultConverter"), // $NON-NLS-1$
401: // "514283"); // $NON-NLS-1$
402: // } catch (ClassNotFoundException e) {
403: // versionsOK = false;
404: // log.warn(e.getLocalizedMessage());
405: // }
406: // checkVersion(StringPropertyConverter.class, "493779"); // $NON-NLS-1$
407: // checkVersion(TestElementConverter.class, "549987"); // $NON-NLS-1$
408: // checkVersion(TestElementPropertyConverter.class, "549987"); // $NON-NLS-1$
409: // checkVersion(ScriptWrapperConverter.class, "514283"); // $NON-NLS-1$
410: // checkVersion(TestResultWrapperConverter.class, "514283"); // $NON-NLS-1$
411: // checkVersion(SampleSaveConfigurationConverter.class,"549936"); // $NON-NLS-1$
412:
413: if (!PROPVERSION.equalsIgnoreCase(propertiesVersion)) {
414: log.warn("Bad _version - expected " + PROPVERSION
415: + ", found " + propertiesVersion + ".");
416: }
417: if (!FILEVERSION.equalsIgnoreCase(fileVersion)) {
418: log.warn("Bad _file_version - expected " + FILEVERSION
419: + ", found " + fileVersion + ".");
420: }
421: if (versionsOK) {
422: log.info("All converter versions present and correct");
423: }
424: return versionsOK;
425: }
426:
427: public static TestResultWrapper loadTestResults(InputStream reader)
428: throws Exception {
429: // Get the InputReader to use
430: InputStreamReader inputStreamReader = getInputStreamReader(reader);
431: TestResultWrapper wrapper = (TestResultWrapper) saver
432: .fromXML(inputStreamReader);
433: inputStreamReader.close();
434: return wrapper;
435: }
436:
437: public static HashTree loadTree(InputStream reader)
438: throws Exception {
439: if (!reader.markSupported()) {
440: reader = new BufferedInputStream(reader);
441: }
442: reader.mark(Integer.MAX_VALUE);
443: ScriptWrapper wrapper = null;
444: try {
445: // Get the InputReader to use
446: InputStreamReader inputStreamReader = getInputStreamReader(reader);
447: wrapper = (ScriptWrapper) saver.fromXML(inputStreamReader);
448: inputStreamReader.close();
449: return wrapper.testPlan;
450: } catch (CannotResolveClassException e) {
451: log.warn("Problem loading new style: "
452: + e.getLocalizedMessage());
453: reader.reset();
454: return OldSaveService.loadSubTree(reader);
455: } catch (NoClassDefFoundError e) {
456: log.warn("Missing class ", e);
457: return null;
458: }
459: }
460:
461: private static InputStreamReader getInputStreamReader(
462: InputStream inStream) {
463: // Check if we have a encoding to use from properties
464: Charset charset = getFileEncodingCharset();
465: if (charset != null) {
466: return new InputStreamReader(inStream, charset);
467: } else {
468: // We use the default character set encoding of the JRE
469: return new InputStreamReader(inStream);
470: }
471: }
472:
473: private static OutputStreamWriter getOutputStreamWriter(
474: OutputStream outStream) {
475: // Check if we have a encoding to use from properties
476: Charset charset = getFileEncodingCharset();
477: if (charset != null) {
478: return new OutputStreamWriter(outStream, charset);
479: } else {
480: // We use the default character set encoding of the JRE
481: return new OutputStreamWriter(outStream);
482: }
483: }
484:
485: /**
486: * Returns the file Encoding specified in saveservice.properties or the default
487: * @param dflt value to return if file encoding was not provided
488: *
489: * @return file encoding or default
490: */
491: // Used by ResultCollector when creating output files
492: public static String getFileEncoding(String dflt) {
493: if (fileEncoding != null && fileEncoding.length() > 0) {
494: return fileEncoding;
495: } else {
496: return dflt;
497: }
498: }
499:
500: private static Charset getFileEncodingCharset() {
501: // Check if we have a encoding to use from properties
502: if (fileEncoding != null && fileEncoding.length() > 0) {
503: return Charset.forName(fileEncoding);
504: } else {
505: // We use the default character set encoding of the JRE
506: return null;
507: }
508: }
509:
510: private static void writeXmlHeader(OutputStreamWriter writer)
511: throws IOException {
512: // Write XML header if we have the charset to use for encoding
513: Charset charset = getFileEncodingCharset();
514: if (charset != null) {
515: // We do not use getEncoding method of Writer, since that returns
516: // the historical name
517: String header = XML_HEADER.replaceAll("<ph>", charset
518: .name());
519: writer.write(header);
520: writer.write('\n');
521: }
522: }
523:
524: public static boolean isSaveTestPlanFormat20() {
525: return IS_TESTPLAN_FORMAT_20;
526: }
527:
528: public static boolean isSaveTestLogFormat20() {
529: return IS_TESTLOG_FORMAT_20;
530: }
531:
532: // New test format - more compressed class names
533: public static boolean isSaveTestPlanFormat22() {
534: return IS_TESTPLAN_FORMAT_22;
535: }
536:
537: // Normal output
538: // ---- Debugging information ----
539: // required-type : org.apache.jorphan.collections.ListedHashTree
540: // cause-message : WebServiceSampler : WebServiceSampler
541: // class : org.apache.jmeter.save.ScriptWrapper
542: // message : WebServiceSampler : WebServiceSampler
543: // line number : 929
544: // path : /jmeterTestPlan/hashTree/hashTree/hashTree[4]/hashTree[5]/WebServiceSampler
545: // cause-exception : com.thoughtworks.xstream.alias.CannotResolveClassException
546: // -------------------------------
547:
548: /**
549: * Simplify getMessage() output from XStream ConversionException
550: * @param ce - ConversionException to analyse
551: * @return string with details of error
552: */
553: public static String CEtoString(ConversionException ce) {
554: String msg = "XStream ConversionException at line: "
555: + ce.get("line number") + "\n" + ce.get("message")
556: + "\nPerhaps a missing jar? See log file.";
557: return msg;
558: }
559:
560: public static String getPropertiesVersion() {
561: return propertiesVersion;
562: }
563:
564: public static String getVERSION() {
565: return VERSION;
566: }
567: }
|