1 /*
2  * Copyright (c) 2019, 2020, 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 
24 /*
25  * @test
26  * @bug 8218998 8219946 8219060 8241190 8242056 8254627
27  * @summary Add metadata to generated API documentation files
28  * @library /tools/lib ../../lib
29  * @modules jdk.javadoc/jdk.javadoc.internal.tool
30  * @modules jdk.compiler/com.sun.tools.javac.api
31  *          jdk.compiler/com.sun.tools.javac.main
32  *          jdk.javadoc/jdk.javadoc.internal.api
33  *          jdk.javadoc/jdk.javadoc.internal.tool
34  * @build toolbox.ToolBox toolbox.JavacTask javadoc.tester.*
35  * @run main TestMetadata
36  */
37 
38 import java.io.IOException;
39 import java.nio.file.Path;
40 import java.util.ArrayList;
41 import java.util.HashSet;
42 import java.util.List;
43 import java.util.Set;
44 import java.util.TreeSet;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47 import java.util.stream.Collectors;
48 
49 import toolbox.ModuleBuilder;
50 import toolbox.ToolBox;
51 
52 import javadoc.tester.JavadocTester;
53 
54 public class TestMetadata extends JavadocTester {
main(String... args)55     public static void main(String... args) throws Exception {
56         TestMetadata tester = new TestMetadata();
57         tester.runTests();
58     }
59 
60     enum Index  { SINGLE, SPLIT };
61     enum Source { PACKAGES, MODULES };
62 
63     final ToolBox tb = new ToolBox();
64     final Set<String> allBodyClassesFound = new HashSet<>();
65     final Set<String> allGeneratorsFound = new HashSet<>();
66 
runTests()67     public void runTests() throws Exception {
68         for (Source s : Source.values()) {
69             Path src = genSource(s);
70                  for (Index i : Index.values()) {
71                      List<String> args = new ArrayList<>();
72                      args.add("-d");
73                      args.add(String.format("out-%s-%s", s, i));
74                      args.add("-use");
75                      args.add("-linksource");
76                      if (i == Index.SPLIT) {
77                          args.add("-splitIndex");
78                      }
79                      if (s == Source.PACKAGES) {
80                          args.add("-sourcepath");
81                          args.add(src.toString());
82                          args.add("pA");
83                          args.add("pB");
84                      } else {
85                          args.add("--module-source-path");
86                          args.add(src.toString());
87                          args.add("--module");
88                          args.add("mA,mB");
89                      }
90                      javadoc(args.toArray(new String[args.size()]));
91                      checkExit(Exit.OK);
92                      checkBodyClasses();
93                      checkMetadata();
94 
95                      // spot check the descriptions for declarations
96                      switch (s) {
97                          case PACKAGES:
98                              checkOutput("pA/package-summary.html", true,
99                                      """
100                                          <meta name="description" content="declaration: package: pA">""");
101                              checkOutput("pA/CA.html", true,
102                                      """
103                                          <meta name="description" content="declaration: package: pA, class: CA">""");
104                              break;
105 
106                          case MODULES:
107                              checkOutput("mA/module-summary.html", true,
108                                      """
109                                          <meta name="description" content="declaration: module: mA">""");
110                              checkOutput("mA/pA/package-summary.html", true,
111                                      """
112                                          <meta name="description" content="declaration: module: mA, package: pA">""");
113                              checkOutput("mA/pA/CA.html", true,
114                                      """
115                                          <meta name="description" content="declaration: module: mA, package: pA, class: CA">""");
116                              break;
117                      }
118                  }
119         }
120 
121         checking ("all generators");
122         if (allGeneratorsFound.equals(allGenerators)) {
123             passed("all generators found");
124         } else {
125             Set<String> notFound = new TreeSet<>(allGenerators);
126             notFound.removeAll(allGeneratorsFound);
127             failed("not found: " + notFound);
128         }
129 
130         checking ("all body classes");
131         if (allBodyClassesFound.equals(allBodyClasses)) {
132             passed("all body classes found");
133         } else {
134             Set<String> notFound = new TreeSet<>(allBodyClasses);
135             notFound.removeAll(allBodyClassesFound);
136             failed("not found: " + notFound);
137         }
138 
139         printSummary();
140     }
141 
142     final Pattern nl = Pattern.compile("[\\r\\n]+");
143     final Pattern bodyPattern = Pattern.compile("<body [^>]*class=\"([^\"]+)\"");
144     final Set<String> allBodyClasses = Set.of(
145         "all-classes-index-page",
146         "all-packages-index-page",
147         "class-declaration-page",
148         "class-use-page",
149         "constants-summary-page",
150         "deprecated-list-page",
151         "doc-file-page",
152         "help-page",
153         "index-page",
154         "index-redirect-page",
155         "module-declaration-page",
156         "module-index-page",
157         "package-declaration-page",
158         "package-index-page",
159         "package-tree-page",
160         "package-use-page",
161         "serialized-form-page",
162         "source-page",
163         "system-properties-page",
164         "tree-page"
165     );
166 
checkBodyClasses()167     void checkBodyClasses() throws IOException {
168         Path outputDirPath = outputDir.toPath();
169         for (Path p : tb.findFiles(".html", outputDirPath)) {
170             checkBodyClass(outputDirPath.relativize(p));
171         }
172     }
173 
checkBodyClass(Path p)174     void checkBodyClass(Path p) {
175         checking("Check body: " + p);
176 
177         List<String> bodyLines = nl.splitAsStream(readOutputFile(p.toString()))
178                 .filter(s -> s.contains("<body class="))
179                 .collect(Collectors.toList());
180 
181         String bodyLine;
182         switch (bodyLines.size()) {
183             case 0:
184                  failed("Not found: <body class=");
185                  return;
186             case 1:
187                  bodyLine = bodyLines.get(0);
188                  break;
189             default:
190                  failed("Multiple found: <body class=");
191                  return;
192         }
193 
194         Matcher m = bodyPattern.matcher(bodyLine);
195         if (m.find()) {
196             String bodyClass = m.group(1);
197             if (allBodyClasses.contains(bodyClass)) {
198                 passed("found: " + bodyClass);
199                 allBodyClassesFound.add(bodyClass);
200             } else {
201                 failed("Unrecognized body class: " + bodyClass);
202             }
203         } else {
204             failed("Unrecognized line:\n" + bodyLine);
205         }
206     }
207 
208     final Pattern contentPattern = Pattern.compile("content=\"([^\"]+)\">");
209     final Pattern generatorPattern = Pattern.compile("content=\"javadoc/([^\"]+)\">");
210     final Set<String> allGenerators = Set.of(
211             "AllClassesIndexWriter",
212             "AllPackagesIndexWriter",
213             "ClassUseWriter",
214             "ClassWriterImpl",
215             "ConstantsSummaryWriterImpl",
216             "DeprecatedListWriter",
217             "DocFileWriter",
218             "HelpWriter",
219             "IndexRedirectWriter",
220             "IndexWriter",
221             "ModuleIndexWriter",
222             "ModuleWriterImpl",
223             "PackageIndexWriter",
224             "PackageTreeWriter",
225             "PackageUseWriter",
226             "PackageWriterImpl",
227             "SerializedFormWriterImpl",
228             "SourceToHTMLConverter",
229             "SystemPropertiesWriter",
230             "TreeWriter"
231             );
232 
checkMetadata()233     void checkMetadata() throws IOException {
234         Path outputDirPath = outputDir.toPath();
235         for (Path p : tb.findFiles(".html", outputDirPath)) {
236             checkMetadata(outputDirPath.relativize(p));
237         }
238     }
239 
checkMetadata(Path p)240     void checkMetadata(Path p) {
241         checking("Check generator: " + p);
242 
243         List<String> generators = nl.splitAsStream(readOutputFile(p.toString()))
244                 .filter(s -> s.contains("<meta name=\"generator\""))
245                 .collect(Collectors.toList());
246 
247         String generator;
248         switch (generators.size()) {
249             case 0:
250                  failed("""
251                      Not found: <meta name="generator\"""");
252                  return;
253             case 1:
254                  generator = generators.get(0);
255                  break;
256             default:
257                  failed("""
258                      Multiple found: <meta name="generator\"""");
259                  return;
260         }
261 
262         Matcher m = generatorPattern.matcher(generator);
263         if (m.find()) {
264             String content = m.group(1);
265             if (allGenerators.contains(content)) {
266                 passed("found: " + content);
267                 allGeneratorsFound.add(content);
268                 checkDescription(p, content);
269             } else {
270                 failed("Unrecognized content: " + content);
271             }
272         } else {
273             failed("Unrecognized line:\n" + generator);
274         }
275 
276     }
277 
checkDescription(Path p, String generator)278     void checkDescription(Path p, String generator) {
279         checking("Check description: " + p);
280 
281         List<String> descriptions = nl.splitAsStream(readOutputFile(p.toString()))
282                 .filter(s -> s.contains("<meta name=\"description\""))
283                 .collect(Collectors.toList());
284 
285         String description;
286         switch (descriptions.size()) {
287             case 0:
288                 if (generator.equals("DocFileWriter")) {
289                     passed("Not found, as expected");
290                 } else {
291                     failed("""
292                         Not found: <meta name="description\"""");
293                 }
294                 return;
295             case 1:
296                 description = descriptions.get(0);
297                 break;
298             default:
299                 failed("""
300                     Multiple found: <meta name="description\"""");
301                 return;
302         }
303 
304         String content;
305         Matcher m = contentPattern.matcher(description);
306         if (m.find()) {
307             content = m.group(1);
308         } else {
309             failed("Unrecognized line:\n" + description);
310             return;
311         }
312 
313         switch (generator) {
314             case "AllClassesIndexWriter":
315             case "AllPackagesIndexWriter":
316             case "ModuleIndexWriter":
317             case "PackageIndexWriter":
318                 check(generator, content, content.contains("index"));
319                 break;
320 
321 
322             case "AnnotationTypeWriterImpl":
323             case "ClassWriterImpl":
324             case "ModuleWriterImpl":
325             case "PackageWriterImpl":
326                 check(generator, content, content.startsWith("declaration: "));
327                 break;
328 
329             case "ClassUseWriter":
330             case "PackageUseWriter":
331                 check(generator, content, content.startsWith("use: "));
332                 break;
333 
334             case "ConstantsSummaryWriterImpl":
335                 check(generator, content, content.contains("constants"));
336                 break;
337 
338             case "DeprecatedListWriter":
339                 check(generator, content, content.contains("deprecated"));
340                 break;
341 
342             case "DocFileWriter":
343                 passed("no constraint for user-provided doc-files");
344                 break;
345 
346             case "HelpWriter":
347                 check(generator, content, content.contains("help"));
348                 break;
349 
350             case "IndexRedirectWriter":
351                 check(generator, content, content.contains("redirect"));
352                 break;
353 
354             case "IndexWriter":
355                 check(generator, content, content.startsWith("index"));
356                 break;
357 
358             case "PackageTreeWriter":
359             case "TreeWriter":
360                 check(generator, content, content.contains("tree"));
361                 break;
362 
363             case "SerializedFormWriterImpl":
364                 check(generator, content, content.contains("serialized"));
365                 break;
366 
367             case "SourceToHTMLConverter":
368                 check(generator, content, content.startsWith("source:"));
369                 break;
370 
371             case "SystemPropertiesWriter":
372                 check(generator, content, content.contains("system properties"));
373                 break;
374 
375             default:
376                 failed("unexpected generator: " + generator);
377                 break;
378         }
379     }
380 
check(String generator, String content, boolean ok)381     void check(String generator, String content, boolean ok) {
382         if (ok) {
383             passed("OK: " + generator + " " + content);
384         } else {
385             failed("unexpected value for " + generator + ": " + content);
386         }
387     }
388 
genSource(Source s)389     Path genSource(Source s) throws IOException {
390         Path src = Path.of("src-" + s);
391         switch (s) {
392             case PACKAGES:
393                 tb.writeJavaFiles(src,
394                     "/** Package pA. {@systemProperty exampleProperty} */ package pA;",
395                     "/** Class pA.CA. */ package pA; public class CA { @Deprecated public static final int ZERO = 0; }",
396                     "/** Anno pA.Anno, */ package pA; public @interface Anno { }",
397                     "/** Serializable pA.Ser, */ package pA; public class Ser implements java.io.Serializable { }",
398                     "/** Package pB. */ package pB;",
399                     "/** Class pB.CB. */ package pB; public class CB { }");
400                 tb.writeFile(src.resolve("pA").resolve("doc-files").resolve("extra.html"),
401                         """
402                             <!doctype html>
403                             <html><head></head><body>Extra</body></html>""");
404                 break;
405 
406             case MODULES:
407                 new ModuleBuilder(tb, "mA")
408                         .exports("pA")
409                         .classes("/** Package mA/pA. */ package pA;")
410                         .classes("/** Class mA/pA.CA. */ package pA; public class CA { @Deprecated public static int ZERO = 0; }")
411                         .write(src);
412                 new ModuleBuilder(tb, "mB")
413                         .exports("pB")
414                         .classes("/** Package mB/pB. */ package pB;")
415                         .classes("/** Class mB/pB.CB. */ package pB; public class CB { }")
416                         .write(src);
417                 break;
418         }
419 
420         return src;
421     }
422 }
423