001/*
002 * Cobertura - http://cobertura.sourceforge.net/
003 *
004 * Copyright (C) 2011 Piotr Tabor
005 *
006 * Note: This file is dual licensed under the GPL and the Apache
007 * Source License (so that it can be used from both the main
008 * Cobertura classes and the ant tasks).
009 *
010 * Cobertura is free software; you can redistribute it and/or modify
011 * it under the terms of the GNU General Public License as published
012 * by the Free Software Foundation; either version 2 of the License,
013 * or (at your option) any later version.
014 *
015 * Cobertura is distributed in the hope that it will be useful, but
016 * WITHOUT ANY WARRANTY; without even the implied warranty of
017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
018 * General Public License for more details.
019 *
020 * You should have received a copy of the GNU General Public License
021 * along with Cobertura; if not, write to the Free Software
022 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
023 * USA
024 */
025
026package net.sourceforge.cobertura.instrument;
027
028import java.util.Collection;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.concurrent.atomic.AtomicInteger;
035import java.util.regex.Pattern;
036
037import net.sourceforge.cobertura.util.RegexUtil;
038
039import org.objectweb.asm.Label;
040import org.objectweb.asm.MethodVisitor;
041import org.objectweb.asm.Opcodes;
042import org.objectweb.asm.tree.AbstractInsnNode;
043import org.objectweb.asm.tree.InsnNode;
044import org.objectweb.asm.tree.MethodInsnNode;
045import org.objectweb.asm.tree.VarInsnNode;
046
047/**
048 * Analyzes given method, assign unique event identifiers to every found
049 * interesting instruction and calls business method in the {@link #touchPointListener}. 
050 * 
051 * @author ptab
052 */
053public class FindTouchPointsMethodAdapter extends ContextMethodAwareMethodAdapter {
054        /**
055         * Source of identifiers for events.
056         * 
057         *  <p>Remember to acquire identifiers using {@link AtomicInteger#incrementAndGet()} (not {@link AtomicInteger#getAndIncrement()}!!!)</p>
058         */
059        private final AtomicInteger eventIdGenerator;
060
061        /**
062         * Backing listener that will be informed about all interesting events found
063         */
064        private TouchPointListener touchPointListener;
065        
066        /**
067         * Line number of current line. 
068         * 
069         *  <p>It it NOT lineId</pl>
070         */
071        private int currentLine;
072        
073        /**
074         * List of patterns to know that we don't want trace lines that are calls to some methods
075         */
076        private Collection<Pattern> ignoreRegexp;
077
078        /**
079         * See {@link AbstractFindTouchPointsClassInstrumenter#duplicatedLinesMap}  
080         */
081        private final Map<Integer, Map<Integer, Integer>> duplicatedLinesMap;
082
083        /**
084         * Map of (line number -> (lineId -> List of eventIds)). 
085         * 
086         * <p>For every line number, and for evere lineId in the line we store list of all generated events</p>
087         * 
088         * <p>When we will detect duplicated block of code in given line - instead of generating new eventIds we will
089         * use the same events</p> 
090         */
091        private final Map<Integer, Map<Integer, LinkedList<Integer>>> line2eventIds = new HashMap<Integer, Map<Integer, LinkedList<Integer>>>();
092        
093        /**
094         * If we are currently processing a new (not duplicated line), it is a list (linked into {@link #line2eventIds}) that we use to store newly generated identifiers into it,
095         * otherwise it  is null.
096         */
097        private LinkedList<Integer> saveEventIdList = null;
098
099        /**
100         * If we are currently processing a duplicated line, it is a list of identifiers that should be used for the line. After processing event, you should remove identifier from the begining of the list.  
101         */
102        private LinkedList<Integer> replyEventIdList = null;
103        
104        /**
105         * State of last N instructions.
106         */
107        private final List<AbstractInsnNode> backlog;
108
109        public FindTouchPointsMethodAdapter(HistoryMethodAdapter mv,
110                        String className, String methodName, String methodSignature,
111                        AtomicInteger eventIdGenerator,
112                        Map<Integer, Map<Integer, Integer>> duplicatedLinesMap,
113                        AtomicInteger lineIdGenerator) {
114                this(mv, mv.backlog(), className, methodName, methodSignature, eventIdGenerator, duplicatedLinesMap, lineIdGenerator);          
115        }
116        
117        public FindTouchPointsMethodAdapter(MethodVisitor mv,
118                        String className, String methodName, String methodSignature,
119                        AtomicInteger eventIdGenerator,
120                        Map<Integer, Map<Integer, Integer>> duplicatedLinesMap,
121                        AtomicInteger lineIdGenerator) {
122                this(mv, Collections. <AbstractInsnNode> emptyList(), 
123                     className, methodName, methodSignature, eventIdGenerator, duplicatedLinesMap, lineIdGenerator);            
124        }
125
126        protected FindTouchPointsMethodAdapter(MethodVisitor mv,
127                        List<AbstractInsnNode> backlog,
128                        String className, String methodName, String methodSignature,
129                        AtomicInteger eventIdGenerator,
130                        Map<Integer, Map<Integer, Integer>> duplicatedLinesMap,
131                        AtomicInteger lineIdGenerator) {
132                super(mv, className, methodName, methodSignature, lineIdGenerator);
133                this.backlog = backlog;
134                this.eventIdGenerator = eventIdGenerator;
135                this.duplicatedLinesMap = duplicatedLinesMap;
136        }
137
138        private int generateNewEventId() {
139                return eventIdGenerator.incrementAndGet();
140        }
141
142        /**
143         * Depending on situation if we are processing a new line or duplicated line, 
144         * generates a new identifier or reuses previously generated for the same event.
145         * @return
146         */
147        private int getEventId() {
148                if (replyEventIdList != null && !replyEventIdList.isEmpty()) {
149                        //in case of a duplicated line
150                        return replyEventIdList.removeFirst();
151                } else {
152                        // in case of a new line
153                        int eventId = generateNewEventId();
154                        if (saveEventIdList != null) {
155                                saveEventIdList.addLast(eventId);
156                        }
157                        return eventId;
158                }
159        }
160
161        @Override
162        public void visitCode() {
163                super.visitCode();
164                touchPointListener.afterMethodStart(mv);
165        }
166
167        /**
168         * Processing information about new line. 
169         * 
170         * Upgrades {@link #replyEventIdList} and {@link #saveEventIdList} and calls {@link TouchPointListener#afterLineNumber(int, Label, int, MethodVisitor, String, String)}
171         */
172        public void visitLineNumber(int line, Label label) {
173                super.visitLineNumber(line, label);
174                currentLine = line;
175
176                if (!isDuplicatedLine(line, lastLineId)) {
177                        /*
178                         * It is a new line (first time seen, so we will save all found
179                         * events)
180                         */
181                        replyEventIdList = null;
182                        saveEventIdList = new LinkedList<Integer>();
183                        Map<Integer, LinkedList<Integer>> eventsMap = line2eventIds
184                                        .get(line);
185                        if (eventsMap == null) {
186                                eventsMap = new HashMap<Integer, LinkedList<Integer>>();
187                                line2eventIds.put(line, eventsMap);
188                        }
189                        eventsMap.put(lastLineId, saveEventIdList);
190                } else {
191                        Integer orgin = getOriginForLine(line, lastLineId);
192                        Map<Integer, LinkedList<Integer>> m = line2eventIds     .get(currentLine);
193                        LinkedList<Integer> eventIds = m.get(orgin);
194                        
195                        /* copy of  current list */
196                        replyEventIdList = new LinkedList<Integer>(eventIds);
197                        saveEventIdList = null;
198                }
199
200                touchPointListener.afterLineNumber(getEventId(), label, currentLine,
201                                mv, methodName, methodSignature);
202        }
203
204        /**
205         * Checks if given line is a duplicate of previously processed line
206         * 
207         * @param line - line number
208         * @param lineId - line identifier
209         * @return true - if line is duplicate of previously processed lines
210         */
211        private boolean isDuplicatedLine(int line, Integer lineId) {
212                return getOriginForLine(line, lineId) != null;
213        }
214        
215        /** 
216         * @param line - line number
217         * @param lineId - line identifier
218         * 
219         * @return lineId of the origin line (first processed line) that the given line is duplicate of. 
220         *    If the current line is not an duplicate of the previously processed line the method returns NULL.  
221         */
222        private Integer getOriginForLine(int line, Integer lineId) {
223                Map<Integer, Integer> labelMap = duplicatedLinesMap.get(line);
224                return labelMap != null ? labelMap.get(lineId) : null;
225        }
226
227        @Override
228        public void visitLabel(Label label) {
229                int eventId = getEventId();
230                touchPointListener.beforeLabel(eventId, label, currentLine, mv);
231                super.visitLabel(label);
232                touchPointListener.afterLabel(eventId, label, currentLine, mv);
233        }
234
235        @Override
236        public void visitJumpInsn(int opcode, Label label) {
237                /* Ignore any jump instructions in the "class init" method.
238                 When initializing static variables, the JVM first checks
239                 that the variable is null before attempting to set it.
240                 This check contains an IFNONNULL jump instruction which
241                 would confuse people if it showed up in the reports.*/
242                if ((opcode != Opcodes.GOTO) 
243                                && (opcode != Opcodes.JSR) && (currentLine != 0)
244                                && (!methodName.equals("<clinit>"))) {
245                        int eventId = getEventId();
246                        touchPointListener.beforeJump(eventId, label, currentLine, mv);
247                        super.visitJumpInsn(opcode, label);
248                        touchPointListener.afterJump(eventId, label, currentLine, mv);
249                } else {
250                        super.visitJumpInsn(opcode, label);
251                }
252        }
253
254        @Override
255        public void visitMethodInsn(int opcode, String owner, String method,
256                        String descr) {
257                super.visitMethodInsn(opcode, owner, method, descr);
258                //We skip lines that contains call to methods that are specified inside ignoreRegexp 
259                if (RegexUtil.matches(ignoreRegexp, owner)) {
260                        touchPointListener.ignoreLine(getEventId(), currentLine);
261                }
262        }
263
264        @Override
265        public void visitLookupSwitchInsn(Label def, int[] values, Label[] labels) {
266                touchPointListener.beforeSwitch(getEventId(), def, labels, currentLine, mv, tryToFindSignatureOfConditionEnum());
267                super.visitLookupSwitchInsn(def, values, labels);
268        }
269
270        @Override
271        public void visitTableSwitchInsn(int min, int max, Label def, Label[] labels) {
272                touchPointListener.beforeSwitch(getEventId(), def, labels, currentLine, mv, tryToFindSignatureOfConditionEnum());
273                super.visitTableSwitchInsn(min, max, def, labels);
274        }
275        
276        enum Abc {A, B};
277        
278/**             
279 * We try to detect such a last 2 instructions and extract the enum signature.
280 * @code{
281    INVOKESTATIC FindTouchPointsMethodAdapter.$SWITCH_TABLE$net$sourceforge$cobertura$instrument$FindTouchPointsMethodAdapter$Abc() : int[]
282    ALOAD 1: a
283    INVOKEVIRTUAL FindTouchPointsMethodAdapter$Abc.ordinal() : int
284    IALOAD
285 * }
286 */
287        private String tryToFindSignatureOfConditionEnum() {
288//              mv.visitMethodInsn(INVOKESTATIC, "net/sourceforge/cobertura/instrument/FindTouchPointsMethodAdapter", "$SWITCH_TABLE$net$sourceforge$cobertura$instrument$FindTouchPointsMethodAdapter$Abc", "()[I");
289//              mv.visitVarInsn(ALOAD, 1);
290//              mv.visitMethodInsn(INVOKEVIRTUAL, "net/sourceforge/cobertura/instrument/FindTouchPointsMethodAdapter$Abc", "ordinal", "()I");
291//              mv.visitInsn(IALOAD);
292                
293                if (backlog == null || backlog.size() < 4) return null;
294                int last = backlog.size() - 1;
295                if ((backlog.get(last) instanceof InsnNode)
296                    && (backlog.get(last - 1) instanceof MethodInsnNode)
297                    && (backlog.get(last - 2) instanceof VarInsnNode)) {
298                        VarInsnNode i2 = (VarInsnNode)backlog.get(last - 2);
299                        MethodInsnNode i3 = (MethodInsnNode)backlog.get(last - 1);
300                        InsnNode i4 = (InsnNode)backlog.get(last);
301                        if ((i2.getOpcode() == Opcodes.ALOAD)
302                        && (i3.getOpcode() == Opcodes.INVOKEVIRTUAL && i3.name.equals("ordinal"))
303                        && (i4.getOpcode() == Opcodes.IALOAD)) {
304                                return i3.owner;
305                        }                                       
306                }
307                return null;
308        }
309        
310        // ===========  Getters and setters =====================
311        
312        /**
313         * Gets backing listener that will be informed about all interesting events found
314         */
315        public TouchPointListener getTouchPointListener() {
316                return touchPointListener;
317        }
318
319        /**Niestety terminalnie.
320         * Sets backing listener that will be informed about all interesting events found
321         */
322        public void setTouchPointListener(TouchPointListener touchPointListener) {
323                this.touchPointListener = touchPointListener;
324        }
325
326        /**
327         * @return list of patterns to know that we don't want trace lines that are calls to some methods
328         */
329        public Collection<Pattern> getIgnoreRegexp() {
330                return ignoreRegexp;
331        }
332
333        /**
334         * sets list of patterns to know that we don't want trace lines that are calls to some methods
335         */
336        public void setIgnoreRegexp(Collection<Pattern> ignoreRegexp) {
337                this.ignoreRegexp = ignoreRegexp;
338        }
339
340}