All Downloads are FREE. Search and download functionalities are using the official Maven repository.

patterntesting.runtime.log.SequenceGrapher Maven / Gradle / Ivy

Go to download

PatternTesting Runtime (patterntesting-rt) is the runtime component for the PatternTesting framework. It provides the annotations and base classes for the PatternTesting testing framework (e.g. patterntesting-check, patterntesting-concurrent or patterntesting-exception) but can be also used standalone for classpath monitoring or profiling. It uses AOP and AspectJ to perform this feat.

There is a newer version: 2.4.0
Show newest version
/*
 * $Id: SequenceGrapher.java,v 1.30 2014/03/22 21:32:48 oboehm Exp $
 *
 * Copyright (c) 2013 by Oliver Boehm
 *
 * 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 orimplied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * (c)reated 06.09.2013 by oliver ([email protected])
 */

package patterntesting.runtime.log;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.Map.Entry;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.*;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.ConstructorSignature;
import org.slf4j.*;

import patterntesting.annotation.check.runtime.NullArgsAllowed;
import patterntesting.runtime.exception.NotFoundException;
import patterntesting.runtime.util.*;
import patterntesting.runtime.util.regex.TypePattern;


/**
 * This class supports the creation of simple sequence diagrams as desribed
 * in the user manual of UML Graph.
 *
 * @author oliver
 * @since 1.3.1 (06.09.2013)
 */
public class SequenceGrapher extends AbstractLogger {

    private static final Logger log = LoggerFactory.getLogger(SequenceGrapher.class);
    private final Writer writer;
    private final List statements = new ArrayList();
    private final Map objnames = new HashMap();
    private final Map placeHolderNames = new HashMap();
    private final Map varnames = new HashMap();
    private final Stack callerNames = new Stack();
    private TypePattern[] excludeFilter = new TypePattern[0];
    private String active = "";
    private int objectNumber = 0;

    /**
     * Instantiates a new SequenceGrapher. The generated sequence diagram is
     * stored in a temporary file.
     */
    public SequenceGrapher() {
        this(createTempLogFile("seq-diagram", ".pic"));
    }

    /**
     * Instantiates a new SequenceGrapher. The generated sequence diagram is
     * stored in the given log file.
     *
     * @param logFile the log file
     */
    public SequenceGrapher(final File logFile) {
        this(logFile, "seq-head.template");
    }

