001    /*
002     * $Id: SequenceGrapher.java,v 1.30 2014/03/22 21:32:48 oboehm Exp $
003     *
004     * Copyright (c) 2013 by Oliver Boehm
005     *
006     * Licensed under the Apache License, Version 2.0 (the "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     *
018     * (c)reated 06.09.2013 by oliver (ob@oasd.de)
019     */
020    
021    package patterntesting.runtime.log;
022    
023    import java.io.*;
024    import java.nio.charset.Charset;
025    import java.util.*;
026    import java.util.Map.Entry;
027    
028    import org.apache.commons.io.IOUtils;
029    import org.apache.commons.lang.*;
030    import org.aspectj.lang.JoinPoint;
031    import org.aspectj.lang.reflect.ConstructorSignature;
032    import org.slf4j.*;
033    
034    import patterntesting.annotation.check.runtime.NullArgsAllowed;
035    import patterntesting.runtime.exception.NotFoundException;
036    import patterntesting.runtime.util.*;
037    import patterntesting.runtime.util.regex.TypePattern;
038    
039    
040    /**
041     * This class supports the creation of simple sequence diagrams as desribed
042     * in the user manual of <a href="http://umlgraph.org/">UML Graph</a>.
043     *
044     * @author oliver
045     * @since 1.3.1 (06.09.2013)
046     */
047    public class SequenceGrapher extends AbstractLogger {
048    
049        private static final Logger log = LoggerFactory.getLogger(SequenceGrapher.class);
050        private final Writer writer;
051        private final List<DrawStatement> statements = new ArrayList<SequenceGrapher.DrawStatement>();
052        private final Map<Object, String> objnames = new HashMap<Object, String>();
053        private final Map<Object, String> placeHolderNames = new HashMap<Object, String>();
054        private final Map<Object, String> varnames = new HashMap<Object, String>();
055        private final Stack<String> callerNames = new Stack<String>();
056        private TypePattern[] excludeFilter = new TypePattern[0];
057        private String active = "";
058        private int objectNumber = 0;
059    
060        /**
061         * Instantiates a new SequenceGrapher. The generated sequence diagram is
062         * stored in a temporary file.
063         */
064        public SequenceGrapher() {
065            this(createTempLogFile("seq-diagram", ".pic"));
066        }
067    
068        /**
069         * Instantiates a new SequenceGrapher. The generated sequence diagram is
070         * stored in the given log file.
071         *
072         * @param logFile the log file
073         */
074        public SequenceGrapher(final File logFile) {
075            this(logFile, "seq-head.template");
076        }
077    
078        /**
079         * Instantiates a new sequence grapher. The header for the generated diagram
080         * is read from the given resource:
081         * <ul>
082         * <li><code>"seq-head.template"</code> (default): default header with with
083         * needed <code>"sequence.pic"</code> and comments inside</li>
084         * <li><code>"seq-head-small.template"</code>: a sort header with copy
085         * instruction for the needed <code>"sequence.pic"</code> and without any
086         * comments.</li>
087         * </ul>
088         *
089         * @param logFile the log file
090         * @param resource the resource ("seq-head.template" or "seq-head-small")
091         */
092        public SequenceGrapher(final File logFile, final String resource) {
093            this(getStreamFor(logFile), resource);
094            log.info("Sequence diagram will be written to \"{}\" with header from \"{}\".", logFile,
095                    resource);
096        }
097    
098        /**
099         * Instantiates a new object logger to the given stream.
100         *
101         * @param ostream the ostream
102         */
103        public SequenceGrapher(final OutputStream ostream) {
104            this(ostream, "seq-head.template");
105        }
106    
107        /**
108         * /**
109         * Instantiates a new sequence grapher. The header for the generated diagram
110         * is read from the given resource:
111         * <ul>
112         * <li><code>"seq-head.template"</code> (default): default header with with
113         * needed <code>"sequence.pic"</code> and comments inside</li>
114         * <li><code>"seq-head-small.template"</code>: a sort header with copy
115         * instruction for the needed <code>"sequence.pic"</code> and without any
116         * comments.</li>
117         * </ul>
118         *
119         * @param ostream the ostream
120         * @param resource the resource ("seq-head.template" or "seq-head-small")
121         */
122        public SequenceGrapher(final OutputStream ostream, final String resource) {
123            super(ostream);
124            this.writer = new BufferedWriter(new OutputStreamWriter(ostream,
125                    Charset.forName("ISO-8859-1")));
126            this.writeTemplate(resource);
127        }
128    
129        private void writeTemplate(final String resource) {
130            InputStream istream = this.getClass().getResourceAsStream(resource);
131            if (istream == null) {
132                log.warn("Resource \"{}\" not found - content will be missing in generated diagram.",
133                        resource);
134            } else {
135                try {
136                    String head = IOUtils.toString(istream, "ISO-8859-1");
137                    this.writer.write(head);
138                } catch (IOException ioe) {
139                    log.warn("Content of \"" + resource + "\" will be missing in generated diagram.",
140                            ioe);
141                } finally {
142                    IOUtils.closeQuietly(istream);
143                }
144            }
145        }
146    
147        /**
148         * Sets the exclude filter. Classes which matches the filter will not appear
149         * in the generated sequence diagram.
150         *
151         * @param pattern the new exclude filter
152         * @since 1.4.1
153         */
154        public void setExcludeFilter(final String[] pattern) {
155            this.excludeFilter = new TypePattern[pattern.length];
156            for (int i = 0; i < pattern.length; i++) {
157                this.excludeFilter[i] = new TypePattern(pattern[i]);
158            }
159        }
160    
161        /**
162         * This method is called at shutdown to close the open writer.
163         *
164         * @see java.lang.Thread#run()
165         */
166        @Override
167        public void run() {
168            this.closeQuietly();
169            super.run();
170        }
171    
172        /**
173         * Closes the stream with the logged objects.
174         */
175        @Override
176        public void close() {
177            this.closeQuietly();
178            super.close();
179        }
180    
181        private void closeQuietly() {
182            this.sortOutEmptyCreateMessages();
183            this.writeDefines();
184            this.writeMessages();
185            this.completeObjects();
186            this.writeTemplate("seq-tail.template");
187            IOUtils.closeQuietly(this.writer);
188        }
189    
190        /**
191         * Here we prepare the cached statements to the file. If a create message is
192         * found with no other activities this creation will be sorted out to keep the
193         * generated sequence diagram simple.
194         */
195        private void sortOutEmptyCreateMessages() {
196            List<DrawStatement> emptyCreateMessages = new ArrayList<SequenceGrapher.DrawStatement>();
197            for (DrawStatement stmt : this.statements) {
198                if ((stmt.getType() == DrawType.CREATE_MESSAGE) && !hasActivities(stmt)) {
199                    log.debug("{} will be ignored because it is a single statement.", stmt);
200                    emptyCreateMessages.add(stmt);
201                    removeValue(this.placeHolderNames, stmt.getTarget());
202                    removeValue(this.varnames, stmt.getTarget());
203                }
204            }
205            this.statements.removeAll(emptyCreateMessages);
206        }
207    
208        private static void removeValue(final Map<Object, String> map, final String name) {
209            for (Map.Entry<Object, String> entry : map.entrySet()) {
210                if (name.equals(entry.getValue())) {
211                    map.remove(entry.getKey());
212                    break;
213                }
214            }
215        }
216    
217        private void writeDefines() {
218            SortedMap<String, Object> sortedObjnames = new TreeMap<String, Object>(
219                    new VarnameComparator());
220            for (Entry<Object, String> entry : objnames.entrySet()) {
221                sortedObjnames.put(entry.getValue(), entry.getKey());
222            }
223            for (Entry<String, Object> entry : sortedObjnames.entrySet()) {
224                Class<?> clazz = entry.getValue().getClass();
225                if (entry.getValue() instanceof Class<?>) {
226                    clazz = (Class<?>) entry.getValue();
227                }
228                this.writeLine(getBoxwidStatementFor(clazz.getSimpleName()));
229                this.writeLine("object(" + entry.getKey() + ",\":" + clazz.getSimpleName() + "\");");
230            }
231            Collection<String> objectNames = new TreeSet<String>(new VarnameComparator());
232            objectNames.addAll(this.placeHolderNames.values());
233            for (String name : objectNames) {
234                this.writeLine("placeholder_object(" + name + ");");
235            }
236        }
237    
238        private static String getBoxwidStatementFor(final String name) {
239            return "boxwid = " + getBoxwidFor(name) + ";";
240        }
241    
242        private static String getBoxwidFor(final String name) {
243            int length = name.length() + 1;
244            if (length > 16) {
245                return "1.5";
246            } else if (length > 10) {
247                return "1.0";
248            } else {
249                return "0.75";
250            }
251        }
252    
253        private void writeMessages() {
254            this.writeLine("step();");
255            this.writeLine("");
256            this.writeComment("Message sequences");
257            if (this.statements.isEmpty()) {
258                log.debug("No draw statemtents are logged.");
259                return;
260            }
261            DrawStatement previous = this.statements.get(0);
262            previous.writeStatement(this.writer);
263            for (int i = 1; i < this.statements.size(); i++) {
264                DrawStatement stmt = this.statements.get(i);
265                if (previous.hasMessageToLeft() && stmt.hasMessageToRight()) {
266                    this.writeLine("step();");
267                }
268                if (stmt.hasMessage()) {
269                    previous = stmt;
270                }
271                stmt.writeStatement(this.writer);
272            }
273        }
274    
275        /**
276         * If the target of the given statement is part of any other
277         * statement 'true' will be returned.
278         *
279         * @param drawStatement the draw statement
280         * @return true, if successful
281         */
282        private boolean hasActivities(final DrawStatement drawStatement) {
283            String target = drawStatement.getTarget();
284            for (DrawStatement stmt : this.statements) {
285                if (drawStatement.equals(stmt)) {
286                    continue;
287                }
288                if (stmt.hasActor(target)) {
289                    return true;
290                }
291            }
292            return false;
293        }
294    
295        private void completeObjects() {
296            this.writeLine("");
297            this.writeComment("Complete the lifelines");
298            this.writeLine("step();");
299            Collection<String> names = new TreeSet<String>(this.varnames.values());
300            for (String name : names) {
301                this.writeLine("complete(" + name + ");");
302            }
303        }
304    
305        /**
306         * Logs the creation of an object in the created sequence diagram.
307         *
308         * @param call the call
309         * @param result the created object
310         */
311        public void createMessage(final JoinPoint call, final Object result) {
312            this.addComment(JoinPointHelper.getAsLongString(call) + " = " + result);
313            Object creator = call.getThis();
314            if (creator == null) {
315                String classname = JoinPointHelper.getCallerOf(call).getClassName();
316                try {
317                    creator = Class.forName(classname);
318                } catch (ClassNotFoundException ex) {
319                    throw new NotFoundException(classname, ex);
320                }
321            }
322            this.createMessage(creator, result);
323        }
324    
325        /**
326         * Logs the creation of an object in the created sequence diagram.
327         *
328         * @param creator the creator
329         * @param createdObject the created object
330         */
331        public void createMessage(final Object creator, final Object createdObject) {
332            if (this.matches(creator) || this.matches(createdObject)) {
333                log.debug("{} --creates--> {} is not logged because of exclude filter.", creator, createdObject);
334                return;
335            }
336            String name = this.varnames.get(createdObject);
337            String typeName = this.addPlaceHolder(createdObject);
338            if (name != null) {
339                log.trace("Creation of {} is already logged.", createdObject);
340                this.objnames.remove(createdObject);
341                return;
342            }
343            name = this.getVarnameFor(creator);
344            Class<?> type = createdObject.getClass();
345            this.addCreateMessage(name, type, typeName);
346        }
347    
348        private boolean matches(final Object creator) {
349            for (int i = 0; i < this.excludeFilter.length; i++) {
350                if (this.excludeFilter[i].matches(creator)) {
351                    return true;
352                }
353            }
354            return false;
355        }
356    
357        private String addPlaceHolder(final Object obj) {
358            String name = this.placeHolderNames.get(obj);
359            if (name == null) {
360                name = this.objnames.get(obj);
361                if (name == null) {
362                    name = this.addVarnameFor(obj);
363                    this.placeHolderNames.put(obj, name);
364                } else {
365                    //this.objnames.remove(obj);
366                    this.placeHolderNames.put(obj, name);
367                }
368            }
369            return name;
370        }
371    
372        private String addObject(final Object obj) {
373            String name = this.addVarnameFor(obj);
374            objnames.put(obj, name);
375            return name;
376        }
377    
378        @NullArgsAllowed
379        private String getVarnameFor(final Object obj) {
380            if (obj == null) {
381                return getActorName();
382            }
383            String name = this.varnames.get(obj);
384            if (name == null) {
385                if (obj instanceof Class<?>) {
386                    name = this.getVarnameFor((Class<?>) obj);
387                } else {
388                    name = this.varnames.get(obj.getClass());
389                }
390            }
391            if (name == null) {
392                name = addObject(obj);
393            }
394            return name;
395        }
396    
397        private String getVarnameFor(final Class<?> clazz) {
398            String name = this.varnames.get(clazz);
399            if (name == null) {
400                for (Entry<Object, String> entry : this.varnames.entrySet()) {
401                    if (clazz.equals(entry.getKey().getClass())) {
402                        return entry.getValue();
403                    }
404                }
405                name = addObject(clazz);
406            }
407            return name;
408        }
409    
410        private String getActorName() {
411            String name = this.varnames.get("Actor");
412            if (name == null) {
413                name = addVarnameFor("Actor");
414                this.writeLine("actor(" + name + ",\"\");");
415            }
416            return name;
417        }
418    
419        private String addVarnameFor(final Object obj) {
420            if (obj instanceof Class<?>) {
421                return addVarnameFor((Class<?>) obj);
422            }
423            String name = toName(obj.getClass());
424            return addVarname(name, obj);
425        }
426    
427        private String addVarnameFor(final Class<?> clazz) {
428            String name = toName(clazz);
429            return addVarname(name, clazz);
430        }
431    
432        private String addVarname(final String name, final Object obj) {
433            if (this.varnames.containsKey(obj)) {
434                log.trace("{} already in map of var names.", obj);
435            } else {
436                this.varnames.put(obj, name);
437                this.objectNumber++;
438            }
439            return this.varnames.get(obj);
440        }
441    
442        private String toName(final Class<?> clazz) {
443            return clazz.getSimpleName().substring(0, 1).toUpperCase()
444                    + Integer.toString(this.objectNumber, Character.MAX_RADIX);
445        }
446    
447        /**
448         * This is the opposite to the toName() method and returns the number part
449         * of a variable name.
450         *
451         * @param name the name
452         * @return the string
453         */
454        private static String toNumber(final String name) {
455            return name.substring(1);
456        }
457    
458    //    /**
459    //     * A message points to left, if the given senderName was created after
460    //     * the given targetName. This information can be derived from the name.
461    //     *
462    //     * @param senderName the sender name
463    //     * @param targetName the target name
464    //     * @return true, if successful
465    //     */
466    //    private static boolean messagePointsToLeft(final String senderName, final String targetName) {
467    //        return toNumber(senderName).compareTo(toNumber(targetName)) > 0;
468    //    }
469    
470        private void setActive(final String name) {
471            this.statements.add(new DrawStatement(DrawType.ACTIVE, name));
472            this.active = name;
473        }
474    
475        private boolean isActive(final String name) {
476            return this.active.equals(name);
477        }
478    
479        /**
480         * Trys to log the call of the given excecution joinpoint. For this reason
481         * we must find the caller which is a little bit tricky. We use the
482         * classname of the mapped variable names to guess which could be the
483         * caller.
484         *
485         * @param execution the execution joinpoint
486         */
487        public void execute(final JoinPoint execution) {
488            this.addComment(JoinPointHelper.getAsLongString(execution));
489            String senderName = getCallerNameOf(execution);
490            String targetName = getTargetName(execution);
491            if (execution.getSignature() instanceof ConstructorSignature) {
492                this.addCreateMessage(senderName, execution.getThis().getClass(), targetName);
493            } else {
494                this.message(senderName, targetName, execution.getSignature().getName(),
495                        execution.getArgs());
496            }
497        }
498    
499        private String getTargetName(final JoinPoint execution) {
500            Object thisObject = execution.getThis();
501            if (thisObject == null) {
502                StackTraceElement element = StackTraceScanner.find(execution.getSignature());
503                try {
504                    return this.getVarnameFor(Class.forName(element.getClassName()));
505                } catch (ClassNotFoundException ex) {
506                    log.debug("Classname of " + element + " not found.", ex);
507                    return this.getVarnameFor(null);
508                }
509            } else {
510                return this.getVarnameFor(thisObject);
511            }
512        }
513    
514        private String getCallerNameOf(final JoinPoint execution) {
515            StackTraceElement caller = JoinPointHelper.getCallerOf(execution);
516            String classname = caller.getClassName();
517            for (Entry<Object, String> entry : varnames.entrySet()) {
518                if (classname.equals(entry.getKey().getClass().getName())) {
519                    log.trace("Caller of {} is {}.", execution, entry);
520                    return entry.getValue();
521                }
522            }
523            log.trace("Caller of {} not found in {}.", execution, varnames);
524            try {
525                return this.addObject(Class.forName(caller.getClassName()));
526            } catch (ClassNotFoundException ex) {
527                log.info("cannot get class for {} because of {}.", caller, ex.getMessage());
528                return getActorName();
529            }
530        }
531    
532        /**
533         * Logs the call of a method to the generated sequence diagram.
534         *
535         * @param call the call
536         */
537        public void message(final JoinPoint call) {
538            this.message(call.getThis(), call);
539        }
540    
541        /**
542         * Logs the call of a method to the generated sequence diagram.
543         *
544         * @param caller the caller
545         * @param call the call
546         */
547        public void message(final Object caller, final JoinPoint call) {
548            this.addComment(JoinPointHelper.getAsLongString(call));
549            this.message(caller, call.getTarget(), call.getSignature().getName(), call.getArgs());
550        }
551    
552        /**
553         * Logs the call of a method to the generated sequence diagram.
554         *
555         * @param sender the sender
556         * @param target the target
557         * @param methodName the method name
558         * @param args the args
559         */
560        public void message(final Object sender, final Object target, final String methodName,
561                final Object[] args) {
562            if (this.matches(sender) || this.matches(target)) {
563                log.debug("{} -----------> {} is not logged because of exclude filter.", sender, target);
564                return;
565            }
566            String senderName = this.getVarnameFor(sender);
567            String targetName = this.getVarnameFor(target);
568            this.message(senderName, targetName, methodName, args);
569        }
570    
571        private void message(final String senderName, final String targetName, final String methodName,
572                final Object[] args) {
573            this.addMessage(senderName, targetName, methodName, args);
574        }
575    
576        private static String getArgsAsString(final Object... args) {
577            return StringEscapeUtils.escapeJava(Converter.toShortString(JoinPointHelper
578                    .getArgsAsString(args)));
579        }
580    
581        /**
582         * Logs the return arrow from the last call to the generated sequence
583         * diagram.
584         *
585         * @param call the call
586         */
587        public void returnMessage(final JoinPoint call) {
588            this.returnMessage(call, "");
589        }
590    
591        /**
592         * Logs the return arrow from the last call to the generated sequence
593         * diagram.
594         *
595         * @param call the call
596         * @param returnValue the return value
597         */
598        public void returnMessage(final JoinPoint call, final Object returnValue) {
599            this.addComment(call.toLongString() + " = " + returnValue);
600            //writeComment(call.toLongString() + " = " + returnValue, cache);
601            this.returnMessage(call.getTarget(), returnValue);
602        }
603    
604        /**
605         * Return message.
606         *
607         * @param returnee the returnee
608         * @param returnValue the return value
609         */
610        public void returnMessage(final Object returnee, final Object returnValue) {
611            if (this.matches(returnee)) {
612                log.debug("{} <--{}-- is not logged because of exclude filter.", returnee, returnValue);
613                return;
614            }
615            this.addReturnMessage(returnee, returnValue);
616        }
617    
618        private static String toEscapedString(final Object returnValue) {
619            if (returnValue == null) {
620                return "null";
621            }
622            return StringEscapeUtils.escapeJava(returnValue.toString());
623        }
624    
625        private void writeComment(final String comment) {
626            this.writeLine("# " + comment);
627        }
628    
629        private void writeLine(final String line) {
630            writeLine(line, this.writer);
631        }
632    
633        private static void writeLine(final String line, final Writer lineWriter) {
634            try {
635                lineWriter.write(line);
636                lineWriter.write("\n");
637            } catch (IOException ioe) {
638                log.debug("Writing to {} failed because of {}.", lineWriter, ioe.getMessage());
639                log.info(line);
640            }
641        }
642    
643        private void addComment(final String comment) {
644            DrawStatement stmt = new DrawStatement(DrawType.COMMENT, comment);
645            this.statements.add(stmt);
646        }
647    
648        private void addCreateMessage(final String senderName, final Class<?> type, final String typeName) {
649            if (!isActive(senderName)) {
650                this.setActive(senderName);
651            }
652            DrawStatement stmt = new DrawStatement(senderName, type, typeName);
653            this.statements.add(stmt);
654        }
655    
656        private void addMessage(final String senderName, final String targetName, final String methodName, final Object[] args) {
657            this.callerNames.push(senderName);
658            DrawStatement stmt = new DrawStatement(senderName, targetName, methodName, args);
659            this.statements.add(stmt);
660        }
661    
662        private void addReturnMessage(final Object returnee, final Object returnValue) {
663            String receiverName = this.callerNames.pop();
664            String returneeName = this.getVarnameFor(returnee);
665            DrawStatement stmt = new DrawStatement(receiverName, returneeName, returnValue);
666            this.statements.add(stmt);
667        }
668    
669    
670    
671        /**
672         * The Class VarnameComparator compares two variable names. The name is of
673         * the form "A999...", e.g. first character is a (uppercase) letter and the
674         * next characters are a number (to base {@link Character#MAX_RADIX}). Only
675         * the number are used for comparison to get the creation order.
676         */
677        private static final class VarnameComparator implements Comparator<String>, Serializable {
678    
679            private static final long serialVersionUID = 20140104L;
680    
681            /**
682             * Compares two variable names.
683             *
684             * @param x1 the x1
685             * @param x2 the x2
686             * @return a negativ valule if x1 < x2, "0" for x1 == x2, otherwise
687             *         positive value
688             * @see java.util.Comparator#compare(Object, Object)
689             */
690            public int compare(final String x1, final String x2) {
691                return toNumber(x1) - toNumber(x2);
692            }
693    
694            private static int toNumber(final String varname) {
695                String numberPart = varname.substring(1);
696                return Integer.parseInt(numberPart, Character.MAX_RADIX);
697            }
698    
699        }
700    
701    
702    
703        //-------------------------------------------------------------------------
704    
705        /**
706         * The different types of drawings.
707         */
708        private enum DrawType {
709            UNKNOWN,
710            COMMENT,
711            ACTIVE,
712            CREATE_MESSAGE,
713            MESSAGE,
714            RETURN_MESSAGE;
715        }
716    
717        /**
718         * Internal class for caching the different draw statements.
719         *
720         * @author oliver
721         * @since 1.4.1 (17.01.2014)
722         */
723        private static class DrawStatement {
724    
725            private final DrawType type;
726            private final String sender;
727            private final String target;
728            private final String methodName;
729            private final Object[] args;
730            private final String comment;
731    
732            /**
733             * Instantiates a new draw statement.
734             *
735             * @param type the type
736             * @param comment the comment or active argument
737             */
738            public DrawStatement(final DrawType type, final String comment) {
739                this.type = type;
740                this.sender = comment;
741                this.target = null;
742                this.methodName = null;
743                this.args = null;
744                this.comment = comment;
745            }
746    
747            /**
748             * Instantiates a new draw statement.
749             *
750             * @param senderName the sender name
751             * @param createdClass the created class
752             * @param typeName the type name
753             */
754            public DrawStatement(final String senderName, final Class<?> createdClass, final String typeName) {
755                this.type = DrawType.CREATE_MESSAGE;
756                this.sender = senderName;
757                this.target = typeName;
758                this.methodName = null;
759                this.args = createObjectArray(createdClass);
760                this.comment = null;
761            }
762    
763            /**
764             * Instantiates a new draw statement.
765             *
766             * @param sender the sender
767             * @param target the target
768             * @param name the name
769             * @param args the args
770             */
771            public DrawStatement(final String sender, final String target, final String name,
772                    final Object[] args) {
773                this.type = DrawType.MESSAGE;
774                this.sender = sender;
775                this.target = target;
776                this.methodName = name;
777                this.args = args;
778                this.comment = null;
779            }
780    
781            /**
782             * Instantiates a new draw statement.
783             *
784             * @param receiverName the receiver name
785             * @param returnee the returnee
786             * @param returnValue the return value
787             */
788            public DrawStatement(final String receiverName, final String returnee, final Object returnValue) {
789                this.type = DrawType.RETURN_MESSAGE;
790                this.sender = receiverName;
791                this.target = returnee;
792                this.methodName = null;
793                this.args = createObjectArray(returnValue);
794                this.comment = null;
795            }
796    
797            private Object[] createObjectArray(final Object...objects) {
798                return objects;
799            }
800    
801            /**
802             * Gets the type.
803             *
804             * @return the type
805             */
806            public DrawType getType() {
807                return this.type;
808            }
809    
810            /**
811             * Gets the target.
812             *
813             * @return the target
814             */
815            public String getTarget() {
816                return this.target;
817            }
818    
819            /**
820             * If the given name is part of any stored actor in this statement
821             * 'true' will be returned.
822             *
823             * @param name the name
824             * @return true, if is involved
825             */
826            public boolean hasActor(final String name) {
827                switch (this.type) {
828                    case CREATE_MESSAGE:
829                    case MESSAGE:
830                        return name.equals(this.target);
831                    case RETURN_MESSAGE:
832                        return name.equals(this.sender) || name.equals(this.target);
833                    default:
834                        return false;
835                }
836            }
837    
838            /**
839             * Checks for message.
840             *
841             * @return true, if successful
842             */
843            public boolean hasMessage() {
844                switch (this.type) {
845                    case CREATE_MESSAGE:
846                    case MESSAGE:
847                    case RETURN_MESSAGE:
848                        return true;
849                    default:
850                        return false;
851                }
852            }
853    
854            /**
855             * A message points to left, if the given senderName was created after
856             * the given targetName. This information can be derived from the name.
857             *
858             * @return true, if successful
859             */
860            public boolean hasMessageToLeft() {
861                switch (this.type) {
862                    case CREATE_MESSAGE:
863                    case MESSAGE:
864                        return toNumber(this.sender).compareTo(toNumber(this.target)) > 0;
865                    case RETURN_MESSAGE:
866                        return toNumber(this.target).compareTo(toNumber(this.sender)) > 0;
867                    default:
868                        return false;
869                }
870            }
871    
872            /**
873             * Checks for message to right.
874             *
875             * @return true, if successful
876             */
877            public boolean hasMessageToRight() {
878                switch (this.type) {
879                    case CREATE_MESSAGE:
880                    case MESSAGE:
881                    case RETURN_MESSAGE:
882                        return !this.hasMessageToLeft();
883                    default:
884                        return false;
885                }
886            }
887    
888            /**
889             * Write statement.
890             *
891             * @param writer the writer
892             */
893            public void writeStatement(final Writer writer) {
894                switch (type) {
895                    case COMMENT:
896                        this.writeComment(writer);
897                        break;
898                    case ACTIVE:
899                        this.writeActive(writer);
900                        break;
901                    case CREATE_MESSAGE:
902                        this.writeCreateMessage(writer);
903                        break;
904                    case MESSAGE:
905                        this.writeMessage(writer);
906                        break;
907                    case RETURN_MESSAGE:
908                        this.writeReturnMessage(writer);
909                        break;
910                    default:
911                        log.warn("{} statement is ignored.", type);
912                        break;
913                }
914            }
915    
916            /**
917             * Write comment.
918             *
919             * @param writer the writer
920             */
921            public void writeComment(final Writer writer) {
922                writeLine(this.toString(), writer);
923            }
924    
925            /**
926             * Write active.
927             *
928             * @param writer the writer
929             */
930            public void writeActive(final Writer writer) {
931                writeLine(this.toString(), writer);
932            }
933    
934            /**
935             * Write create message.
936             *
937             * @param writer the writer
938             */
939            public void writeCreateMessage(final Writer writer) {
940                String classname = getTargetType();
941                writeLine(getBoxwidStatementFor(classname), writer);
942                writeLine(this.toString(), writer);
943            }
944    
945            private String getTargetType() {
946                Class<?> targetType = (Class<?>) this.args[0];
947                return targetType.getSimpleName();
948            }
949    
950            /**
951             * Write message.
952             *
953             * @param writer the writer
954             */
955            public void writeMessage(final Writer writer) {
956                if (this.hasMessageToLeft()) {
957                    writeLine("step();", writer);
958                }
959                writeLine(this.toString(), writer);
960                writeLine("active(" + this.target + ");", writer);
961            }
962    
963            /**
964             * Write return message.
965             *
966             * @param writer the writer
967             */
968            public void writeReturnMessage(final Writer writer) {
969                if (this.hasMessageToLeft()) {
970                    writeLine("step();", writer);
971                }
972                writeLine(this.toString(), writer);
973                writeLine("inactive(" + this.target + ");", writer);
974            }
975    
976            /**
977             * Hash code.
978             *
979             * @return the int
980             * @see java.lang.Object#hashCode()
981             */
982            @Override
983            public int hashCode() {
984                return this.type.hashCode() + ((this.sender == null) ? 0 : this.sender.hashCode());
985            }
986    
987            /**
988             * Equals.
989             *
990             * @param obj the obj
991             * @return true, if successful
992             * @see java.lang.Object#equals(java.lang.Object)
993             */
994            @Override
995            public boolean equals(final Object obj) {
996                if (!(obj instanceof DrawStatement)) {
997                    return false;
998                }
999                DrawStatement other = (DrawStatement) obj;
1000                return (this.type == other.type)
1001                        && StringUtils.equals(this.sender, other.sender)
1002                        && StringUtils.equals(this.target, other.target)
1003                        && StringUtils.equals(this.methodName, other.methodName)
1004                        && StringUtils.equals(this.comment, other.comment)
1005                        && Arrays.equals(this.args, other.args);
1006            }
1007    
1008            /**
1009             * To string.
1010             *
1011             * @return the string
1012             * @see java.lang.Object#toString()
1013             */
1014            @Override
1015            public String toString() {
1016                switch (type) {
1017                    case COMMENT:
1018                        return "# " + this.comment;
1019                    case ACTIVE:
1020                        return "active(" + this.sender + ");";
1021                    case CREATE_MESSAGE:
1022                        String classname = this.getTargetType();
1023                        return "create_message(" + this.sender + "," + this.target + ",\":" + classname + "\");";
1024                    case MESSAGE:
1025                        return "message(" + this.sender + "," + this.target + ",\"" + this.methodName
1026                                + getArgsAsString(this.args) + "\");";
1027                    case RETURN_MESSAGE:
1028                        return "return_message(" + this.target + "," + this.sender + ",\""
1029                                + toEscapedString(Converter.toShortString(this.args[0])) + "\");";
1030                    default:
1031                        return "# " + this.type + " statement";
1032                }
1033            }
1034    
1035        }
1036    
1037    }