1/*
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24import java.lang.annotation.RetentionPolicy;
25import java.util.*;
26import java.util.stream.Collectors;
27
28public class TestCase {
29
30    /**
31     * The top-level classes of the test case.
32     */
33    public final Map<String, TestClassInfo> classes = new LinkedHashMap<>();
34
35    /**
36     * Constructs a test class info with {@code classType} as top-level class,
37     * with {@code outerClassName} as name and {@code mods} as modifiers.
38     *
39     * @param classType a class type
40     * @param outerClassName a name
41     * @param mods an array of modifiers
42     */
43    public TestClassInfo addClassInfo(ClassType classType, String outerClassName, String...mods) {
44        return addClassInfo(null, classType, outerClassName, mods);
45    }
46
47    /**
48     * Constructs a test class info with {@code classType} as top-level class,
49     * with {@code outerClassName} as name, {@code parent} class name
50     * as parent class and {@code mods} as modifiers.
51     *
52     * @param classType a class type
53     * @param outerClassName a name
54     * @param mods an array of modifiers
55     */
56    public TestClassInfo addClassInfo(String parent, ClassType classType, String outerClassName, String...mods) {
57        TestClassInfo clazz = new TestClassInfo(classType, outerClassName, parent, mods);
58        if (classes.put(outerClassName, clazz) != null) {
59            throw new IllegalArgumentException("Duplicate class name: " + outerClassName);
60        }
61        return clazz;
62    }
63
64    public String generateSource() {
65        return classes.values().stream()
66                .map(TestMemberInfo::generateSource)
67                .collect(Collectors.joining("\n"));
68    }
69
70    /**
71     * Returns {@code TestClassInfo} by class signature.
72     * Example, {@code getTestClassInfo(&quot;Test$1Local&quot;)}
73     * returns local inner class of class {@code Test}.
74     *
75     * @param classSignature a class signature
76     * @return {@code TestClassInfo} by class signature
77     */
78    public TestClassInfo getTestClassInfo(String classSignature) {
79        String[] cs = classSignature.split("\\$");
80        if (cs.length > 0 && classes.containsKey(cs[0])) {
81            // check signature corresponds to top level class
82            if (cs.length == 1) {
83                return classes.get(cs[0]);
84            }
85        } else {
86            throw new IllegalArgumentException("Cannot find class : " + classSignature);
87        }
88        TestClassInfo current = classes.get(cs[0]);
89        // find class info in the inner classes
90        for (int i = 1; i < cs.length; ++i) {
91            Map<String, TestClassInfo> innerClasses = current.innerClasses;
92            Map<String, TestMethodInfo> methods = current.methods;
93            current = innerClasses.get(cs[i]);
94            // if current is null then class info does not exist or the class is local
95            if (current == null) {
96                if (!cs[i].isEmpty()) {
97                    // the class is local, remove leading digit
98                    String className = cs[i].substring(1);
99                    Optional<TestClassInfo> opt = methods.values().stream()
100                            .flatMap(c -> c.localClasses.values().stream())
101                            .filter(c -> c.name.equals(className)).findAny();
102                    if (opt.isPresent()) {
103                        current = opt.get();
104                        // continue analysis of local class
105                        continue;
106                    }
107                }
108                throw new IllegalArgumentException("Cannot find class : " + classSignature);
109            }
110        }
111        return current;
112    }
113
114    /**
115     * Class represents a program member.
116     */
117    public static abstract class TestMemberInfo {
118        // next two fields are used for formatting
119        protected final int indention;
120        protected final ClassType containerType;
121        public final List<String> mods;
122        public final String name;
123        public final Map<String, TestAnnotationInfo> annotations;
124
125        TestMemberInfo(int indention, ClassType containerType, String name, String... mods) {
126            this.indention = indention;
127            this.containerType = containerType;
128            this.mods = Arrays.asList(mods);
129            this.name = name;
130            this.annotations = new HashMap<>();
131        }
132
133        public abstract String generateSource();
134
135        public boolean isAnnotated(RetentionPolicy policy) {
136            return annotations.values().stream()
137                    .filter(a -> a.policy == policy)
138                    .findAny().isPresent();
139        }
140
141        public Set<String> getRuntimeVisibleAnnotations() {
142            return getRuntimeAnnotations(RetentionPolicy.RUNTIME);
143        }
144
145        public Set<String> getRuntimeInvisibleAnnotations() {
146            return getRuntimeAnnotations(RetentionPolicy.CLASS);
147        }
148
149        private Set<String> getRuntimeAnnotations(RetentionPolicy policy) {
150            return annotations.values().stream()
151                    .filter(e -> e.policy == policy)
152                    .map(a -> a.annotationName)
153                    .distinct()
154                    .collect(Collectors.toSet());
155        }
156
157        /**
158         * Generates source for annotations.
159         *
160         * @param prefix a leading text
161         * @param suffix a trailing text
162         * @param joining a text between annotations
163         * @return source for annotations
164         */
165        protected String generateSourceForAnnotations(String prefix, String suffix, String joining) {
166            StringBuilder sb = new StringBuilder();
167            for (TestAnnotationInfo annotation : annotations.values()) {
168                sb.append(prefix);
169                if (annotation.isContainer) {
170                    // the annotation is repeatable
171                    // container consists of an array of annotations
172                    TestAnnotationInfo.TestArrayElementValue containerElementValue =
173                            (TestAnnotationInfo.TestArrayElementValue) annotation.elementValues.get(0).elementValue;
174                    // concatenate sources of repeatable annotations
175                    sb.append(containerElementValue.values.stream()
176                            .map(TestAnnotationInfo.TestElementValue::toString)
177                            .collect(Collectors.joining(joining)));
178                } else {
179                    sb.append(annotation);
180                }
181                sb.append(suffix);
182            }
183            String src = sb.toString();
184            return src.trim().isEmpty() ? "" : src;
185
186        }
187
188        /**
189         * Generates source for annotations.
190         *
191         * @return source for annotations
192         */
193        public String generateSourceForAnnotations() {
194            return generateSourceForAnnotations(indention(), "\n", "\n" + indention());
195        }
196
197        /**
198         * Adds annotation info to the member.
199         *
200         * @param anno an annotation info
201         */
202        public void addAnnotation(TestAnnotationInfo anno) {
203            String containerName = anno.annotationName + "Container";
204            TestAnnotationInfo annotation = annotations.get(anno.annotationName);
205            TestAnnotationInfo containerAnnotation = annotations.get(containerName);
206
207            if (annotation == null) {
208                // if annotation is null then either it is first adding of the annotation to the member
209                // or there is the container of the annotation.
210                if (containerAnnotation == null) {
211                    // first adding to the member
212                    annotations.put(anno.annotationName, anno);
213                } else {
214                    // add annotation to container
215                    TestAnnotationInfo.TestArrayElementValue containerElementValue =
216                            ((TestAnnotationInfo.TestArrayElementValue) containerAnnotation.elementValues.get(0).elementValue);
217                    containerElementValue.values.add(new TestAnnotationInfo.TestAnnotationElementValue(anno.annotationName, anno));
218                }
219            } else {
220                // remove previously added annotation and add new container of repeatable annotation
221                // which contains previously added and new annotation
222                annotations.remove(anno.annotationName);
223                containerAnnotation = new TestAnnotationInfo(
224                        containerName,
225                        anno.policy,
226                        true,
227                        new TestAnnotationInfo.Pair("value",
228                                new TestAnnotationInfo.TestArrayElementValue(
229                                        new TestAnnotationInfo.TestAnnotationElementValue(anno.annotationName, annotation),
230                                        new TestAnnotationInfo.TestAnnotationElementValue(anno.annotationName, anno))));
231                annotations.put(containerName, containerAnnotation);
232            }
233        }
234
235        public String indention() {
236            char[] a = new char[4 * indention];
237            Arrays.fill(a, ' ');
238            return new String(a);
239        }
240
241        public String getName() {
242            return name;
243        }
244    }
245
246    /**
247     * The class represents a class.
248     */
249    public static class TestClassInfo extends TestMemberInfo {
250        public final ClassType classType;
251        public final String parent;
252        public final Map<String, TestClassInfo> innerClasses;
253        public final Map<String, TestMethodInfo> methods;
254        public final Map<String, TestFieldInfo> fields;
255
256        TestClassInfo(int indention, ClassType classType, String className, String... mods) {
257            this(indention, classType, className, null, mods);
258        }
259
260        TestClassInfo(ClassType classType, String className, String parent, String... mods) {
261            this(0, classType, className, parent, mods);
262        }
263
264        TestClassInfo(int indention, ClassType classType, String className, String parent, String... mods) {
265            super(indention, null, className, mods);
266            this.classType = classType;
267            this.parent = parent;
268            innerClasses = new LinkedHashMap<>();
269            methods = new LinkedHashMap<>();
270            fields = new LinkedHashMap<>();
271        }
272
273        /**
274         * Generates source which represents the class.
275         *
276         * @return source which represents the class
277         */
278        @Override
279        public String generateSource() {
280            String sourceForAnnotations = generateSourceForAnnotations();
281            String classModifiers = mods.stream().collect(Collectors.joining(" "));
282            return sourceForAnnotations
283                    + String.format("%s%s %s %s %s {%n",
284                    indention(),
285                    classModifiers,
286                    classType.getDescription(),
287                    name,
288                    parent == null ? "" : "extends " + parent)
289                    + classType.collectFields(fields.values())
290                    + classType.collectMethods(methods.values())
291                    + classType.collectInnerClasses(innerClasses.values())
292                    + indention() + "}";
293        }
294
295        /**
296         * Adds a new inner class to the class.
297         *
298         * @param classType a class type
299         * @param className a class name
300         * @param mods modifiers
301         * @return a new added inner class to the class
302         */
303        public TestClassInfo addInnerClassInfo(ClassType classType, String className, String... mods) {
304            TestClassInfo testClass = new TestClassInfo(indention + 1, classType, className, mods);
305            if (innerClasses.put(className, testClass) != null) {
306                throw new IllegalArgumentException("Duplicated class : " + className);
307            }
308            return testClass;
309        }
310
311        /**
312         * Adds a new method to the class.
313         *
314         * @param methodName a method name
315         * @param mods modifiers
316         * @return a new inner class to the class
317         */
318        public TestMethodInfo addMethodInfo(String methodName, String... mods) {
319            return addMethodInfo(methodName, false, mods);
320        }
321
322        /**
323         * Adds a new method to the class.
324         *
325         * @param methodName a method name
326         * @param isSynthetic if {@code true} the method is synthetic
327         * @param mods modifiers
328         * @return a new method added to the class
329         */
330        public TestMethodInfo addMethodInfo(String methodName, boolean isSynthetic, String... mods) {
331            boolean isConstructor = methodName.contains("<init>");
332            if (isConstructor) {
333                methodName = methodName.replace("<init>", name);
334            }
335            TestMethodInfo testMethod = new TestMethodInfo(indention + 1, classType, methodName, isConstructor, isSynthetic, mods);
336            if (methods.put(methodName, testMethod) != null) {
337                throw new IllegalArgumentException("Duplicated method : " + methodName);
338            }
339            return testMethod;
340        }
341
342        /**
343         * Adds a new field to the class.
344         *
345         * @param fieldName a method name
346         * @param mods modifiers
347         * @return a new field added to the class
348         */
349        public TestFieldInfo addFieldInfo(String fieldName, String... mods) {
350            TestFieldInfo field = new TestFieldInfo(indention + 1, classType, fieldName, mods);
351            if (fields.put(fieldName, field) != null) {
352                throw new IllegalArgumentException("Duplicated field : " + fieldName);
353            }
354            return field;
355        }
356
357        public TestMethodInfo getTestMethodInfo(String methodName) {
358            return methods.get(methodName);
359        }
360
361        public TestFieldInfo getTestFieldInfo(String fieldName) {
362            return fields.get(fieldName);
363        }
364    }
365
366    public static class TestMethodInfo extends TestMemberInfo {
367        public final boolean isConstructor;
368        public final boolean isSynthetic;
369        public final Map<String, TestClassInfo> localClasses;
370        public final List<TestParameterInfo> parameters;
371
372        TestMethodInfo(int indention, ClassType containerType, String methodName,
373                               boolean isConstructor, boolean isSynthetic, String... mods) {
374            super(indention, containerType, methodName, mods);
375            this.isSynthetic = isSynthetic;
376            this.localClasses = new LinkedHashMap<>();
377            this.parameters = new ArrayList<>();
378            this.isConstructor = isConstructor;
379        }
380
381        public boolean isParameterAnnotated(RetentionPolicy policy) {
382            return parameters.stream()
383                    .filter(p -> p.isAnnotated(policy))
384                    .findFirst().isPresent();
385        }
386
387        public TestParameterInfo addParameter(String type, String name) {
388            TestParameterInfo testParameter = new TestParameterInfo(type, name);
389            parameters.add(testParameter);
390            return testParameter;
391        }
392
393        /**
394         * Adds a local class to the method.
395         *
396         * @param className a class name
397         * @param mods modifiers
398         * @return a local class added to the method
399         */
400        public TestClassInfo addLocalClassInfo(String className, String... mods) {
401            TestClassInfo testClass = new TestClassInfo(indention + 1, ClassType.CLASS, className, mods);
402            if (localClasses.put(className, testClass) != null) {
403                throw new IllegalArgumentException("Duplicated class : " + className);
404            }
405            return testClass;
406        }
407
408        @Override
409        public String generateSource() {
410            if (isSynthetic) {
411                return "";
412            }
413            return generateSourceForAnnotations() +
414                    containerType.methodToString(this);
415        }
416
417        @Override
418        public String getName() {
419            return name.replaceAll("\\(.*\\)", "");
420        }
421    }
422
423    /**
424     * The class represents a method parameter.
425     */
426    public static class TestParameterInfo extends TestMemberInfo {
427        public final String type;
428
429        TestParameterInfo(String type, String name) {
430            super(0, null, name);
431            this.type = type;
432        }
433
434        @Override
435        public String generateSource() {
436            return generateSourceForAnnotations() + type + " " + name;
437        }
438
439        public String generateSourceForAnnotations() {
440            return generateSourceForAnnotations("", " ", " ");
441        }
442    }
443
444    /**
445     * The class represents a field.
446     */
447    public static class TestFieldInfo extends TestMemberInfo {
448
449        TestFieldInfo(int indention, ClassType containerType, String fieldName, String... mods) {
450            super(indention, containerType, fieldName, mods);
451        }
452
453        @Override
454        public String generateSource() {
455            return generateSourceForAnnotations() +
456                    containerType.fieldToString(this);
457        }
458    }
459}
460