    /**
     * Instantiates a new sequence grapher. The header for the generated diagram
     * is read from the given resource:
     * 
    *
  • "seq-head.template" (default): default header with with * needed "sequence.pic" and comments inside
  • *
  • "seq-head-small.template": a sort header with copy * instruction for the needed "sequence.pic" and without any * comments.
  • *
* * @param logFile the log file * @param resource the resource ("seq-head.template" or "seq-head-small") */ public SequenceGrapher(final File logFile, final String resource) { this(getStreamFor(logFile), resource); log.info("Sequence diagram will be written to \"{}\" with header from \"{}\".", logFile, resource); } /** * Instantiates a new object logger to the given stream. * * @param ostream the ostream */ public SequenceGrapher(final OutputStream ostream) { this(ostream, "seq-head.template"); } /** * /** * Instantiates a new sequence grapher. The header for the generated diagram * is read from the given resource: *
    *
  • "seq-head.template" (default): default header with with * needed "sequence.pic" and comments inside
  • *
  • "seq-head-small.template": a sort header with copy * instruction for the needed "sequence.pic" and without any * comments.
  • *
* * @param ostream the ostream * @param resource the resource ("seq-head.template" or "seq-head-small") */ public SequenceGrapher(final OutputStream ostream, final String resource) { super(ostream); this.writer = new BufferedWriter(new OutputStreamWriter(ostream, Charset.forName("ISO-8859-1"))); this.writeTemplate(resource); } private void writeTemplate(final String resource) { InputStream istream = this.getClass().getResourceAsStream(resource); if (istream == null) { log.warn("Resource \"{}\" not found - content will be missing in generated diagram.", resource); } else { try { String head = IOUtils.toString(istream, "ISO-8859-1"); this.writer.write(head); } catch (IOException ioe) { log.warn("Content of \"" + resource + "\" will be missing in generated diagram.", ioe); } finally { IOUtils.closeQuietly(istream); } } } /** * Sets the exclude filter. Classes which matches the filter will not appear * in the generated sequence diagram. * * @param pattern the new exclude filter * @since 1.4.1 */ public void setExcludeFilter(final String[] pattern) { this.excludeFilter = new TypePattern[pattern.length]; for (int i = 0; i < pattern.length; i++) { this.excludeFilter[i] = new TypePattern(pattern[i]); } } /** * This method is called at shutdown to close the open writer. * * @see java.lang.Thread#run() */ @Override public void run() { this.closeQuietly(); super.run(); } /** * Closes the stream with the logged objects. */ @Override public void close() { this.closeQuietly(); super.close(); } private void closeQuietly() { this.sortOutEmptyCreateMessages(); this.writeDefines(); this.writeMessages(); this.completeObjects(); this.writeTemplate("seq-tail.template"); IOUtils.closeQuietly(this.writer); } /** * Here we prepare the cached statements to the file. If a create message is * found with no other activities this creation will be sorted out to keep the * generated sequence diagram simple. */ private void sortOutEmptyCreateMessages() { List emptyCreateMessages = new ArrayList(); for (DrawStatement stmt : this.statements) { if ((stmt.getType() == DrawType.CREATE_MESSAGE) && !hasActivities(stmt)) { log.debug("{} will be ignored because it is a single statement.", stmt); emptyCreateMessages.add(stmt); removeValue(this.placeHolderNames, stmt.getTarget()); removeValue(this.varnames, stmt.getTarget()); } } this.statements.removeAll(emptyCreateMessages); } private static void removeValue(final Map map, final String name) { for (Map.Entry entry : map.entrySet()) { if (name.equals(entry.getValue())) { map.remove(entry.getKey()); break; } } } private void writeDefines() { SortedMap sortedObjnames = new TreeMap( new VarnameComparator()); for (Entry entry : objnames.entrySet()) { sortedObjnames.put(entry.getValue(), entry.getKey()); } for (Entry entry : sortedObjnames.entrySet()) { Class clazz = entry.getValue().getClass(); if (entry.getValue() instanceof Class) { clazz = (Class) entry.getValue(); } this.writeLine(getBoxwidStatementFor(clazz.getSimpleName())); this.writeLine("object(" + entry.getKey() + ",\":" + clazz.getSimpleName() + "\");"); } Collection objectNames = new TreeSet(new VarnameComparator()); objectNames.addAll(this.placeHolderNames.values()); for (String name : objectNames) { this.writeLine("placeholder_object(" + name + ");"); } } private static String getBoxwidStatementFor(final String name) { return "boxwid = " + getBoxwidFor(name) + ";"; } private static String getBoxwidFor(final String name) { int length = name.length() + 1; if (length > 16) { return "1.5"; } else if (length > 10) { return "1.0"; } else { return "0.75"; } } private void writeMessages() { this.writeLine("step();"); this.writeLine(""); this.writeComment("Message sequences"); if (this.statements.isEmpty()) { log.debug("No draw statemtents are logged."); return; } DrawStatement previous = this.statements.get(0); previous.writeStatement(this.writer); for (int i = 1; i < this.statements.size(); i++) { DrawStatement stmt = this.statements.get(i); if (previous.hasMessageToLeft() && stmt.hasMessageToRight()) { this.writeLine("step();"); } if (stmt.hasMessage()) { previous = stmt; } stmt.writeStatement(this.writer); } } /** * If the target of the given statement is part of any other * statement 'true' will be returned. * * @param drawStatement the draw statement * @return true, if successful */ private boolean hasActivities(final DrawStatement drawStatement) { String target = drawStatement.getTarget(); for (DrawStatement stmt : this.statements) { if (drawStatement.equals(stmt)) { continue; } if (stmt.hasActor(target)) { return true; } } return false; } private void completeObjects() { this.writeLine(""); this.writeComment("Complete the lifelines"); this.writeLine("step();"); Collection names = new TreeSet(this.varnames.values()); for (String name : names) { this.writeLine("complete(" + name + ");"); } } /** * Logs the creation of an object in the created sequence diagram. * * @param call the call * @param result the created object */ public void createMessage(final JoinPoint call, final Object result) { this.addComment(JoinPointHelper.getAsLongString(call) + " = " + result); Object creator = call.getThis(); if (creator == null) { String classname = JoinPointHelper.getCallerOf(call).getClassName(); try { creator = Class.forName(classname); } catch (ClassNotFoundException ex) { throw new NotFoundException(classname, ex); } } this.createMessage(creator, result); } /** * Logs the creation of an object in the created sequence diagram. * * @param creator the creator * @param createdObject the created object */ public void createMessage(final Object creator, final Object createdObject) { if (this.matches(creator) || this.matches(createdObject)) { log.debug("{} --creates--> {} is not logged because of exclude filter.", creator, createdObject); return; } String name = this.varnames.get(createdObject); String typeName = this.addPlaceHolder(createdObject); if (name != null) { log.trace("Creation of {} is already logged.", createdObject); this.objnames.remove(createdObject); return; } name = this.getVarnameFor(creator); Class type = createdObject.getClass(); this.addCreateMessage(name, type, typeName); } private boolean matches(final Object creator) { for (int i = 0; i < this.excludeFilter.length; i++) { if (this.excludeFilter[i].matches(creator)) { return true; } } return false; } private String addPlaceHolder(final Object obj) { String name = this.placeHolderNames.get(obj); if (name == null) { name = this.objnames.get(obj); if (name == null) { name = this.addVarnameFor(obj); this.placeHolderNames.put(obj, name); } else { //this.objnames.remove(obj); this.placeHolderNames.put(obj, name); } } return name; } private String addObject(final Object obj) { String name = this.addVarnameFor(obj); objnames.put(obj, name); return name; } @NullArgsAllowed private String getVarnameFor(final Object obj) { if (obj == null) { return getActorName(); } String name = this.varnames.get(obj); if (name == null) { if (obj instanceof Class) { name = this.getVarnameFor((Class) obj); } else { name = this.varnames.get(obj.getClass()); } } if (name == null) { name = addObject(obj); } return name; } private String getVarnameFor(final Class clazz) { String name = this.varnames.get(clazz); if (name == null) { for (Entry entry : this.varnames.entrySet()) { if (clazz.equals(entry.getKey().getClass())) { return entry.getValue(); } } name = addObject(clazz); } return name; } private String getActorName() { String name = this.varnames.get("Actor"); if (name == null) { name = addVarnameFor("Actor"); this.writeLine("actor(" + name + ",\"\");"); } return name; } private String addVarnameFor(final Object obj) { if (obj instanceof Class) { return addVarnameFor((Class) obj); } String name = toName(obj.getClass()); return addVarname(name, obj); } private String addVarnameFor(final Class clazz) { String name = toName(clazz); return addVarname(name, clazz); } private String addVarname(final String name, final Object obj) { if (this.varnames.containsKey(obj)) { log.trace("{} already in map of var names.", obj); } else { this.varnames.put(obj, name); this.objectNumber++; } return this.varnames.get(obj); } private String toName(final Class clazz) { return clazz.getSimpleName().substring(0, 1).toUpperCase() + Integer.toString(this.objectNumber, Character.MAX_RADIX); } /** * This is the opposite to the toName() method and returns the number part * of a variable name. * * @param name the name * @return the string */ private static String toNumber(final String name) { return name.substring(1); } // /** // * A message points to left, if the given senderName was created after // * the given targetName. This information can be derived from the name. // * // * @param senderName the sender name // * @param targetName the target name // * @return true, if successful // */ // private static boolean messagePointsToLeft(final String senderName, final String targetName) { // return toNumber(senderName).compareTo(toNumber(targetName)) > 0; // } private void setActive(final String name) { this.statements.add(new DrawStatement(DrawType.ACTIVE, name)); this.active = name; } private boolean isActive(final String name) { return this.active.equals(name); } /** * Trys to log the call of the given excecution joinpoint. For this reason * we must find the caller which is a little bit tricky. We use the * classname of the mapped variable names to guess which could be the * caller. * * @param execution the execution joinpoint */ public void execute(final JoinPoint execution) { this.addComment(JoinPointHelper.getAsLongString(execution)); String senderName = getCallerNameOf(execution); String targetName = getTargetName(execution); if (execution.getSignature() instanceof ConstructorSignature) { this.addCreateMessage(senderName, execution.getThis().getClass(), targetName); } else { this.message(senderName, targetName, execution.getSignature().getName(), execution.getArgs()); } } private String getTargetName(final JoinPoint execution) { Object thisObject = execution.getThis(); if (thisObject == null) { StackTraceElement element = StackTraceScanner.find(execution.getSignature()); try { return this.getVarnameFor(Class.forName(element.getClassName())); } catch (ClassNotFoundException ex) { log.debug("Classname of " + element + " not found.", ex); return this.getVarnameFor(null); } } else { return this.getVarnameFor(thisObject); } } private String getCallerNameOf(final JoinPoint execution) { StackTraceElement caller = JoinPointHelper.getCallerOf(execution); String classname = caller.getClassName(); for (Entry entry : varnames.entrySet()) { if (classname.equals(entry.getKey().getClass().getName())) { log.trace("Caller of {} is {}.", execution, entry); return entry.getValue(); } } log.trace("Caller of {} not found in {}.", execution, varnames); try { return this.addObject(Class.forName(caller.getClassName())); } catch (ClassNotFoundException ex) { log.info("cannot get class for {} because of {}.", caller, ex.getMessage()); return getActorName(); } } /** * Logs the call of a method to the generated sequence diagram. * * @param call the call */ public void message(final JoinPoint call) { this.message(call.getThis(), call); } /** * Logs the call of a method to the generated sequence diagram. * * @param caller the caller * @param call the call */ public void message(final Object caller, final JoinPoint call) { this.addComment(JoinPointHelper.getAsLongString(call)); this.message(caller, call.getTarget(), call.getSignature().getName(), call.getArgs()); } /** * Logs the call of a method to the generated sequence diagram. * * @param sender the sender * @param target the target * @param methodName the method name * @param args the args */ public void message(final Object sender, final Object target, final String methodName, final Object[] args) { if (this.matches(sender) || this.matches(target)) { log.debug("{} -----------> {} is not logged because of exclude filter.", sender, target); return; } String senderName = this.getVarnameFor(sender); String targetName = this.getVarnameFor(target); this.message(senderName, targetName, methodName, args); } private void message(final String senderName, final String targetName, final String methodName, final Object[] args) { this.addMessage(senderName, targetName, methodName, args); } private static String getArgsAsString(final Object... args) { return StringEscapeUtils.escapeJava(Converter.toShortString(JoinPointHelper .getArgsAsString(args))); } /** * Logs the return arrow from the last call to the generated sequence * diagram. * * @param call the call */ public void returnMessage(final JoinPoint call) { this.returnMessage(call, ""); } /** * Logs the return arrow from the last call to the generated sequence * diagram. * * @param call the call * @param returnValue the return value */ public void returnMessage(final JoinPoint call, final Object returnValue) { this.addComment(call.toLongString() + " = " + returnValue); //writeComment(call.toLongString() + " = " + returnValue, cache); this.returnMessage(call.getTarget(), returnValue); } /** * Return message. * * @param returnee the returnee * @param returnValue the return value */ public void returnMessage(final Object returnee, final Object returnValue) { if (this.matches(returnee)) { log.debug("{} <--{}-- is not logged because of exclude filter.", returnee, returnValue); return; } this.addReturnMessage(returnee, returnValue); } private static String toEscapedString(final Object returnValue) { if (returnValue == null) { return "null"; } return StringEscapeUtils.escapeJava(returnValue.toString()); } private void writeComment(final String comment) { this.writeLine("# " + comment); } private void writeLine(final String line) { writeLine(line, this.writer); } private static void writeLine(final String line, final Writer lineWriter) { try { lineWriter.write(line); lineWriter.write("\n"); } catch (IOException ioe) { log.debug("Writing to {} failed because of {}.", lineWriter, ioe.getMessage()); log.info(line); } } private void addComment(final String comment) { DrawStatement stmt = new DrawStatement(DrawType.COMMENT, comment); this.statements.add(stmt); } private void addCreateMessage(final String senderName, final Class type, final String typeName) { if (!isActive(senderName)) { this.setActive(senderName); } DrawStatement stmt = new DrawStatement(senderName, type, typeName); this.statements.add(stmt); } private void addMessage(final String senderName, final String targetName, final String methodName, final Object[] args) { this.callerNames.push(senderName); DrawStatement stmt = new DrawStatement(senderName, targetName, methodName, args); this.statements.add(stmt); } private void addReturnMessage(final Object returnee, final Object returnValue) { String receiverName = this.callerNames.pop(); String returneeName = this.getVarnameFor(returnee); DrawStatement stmt = new DrawStatement(receiverName, returneeName, returnValue); this.statements.add(stmt); } /** * The Class VarnameComparator compares two variable names. The name is of * the form "A999...", e.g. first character is a (uppercase) letter and the * next characters are a number (to base {@link Character#MAX_RADIX}). Only * the number are used for comparison to get the creation order. */ private static final class VarnameComparator implements Comparator, Serializable { private static final long serialVersionUID = 20140104L; /** * Compares two variable names. * * @param x1 the x1 * @param x2 the x2 * @return a negativ valule if x1 < x2, "0" for x1 == x2, otherwise * positive value * @see java.util.Comparator#compare(Object, Object) */ public int compare(final String x1, final String x2) { return toNumber(x1) - toNumber(x2); } private static int toNumber(final String varname) { String numberPart = varname.substring(1); return Integer.parseInt(numberPart, Character.MAX_RADIX); } } //------------------------------------------------------------------------- /** * The different types of drawings. */ private enum DrawType { UNKNOWN, COMMENT, ACTIVE, CREATE_MESSAGE, MESSAGE, RETURN_MESSAGE; } /** * Internal class for caching the different draw statements. * * @author oliver * @since 1.4.1 (17.01.2014) */ private static class DrawStatement { private final DrawType type; private final String sender; private final String target; private final String methodName; private final Object[] args; private final String comment; /** * Instantiates a new draw statement. * * @param type the type * @param comment the comment or active argument */ public DrawStatement(final DrawType type, final String comment) { this.type = type; this.sender = comment; this.target = null; this.methodName = null; this.args = null; this.comment = comment; } /** * Instantiates a new draw statement. * * @param senderName the sender name * @param createdClass the created class * @param typeName the type name */ public DrawStatement(final String senderName, final Class createdClass, final String typeName) { this.type = DrawType.CREATE_MESSAGE; this.sender = senderName; this.target = typeName; this.methodName = null; this.args = createObjectArray(createdClass); this.comment = null; } /** * Instantiates a new draw statement. * * @param sender the sender * @param target the target * @param name the name * @param args the args */ public DrawStatement(final String sender, final String target, final String name, final Object[] args) { this.type = DrawType.MESSAGE; this.sender = sender; this.target = target; this.methodName = name; this.args = args; this.comment = null; } /** * Instantiates a new draw statement. * * @param receiverName the receiver name * @param returnee the returnee * @param returnValue the return value */ public DrawStatement(final String receiverName, final String returnee, final Object returnValue) { this.type = DrawType.RETURN_MESSAGE; this.sender = receiverName; this.target = returnee; this.methodName = null; this.args = createObjectArray(returnValue); this.comment = null; } private Object[] createObjectArray(final Object...objects) { return objects; } /** * Gets the type. * * @return the type */ public DrawType getType() { return this.type; } /** * Gets the target. * * @return the target */ public String getTarget() { return this.target; } /** * If the given name is part of any stored actor in this statement * 'true' will be returned. * * @param name the name * @return true, if is involved */ public boolean hasActor(final String name) { switch (this.type) { case CREATE_MESSAGE: case MESSAGE: return name.equals(this.target); case RETURN_MESSAGE: return name.equals(this.sender) || name.equals(this.target); default: return false; } } /** * Checks for message. * * @return true, if successful */ public boolean hasMessage() { switch (this.type) { case CREATE_MESSAGE: case MESSAGE: case RETURN_MESSAGE: return true; default: return false; } } /** * A message points to left, if the given senderName was created after * the given targetName. This information can be derived from the name. * * @return true, if successful */ public boolean hasMessageToLeft() { switch (this.type) { case CREATE_MESSAGE: case MESSAGE: return toNumber(this.sender).compareTo(toNumber(this.target)) > 0; case RETURN_MESSAGE: return toNumber(this.target).compareTo(toNumber(this.sender)) > 0; default: return false; } } /** * Checks for message to right. * * @return true, if successful */ public boolean hasMessageToRight() { switch (this.type) { case CREATE_MESSAGE: case MESSAGE: case RETURN_MESSAGE: return !this.hasMessageToLeft(); default: return false; } } /** * Write statement. * * @param writer the writer */ public void writeStatement(final Writer writer) { switch (type) { case COMMENT: this.writeComment(writer); break; case ACTIVE: this.writeActive(writer); break; case CREATE_MESSAGE: this.writeCreateMessage(writer); break; case MESSAGE: this.writeMessage(writer); break; case RETURN_MESSAGE: this.writeReturnMessage(writer); break; default: log.warn("{} statement is ignored.", type); break; } } /** * Write comment. * * @param writer the writer */ public void writeComment(final Writer writer) { writeLine(this.toString(), writer); } /** * Write active. * * @param writer the writer */ public void writeActive(final Writer writer) { writeLine(this.toString(), writer); } /** * Write create message. * * @param writer the writer */ public void writeCreateMessage(final Writer writer) { String classname = getTargetType(); writeLine(getBoxwidStatementFor(classname), writer); writeLine(this.toString(), writer); } private String getTargetType() { Class targetType = (Class) this.args[0]; return targetType.getSimpleName(); } /** * Write message. * * @param writer the writer */ public void writeMessage(final Writer writer) { if (this.hasMessageToLeft()) { writeLine("step();", writer); } writeLine(this.toString(), writer); writeLine("active(" + this.target + ");", writer); } /** * Write return message. * * @param writer the writer */ public void writeReturnMessage(final Writer writer) { if (this.hasMessageToLeft()) { writeLine("step();", writer); } writeLine(this.toString(), writer); writeLine("inactive(" + this.target + ");", writer); } /** * Hash code. * * @return the int * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return this.type.hashCode() + ((this.sender == null) ? 0 : this.sender.hashCode()); } /** * Equals. * * @param obj the obj * @return true, if successful * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (!(obj instanceof DrawStatement)) { return false; } DrawStatement other = (DrawStatement) obj; return (this.type == other.type) && StringUtils.equals(this.sender, other.sender) && StringUtils.equals(this.target, other.target) && StringUtils.equals(this.methodName, other.methodName) && StringUtils.equals(this.comment, other.comment) && Arrays.equals(this.args, other.args); } /** * To string. * * @return the string * @see java.lang.Object#toString() */ @Override public String toString() { switch (type) { case COMMENT: return "# " + this.comment; case ACTIVE: return "active(" + this.sender + ");"; case CREATE_MESSAGE: String classname = this.getTargetType(); return "create_message(" + this.sender + "," + this.target + ",\":" + classname + "\");"; case MESSAGE: return "message(" + this.sender + "," + this.target + ",\"" + this.methodName + getArgsAsString(this.args) + "\");"; case RETURN_MESSAGE: return "return_message(" + this.target + "," + this.sender + ",\"" + toEscapedString(Converter.toShortString(this.args[0])) + "\");"; default: return "# " + this.type + " statement"; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy