diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d99a3d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +**.iml +.DS_store +.idea +/.src-rev +/build.properties +attic +build +dist +nbproject +out +play +tmp +webrev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2d9ba5a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +# Contributing to APIDiff + +APIDiff is part of the OpenJDK [CodeTools] Project. + +Please see for how to contribute. + + +[CodeTools]: https://openjdk.org/projects/code-tools \ No newline at end of file diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..9dcceea --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,20 @@ +Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + +This code is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License version 2 only, as +published by the Free Software Foundation. + +This code is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +version 2 for more details (a copy is included in the LICENSE file that +accompanied this code). + +You should have received a copy of the GNU General Public License version +2 along with this work; if not, write to the Free Software Foundation, +Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +or visit www.oracle.com if you need additional information or have any +questions. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eeab58c --- /dev/null +++ b/LICENSE @@ -0,0 +1,347 @@ +The GNU General Public License (GPL) + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Library General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for this service if you wish), +that you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs; and that you know you +can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. You must +make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program proprietary. +To prevent this, we have made it clear that any patent must be licensed for +everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included +without limitation in the term "modification".) Each licensee is addressed as +"you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as +you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or + in part contains or is derived from the Program or any part thereof, to be + licensed as a whole at no charge to all third parties under the terms of + this License. + + c) If the modified program normally reads commands interactively when run, + you must cause it, when started running for such interactive use in the + most ordinary way, to print or display an announcement including an + appropriate copyright notice and a notice that there is no warranty (or + else, saying that you provide a warranty) and that users may redistribute + the program under these conditions, and telling the user how to view a copy + of this License. (Exception: if the Program itself is interactive but does + not normally print such an announcement, your work based on the Program is + not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms +of this License, whose permissions for other licensees extend to the entire +whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 and +2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 above + on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of physically + performing source distribution, a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed only + for noncommercial distribution and only if you received the program in + object code or executable form with such an offer, in accord with + Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code +distributed need not include anything that is normally distributed (in either +source or binary form) with the major components (compiler, kernel, and so on) +of the operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source +code from the same place counts as distribution of the source code, even though +third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or +any work based on the Program), you indicate your acceptance of this License to +do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by +third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), conditions +are imposed on you (whether by court order, agreement or otherwise) that +contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In +such case, this License incorporates the limitation as if written in the body +of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software Foundation. +If the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE +PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, +YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE +PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR +INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA +BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER +OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + + Copyright (C) + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., 59 + Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes + with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free + software, and you are welcome to redistribute it under certain conditions; + type 'show c' for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than 'show w' and 'show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + 'Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General Public +License instead of this License. + + +"CLASSPATH" EXCEPTION TO THE GPL + +Certain source files distributed by Sun Microsystems, Inc. are subject to +the following clarification and special exception to the GPL, but only where +Sun has expressly included in the particular source file's header the words +"Sun designates this particular file as subject to the "Classpath" exception +as provided by Sun in the LICENSE file that accompanied this code." + + Linking this library statically or dynamically with other modules is making + a combined work based on this library. Thus, the terms and conditions of + the GNU General Public License cover the whole combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent modules, + and to copy and distribute the resulting executable under terms of your + choice, provided that you also meet, for each linked independent module, + the terms and conditions of the license of that module. An independent + module is a module which is not derived from or based on this library. If + you modify this library, you may extend this exception to your version of + the library, but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c2f3b6 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# APIDiff + +APIDiff is a utility to compare two or more versions of an API, each as +defined by a series of options similar to those supported by `javac`. + +## Building `apidiff` + +`apidiff` uses the following dependencies: + +* _[Daisy Diff]_: an HTML comparison library, required when building `apidiff` +* _[Java Diff Utils]_: a plain-text comparison library, required when building `apidiff` +* _[TestNG]_: the testing framework, used to run some of the tests for `apidiff` + +Suitable versions of these dependencies can be downloaded by running +`make/build.sh`. + +### Building with Apache Ant + +The default configuration assumes that dependencies have been downloaded +into the `build/deps` directory. +These values can be overridden with project-specific local configuration values +in `build.properties`, if it exists in the root directory of the repo. + +```` + ant -f make/build.xml +```` + +### Building with GNU Make + +The default configuration uses values provided in `make/dependencies.gmk`, +unless these values have been overridden on the command line used to run `make`. + +```` + make -C make Makefile +```` + +### Building with an IDE + +An IDE such as IntelliJ IDEA needs the following configuration: + +* Sources Root: `src` +* TestNG Test Root: `test/testng` +* Libraries: + * _Daisy Diff_, _Java Diff Utils_ available for compilation + * _TestNG_ available for testing + +In addition, some TestNG tests require access to internal classes in +the `jdk.compiler` and `jdk.jdeps` modules: + +```` +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-modules jdk.jdeps +--add-exports jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED +```` + +The following compiler options may also be desirable: + +```` +-Xdoclint:missing/protected +-Xlint:unchecked +```` + +_Note:_ When working on the test files, be careful not to mark module source directories +as `Test Sources Root` at the same time as any source directories that are not +for a module. + +## Documentation + +A "man" page is generated as part of the build, and is the primary source +of information for how to run `apidiff`. + +## ShowDocs + +`showDocs` is a small utility program that is a simple wrapper around the +`APIReader` and `SerializedFormReader` classes in `apidiff`, that can provide +filtered views of the files generated by `javadoc`, in order to view the +parts of those files that will be compared by `apidiff`. + + +[Daisy Diff]: https://github.com/DaisyDiff/DaisyDiff +[Java Diff Utils]: https://github.com/java-diff-utils/java-diff-utils +[TestNG]: https://testng.org/ diff --git a/make/Build.java b/make/Build.java new file mode 100644 index 0000000..257b689 --- /dev/null +++ b/make/Build.java @@ -0,0 +1,1367 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* +# This program will download/build the dependencies for apidiff and then +# build apidiff. Downloaded files are verified against known/specified +# checksums. +# +# The program can be executed directly as a single source-file program +# by the Java launcher, using JDK 12 or later. +# +# $ /path/to/jdk make/Build.java options +# +# For help on command-line options, use the --help option. +# Note: apidiff itself requires JDK 17 or later. + +# As a side effect, the program writes a file build/make.sh which +# can subsequently be used directly to build apidiff, bypassing +# the need to rerun this program if all the dependencies are still +# available. + +# The default version to use when building apidiff can be found in the +# make/version-numbers file, where the default versions and +# corresponding known checksums for the dependencies are also +# specified. Almost all the defaults can be overridden by setting +# the properties on the command line, or in a properties file, +# or as environment variables. + +# For each of the dependency the following steps are applied and the +# first successful one is used: +# +# 1. Check if the dependency is available locally +# 2. Download a prebuilt version of the dependency +# 3. Build the dependency from source, downloading the source archive +# first +# +# In particular, when not found locally the dependencies will be +# handled as follows: +# +# * JUnit, Java Diff Utils, and HtmlCleaner are by default downloaded from Maven Central. +# * Daisy Diff is by default built from source. +# + +# Some noteworthy control variables: +# +# MAVEN_REPO_URL_BASE (e.g. "https://repo1.maven.org/maven2") +# The base URL for the maven central repository. +# +# APIDIFF_VERSION (e.g. "1.0") +# APIDIFF_VERSION_STRING (e.g. "apidiff-1.0+8" +# APIDIFF_BUILD_NUMBER (e.g. "8") +# APIDIFF_BUILD_MILESTONE (e.g. "dev") +# The version information to use for when building apidiff. +# Additional arguments to pass to make when building apidiff. +# +# RM, TAR, UNZIP +# Paths to standard POSIX commands. + +# The control variables for dependencies are on the following general +# form (not all of them are relevant for all dependencies): +# +# _URL (e.g. DAISYDIFF_BIN_ARCHIVE_URL) +# The full URL for the dependency. +# +# _URL_BASE (e.g. DAISYDIFF_BIN_ARCHIVE_URL_BASE) +# The base URL for the dependency. Requires additional dependency +# specific variables to be specified. +# +# _CHECKSUM (e.g. DAISYDIFF_BIN_ARCHIVE_CHECKSUM) +# The expected checksum of the download file. +# + +# The below outlines the details of how the dependencies are +# handled. For each dependency the steps are tried in order and the +# first successful one will be used. +# +# JDK +# Checksum variables: +# JDK_ARCHIVE_CHECKSUM: checksum of binary archive +# +# 1. JAVA_HOME +# The path to the JDK. +# 2a. JDK_ARCHIVE_URL +# The full URL for the archive. +# 2b. JDK_ARCHIVE_URL_BASE + JDK_VERSION + JDK_BUILD_NUMBER + JDK_FILE +# The individual URL components used to construct the full URL. +# +# Java Diff Utils +# Checksum variables: +# JAVADIFFUTILS_JAR_CHECKSUM: checksum of jar +# JAVADIFFUTILS_LICENSE_CHECKSUM: checksum of LICENSE file +# +# 1. JAVADIFFUTILS_JAR + JAVADIFFUTILS_LICENSE +# The path to java-diff-utils.jar and LICENSE.txt respectively. +# 2a. JAVADIFFUTILS_JAR_URL +# The full URL for the jar. +# 2b. JAVADIFFUTILS_JAR_URL_BASE + JAVADIFFUTILS_VERSION + JAVADIFFUTILS_FILE +# The individual URL components used to construct the full URL. +# +# Daisy Diff +# Checksum variables: +# DAISYDIFF_BIN_ARCHIVE_CHECKSUM: checksum of binary archive +# DAISYDIFF_LICENSE_CHECKSUM: checksum of LICENSE file +# +# 1. DAISYDIFF_JAR + DAISYDIFF_LICENSE +# The path to daisydiff.jar and LICENSE.txt respectively. +# 2a. DAISYDIFF_JAR_URL +# The full URL for the jar. +# 2b. DAISYDIFF_JAR_URL_BASE + DAISYDIFF_BIN_VERSION + DAISYDIFF_FILE +# The individual URL components used to construct the full URL. +# +# Html Cleaner +# Checksum variables: +# HTMLCLEANER_JAR_CHECKSUM: checksum of jar +# HTMLCLEANER_LICENSE_CHECKSUM: checksum of LICENSE file +# +# 1. HTMLCLEANER_JAR + HTMLCLEANER_LICENSE +# The path to htmlcleaner.jar and licence.txt respectively. +# 2a. HTMLCLEANER_JAR_URL +# The full URL for the jar. +# 2b. HTMLCLEANER_JAR_URL_BASE + HTMLCLEANER_VERSION + HTMLCLEANER_FILE +# The individual URL components used to construct the full URL. +# +# JUnit, for running self-tests +# Checksum variables: +# JUNIT_JAR_CHECKSUM: checksum of binary archive +# +# 1. JUNIT_JAR + JUNIT_LICENSE +# The path to junit.jar and LICENSE respectively. +# 2a. JUNIT_JAR_URL +# The full URL for the jar. +# 2b. JUNIT_JAR_URL_BASE + JUNIT_VERSION + JUNIT_FILE +# The individual URL components used to construct the full URL. +# +# Some control variables can be overridden by command-line options. +# Use the --help option for details. +*/ + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.Reader; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Utility to download the dependencies needed to build APIDiff, + * based on command-line parameters, configuration info in + * make/build-support/version-numbers, and environment variables. + * + *

The class can be executed directly by the Java source code launcher, + * using JDK 17 or later. + */ +public class Build { + public enum Exit { + OK, BAD_OPTION, ERROR + } + + /** + * Execute the main program. + * + * @param args command-line arguments + */ + public static void main(String... args) { + try { + PrintWriter outWriter = new PrintWriter(System.out); + PrintWriter errWriter = new PrintWriter(System.err, true); + try { + try { + new Build().run(outWriter, errWriter, args); + } finally { + outWriter.flush(); + } + } finally { + errWriter.flush(); + } + System.exit(Exit.OK.ordinal()); + } catch (BadOption e) { + System.err.println("Error: " + e.getMessage()); + System.exit(Exit.BAD_OPTION.ordinal()); + } catch (Fault e) { + System.err.println("Error: " + e.getMessage()); + System.exit(Exit.ERROR.ordinal()); + } + } + + /** + * The root directory for the repo containing this class. + */ + private final Path rootDir; + + /** + * The minimum version of JDK required to build apidiff. + */ + private static final int requiredJDKVersion = 17; + + /** + * Creates an instance of the utility. + * + * @throws Fault if an unrecoverable error occurs while determining the root directory + */ + Build() throws Fault { + rootDir = getRootDir(); + } + + /** + * The main worker method for the utility. + * + * @param out the stream to which to write any requested output + * @param err the stream to which to write any logging or error output + * @param args any command-line arguments + * @throws BadOption if there is an error in any of the command-line arguments + * @throws Fault if there is an unrecoverable error + */ + public void run(PrintWriter out, PrintWriter err, String... args) throws BadOption, Fault { + + // The collection of values specified by the command-line options. + var options = Options.handle(rootDir, List.of(args)); + + // The collection of values derived from command-line options, + // the make/build-support/version-numbers file, and default values. + var config = new Config(rootDir, options, out, err); + + var done = false; + + if (options.help) { + options.showCommandHelp(config.out); + done = true; + } + + if (options.showDefaultVersions) { + showProperties(config.properties, config.out); + done = true; + } + + if (options.showConfigDetails) { + if (config.properties.isEmpty()) { + config.out.println("no custom configuration values"); + } else { + showProperties(config.properties, config.out); + } + done = true; + } + + if (done) { + return; + } + + DaisyDiff dd; + var dependencies = List.of( + new BuildInfo(config), + dd = new DaisyDiff(config), + new Equinox(config, dd), + new HtmlCleaner(config), + new JavaDiffUtils(config), + new JUnit(config) + ); + + for (var d : dependencies) { + d.setup(); + } + + for (var d : dependencies) { + d.verify(); + } + + var makeScript = config.buildDir.resolve("make.sh"); + new MakeScript(config).writeFile(makeScript, dependencies); + + if (!options.skipMake) { + config.log("Building"); + config.out.flush(); + config.err.flush(); + execScript(makeScript, config.options.makeArgs); + } + } + + /** + * Writes a set of properties to a given output stream. + * + * @param p the properties + * @param out the output stream + */ + private static void showProperties(Properties p, PrintWriter out) { + p.stringPropertyNames().stream() + .sorted() + .forEach(k -> out.println(k + "=" + p.getProperty(k))); + } + + /** + * Executes a shell script. + * + * @param script the path for the script + * @param args the arguments, if any, for the script + * @throws Fault if an error occurs while executing the script + */ + private static void execScript(Path script, List args) throws Fault { + try { + Process p = new ProcessBuilder(join("sh", join(script.toString(), args))) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start(); + p.waitFor(); + int rc = p.exitValue(); + if (rc != 0) { + throw new Fault("Error while running " + script + ": rc=" + rc); + } + } catch (IOException | InterruptedException e) { + throw new Fault("error running " + script + ": " + e); + } + } + + /** + * Forms a single list from a string and a list of strings. + * + * @param cmd the string + * @param args the list of strings + * @return a list formed from the string and list of strings + */ + private static List join(String cmd, List args) { + if (args.isEmpty()) { + return List.of(cmd); + } + var list = new ArrayList(); + list.add(cmd); + list.addAll(args); + return list; + } + + /** + * Returns the root directory for the repo containing this class, + * as determined by checking enclosing directories for the marker + * file make/Makefile. + * + * @return the root directory + * @throws Fault if the root directory cannot be determined + */ + private static Path getRootDir() throws Fault { + Path dir = getThisClass().getParent(); + Path marker = Path.of("make").resolve("Makefile"); + while (dir != null) { + if (Files.isRegularFile(dir.resolve(marker))) { + return dir; + } + dir = dir.getParent(); + } + throw new Fault("cannot determine root directory"); + } + + /** + * Returns the path for this class, determined from the location in + * the class' protection domain. + * + * @return the path + * @throws Fault if an error occurs + */ + private static Path getThisClass() throws Fault { + try { + return Path.of(Build.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + } catch (URISyntaxException e) { + throw new Fault("cannot determine location of this class"); + } + } + + /** + * Exception used to report a bad command-line option. + */ + static class BadOption extends Exception { + BadOption(String message) { + super(message); + } + BadOption(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Exception used to report an unrecoverable error. + */ + static class Fault extends Exception { + Fault(String message) { + super(message); + } + Fault(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * The set of allowable command-line options. + */ + enum Option { + @Description("Show this message") + HELP("--help -h -help -?", null) { + @Override + void process(String opt, String arg, Options options) { + options.help = true; + } + }, + + @Description("Path to JDK; must be JDK " + requiredJDKVersion + " or higher") + JDK("--jdk", "") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.jdk = asExistingPath(arg); + } + }, + + @Description("Reduce the logging output") + QUIET("--quiet -q", null) { + @Override + void process(String opt, String arg, Options options) { + options.quiet = true; + } + }, + + @Description("Show default versions of external components") + SHOW_DEFAULT_VERSIONS("--show-default-versions", null) { + @Override + void process(String opt, String arg, Options options) { + options.showDefaultVersions = true; + } + }, + + @Description("Show configuration details") + SHOW_CONFIG_DETAILS("--show-config-details", null) { + @Override + void process(String opt, String arg, Options options) { + options.showConfigDetails = true; + } + }, + + @Description("Skip checksum check") + SKIP_CHECKSUM_CHECK("--skip-checksum-check", null) { + @Override + void process(String opt, String arg, Options options) { + options.skipChecksumCheck = true; + } + }, + + @Description("Skip downloads if file available locally") + SKIP_DOWNLOAD("--skip-download", null) { + @Override + void process(String opt, String arg, Options options) { + options.skipDownloads = true; + } + }, + + @Description("Skip running 'make' (just download dependencies if needed)") + SKIP_MAKE("--skip-make", null) { + @Override + void process(String opt, String arg, Options options) { + options.skipMake = true; + } + }, + + @Description("Provide an alternate file containing dependency version information") + VERSION_NUMBERS("--version-numbers", "") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.versionNumbers = asExistingPath(arg); + } + }, + + @Description("Provide an alternate file containing configuration details") + CONFIG_FILE("--config", "") { + @Override + void process(String opt, String arg, Options options) throws BadOption, Fault { + var p = asExistingPath(arg); + try (BufferedReader r = Files.newBufferedReader(p)) { + options.configProperties.load(r); + } catch (IOException e) { + throw new Fault("error reading " + p + ": " + e, e); + } + } + }, + + @Description("Override a specific configuration value") + CONFIG_VALUE("NAME=VALUE", null), + + @Description("Subsequent arguments are for 'make'") + MAKE_ARGS("--", null); + + @Retention(RetentionPolicy.RUNTIME) + @interface Description { + String value(); + } + + final List names; + final String arg; + + Option(String names, String arg) { + this.names = Arrays.asList(names.split("\\s+")); + this.arg = arg; + } + + void process(String opt, String arg, Options options) throws BadOption, Fault { + throw new Error("internal error"); + } + + static Path asPath(String p) throws BadOption { + try { + return Path.of(p); + } catch (InvalidPathException e) { + throw new BadOption("File not found: " + p, e); + } + } + + static Path asExistingPath(String p) throws BadOption { + var path = asPath(p); + if (!Files.exists(path)) { + throw new BadOption("File not found: " + p); + } + return path; + } + } + + /** + * The set of values given by the command-line options. + */ + static class Options { + boolean help; + Path jdk; + boolean quiet; + boolean showDefaultVersions; + boolean showConfigDetails; + boolean skipChecksumCheck; + boolean skipDownloads; + boolean skipMake; + private Path versionNumbers; + private List makeArgs = List.of(); + + final private Properties configProperties; + + Options(Path rootDir) { + var dir = rootDir.resolve("make").resolve("build-support"); + versionNumbers = dir.resolve("version-numbers"); + configProperties = new Properties(); + } + + static Options handle(Path rootDir, List args) throws BadOption, Fault { + Options options = new Options(rootDir); + + Map map = new HashMap<>(); + for (Option o : Option.values()) { + o.names.forEach(n -> map.put(n, o)); + } + + for (int i = 0; i < args.size(); i++) { + String arg = args.get(i); + // currently no support for positional args + String optName, optValue; + int eq = arg.indexOf("="); + if (eq == -1) { + optName = arg; + optValue = null; + } else { + optName = arg.substring(0, eq); + optValue = arg.substring(eq + 1); + } + if (optName.isEmpty()) { + throw new BadOption("bad argument: " + arg); + } else { + Option opt = map.get(optName); + if (opt == null) { + if (optName.matches("[A-Z_]+")) { + options.configProperties.setProperty(optName, optValue); + } else { + throw new BadOption("unknown option: " + optName); + } + } else { + if (opt == Option.MAKE_ARGS) { + options.makeArgs = args.subList(i + 1, args.size()); + i = args.size(); + } else if (opt.arg == null) { + // no value for option required + if (optValue != null) { + throw new BadOption("unexpected value for " + optName + " option: " + optValue); + } else { + opt.process(optName, null, options); + } + } else { + // value for option required; use next arg if not found after '=' + if (optValue == null) { + if (i + 1 < args.size()) { + optValue = args.get(++i); + } else { + throw new BadOption("no value for " + optName + " option"); + } + } + opt.process(optName, optValue, options); + } + } + } + } + + return options; + } + + void showCommandHelp(PrintWriter out) { + out.println("Usage: java " + Build.class.getSimpleName() + ".java " + + " [ -- ]" ); + out.println("Options:"); + for (var o : Option.values()) { + out.println(o.names.stream() + .map(n -> n + (o.arg == null ? "" : " " + o.arg)) + .collect(Collectors.joining(", ", " ", ""))); + try { + Field f = Option.class.getDeclaredField(o.name()); + Option.Description d = f.getAnnotation(Option.Description.class); + out.println(" " + d.value()); + } catch (ReflectiveOperationException e) { + throw new Error(e); + } + } + } + } + + /** + * The set of configuration values determined from command-line options, + * the make/build-support/version-numbers file, and any defaults. + */ + static class Config { + final Path rootDir; + final Options options; + final PrintWriter out; + final PrintWriter err; + private final Path buildDir; + private final Properties properties; + private final Path jdk; + private final MapsysEnv; + + Config(Path rootDir, Options options, PrintWriter out, PrintWriter err) throws Fault { + this.rootDir = rootDir; + this.options = options; + this.out = out; + this.err = err; + + this.buildDir = rootDir.resolve("build"); + + var versionNumbers = readProperties(options.versionNumbers); + properties = new Properties(versionNumbers); + properties.putAll(options.configProperties); + + sysEnv = System.getenv(); + + var jdk = options.jdk; + if (jdk == null) { + jdk = getPath("JAVA_HOME"); + } + if (jdk == null) { + jdk = Path.of(System.getProperty("java.home")); + } + this.jdk = jdk; + } + + void log(String line) { + if (!options.quiet) { + err.println(line); + } + } + + void error(String lines) { + lines.lines().forEach(err::println); + } + + private String getString(String key) { + var v = properties.getProperty(key); + if (v == null) { + if (key.endsWith("_VERSION") + || key.endsWith("_CHECKSUM") + || key.endsWith("_SRC_TAG") + || key.contains("_LICENSE_")) { + v = properties.getProperty("DEFAULT_" + key); + } + + if (v == null) { + v = sysEnv.get(key); + } + } + return v; + } + + private String getRequiredString(String key) throws Fault { + var v = getString(key); + if (v == null) { + throw new Fault("no configuration value for " + key); + } + return v; + } + + public Path getPath(String key) throws Fault { + String v = getString(key); + try { + return v == null ? null : Path.of(v); + } catch (InvalidPathException e) { + throw new Fault("bad path: " + v + ": " + e); + } + } + + public Path getCommandPath(String name) throws Fault { + String n = name.toUpperCase(Locale.ROOT); + Path p = getPath(n); + if (p == null) { + p = which(name); + if (p != null) { + properties.put(n, p.toString()); + } + } + return p; + } + + public URL getURL(String key) { + var v = getString(key); + try { + return v == null ? null : new URL(v); + } catch (MalformedURLException e) { + throw new Error("Bad URL for " + key + ": " + v + ": " + e); + } + } + + private Properties readProperties(Path file) throws Fault { + Properties p = new Properties(); + if (file != null) { + try (Reader r = Files.newBufferedReader(file)) { + p.load(r); + } catch (IOException e) { + throw new Fault("error reading " + file + ": " + e, e); + } + } + return p; + } + + Path which(String cmd) throws Fault { + try { + Process p = new ProcessBuilder(List.of("which", cmd)) + .redirectErrorStream(true) + .start(); + try (var r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + String out = r.lines().collect(Collectors.joining()); + p.waitFor(); + int rc = p.exitValue(); + if (rc != 0) { + throw new Fault("error running '" + cmd + "': rc=" + rc); + } + return out.isEmpty() ? null : Path.of(out); + } + } catch (InvalidPathException e) { + throw new Fault("Unexpected output from 'which " + cmd + "': " + e, e); + } catch (IOException | InterruptedException e) { + throw new Fault("error running '" + cmd +"': " + e); + } + } + } + /** + * Base class for a dependency to be made available for the build. + */ + static abstract class Dependency { + protected final String name; + protected final Path depsDir; + protected final Config config; + + private static final String DEFAULT_MAVEN_URL = "https://repo1.maven.org/maven2"; + + Dependency(String name, Config config) { + this.name = name; + this.config = config; + this.depsDir = config.rootDir.resolve("build").resolve("deps").resolve(name); + } + + public abstract void setup() throws Fault; + + public abstract void verify() throws Fault; + + public Map getMakeArgs() { + return Collections.emptyMap(); + } + + protected void createDepsDir() throws Fault { + try { + Files.createDirectories(depsDir); + } catch (IOException e) { + throw new Fault("Failed to create " + depsDir + ": " + e, e); + } + } + + protected Path download(URL url, Path file, String checksum) throws Fault { + if (Files.isDirectory(file)) { + file = file.resolve(baseName(url)); + } + + if (Files.isReadable(file) && config.options.skipDownloads) { + return file; + } + + config.log("Downloading " + url); + try { + Files.createDirectories(file.getParent()); + } catch (IOException e) { + throw new Fault("Error creating directory for " + file + ": " + e); + } + + try (var in = url.openStream()) { + var md = MessageDigest.getInstance("SHA-1"); + try (var in2 = new DigestInputStream(in, md)) { + Files.copy(in2, file, StandardCopyOption.REPLACE_EXISTING); + } + var digest = toString(md.digest()); + if ((!config.options.skipChecksumCheck && !checksum.equals("--")) + && !checksum.equals(digest)) { + config.error("Checksum error for " + url + "\n" + + " expect: " + checksum + "\n" + + " actual: " + digest); + } + } catch (IOException | NoSuchAlgorithmException e) { + throw new Fault("Error downloading " + url + ": " + e, e); + } + + return file; + } + + protected Path downloadStandardJar(BiFunction makeDefaultURL) throws Fault { + createDepsDir(); + var prefix = name.toUpperCase(Locale.ROOT).replaceAll("[^A-Z_]+", ""); + var jarURL = config.getURL(prefix + "_JAR_URL"); + if (jarURL == null) { + var jarURLBase = config.getURL(prefix + "_JAR_URL_BASE"); + if (jarURLBase == null) { + jarURLBase = config.getURL("MAVEN_REPO_URL_BASE"); + if (jarURLBase == null) { + jarURLBase = newURL(DEFAULT_MAVEN_URL); + } + } + var version = config.getString(prefix + "_VERSION"); + jarURL = newURL(makeDefaultURL.apply(jarURLBase, version)); + } + var checksum = config.getString(prefix + "_JAR_CHECKSUM"); + return download(jarURL, depsDir, checksum); + } + + protected Path unpack(Path archive, Path dir) throws Fault { + try (var ds = Files.newDirectoryStream(depsDir, Files::isDirectory)) { + for (var d : ds) { + exec(config.getCommandPath("rm"), List.of("-rf", d.toString())); + } + } catch (IOException e) { + throw new Fault("error listing " + depsDir +": " + e, e); + } + + String s = archive.getFileName().toString(); + if (s.endsWith(".tar.gz")) { + exec(config.getCommandPath("tar"), + List.of("-xzf", archive.toString(), "-C", dir.toString())); + } else if (s.endsWith(".zip")) { + // cannot extract files with permissions using standard ZipFile API + // so resort to the unzip command + exec(config.getCommandPath("unzip"), + List.of("-q", archive.toString(), "-d", dir.toString())); + } else { + throw new Fault("unrecognized archive type for file " + archive); + } + + try (DirectoryStream ds = Files.newDirectoryStream(dir, Files::isDirectory)) { + Path bestSoFar = null; + FileTime bestSoFarTime = null; + for (var p : ds) { + var pTime = Files.getLastModifiedTime(p); + if (bestSoFar == null || pTime.compareTo(bestSoFarTime) > 0) { + bestSoFar = p; + } + bestSoFarTime = pTime; + } + return bestSoFar; + } catch (IOException e) { + throw new Fault("Error listing contents of " + dir + ": " + e, e); + } + } + + protected void checkFile(Path file) throws Fault { + config.log("Checking " + file); + if (!(Files.isRegularFile(file) && Files.isReadable(file))) { + throw new Fault(file + " is not a readable file"); + } + } + + protected void checkDirectory(Path dir) throws Fault { + config.log("Checking " + dir); + if (!Files.isDirectory(dir)) { + throw new Fault(dir + " is not a directory"); + } + } + + private String toString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (var b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + protected URL newURL(String u) throws Fault { + try { + return new URL(u); + } catch (MalformedURLException e) { + throw new Fault("Error creating URL " + u + ": " + e); + } + } + + protected String baseName(URL url) { + var p = url.getPath(); + var lastSep = p.lastIndexOf("/"); + return lastSep == -1 ? p : p.substring(lastSep+ 1); + } + + protected void exec(Path cmd, List args) throws Fault { + config.out.flush(); + config.err.flush(); +// System.err.println("exec: " + cmd + " " + args); + try { + Process p = new ProcessBuilder(join(cmd.toString(), args)) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start(); + p.waitFor(); + int rc = p.exitValue(); + if (rc != 0) { + throw new Fault("error running '" + cmd + "': rc=" + rc); + } + } catch (IOException | InterruptedException e) { + throw new Fault("error running '" + cmd + "': " + e); + } + } + } + + /** + * A pseudo-dependency to provide build version details. + */ + static class BuildInfo extends Dependency { + String version; + String buildMileStone; + String buildNumber; + String versionString; + + BuildInfo(Config config) { + super("apidiff", config); + } + + @Override + public void setup() throws Fault { + var prefix = name.toUpperCase(Locale.ROOT); + version = config.getRequiredString(prefix + "_VERSION"); + + buildMileStone = config.getString(prefix + "_BUILD_MILESTONE"); + if (buildMileStone == null) { + buildMileStone = "dev"; + } + + buildNumber = config.getString(prefix + "_BUILD_NUMBER"); + if (buildNumber == null) { + buildNumber = "0"; + } + + versionString = config.getString(prefix + "_VERSION_STRING"); + if (versionString == null) { + versionString = version + + (buildMileStone.isEmpty() ? "" : "-" + buildMileStone) + + "+" + buildNumber; + } + } + + @Override + public void verify() throws Fault { + int version; + if (config.jdk.equals(Path.of(System.getProperty("java.home")))) { + version = Runtime.version().feature(); + } else { + var javaCmd = config.jdk.resolve("bin").resolve("java"); + try { + Process p = new ProcessBuilder(List.of(javaCmd.toString(), "-version")) + .redirectErrorStream(true) + .start(); + try (var r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + String out = r.lines() + .filter(l -> l.matches(".*(java|openjdk).*")) + .findFirst() + .orElse(""); + var m = Pattern.compile("\"(1.)?(?[0-9]+)[^ \"]*\"").matcher(out); + if (m.find()) { + version = Integer.parseInt(m.group("v")); + } else { + throw new Fault("version info not found in output from '" + javaCmd + " -version'"); + } + } + } catch (IOException e) { + throw new Fault("Error running '" + javaCmd + " -version': " + e, e); + } + } + + if (version < requiredJDKVersion) { + throw new Fault("JDK " + requiredJDKVersion + " or newer is required to build apidiff"); + } + } + + @Override + public Map getMakeArgs() { + return Map.of( + "BUILDDIR", config.buildDir.toString(), + "JDKHOME", config.jdk.toString(), + "BUILD_VERSION", version, + "BUILD_MILESTONE", buildMileStone, + "BUILD_NUMBER", buildNumber, + "BUILD_VERSION_STRING", versionString + ); + } + } + + /** + * DaisyDiff, providing the ability to compare HTML files. + * + * @see DaisyDiff + */ + static class DaisyDiff extends Dependency { + private Path jar; + private Path src; + private Path license; + + static final String DEFAULT_REPO_URL = "https://github.com/guyvdbroeck/daisydiff-1"; + + DaisyDiff(Config config) { + super("daisydiff", config); + } + + @Override + public void setup() throws Fault { + jar = config.getPath("DAISYDIFF_JAR"); + if (jar == null) { + createDepsDir(); + src = config.getPath("DAISYDIFF_SRC"); + if (src == null) { + var srcArchiveURL = config.getURL("DAISYDIFF_SRC_ARCHIVE_URL"); + if (srcArchiveURL == null) { + // build URL from base and version number + var srcArchiveURLBase = config.getURL("DAISYDIFF_SRC_ARCHIVE_URL_BASE"); + if (srcArchiveURLBase == null) { + var repoURLBase = config.getURL("DAISYDIFF_REPO_URL_BASE"); + if (repoURLBase == null) { + repoURLBase = newURL(DEFAULT_REPO_URL); + } + srcArchiveURLBase = repoURLBase; + } + var srcVersion = config.getString("DAISYDIFF_SRC_VERSION"); + srcArchiveURL = newURL(srcArchiveURLBase + + "/archive/refs/tags/release-" + + srcVersion + + ".tar.gz"); + } + var checksum = config.getString("DAISYDIFF_SRC_ARCHIVE_CHECKSUM"); + var srcArchive = download(srcArchiveURL, depsDir, checksum); + src = unpack(srcArchive, depsDir).resolve("src"); + } + } + + license = config.getPath("DAISYDIFF_LICENSE"); + if (license == null) { + var version = config.getString("DAISYDIFF_LICENSE_VERSION"); + var licenseURL = newURL("https://raw.githubusercontent.com/DaisyDiff/DaisyDiff/" + + version + + "/LICENSE.txt"); + var licenseChecksum = config.getString("DAISYDIFF_LICENSE_CHECKSUM"); + license = download(licenseURL, depsDir, licenseChecksum); + } + } + + @Override + public void verify() throws Fault { + if (jar == null && src == null) { + throw new Fault("jar file or source directory not found for DaisyDiff"); + } + if (jar != null) { + checkFile(jar); + } + if (src != null) { + checkDirectory(src); + } + checkFile(license); + } + + @Override + public Map getMakeArgs() { + var args = new HashMap(); + if (jar != null) { + args.put("DAISYDIFF_JAR", jar.toString()); + } + if (src != null) { + args.put("DAISYDIFF_SRC", src.toString()); + } + args.put("DAISYDIFF_LICENSE", license.toString()); + return args; + } + } + + /** + * Eclipse Equinox, required when building DaisyDiff from source. + * + * @see Common Eclipse Runtime + */ + static class Equinox extends Dependency { + Path jar; + Path license; + DaisyDiff daisyDiff; + + private static final String DEFAULT_LICENSE_URL = "https://www.eclipse.org/org/documents/epl-v10.html"; + + Equinox(Config config, DaisyDiff daisyDiff) { + super("equinox", config); + this.daisyDiff = daisyDiff; + } + + @Override + public void setup() throws Fault { + // Only need equinox when building daisydiff from source + if (daisyDiff.src == null) { + return; + } + + jar = config.getPath("EQUINOX_JAR"); + if (jar == null) { + jar = downloadStandardJar((urlBase, version) -> + urlBase + + "/org/eclipse/equinox/org.eclipse.equinox.common/" + + version + + "/org.eclipse.equinox.common-" + version + ".jar" + ); + } + + license = config.getPath("EQUINOX_LICENSE"); + if (license == null) { + var licenseURL = newURL(DEFAULT_LICENSE_URL); + var licenseChecksum = config.getString("EQUINOX_LICENSE_CHECKSUM"); + license = download(licenseURL, depsDir, licenseChecksum); + } + } + + @Override + public void verify() throws Fault { + checkFile(jar); + checkFile(license); + } + + @Override + public Map getMakeArgs() { + return daisyDiff.src == null + ? Collections.emptyMap() + : Map.of( + "EQUINOX_JAR", jar.toString(), + "EQUINOX_LICENSE", license.toString()); + } + } + + /** + * HtmlCleaner, to transform dirty HTML to well-formed XML. + * + * @see HtmlCleaner + */ + static class HtmlCleaner extends Dependency { + private Path jar; + private Path license; + + HtmlCleaner(Config config) { + super("htmlcleaner", config); + } + + @Override + public void setup() throws Fault { + jar = config.getPath("HTMLCLEANER_JAR"); + if (jar == null) { + jar = downloadStandardJar((urlBase, version) -> + urlBase + + "/net/sourceforge/htmlcleaner/htmlcleaner/" + + version + + "/htmlcleaner-" + version + ".jar"); + } + + license = config.getPath("HTMLCLEANER_LICENSE"); + if (license == null) { + var version = config.getString("HTMLCLEANER_VERSION"); + var licenseURL = newURL("https://sourceforge.net/p/htmlcleaner/code/HEAD/tree/tags/" + + "htmlcleaner-" + version + + "/licence.txt?format=raw"); + var licenseChecksum = config.getString("HTMLCLEANER_LICENSE_CHECKSUM"); + license = download(licenseURL, depsDir, licenseChecksum); + } + } + + @Override + public void verify() throws Fault { + checkFile(jar); + checkFile(license); + } + + @Override + public Map getMakeArgs() { + return Map.of( + "HTMLCLEANER_JAR", jar.toString(), + "HTMLCLEANER_LICENSE", license.toString()); + } + } + + /** + * Java Diff Utilities, to compare text files. + * + * @see Java Diff Utilities + */ + static class JavaDiffUtils extends Dependency { + private Path jar; + private Path license; + + JavaDiffUtils(Config config) { + super("java-diff-utils", config); + } + + @Override + public void setup() throws Fault { + jar = config.getPath("JAVADIFFUTILS_JAR"); + if (jar == null) { + jar = downloadStandardJar((urlBase, version) -> + urlBase + + "/io/github/java-diff-utils/java-diff-utils/" + + version + + "/java-diff-utils-" + version + ".jar"); + } + + license = config.getPath("JAVADIFFUTILS_LICENSE"); + if (license == null) { + var version = config.getString("JAVADIFFUTILS_LICENSE_VERSION"); + var licenseURL = newURL("https://raw.githubusercontent.com/java-diff-utils/java-diff-utils/" + + "java-diff-utils-" + version + + "/LICENSE"); + var licenseChecksum = config.getString("JAVADIFFUTILS_LICENSE_CHECKSUM"); + license = download(licenseURL, depsDir, licenseChecksum); + } + } + + @Override + public void verify() throws Fault { + checkFile(jar); + } + + @Override + public Map getMakeArgs() { + return Map.of( + "JAVADIFFUTILS_JAR", jar.toString(), + "JAVADIFFUTILS_LICENSE", license.toString()); + } + } + + /** + * JUnit, to run tests for APIDiff. + * + * @see JUnit + */ + static class JUnit extends Dependency { + private Path jar; + + JUnit(Config config) { + super("junit", config); + } + + @Override + public void setup() throws Fault { + jar = config.getPath("JUNIT_JAR"); + if (jar == null) { + jar = downloadStandardJar((urlBase, version) -> + urlBase + + "/org/junit/platform/junit-platform-console-standalone/" + + version + + "/junit-platform-console-standalone-" + version + ".jar"); + } + } + + @Override + public void verify() throws Fault { + checkFile(jar); + } + + @Override + public Map getMakeArgs() { + return Map.of("JUNIT_JAR", jar.toString()); + } + } + + /** + * Generates a script to run "make", based on the set of dependencies. + */ + static class MakeScript { + private final Config config; + MakeScript(Config config) { + this.config = config; + } + + void writeFile(Path file, List deps) throws Fault { + var allMakeArgs = new TreeMap(); + deps.forEach(d -> allMakeArgs.putAll(d.getMakeArgs())); + + try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(file))) { + out.println("#!/bin/sh"); + out.println(); + out.println("cd \"" + config.rootDir.resolve("make") + "\""); + out.println("make \\"); + allMakeArgs.forEach((name, value) -> + out.printf(" %s=\"%s\" \\%n", name, value)); + out.println(" \"$@\""); + } catch (IOException e) { + throw new Fault("Error writing make command script: " + file + ": " + e); + } + } + } + +} diff --git a/make/Defs.gmk b/make/Defs.gmk new file mode 100644 index 0000000..01ad449 --- /dev/null +++ b/make/Defs.gmk @@ -0,0 +1,171 @@ +# +# Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +# include host-specific defs, if any +-include Defs-$(shell hostname).gmk + +# TOPDIR set in Makefile +ABSTOPDIR = $(shell cd $(TOPDIR); pwd) + +# clobber settings from user's environment +JAVA_HOME= +CLASSPATH= +JAVA_COMPILER= +LD_LIBRARY_PATH= + +#---------------------------------------------------------------------- +# +# Support for Cygwin + +SYSTEM_UNAME := $(shell uname) + +# Where is unwanted output to be delivered? +# On Windows, MKS uses the special file "NUL", cygwin uses the customary unix file. +ifeq ($(SYSTEM_UNAME), Windows_NT) +DEV_NULL = NUL +else +DEV_NULL = /dev/null +endif + +ifneq (,$(findstring CYGWIN,$(SYSTEM_UNAME))) +USING_CYGWIN = true +endif + +ifdef USING_CYGWIN +define FullPath +$(shell cygpath -a -m $1 2> $(DEV_NULL)) +endef +define PosixPath +$(shell cygpath -a -u $1 2> $(DEV_NULL)) +endef +else +define FullPath +$(abspath $1) +endef +define PosixPath +$1 +endef +endif + +ifndef BUILDDIR + BUILDDIR = $(TOPDIR)/build +endif +override BUILDDIR := $(call FullPath, $(BUILDDIR)) + +BUILDTESTDIR=$(BUILDDIR)/test + + +#---------------------------------------------------------------------- +# +# Parameters to control what to build and test with. + +JAVA = $(JDKHOME)/bin/java +JAVAC = $(JDKHOME)/bin/javac +JAVADOC = $(JDKHOME)/bin/javadoc +JAR = $(JDKHOME)/bin/jar + +#----- Unix commands + +AWK = /usr/bin/awk +CAT = /bin/cat +CHMOD = /bin/chmod +CP = /bin/cp +DIFF = /usr/bin/diff +ECHO = /bin/echo +FIND = /usr/bin/find +GREP := $(shell if [ -r /bin/grep ]; then echo /bin/grep ; else echo /usr/bin/grep ; fi ) +LN = /bin/ln +MKDIR = /bin/mkdir +MV = /bin/mv +PANDOC = $(shell if [ -r /usr/bin/pandoc ]; then \ + echo /usr/bin/pandoc ; \ + elif [ -r /usr/local/bin/pandoc ]; then \ + echo /usr/local/bin/pandoc ; \ + elif [ -r /opt/homebrew/bin/pandoc ]; then \ + echo /opt/homebrew/bin/pandoc ; \ + else \ + echo /bin/echo "pandoc not available" ; \ + fi ) +PERL = /usr/bin/perl +PRINTF = /usr/bin/printf +RM = /bin/rm -rf +SED := $(shell if [ -r /bin/sed ]; then echo /bin/sed ; else echo /usr/bin/sed ; fi ) +SH = /bin/sh +SORT = /usr/bin/sort +TEST = /usr/bin/test +# tidy needs to support HTML 5; typically means `tidy -version` reports 5.x or higher +ifeq ($(SYSTEM_UNAME), Darwin) +TIDY := $(shell if [ -r /usr/local/bin/tidy ]; then \ + echo /usr/local/bin/tidy ; \ + elif [ -r /opt/homebrew/bin/tidy ]; then \ + echo /opt/homebrew/bin/tidy ; \ + else \ + echo /usr/bin/tidy ; \ + fi ) +else +TIDY = /usr/bin/tidy +endif +TOUCH = /usr/bin/touch +UNZIP = /usr/bin/unzip +WC = /usr/bin/wc +ZIP = /usr/bin/zip + + +#---------------------------------------------------------------------- +# +# Identification of parts of the system + +SRCDIR = $(TOPDIR)/src +JAVADIR = $(SRCDIR)/share/classes +SRCDOCDIR = $(SRCDIR)/share/doc +SRCSHAREBINDIR = $(SRCDIR)/share/bin +TESTDIR = $(TOPDIR)/test + +CLASSDIR = $(BUILDDIR)/classes +ABSCLASSDIR = $(cd $(CLASSDIR); pwd) + +IMAGES_DIR = $(BUILDDIR)/images +APIDIFF_IMAGEDIR = $(IMAGES_DIR)/apidiff +APIDIFF_IMAGEDOCDIR = $(APIDIFF_IMAGEDIR)/doc +APIDIFF_IMAGEJARDIR = $(APIDIFF_IMAGEDIR)/lib +ABS_APIDIFF_IMAGEJARDIR = $(shell cd $(APIDIFF_IMAGEJARDIR); pwd) + +# source bundle locations +IMAGESRC_SRCDIR = $(IMAGESRC_TOPDIR)/src/share/classes + +#---------------------------------------------------------------------- +# +# Version tags +# +BUILD_VERSION = 0.0 +BUILD_MILESTONE = dev +BUILD_NUMBER = b00 + +# munge the BUILD values suitable for use in the bundle name +ZIPSFX_VERSION_sh = echo '$(BUILD_VERSION)' +ZIPSFX_MILESTONE_sh = echo '$(BUILD_MILESTONE)' | sed -e 's/\(..*\)/-\1/' +ZIPSFX_BUILD_sh = echo '$(BUILD_NUMBER)' | sed -e 's|[^[0-9]||g' | xargs printf "%d" + +VERBOSE_ZIP_SUFFIX = $(shell $(ZIPSFX_VERSION_sh))$(shell $(ZIPSFX_ MILESTONE_sh))+$(shell $(ZIPSFX_BUILD_sh))_bin \ No newline at end of file diff --git a/make/Makefile b/make/Makefile new file mode 100644 index 0000000..2525833 --- /dev/null +++ b/make/Makefile @@ -0,0 +1,99 @@ +# +# Copyright (c) 1999, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +TOPDIR = .. + +include Defs.gmk + +default: build + +all: build test + +#---------------------------------------------------------------------- + +include apidiff.gmk +include $(TOPDIR)/test/*/*.gmk +include Rules.gmk + +build: check-build-vars $(BUILDFILES) + +images: $(VERBOSEZIPFILES) + +test: check-test-vars $(INITIAL_TESTS) $(TESTS) $(FINAL_TESTS) + count=`echo $+ | wc -w` ; \ + echo "All ($${count}) selected tests completed successfully" + +clean: + $(RM) $(BUILDDIR) + +.NO_PARALLEL: clean + +sanity: + @echo "JDKHOME = $(JDKHOME)" + @echo "JUNIT_JAR = $(JUNIT_JAR)" + @echo "JCOMMANDER_JAR = $(JCOMMANDER_JAR)" + @echo "JAVADIFFUTILS_JAR = $(JAVADIFFUTILS_JAR)" + @echo "JAVADIFFUTILS_LICENSE = $(JAVADIFFUTILS_LICENSE)" + @echo "DAISYDIFF_JAR = $(DAISYDIFF_JAR)" + @echo "DAISYDIFF_LICENSE = $(DAISYDIFF_LICENSE)" + @echo "HTMLCLEANER_JAR = $(HTMLCLEANER_JAR)" + @echo "HTMLCLEANER_LICENSE = $(HTMLCLEANER_LICENSE)" + +check-build-vars: + @if [ -z "$(JDKHOME)" ]; then \ + echo "JDKHOME not set; must be JDK 17 or later" ; exit 1 ; \ + fi + @if [ -z "$(DAISYDIFF_JAR)" -a -z "$(DAISYDIFF_SRC)" ]; then \ + echo "DAISYDIFF_JAR or DAISYDIFF_SRC not set" ; exit 1 ; \ + fi + @if [ -z "$(DAISYDIFF_LICENSE)" ]; then \ + echo "DAISYDIFF_LICENSE not set (will not be included)" ; \ + fi + @if [ -z "$(HTMLCLEANER_JAR)" ]; then \ + echo "HTMLCLEANER_JAR not set" ; exit 1 ; \ + fi + @if [ -z "$(HTMLCLEANER_LICENSE)" ]; then \ + echo "HTMLCLEANER_LICENSE not set (will not be included)" ; \ + fi + @if [ -z "$(JAVADIFFUTILS_JAR)" ]; then \ + echo "JAVADIFFUTILS_JAR not set" ; exit 1 ; \ + fi + @if [ -z "$(JAVADIFFUTILS_LICENSE)" ]; then \ + echo "JAVADIFFUTILS_LICENSE not set (will not be included)" ; \ + fi + +check-test-vars: + @if [ -z "$(JUNIT_JAR)" ]; then \ + echo "JUNIT_JAR not set" ; exit 1 ; \ + fi + +dependencies: check-build-vars check-test-vars + +#---------------------------------------------------------------------- + + +.PHONY: default all build test images clean sanity + + diff --git a/make/README.md b/make/README.md new file mode 100644 index 0000000..01d6c20 --- /dev/null +++ b/make/README.md @@ -0,0 +1,77 @@ +# Building _apidiff_ + +The fundamental way to build _apidiff_ is with GNU `make`, although there is +a convenient wrapper script `make/build.sh` to help download the necessary +dependencies before invoking `make`. Once the dependencies have been downloaded, +you can also configure an IDE to build the tool and run the tests. + +_apidiff_ has various external dependencies: + +* _JDK_: must be at least JDK 17 +* _Java Diff Utils_ +* _Daisy Diff_ +* _TestNG_ and _JCommander_ (for testing only) + +## Using `make/build.sh` + +`make/build.sh` is a script that can download the necessary dependencies +for _apidiff_ and then run `make`. You can configure all values used +by the script by setting environment variables; you can also configure +some commonly used options with command-line argume nts for the script. + +The `make/build.sh` script reads the details of the dependencies from +a file, which defaults to `make/version-numbers`, although an alternate +file can be specified, depending on the build environment. + +The script supports the following build scenarios: + +* Download dependencies from standard default locations such as Maven Central + and Google Code Archive. This is the default. + +* Download dependencies from other available locations, such as an instance of + Artifactory. The details can be specified in an alternate `version-numbers` + file. + +* Use local copies of the dependencies on the same machine. + The details can be specified in an alternate `version-numbers` file, + or you can bypas the script entirely and invoke `make` directly. + +For more details, see the comments in `make/build.sh` and use the `--help` +option when running the script. + +The makefile provides the following targets: + +* `build`: build _apidiff_ + + Requires the following to be set: + `JDKHOME`, `JAVA_DIFF_UTILS_JAR`, `JAVA_DIFF_UTILS_LICENSE`, `DAISYDIFF_JAR`, `DAISYDIFF_LICENSE`. + +* `test`: run tests + + Requires `TESTNG_JAR` and `JCOMMANDER_JAR` to be set. + +* `clean`: delete the `build` directory and its contents + +* `images`: create bundles for uploading to an available server + +* `sanity`: show the settings of the standard variables. + +### Examples: + + $ JDKHOME=/opt/jdk/17 sh build.sh + + $ JDKHOME=/opt/jdk/17 sh build.sh build test images + + + +## File Locations + +| Files | GNU Make | Ant | IntelliJ | +|----------------------|-----------------------------------|-----------------------------------------|-------------------| +| Default Dependencies | build/deps | build/deps | build/deps | +| Main Classes | build/classes | build/classes | out/production | +| Test Classes | build/TestNGTests/classes | build/test/classes | out/test | +| Test Work | build/TestNGTests/work | build/test/work | build/test/work | +| Test Report | build/TestNGTests/report | build/test/report | | +| Image | build/images/apidiff | dist/apidiff | | +| Bundle | build/images/apidiff.zip | dist/apidiff.zip | | diff --git a/make/Rules.gmk b/make/Rules.gmk new file mode 100644 index 0000000..e9f4db3 --- /dev/null +++ b/make/Rules.gmk @@ -0,0 +1,114 @@ +# +# Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + + +#--------------------------------------------------------------------- +# +# Copy resources (*.properties, etc) into classes directory from source tree + +$(CLASSDIR)/%.properties: $(JAVADIR)/%.properties + $(RM) -f $@ + if [ ! -d $(@D) ] ; then $(MKDIR) -p $(@D) ; fi + $(CP) $(@:$(CLASSDIR)/%=$(JAVADIR)/%) $@ + +$(CLASSDIR)/%.gif: $(JAVADIR)/%.gif + $(RM) -f $@ + if [ ! -d $(@D) ] ; then $(MKDIR) -p $(@D) ; fi + $(CP) $(@:$(CLASSDIR)/%=$(JAVADIR)/%) $@ + +$(CLASSDIR)/%.png: $(JAVADIR)/%.png + $(RM) -f $@ + if [ ! -d $(@D) ] ; then $(MKDIR) -p $(@D) ; fi + $(CP) $(@:$(CLASSDIR)/%=$(JAVADIR)/%) $@ + +$(CLASSDIR)/%.css: $(JAVADIR)/%.css + $(RM) -f $@ + if [ ! -d $(@D) ] ; then $(MKDIR) -p $(@D) ; fi + $(CP) $(@:$(CLASSDIR)/%=$(JAVADIR)/%) $@ + +#--------------------------------------------------------------------- + +$(CLASSDIR) $(BUILDDIR): + $(MKDIR) -p $@ + +#---------------------------------------------------------------------- +# +# Build a JAR file containing the contents of any classes/* files +# listed in the FILES.JAR.% + +# default copyright; override as necessary +JAR_COPYRIGHT = -C $(TOPDIR) COPYRIGHT + +$(IMAGES_DIR)/%.jar: pkgsToFiles.sh + $(RM) $@ $(@:$(IMAGES_DIR)/%.jar=$(BUILDDIR)/jarData/%) + $(MKDIR) -p $(@D) + $(MKDIR) -p $(@:$(IMAGES_DIR)/%.jar=$(BUILDDIR)/jarData/%) + ( if [ ! -z "$(JAR_MAINCLASS)" ]; then echo "Main-class: $(JAR_MAINCLASS)" ; fi ; \ + if [ ! -z "$(JAR_CLASSPATH)" ]; then echo "Class-Path: $(JAR_CLASSPATH)" ; fi ; \ + echo "$(@F:%.jar=%)-Name: $(@F:%.jar=%)" ; \ + echo "$(@F:%.jar=%)-Version: $(BUILD_VERSION)" ; \ + echo "$(@F:%.jar=%)-Milestone: $(BUILD_MILESTONE)" ; \ + echo "$(@F:%.jar=%)-Build: $(BUILD_NUMBER)" ; \ + echo "$(@F:%.jar=%)-BuildJavaVersion: `$(JAVA) -fullversion 2>&1 | awk '{print $$NF}' | \ + sed -e 's|^"\(.*\)"$$|Java(TM) 2 SDK, Version \1|'`" ; \ + echo "$(@F:%.jar=%)-BuildDate: `/bin/date +'%B %d, %Y'`" ; \ + ) \ + > $(@:$(IMAGES_DIR)/%.jar=$(BUILDDIR)/jarData/%/manifest.txt) + $(JAR) -cmf $(@:$(IMAGES_DIR)/%.jar=$(BUILDDIR)/jarData/%/manifest.txt) $@ \ + $(JAR_COPYRIGHT) \ + `sh pkgsToFiles.sh $(CLASSDIR) $($(@F:%.jar=PKGS.JAR.%))` \ + $(patsubst $(CLASSDIR)/%,-C $(CLASSDIR) %,$(sort $(FILES.JAR.$(@F:%.jar=%)))) \ + $(JAR_EXTRAS) + $(CHMOD) a-w $@ + +#---------------------------------------------------------------------- +# +# Build zips with verbose names + +%-$(VERBOSE_ZIP_SUFFIX).zip: %.zip + ln $(@:%-$(VERBOSE_ZIP_SUFFIX).zip=%.zip) $@ + +#---------------------------------------------------------------------- +# +# cancel implicit rules + +%: %.o +%: %.obj +%: %.dll +%: %.c +%: %.cc +%: %.cpp +%: %.C +%: %.p +%: %.f +%: %.s +%: %.F +%: %.r +%: %.S +%: %.mod +%: %.sh + + + diff --git a/make/apidiff.gmk b/make/apidiff.gmk new file mode 100644 index 0000000..0da6b60 --- /dev/null +++ b/make/apidiff.gmk @@ -0,0 +1,329 @@ +# +# Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +#---------------------------------------------------------------------- +# +# compile jdk.codetools.apidiff + +JAVAFILES.jdk.codetools.apidiff := \ + $(shell $(FIND) $(JAVADIR) -name \*.java -print ) + +ifneq ($(DAISYDIFF_SRC),) + DAISYDIFF_SRC_JAVA = $(DAISYDIFF_SRC)/main/java + DAISYDIFF_SRC_RESOURCES = $(DAISYDIFF_SRC)/main/resources +endif + +$(BUILDDIR)/classes.jdk.codetools.apidiff.ok: $(JAVAFILES.jdk.codetools.apidiff) + $(JAVAC) $(JAVAC_OPTIONS) \ + -cp $(JAVADIFFUTILS_JAR):$(DAISYDIFF_JAR):$(DAISYDIFF_SRC_JAVA):$(EQUINOX_JAR):$(HTMLCLEANER_JAR) \ + -d $(CLASSDIR) \ + $(JAVAFILES.jdk.codetools.apidiff) + echo "classes built at `date`" > $@ + +TARGETS.jdk.codetools.apidiff += $(BUILDDIR)/classes.jdk.codetools.apidiff.ok + +#---------------------------------------------------------------------- +# +# resources required for jdk.codetools.apidiff + +RESOURCES.jdk.codetools.apidiff = \ + $(CLASSDIR)/jdk/codetools/apidiff/resources/help.properties \ + $(CLASSDIR)/jdk/codetools/apidiff/resources/log.properties \ + $(CLASSDIR)/jdk/codetools/apidiff/report/html/resources/apidiff.css \ + $(CLASSDIR)/jdk/codetools/apidiff/report/html/resources/report.properties + +TARGETS.jdk.codetools.apidiff += $(RESOURCES.jdk.codetools.apidiff) + +ifneq ($(DAISYDIFF_SRC),) +DAISYDIFF_RESOURCE_FILES = $(shell $(FIND) $(DAISYDIFF_SRC_RESOURCES) -name \*.properties -print ) +RESOURCES.daisydiff = $(DAISYDIFF_RESOURCE_FILES:$(DAISYDIFF_SRC_RESOURCES)/%.properties=$(CLASSDIR)/%.properties) + +$(CLASSDIR)/%.properties: $(DAISYDIFF_SRC_RESOURCES)/%.properties + $(RM) -f $@ + if [ ! -d $(@D) ] ; then $(MKDIR) -p $(@D) ; fi + $(CP) $(@:$(CLASSDIR)/%=$(DAISYDIFF_SRC_RESOURCES)/%) $@ + +TARGETS.jdk.codetools.apidiff += $(RESOURCES.daisydiff) +endif + +#---------------------------------------------------------------------- +# +# Misc. doc files + +APIDIFF_COPYRIGHT = $(APIDIFF_IMAGEDIR)/COPYRIGHT +APIDIFF_LICENSE = $(APIDIFF_IMAGEDIR)/LICENSE +APIDIFF_MAN_HTML = $(APIDIFF_IMAGEDIR)/doc/apidiff.html +APIDIFF_MAN_NROFF = $(APIDIFF_IMAGEDIR)/man/man1/apidiff.1 +APIDIFF_README = $(APIDIFF_IMAGEDIR)/README +APIDIFF_USAGE = $(APIDIFF_IMAGEDIR)/doc/usage.txt + +APIDIFF_DOCS = \ + $(APIDIFF_COPYRIGHT) \ + $(APIDIFF_LICENSE) \ + $(APIDIFF_MAN_HTML) \ + $(APIDIFF_MAN_NROFF) \ + $(APIDIFF_README) \ + $(APIDIFF_USAGE) + +$(APIDIFF_COPYRIGHT): $(TOPDIR)/COPYRIGHT + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $< $@ + +$(APIDIFF_README): $(SRCDOCDIR)/README + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $< $@ + +$(APIDIFF_USAGE): $(BUILDDIR)/apidiff-usage.txt + $(MKDIR) -p $(@D) + $(CP) $^ $@ + +$(APIDIFF_LICENSE): $(TOPDIR)/LICENSE + $(MKDIR) -p $(@D) + $(CP) $^ $@ + +$(BUILDDIR)/apidiff-usage.txt: \ + $(BUILDDIR)/classes.jdk.codetools.apidiff.ok \ + $(CLASSDIR)/jdk/codetools/apidiff/resources/help.properties \ + $(CLASSDIR)/jdk/codetools/apidiff/resources/log.properties + $(JAVA) -cp "$(CLASSDIR)" \ + -Dprogram=apidiff jdk.codetools.apidiff.Main --help > $@ + +$(APIDIFF_MAN_HTML): $(SRCDOCDIR)/apidiff.md $(SRCDOCDIR)/apidiff.css + $(MKDIR) -p $(@D) + $(CP) $(SRCDOCDIR)/apidiff.css $(@D) + $(PANDOC) --standalone --to html5 --css apidiff.css $(SRCDOCDIR)/apidiff.md | \ + $(SED) -e '/class="title"/s|>.*<|>apidiff<|' -e 's|

.*

||' \ + > $@ + +$(APIDIFF_MAN_NROFF): $(SRCDOCDIR)/apidiff.md + $(MKDIR) -p $(@D) + $(PANDOC) --standalone --to man -o $@ $^ + +TARGETS.ZIP.apidiff += $(APIDIFF_DOCS) + +#---------------------------------------------------------------------- +# +# create apidiff.jar + +PKGS.JAR.apidiff += \ + jdk.codetools.apidiff \ + jdk.codetools.apidiff.resources \ + jdk.codetools.apidiff.html \ + jdk.codetools.apidiff.model \ + jdk.codetools.apidiff.report \ + jdk.codetools.apidiff.report.html \ + jdk.codetools.apidiff.report.html.resources + +ifneq ($(DAISYDIFF_SRC),) + PKGS.JAR.apidiff += \ + l10n \ + org.eclipse.compare.internal \ + org.eclipse.compare.rangedifferencer \ + org.outerj.daisy.diff.html \ + org.outerj.daisy.diff.html.modification \ + org.outerj.daisy.diff.html.ancestor \ + org.outerj.daisy.diff.html.ancestor.tagtostring \ + org.outerj.daisy.diff.html.dom \ + org.outerj.daisy.diff.html.dom.helper \ + org.outerj.daisy.diff.output +endif + +TARGETS.JAR.apidiff += $(TARGETS.jdk.codetools.apidiff) + +$(APIDIFF_IMAGEDIR)/lib/apidiff.jar: JAR_MAINCLASS = jdk.codetools.apidiff.Main +$(APIDIFF_IMAGEDIR)/lib/apidiff.jar: JAR_EXTRAS = -C $(JAVADIR) META-INF/services/java.util.spi.ToolProvider + +$(APIDIFF_IMAGEJARDIR)/apidiff.jar: \ + $(TARGETS.JAR.apidiff) + +TARGETS.ZIP.apidiff += $(APIDIFF_IMAGEJARDIR)/apidiff.jar + +debug: + echo TARGETS.ZIP.apidiff $(TARGETS.ZIP.apidiff) + +#---------------------------------------------------------------------- +# +# executable scripts + +$(APIDIFF_IMAGEDIR)/bin/apidiff: $(SRCSHAREBINDIR)/apidiff.sh + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $< $@ + $(CHMOD) a+x,a-w $@ + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/bin/apidiff + +#---------------------------------------------------------------------- +# +# dependencies + +$(APIDIFF_IMAGEDIR)/lib/$(notdir $(JAVADIFFUTILS_JAR)): $(JAVADIFFUTILS_JAR) + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $(JAVADIFFUTILS_JAR) $@ + $(CHMOD) a-w $@ + +ifneq ($(JAVADIFFUTILS_LICENSE),) +$(APIDIFF_IMAGEDIR)/legal/java-diff-utils/LICENSE: $(JAVADIFFUTILS_LICENSE) + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $(JAVADIFFUTILS_LICENSE) $@ + $(CHMOD) a-w $@ + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/legal/java-diff-utils/LICENSE +endif + +JAR_CLASSPATH += $(notdir $(JAVADIFFUTILS_JAR)) + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/lib/$(notdir $(JAVADIFFUTILS_JAR)) + +$(APIDIFF_IMAGEDIR)/lib/$(notdir $(DAISYDIFF_JAR)): $(DAISYDIFF_JAR) + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $(DAISYDIFF_JAR) $@ + $(CHMOD) a-w $@ + +ifneq ($(DAISYDIFF_JAR),) +JAR_CLASSPATH += $(notdir $(DAISYDIFF_JAR)) + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/lib/$(notdir $(DAISYDIFF_JAR)) +endif + +ifneq ($(DAISYDIFF_SRC),) +$(APIDIFF_IMAGEDIR)/lib/$(notdir $(EQUINOX_JAR)): $(EQUINOX_JAR) + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $(EQUINOX_JAR) $@ + $(CHMOD) a-w $@ + +JAR_CLASSPATH += $(notdir $(EQUINOX_JAR)) + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/lib/$(notdir $(EQUINOX_JAR)) +endif + +ifneq ($(DAISYDIFF_LICENSE),) +$(APIDIFF_IMAGEDIR)/legal/daisydiff/LICENSE: $(DAISYDIFF_LICENSE) + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $(DAISYDIFF_LICENSE) $@ + $(CHMOD) a-w $@ + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/legal/daisydiff/LICENSE +endif + +$(APIDIFF_IMAGEDIR)/lib/$(notdir $(HTMLCLEANER_JAR)): $(HTMLCLEANER_JAR) + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $(HTMLCLEANER_JAR) $@ + $(CHMOD) a-w $@ + +JAR_CLASSPATH += $(notdir $(HTMLCLEANER_JAR)) + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/lib/$(notdir $(HTMLCLEANER_JAR)) + +ifneq ($(HTMLCLEANER_LICENSE),) +$(APIDIFF_IMAGEDIR)/legal/htmlcleaner/LICENSE: $(HTMLCLEANER_LICENSE) + $(MKDIR) -p $(@D) + $(RM) $@ + $(CP) $(HTMLCLEANER_LICENSE) $@ + $(CHMOD) a-w $@ + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/legal/htmlcleaner/LICENSE +endif + +#---------------------------------------------------------------------- +# +# release info + +# based on code in OpenJDK make/SourceRevision.gmk +ID_COMMAND := $(PRINTF) "git:%s%s\n" \ + "$$(git log -n1 --format=%H | cut -c1-12)" \ + "$$(if test -n "$$(git status --porcelain)"; then printf '+'; fi)" + +$(APIDIFF_IMAGEDIR)/release: + echo "APIDIFF_VERSION=$(BUILD_VERSION) $(BUILD_NUMBER)" > $@ + echo "BUILD_DATE=`/bin/date +'%B %d, %Y'`" >> $@ + if [ -r $(TOPDIR)/.git ]; then \ + echo "SOURCE=$$($(ID_COMMAND))" >> $@ ; \ + elif [ -r $(TOPDIR)/.src-rev ]; then \ + echo "SOURCE=\"$$($(CAT) $(TOPDIR)/.src-rev | $(SED) -e 's/:/:git:/' -e 's/ *$$//')\"" >> $@ ; \ + fi + +TARGETS.ZIP.apidiff += \ + $(APIDIFF_IMAGEDIR)/release + +#---------------------------------------------------------------------- +# +# create apidiff.zip bundles + +APIDIFF_ZIP = $(IMAGES_DIR)/apidiff.zip + +$(APIDIFF_ZIP): $(TARGETS.ZIP.apidiff) + $(RM) $@ + cd $(IMAGES_DIR); $(ZIP) -rq $@ $(@F:%.zip=%) + +APIDIFF_ZIPFILES = $(APIDIFF_ZIP) + +#---------------------------------------------------------------------- +# +# create javadoc bundles + +$(BUILDDIR)/api.jdk.codetools.apidiff.ok: \ + $(JAVAFILES.jdk.codetools.apidiff) \ + $(SRCDOCDIR)/overview.html \ + $(SRCDOCDIR)/jdk17.api/element-list + $(JAVADOC) $(JAVADOC_OPTIONS) \ + -Xdoclint:-missing \ + -quiet \ + -cp $(JAVADIFFUTILS_JAR):$(DAISYDIFF_JAR) \ + -overview $(SRCDOCDIR)/overview.html \ + -linkoffline \ + https://docs.oracle.com/en/java/javase/11/docs/api/index.html \ + $(SRCDOCDIR)/jdk17.api \ + -d $(BUILDDIR)/api \ + $(JAVAFILES.jdk.codetools.apidiff) + echo "api built at `date`" > $@ + +TARGETS.jdk.codetools.apidiff += $(BUILDDIR)/api.jdk.codetools.apidiff.ok + +#---------------------------------------------------------------------- + +BUILDFILES += $(APIDIFF_ZIPFILES) + +VERBOSEZIPFILES += $(APIDIFF_ZIPFILES:%.zip=%-$(VERBOSE_ZIP_SUFFIX).zip) + +#APIDIFF_OPTS = $(APIDIFF_JAVA_OPTS:%=-J%) + +TESTS += $(TESTS.apidiff) diff --git a/make/build-support/build-common.sh b/make/build-support/build-common.sh new file mode 100644 index 0000000..beb9962 --- /dev/null +++ b/make/build-support/build-common.sh @@ -0,0 +1,303 @@ +# +# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +log_message() { + local level="$1" + shift + echo "[${log_module}][${level}] $@" +} + +error() { + log_message "ERROR" "$@" +} + +info() { + if [ -z "${QUIET:-}" ]; then + log_message "INFO" "$@" + fi +} + +## +# Helper used to ensure the correct number of arguments is passed to bash functions +check_arguments() { + local name="$1" + local expected="$2" + local actual="$3" + + if [ ! "${expected}" = "${actual}" ]; then + error "Incorrect number of arguments to function '${name}' (expecting ${expected} but got ${actual})" + exit 1 + fi +} + +## +# Print an absolute path +abspath() { + check_arguments "${FUNCNAME}" 1 $# + + local path="$1" + + if [[ ${path} = /* ]]; then + echo "${path}" + else + echo "$PWD/${path}" + fi +} + +## +# Set up the checksum tool to use +# +setup_shasum() { + if [ -n "${SHASUM:-}" ]; then + return + fi + + if [ -n "$(which sha1sum)" ]; then + SHASUM="sha1sum" + SHASUM_OPTIONS="" + elif [ -n "$(which shasum)" ]; then + SHASUM="shasum" + SHASUM_OPTIONS="-a 1" + else + error "Can't find shasum or sha1sum" + exit 1 + fi +} + +native_path() { + check_arguments "${FUNCNAME}" 1 $# + + if [ $CYGWIN -eq 1 ]; then echo $(cygpath -w $1); else echo "$1"; fi +} + +mixed_path() { + check_arguments "${FUNCNAME}" 1 $# + + if [ $CYGWIN -eq 1 ]; then echo $(cygpath -m $1); else echo "$1"; fi +} + +## +# Download a file using wget +# +# wget options can be provided through the WGET_OPTIONS environment +# variable +# +download_using_wget() { + check_arguments "${FUNCNAME}" 2 $# + + local url="$1" + local destfile="$2" + + set +e + "${WGET}" ${WGET_OPTIONS} "${url}" -O "${destfile}" + ret=$? + if [ ! ${ret} = 0 ]; then + error "wget exited with exit code ${ret}" + exit 1 + fi + set -e +} + +## +# Download a file using curl +# +# curl options can be provided through the CURL_OPTIONS environment +# variable +# +download_using_curl() { + check_arguments "${FUNCNAME}" 2 $# + + local url="$1" + local destfile="$2" + + set +e + "${CURL}" ${CURL_OPTIONS} "${url}" -o "${destfile}" + ret=$? + if [ ! ${ret} = 0 ]; then + error "curl exited with exit code ${ret}" + exit 1 + fi + set -e +} + +## +# Download a file +# +# Will attempt to skip the download if the SKIP_DOWNLOAD environment +# variable is set and the destination file already exists +# +download() { + check_arguments "${FUNCNAME}" 2 $# + + local url="$1" + local destfile="$2" + + if [ "${SKIP_DOWNLOAD:-}" != "" -a -r "${destfile}" ]; then + info "Skipping download of ${url}..." + return + fi + + info "Downloading ${url} to ${destfile}" + mkdir -p "$(dirname "${destfile}")" + if [ -n "${WGET}" ]; then + download_using_wget "${url}" "${destfile}" + elif [ -n "${CURL}" ]; then + download_using_curl "${url}" "${destfile}" + else + error "Cannot find a suitable tool for downloading fils (tried 'wget' and 'curl')" + exit 1 + fi +} + +## +# Checksum a file +# +checksum() { + check_arguments "${FUNCNAME}" 2 $# + + local file="$1" + local expected="$2" + + if [ -n "${SKIP_CHECKSUM_CHECK:-}" ]; then + return + fi + + if [ x"${expected}" = x"" ]; then + error "Expected checksum unexpectedly empty.." + exit 1 + fi + + local actual="$("${SHASUM}" ${SHASUM_OPTIONS} "${dest}" | awk '{ print $1; }')" + if [ ! x"${actual}" = x"${expected}" ]; then + error "Checksum mismatch for ${dest}:" + error "Expected: ${expected}" + error "Actual : ${actual}" + exit 1 + fi +} + +## +# Download and checksum a file +# +download_and_checksum() { + check_arguments "${FUNCNAME}" 3 $# + + local url="$1" + local dest="$2" + local shasum="$3" + + download "${url}" "${dest}" + checksum "${dest}" "${shasum}" +} + +## +# Unpack an archive +# +unpack() { + check_arguments "${FUNCNAME}" 2 $# + + local file="$1" + local unpackdir="$2" + + info "Unpacking $file in $unpackdir" + + ( + mkdir -p "${unpackdir}" + case ${file} in + *.tar.gz) + "${TAR_CMD}" -xzf "$1" -C "${unpackdir}" + ;; + *.zip) + "${UNZIP_CMD}" -q "$1" -d "${unpackdir}" + ;; + *) + error "Unknown archive type for file '${file}'" + exit 1 + esac + ) +} + +## +# Download and unpack an archive without performing a checksum check +# +get_archive_no_checksum() { + check_arguments "${FUNCNAME}" 3 $# + + local url="$1" + local destfile="$2" + local unpackdir="$3" + + download "${url}" "${destfile}" + unpack "${destfile}" "${unpackdir}" +} + +## +# Download, checksum, and unpack an archive +# +get_archive() { + check_arguments "${FUNCNAME}" 4 $# + + local url="$1" + local destfile="$2" + local unpackdir="$3" + local shasum="$4" + + download_and_checksum "${url}" "${destfile}" "${shasum}" + unpack "${destfile}" "${unpackdir}" +} + +set -e +set -u + +if [ -z "${mydir:-}" ]; then + error "mydir not set in caller (line/file): $(caller)" + exit 1 +fi +if [ -z "${log_module:-}" ]; then + error "log_module not set in caller (line/file): $(caller)" + exit 1 +fi + +ROOT="$(abspath ${ROOT:-${mydir}/..})" +BUILD_DIR="$(abspath "${BUILD_DIR:-${ROOT}/build}")" +DEPS_DIR="${BUILD_DIR}/deps" + +export TAR_CMD="${TAR_CMD:-tar}" +export TAR_OPTIONS="${TAR_OPTIONS:-}" +export UNZIP_CMD="${UNZIP_CMD:-unzip}" +export UNZIP_OPTIONS="${UNZIP_OPTIONS:--q} -u" +export WGET="${WGET:-$(which wget)}" +export WGET_OPTIONS="${WGET_OPTIONS:--q}" +export CURL="${CURL:-$(which curl)}" +export CURL_OPTIONS="${CURL_OPTIONS:--s -f -L}" + +export MAVEN_REPO_URL_BASE="${MAVEN_REPO_URL_BASE:-https://repo1.maven.org/maven2}" +export GOOGLE_CODE_URL_BASE="${GOOGLE_CODE_URL_BASE:-https://storage.googleapis.com/google-code-archive-downloads/v2}" +export DAISYDIFF_REPO_URL_BASE=${DAISYDIFF_REPO_URL_BASE:-"https://github.com/guyvdbroeck/daisydiff-1"} +export EQUINOX_REPO_URL_BASE=${EQUINOX_REPO_URL_BASE:-"https://github.com/eclipse-equinox"} + +setup_shasum + +case $(uname) in CYGWIN*) CYGWIN=1 ;; *) CYGWIN=0 ;; esac diff --git a/make/build-support/version-numbers b/make/build-support/version-numbers new file mode 100644 index 0000000..8c7d36c --- /dev/null +++ b/make/build-support/version-numbers @@ -0,0 +1,53 @@ +# +# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +APIDIFF_VERSION=1 + +DEFAULT_DAISYDIFF_BIN_VERSION=1.2 +DEFAULT_DAISYDIFF_BIN_ARCHIVE_CHECKSUM=fba904baf92f5208f36dc3ed3d6a19fbe9502b46 +DEFAULT_DAISYDIFF_LICENSE_VERSION=master +DEFAULT_DAISYDIFF_LICENSE_CHECKSUM=7df059597099bb7dcf25d2a9aedfaf4465f72d8d + +DEFAULT_DAISYDIFF_SRC_VERSION=1.2-NX4 +DEFAULT_DAISYDIFF_SRC_ARCHIVE_CHECKSUM=742a36fe6471790f91190cdf33e4cb348b6754b2 + +DEFAULT_EQUINOX_VERSION=3.6.0 +DEFAULT_EQUINOX_JAR_CHECKSUM=78e5d0b8516b042495660da36ce5114650f8f156 +DEFAULT_EQUINOX_LICENSE_CHECKSUM=8d80da0c92c1269b610b03cc8061556004898c85 + +DEFAULT_HTMLCLEANER_VERSION=2.29 +DEFAULT_HTMLCLEANER_JAR_CHECKSUM=7b42d564b7d2a4674612ef0ec3696985cbd38343 +DEFAULT_HTMLCLEANER_LICENSE_CHECKSUM=800231adc60dec964ec28270f0f0b94398ce9b3f + +DEFAULT_JAVADIFFUTILS_VERSION=4.12 +DEFAULT_JAVADIFFUTILS_JAR_CHECKSUM=1a712a91324d566eef39817fc5c9980eb10c21db +DEFAULT_JAVADIFFUTILS_LICENSE_VERSION=parent-4.12 +DEFAULT_JAVADIFFUTILS_LICENSE_CHECKSUM=7df059597099bb7dcf25d2a9aedfaf4465f72d8d + +# for testing +# JUnit 5 = JUnit Platform 1.y.z + JUnit Jupiter 5.y.z + JUnit Vintage 5.y.z +DEFAULT_JUNIT_VERSION=1.9.2 +DEFAULT_JUNIT_JAR_CHECKSUM=bb856bc86a6e6cd48080546afcaf7a210713ea21 +DEFAULT_JUNIT_LICENSE_FILE=LICENSE-junit.txt \ No newline at end of file diff --git a/make/build.properties b/make/build.properties new file mode 100644 index 0000000..a955b77 --- /dev/null +++ b/make/build.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2007, 2016, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +# set locations here, or in ${root}/build.properties, or set on the +# ant command line + +apidiff.build.resources = /opt + +build.version = 1.0 +build.milestone = dev +build.number = b00 diff --git a/make/build.sh b/make/build.sh new file mode 100644 index 0000000..417b01d --- /dev/null +++ b/make/build.sh @@ -0,0 +1,687 @@ +#!/bin/sh + +# +# Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +# This script will download/build the dependencies for apidiff and then +# build apidiff. Downloaded files are verified against known/specified +# specified checksums. + +# The default version to use when building apidiff can be found in the +# make/version-numbers file, where the default versions and +# corresponding known checksums for the dependencies are also +# specified. Almost all of the defaults can be overridden by setting +# the respective environment variables. + +# For each of the dependency the following steps are applied and the +# first successful one is used: +# +# 1. Check if the dependency is available locally +# 2. Download a prebuilt version of the dependency +# +# In particular, when not found locally the dependencies will be +# handled as follows: +# +# * JUnit, Java Diff Utils, and HtmlCleaner are by default downloaded from Maven Central. +# * Daisy Diff is by default downloaded from Google Code Archive. +# * The JDK dependency is downloaded. No default URL is set. +# + +# Some noteworthy control variables: +# +# MAVEN_REPO_URL_BASE (e.g. "https://repo1.maven.org/maven2") +# The base URL for the maven central repository. +# +# GOOGLE_CODE_URL_BASE (e.g. "https://code.google.com/archive/p") +# The base URL for the Google Code Archive repository. +# +# APIDIFF_VERSION (e.g. "1.0") +# APIDIFF_VERSION_STRING (e.g. "apidiff-1.0+8" +# APIDIFF_BUILD_NUMBER (e.g. "8") +# APIDIFF_BUILD_MILESTONE (e.g. "dev") +# The version information to use for when building apidiff. +# +# MAKE_ARGS (e.g. "-j4 all") +# Additional arguments to pass to make when building apidiff. +# +# WGET +# The wget-like executable to use when downloading files. +# +# WGET_OPTS (e.g. "-v") +# Additional arguments to pass to WGET when downloading files. +# +# CURL (e.g. "/path/to/my/wget") +# The curl-like executable to use when downloading files. +# Note: If available, wget will be prefered. +# +# CURL_OPTS (e.g. "-v") +# Additional arguments to pass to CURL when downloading files. +# +# SKIP_DOWNLOAD +# Skip the downloads if the file is already present locally. +# +# SKIP_CHECKSUM_CHECK +# Skip the checksum verification for downloaded files. + +# The control variables for dependencies are on the following general +# form (not all of them are relevant for all dependencies): +# +# _URL (e.g. DAISYDIFF_BIN_ARCHIVE_URL) +# The full URL for the dependency. +# +# _URL_BASE (e.g. DAISYDIFF_BIN_ARCHIVE_URL_BASE) +# The base URL for the dependency. Requires additional dependency +# specific variables to be specified. +# +# _CHECKSUM (e.g. DAISYDIFF_BIN_ARCHIVE_CHECKSUM) +# The expected checksum of the download file. +# + +# The below outlines the details of how the dependencies are +# handled. For each dependency the steps are tried in order and the +# first successful one will be used. +# +# JDK +# Checksum variables: +# JDK_ARCHIVE_CHECKSUM: checksum of binary archive +# +# 1. JAVA_HOME +# The path to the JDK. +# 2a. JDK_ARCHIVE_URL +# The full URL for the archive. +# 2b. JDK_ARCHIVE_URL_BASE + JDK_VERSION + JDK_BUILD_NUMBER + JDK_FILE +# The individual URL components used to construct the full URL. +# +# Java Diff Utils +# Checksum variables: +# JAVADIFFUTILS_JAR_CHECKSUM: checksum of jar +# JAVADIFFUTILS_LICENSE_CHECKSUM: checksum of LICENSE file +# +# 1. JAVADIFFUTILS_JAR + JAVADIFFUTILS_LICENSE +# The path to java-diff-utils.jar and LICENSE.txt respectively. +# 2a. JAVADIFFUTILS_JAR_URL +# The full URL for the jar. +# 2b. JAVADIFFUTILS_JAR_URL_BASE + JAVADIFFUTILS_VERSION + JAVADIFFUTILS_FILE +# The individual URL components used to construct the full URL. +# +# Daisy Diff +# Checksum variables: +# DAISYDIFF_BIN_ARCHIVE_CHECKSUM: checksum of binary archive +# DAISYDIFF_LICENSE_CHECKSUM: checksum of LICENSE file +# +# 1. DAISYDIFF_JAR + DAISYDIFF_LICENSE +# The path to daisydiff.jar and LICENSE.txt respectively. +# 2a. DAISYDIFF_JAR_URL +# The full URL for the jar. +# 2b. DAISYDIFF_JAR_URL_BASE + DAISYDIFF_BIN_VERSION + DAISYDIFF_FILE +# The individual URL components used to construct the full URL. +# +# Html Cleaner +# Checksum variables: +# HTMLCLEANER_JAR_CHECKSUM: checksum of jar +# HTMLCLEANER_LICENSE_CHECKSUM: checksum of LICENSE file +# +# 1. HTMLCLEANER_JAR + HTMLCLEANER_LICENSE +# The path to htmlcleaner.jar and licence.txt respectively. +# 2a. HTMLCLEANER_JAR_URL +# The full URL for the jar. +# 2b. HTMLCLEANER_JAR_URL_BASE + HTMLCLEANER_VERSION + HTMLCLEANER_FILE +# The individual URL components used to construct the full URL. +# +# JUnit, for running self-tests +# Checksum variables: +# JUNIT_JAR_CHECKSUM: checksum of binary archive +# +# 1. JUNIT_JAR + JUNIT_LICENSE +# The path to junit.jar and LICENSE respectively. +# 2a. JUNIT_JAR_URL +# The full URL for the jar. +# 2b. JUNIT_JAR_URL_BASE + JUNIT_VERSION + JUNIT_FILE +# The individual URL components used to construct the full URL. +# +# Some control variables can be overridden by command-line options. +# Use the --help option for details. + +mydir="$(dirname ${BASH_SOURCE[0]})" +log_module="$(basename "${BASH_SOURCE[0]}")" +. "${mydir}/build-support/build-common.sh" + +usage() { + echo "Usage: $0 [ [--] ]" + echo "--help" + echo " Show this message" + echo "--jdk /path/to/jdk" + echo " Path to JDK; must be JDK 17 or higher" + echo "--quiet | -q" + echo " Reduce the logging output." + echo "--show-default-versions" + echo " Show default versions of external components" + echo "--show-config-details" + echo " Show configuration details" + echo "--skip-checksum-check" + echo " Skip the checksum check for downloaded files." + echo "--skip-download" + echo " Skip downloading files if file already available" + echo "--skip-make" + echo " Skip running 'make' (just download dependencies if needed)" + echo "--version-numbers file" + echo " Provide an alternate file containing dependency version information" + echo "--" + echo " Subsequent arguments are for 'make'" +} + +ensure_arg() { + check_arguments "${FUNCNAME}" 2 $# + local option="$1" + local arg_count="$2" + if [ "$2" -lt "2" ]; then + echo "The $option option requires an argument" + exit + fi +} + +process_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --help|-h ) HELP=1 ; shift ;; + --jdk ) ensure_arg "$1" $# ; JAVA_HOME="$2" ; shift ; shift ;; + --quiet|-q ) export QUIET=1 ; shift ;; + --show-config-details ) SHOW_CONFIG_DETAILS=1 ; shift ;; + --show-default-versions ) SHOW_DEFAULT_VERSIONS=1 ; shift ;; + --skip-checksum-check ) export SKIP_CHECKSUM_CHECK=1 ; shift ;; + --skip-download ) export SKIP_DOWNLOAD=1 ; shift ;; + --skip-make ) SKIP_MAKE=1 ; shift ;; + --version-numbers ) ensure_arg "$1" $# ; VERSION_NUMBERS="$2" ; shift ; shift ;; + -- ) shift ; MAKE_ARGS="$@" ; break ;; + -* ) error "unknown option: '$1'" ; exit 1 ;; + * ) MAKE_ARGS="$@" ; break ;; + esac + done +} + +process_args "$@" + +if [ -n "${HELP:-}" ]; then + usage + exit +fi + +. "${VERSION_NUMBERS:-${mydir}/build-support/version-numbers}" + +APIDIFF_VERSION="${APIDIFF_VERSION:-}" + +DAISYDIFF_BIN_VERSION="${DAISYDIFF_BIN_VERSION:-${DEFAULT_DAISYDIFF_BIN_VERSION}}" +# uncomment or override to download a precompiled jar file for daisydiff +#DAISYDIFF_BIN_ARCHIVE_URL_BASE="${DAISYDIFF_BIN_ARCHIVE_URL_BASE:-${GOOGLE_CODE_URL_BASE}}" +#DAISYDIFF_BIN_ARCHIVE_CHECKSUM="${DAISYDIFF_BIN_ARCHIVE_CHECKSUM:-${DEFAULT_DAISYDIFF_BIN_ARCHIVE_CHECKSUM}}" +DAISYDIFF_LICENSE_VERSION="${DAISYDIFF_LICENSE_VERSION:-${DEFAULT_DAISYDIFF_LICENSE_VERSION:-${DAISYDIFF_BIN_VERSION}}}" +DAISYDIFF_LICENSE_CHECKSUM="${DAISYDIFF_LICENSE_CHECKSUM:-${DEFAULT_DAISYDIFF_LICENSE_CHECKSUM}}" + +DAISYDIFF_SRC_VERSION="${DAISYDIFF_SRC_VERSION:-${DEFAULT_DAISYDIFF_SRC_VERSION}}" +DAISYDIFF_SRC_ARCHIVE_URL_BASE="${DAISYDIFF_SRC_ARCHIVE_URL_BASE:-${DAISYDIFF_REPO_URL_BASE}}" +DAISYDIFF_SRC_ARCHIVE_CHECKSUM="${DAISYDIFF_SRC_ARCHIVE_CHECKSUM:-${DEFAULT_DAISYDIFF_SRC_ARCHIVE_CHECKSUM}}" + +EQUINOX_VERSION="${EQUINOX_VERSION:-${DEFAULT_EQUINOX_VERSION}}" +EQUINOX_JAR_URL_BASE="${EQUINOX_JAR_URL_BASE:-${MAVEN_REPO_URL_BASE}}" +EQUINOX_JAR_CHECKSUM="${EQUINOX_JAR_CHECKSUM:-${DEFAULT_EQUINOX_JAR_CHECKSUM}}" +EQUINOX_LICENSE_CHECKSUM="${EQUINOX_LICENSE_CHECKSUM:-${DEFAULT_EQUINOX_LICENSE_CHECKSUM}}" + +HTMLCLEANER_VERSION="${HTMLCLEANER_VERSION:-${DEFAULT_HTMLCLEANER_VERSION}}" +HTMLCLEANER_JAR_URL_BASE="${HTMLCLEANER_JAR_URL_BASE:-${MAVEN_REPO_URL_BASE}}" +HTMLCLEANER_JAR_CHECKSUM="${HTMLCLEANER_JAR_CHECKSUM:-${DEFAULT_HTMLCLEANER_JAR_CHECKSUM}}" +HTMLCLEANER_LICENSE_CHECKSUM="${HTMLCLEANER_LICENSE_CHECKSUM:-${DEFAULT_HTMLCLEANER_LICENSE_CHECKSUM}}" + +JAVADIFFUTILS_VERSION="${JAVADIFFUTILS_VERSION:-${DEFAULT_JAVADIFFUTILS_VERSION}}" +JAVADIFFUTILS_JAR_URL_BASE="${JAVADIFFUTILS_JAR_URL_BASE:-${MAVEN_REPO_URL_BASE}}" +JAVADIFFUTILS_JAR_CHECKSUM="${JAVADIFFUTILS_JAR_CHECKSUM:-${DEFAULT_JAVADIFFUTILS_JAR_CHECKSUM}}" +JAVADIFFUTILS_LICENSE_VERSION="${JAVADIFFUTILS_LICENSE_VERSION:-${DEFAULT_JAVADIFFUTILS_LICENSE_VERSION:-${JAVADIFFUTILS_VERSION}}}" +JAVADIFFUTILS_LICENSE_CHECKSUM="${JAVADIFFUTILS_LICENSE_CHECKSUM:-${DEFAULT_JAVADIFFUTILS_LICENSE_CHECKSUM}}" + +JUNIT_VERSION="${JUNIT_VERSION:-${DEFAULT_JUNIT_VERSION}}" +JUNIT_JAR_URL_BASE="${JUNIT_JAR_URL_BASE:-${MAVEN_REPO_URL_BASE}}" +JUNIT_JAR_CHECKSUM="${JUNIT_JAR_CHECKSUM:-${DEFAULT_JUNIT_JAR_CHECKSUM}}" +JUNIT_LICENSE_FILE="${JUNIT_LICENSE_FILE:-${DEFAULT_JUNIT_LICENSE_FILE}}" + +if [ "${SHOW_DEFAULT_VERSIONS:-}" != "" ]; then + find ${mydir} -name version-numbers | \ + xargs cat | \ + grep -v '^#' | \ + grep -E 'DEFAULT.*(_VERSION|_SRC_TAG)' | \ + sort -u + exit +fi + +if [ "${SHOW_CONFIG_DETAILS:-}" != "" ]; then + ( set -o posix ; set ) | \ + grep -E '^(DAISYDIFF|JAVADIFFUTILS|JUNIT)_[A-Z_]*=' | \ + sort -u + exit +fi + +setup_java_home() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${JAVA_HOME:-}" ]; then + return + fi + + if [ -z "${JDK_ARCHIVE_URL:-}" ]; then + if [ -n "${JDK_ARCHIVE_URL_BASE:-}" ]; then + if [ -z "${JDK_VERSION:-}" ]; then + error "JDK_VERSION not set" + exit 1 + fi + if [ -z "${JDK_BUILD_NUMBER:-}" ]; then + error "JDK_BUILD_NUMBER not set" + exit 1 + fi + if [ -z "${JDK_FILE:-}" ]; then + error "JDK_FILE not set" + exit 1 + fi + JDK_ARCHIVE_URL="${JDK_ARCHIVE_URL_BASE}/${JDK_VERSION}/${JDK_BUILD_NUMBER}/${JDK_FILE}" + fi + fi + + local JDK_DEPS_DIR="${DEPS_DIR}" + + if [ -n "${JDK_ARCHIVE_URL:-}" ]; then + local JDK_LOCAL_ARCHIVE_FILE="${JDK_DEPS_DIR}/$(basename "${JDK_ARCHIVE_URL}")" + if [ -n "${JDK_ARCHIVE_CHECKSUM:-}" ]; then + get_archive "${JDK_ARCHIVE_URL}" "${JDK_LOCAL_ARCHIVE_FILE}" "${JDK_DEPS_DIR}" "${JDK_ARCHIVE_CHECKSUM}" + else + get_archive_no_checksum "${JDK_ARCHIVE_URL}" "${JDK_LOCAL_ARCHIVE_FILE}" "${JDK_DEPS_DIR}" + fi + local JDK_JAVAC="$(find "${JDK_DEPS_DIR}" -path '*/bin/javac')" + JAVA_HOME="$(dirname $(dirname "${JDK_JAVAC}"))" + return + fi + + error "None of --jdk, JAVA_HOME, JDK_ARCHIVE_URL or JDK_ARCHIVE_URL_BASE are set" + exit 1 +} + +sanity_check_java_home() { + if [ -z "${JAVA_HOME:-}" ]; then + error "No JAVA_HOME set" + exit 1 + fi + + if [ ! -d "${JAVA_HOME}" ]; then + error "'${JAVA_HOME}' is not a directory" + exit 1 + fi + + if [ ! -x "${JAVA_HOME}/bin/java" ]; then + error "Could not find an executable binary at '${JAVA_HOME}/bin/java'" + exit 1 + fi + + local version=$(${JAVA_HOME}/bin/java -version 2>&1) + local vnum=$(echo "${version}" | \ + grep -i -E '^(java|openjdk)' | + head -n 1 | \ + sed -e 's/^[^0-9]*//' -e 's/[^0-9].*//' ) + if [ "${vnum:-0}" -lt "17" ]; then + error "JDK 17 or newer is required to build apidiff" + exit 1 + fi +} +setup_java_home +sanity_check_java_home +export JAVA_HOME +info "JAVA_HOME: ${JAVA_HOME}" + +#----- Daisy Diff ----- +setup_daisydiff_jar() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${DAISYDIFF_JAR:-}" ]; then + return + fi + + local DAISYDIFF_DEPS_DIR="${DEPS_DIR}/daisydiff" + + if [ -z "${DAISYDIFF_SRC_ARCHIVE_URL:-}" ]; then + if [ -n "${DAISYDIFF_SRC_ARCHIVE_URL_BASE:-}" ]; then + DAISYDIFF_SRC_ARCHIVE_URL="${DAISYDIFF_SRC_ARCHIVE_URL_BASE}/archive/refs/tags/release-${DAISYDIFF_SRC_VERSION}.tar.gz" + fi + fi + + if [ -n "${DAISYDIFF_SRC_ARCHIVE_URL:-}" ]; then + local DAISYDIFF_LOCAL_ARCHIVE_FILE="${DAISYDIFF_DEPS_DIR}/$(basename "${DAISYDIFF_SRC_ARCHIVE_URL}")" + get_archive "${DAISYDIFF_SRC_ARCHIVE_URL}" "${DAISYDIFF_LOCAL_ARCHIVE_FILE}" "${DAISYDIFF_DEPS_DIR}" "${DAISYDIFF_SRC_ARCHIVE_CHECKSUM}" + DAISYDIFF_SRC=$(cd "${DAISYDIFF_DEPS_DIR}"/*/src; pwd) + return + fi + + info "None of DAISYDIFF_JAR, DAISYDIFF_SRC_ARCHIVE_URL, DAISYDIFF_SRC_ARCHIVE_URL_BASE are set" +} + +setup_daisydiff_jar +if [ -n "${DAISYDIFF_JAR:-}" ]; then + info "DAISYDIFF_JAR: ${DAISYDIFF_JAR}" +else + info "DAISYDIFF_SRC: ${DAISYDIFF_SRC}" +fi + +#----- Daisy Diff License ----- +setup_daisydiff_license() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${DAISYDIFF_LICENSE:-}" ]; then + return + fi + + local DAISYDIFF_LICENSE_DEPS_DIR="${DEPS_DIR}/daisydiff-license" + DAISYDIFF_LICENSE="${DAISYDIFF_LICENSE_DEPS_DIR}/LICENSE" + download_and_checksum "https://raw.githubusercontent.com/DaisyDiff/DaisyDiff/${DAISYDIFF_LICENSE_VERSION}/LICENSE.txt" "${DAISYDIFF_LICENSE}" "${DAISYDIFF_LICENSE_CHECKSUM}" +} +setup_daisydiff_license +info "DAISYDIFF_LICENSE: ${DAISYDIFF_LICENSE}" + +#----- Eclipse Equinox Common Runtime +setup_equinox_jar() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${EQUINOX_JAR:-}" ]; then + return + fi + + if [ -z "${EQUINOX_JAR_URL:-}" ]; then + if [ -n "${EQUINOX_JAR_URL_BASE:-}" ]; then + EQUINOX_JAR_URL="${EQUINOX_JAR_URL_BASE}/org/eclipse/equinox/org.eclipse.equinox.common/${EQUINOX_VERSION}/org.eclipse.equinox.common-${EQUINOX_VERSION}.jar" + fi + fi + + local EQUINOX_DEPS_DIR="${DEPS_DIR}/equinox" + + if [ -n "${EQUINOX_JAR_URL:-}" ]; then + EQUINOX_JAR="${EQUINOX_DEPS_DIR}/$(basename "${EQUINOX_JAR_URL}")" + download_and_checksum "${EQUINOX_JAR_URL}" "${EQUINOX_JAR}" "${EQUINOX_JAR_CHECKSUM}" + return + fi + + error "Neither EQUINOX_JAR_URL nor EQUINOX_JAR_URL_BASE is set" + exit 1 +} + +if [ -n "${DAISYDIFF_SRC:-}" ]; then + setup_equinox_jar + info "EQUINOX_JAR: ${EQUINOX_JAR}" +fi + +#----- Eclipse Equinox Common Runtime License ----- +setup_equinox_license() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${EQUINOX_LICENSE:-}" ]; then + return + fi + + local EQUINOX_LICENSE_DEPS_DIR="${DEPS_DIR}/equinox-license" + EQUINOX_LICENSE="${EQUINOX_LICENSE_DEPS_DIR}/epl-v10.html" + download_and_checksum "http://www.eclipse.org/org/documents/epl-v10.html" "${EQUINOX_LICENSE}" "${EQUINOX_LICENSE_CHECKSUM}" + +} + +if [ -n "${DAISYDIFF_SRC:-}" ]; then + setup_equinox_license + info "EQUINOX_LICENSE: ${EQUINOX_LICENSE}" +fi + +#----- Html Cleaner +setup_htmlcleaner_jar() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${HTMLCLEANER_JAR:-}" ]; then + return + fi + + if [ -z "${HTMLCLEANER_JAR_URL:-}" ]; then + if [ -n "${HTMLCLEANER_JAR_URL_BASE:-}" ]; then + HTMLCLEANER_JAR_URL="${HTMLCLEANER_JAR_URL_BASE}/net/sourceforge/htmlcleaner/htmlcleaner/${HTMLCLEANER_VERSION}/htmlcleaner-${HTMLCLEANER_VERSION}.jar" + fi + fi + + local HTMLCLEANER_DEPS_DIR="${DEPS_DIR}/htmlcleaner" + + if [ -n "${HTMLCLEANER_JAR_URL:-}" ]; then + HTMLCLEANER_JAR="${HTMLCLEANER_DEPS_DIR}/$(basename "${HTMLCLEANER_JAR_URL}")" + download_and_checksum "${HTMLCLEANER_JAR_URL}" "${HTMLCLEANER_JAR}" "${HTMLCLEANER_JAR_CHECKSUM}" + return + fi + + error "Neither HTMLCLEANER_JAR_URL nor HTMLCLEANER_JAR_URL_BASE is set" + exit 1 +} +setup_htmlcleaner_jar +info "HTMLCLEANER_JAR: ${HTMLCLEANER_JAR}" + +#----- Html Cleaner License ----- +setup_htmlcleaner_license() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${HTMLCLEANER_LICENSE:-}" ]; then + return + fi + + local HTMLCLEANER_LICENSE_DEPS_DIR="${DEPS_DIR}/htmlcleaner-license" + HTMLCLEANER_LICENSE="${HTMLCLEANER_LICENSE_DEPS_DIR}/licence.txt" + download_and_checksum "https://sourceforge.net/p/htmlcleaner/code/HEAD/tree/tags/htmlcleaner-${HTMLCLEANER_VERSION}/licence.txt?format=raw" "${HTMLCLEANER_LICENSE}" "${HTMLCLEANER_LICENSE_CHECKSUM}" + +} +setup_htmlcleaner_license +info "HTMLCLEANER_LICENSE: ${HTMLCLEANER_LICENSE}" + + +#----- Java Diff Utils ----- +setup_javadiffutils() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${JAVADIFFUTILS_JAR:-}" ]; then + return + fi + + if [ -z "${JAVADIFFUTILS_JAR_URL:-}" ]; then + if [ -n "${JAVADIFFUTILS_JAR_URL_BASE:-}" ]; then + JAVADIFFUTILS_JAR_URL="${JAVADIFFUTILS_JAR_URL_BASE}/io/github/java-diff-utils/java-diff-utils/${JAVADIFFUTILS_VERSION}/java-diff-utils-${JAVADIFFUTILS_VERSION}.jar" + fi + fi + + local JAVADIFFUTILS_DEPS_DIR="${DEPS_DIR}/java-diff-utils" + + if [ -n "${JAVADIFFUTILS_JAR_URL:-}" ]; then + JAVADIFFUTILS_JAR="${JAVADIFFUTILS_DEPS_DIR}/$(basename "${JAVADIFFUTILS_JAR_URL}")" + download_and_checksum "${JAVADIFFUTILS_JAR_URL}" "${JAVADIFFUTILS_JAR}" "${JAVADIFFUTILS_JAR_CHECKSUM}" + return + fi + + error "Neither JAVADIFFUTILS_JAR_URL nor JAVADIFFUTILS_JAR_URL_BASE is set" + exit 1 +} +setup_javadiffutils +info "JAVADIFFUTILS_JAR: ${JAVADIFFUTILS_JAR}" + +#----- Java Diff Utils License ----- +setup_javadiffutils_license() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${JAVADIFFUTILS_LICENSE:-}" ]; then + return + fi + + local JAVADIFFUTILS_LICENSE_DEPS_DIR="${DEPS_DIR}/javadiffutils-license" + JAVADIFFUTILS_LICENSE="${JAVADIFFUTILS_LICENSE_DEPS_DIR}/LICENSE" + download_and_checksum "https://raw.githubusercontent.com/java-diff-utils/java-diff-utils/java-diff-utils-${JAVADIFFUTILS_LICENSE_VERSION}/LICENSE" "${JAVADIFFUTILS_LICENSE}" "${JAVADIFFUTILS_LICENSE_CHECKSUM}" +} +setup_javadiffutils_license +info "JAVADIFFUTILS_LICENSE: ${JAVADIFFUTILS_LICENSE}" + + + +#----- JUnit ----- +setup_junit() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${JUNIT_JAR:-}" ]; then + return + fi + + if [ -z "${JUNIT_JAR_URL:-}" ]; then + if [ -n "${JUNIT_JAR_URL_BASE:-}" ]; then + JUNIT_JAR_URL="${JUNIT_JAR_URL_BASE}/org/junit/platform/junit-platform-console-standalone/${JUNIT_VERSION}/junit-platform-console-standalone-${JUNIT_VERSION}.jar" + fi + fi + + local JUNIT_DEPS_DIR="${DEPS_DIR}/junit" + + if [ -n "${JUNIT_JAR_URL:-}" ]; then + JUNIT_JAR="${JUNIT_DEPS_DIR}/$(basename ${JUNIT_JAR_URL})" + download_and_checksum "${JUNIT_JAR_URL}" "${JUNIT_JAR}" "${JUNIT_JAR_CHECKSUM}" + return + fi + + error "None of JUNIT_JAR, JUNIT_JAR_URL or JUNIT_JAR_URL_BASE is set" + exit 1 +} +setup_junit +info "JUNIT_JAR ${JUNIT_JAR}" + +#----- JUnit license ----- +setup_junit_license() { + check_arguments "${FUNCNAME}" 0 $# + + if [ -n "${JUNIT_LICENSE:-}" ]; then + return + fi + + local JUNIT_LICENSE_DEPS_DIR="${DEPS_DIR}/junit-license" + "${UNZIP_CMD}" ${UNZIP_OPTIONS} "${JUNIT_JAR}" ${JUNIT_LICENSE_FILE} -d "${JUNIT_LICENSE_DEPS_DIR}" + JUNIT_LICENSE="${JUNIT_LICENSE_DEPS_DIR}/${JUNIT_LICENSE_FILE}" +} +setup_junit_license +info "JUNIT_LICENSE: ${JUNIT_LICENSE}" + +## +# Build number defaults to 0 +# +setup_build_info() { + check_arguments "${FUNCNAME}" 0 $# + + APIDIFF_BUILD_MILESTONE="${APIDIFF_BUILD_MILESTONE:-dev}" + APIDIFF_BUILD_NUMBER="${APIDIFF_BUILD_NUMBER:-0}" + + if [ -z "${APIDIFF_VERSION_STRING:-}" ]; then + MILESTONE="" + if [ -n "${APIDIFF_BUILD_MILESTONE}" ]; then + MILESTONE="-${APIDIFF_BUILD_MILESTONE}" + fi + APIDIFF_VERSION_STRING="${APIDIFF_VERSION}${MILESTONE}+${APIDIFF_BUILD_NUMBER}" + fi +} +setup_build_info +info "APIDIFF_VERSION: ${APIDIFF_VERSION}" +info "APIDIFF_BUILD_NUMBER: ${APIDIFF_BUILD_NUMBER}" +info "APIDIFF_BUILD_MILESTONE: ${APIDIFF_BUILD_MILESTONE}" + +check_file() { + check_arguments "${FUNCNAME}" 1 $# + + info "Checking $1" + if [ ! -f "$1" ]; then + error "Missing: $1" + exit 1 + fi +} + +check_dir() { + check_arguments "${FUNCNAME}" 1 $# + + info "Checking $1" + if [ ! -d "$1" ]; then + error "Missing: $1" + exit 1 + fi +} + +check_dir "${JAVA_HOME}" +check_file "${JUNIT_JAR}" +check_file "${JAVADIFFUTILS_JAR}" +if [ -n "${JAVADIFFUTILS_LICENSE:-}" ]; then + check_file "${JAVADIFFUTILS_LICENSE}" +fi +if [ -n "${DAISYDIFF_JAR:-}" ]; then + check_file "${DAISYDIFF_JAR}" +fi +if [ -n "${DAISYDIFF_SRC:-}" ]; then + check_dir "${DAISYDIFF_SRC}" +fi +if [ -n "${DAISYDIFF_LICENSE:-}" ]; then + check_file "${DAISYDIFF_LICENSE}" +fi +if [ -n "${EQUINOX_JAR:-}" ]; then + check_file "${EQUINOX_JAR}" +fi +if [ -n "${EQUINOX_LICENSE:-}" ]; then + check_file "${EQUINOX_LICENSE}" +fi +check_file "${HTMLCLEANER_JAR}" +if [ -n "${HTMLCLEANER_LICENSE:-}" ]; then + check_file "${HTMLCLEANER_LICENSE}" +fi + +if [ -n "${SKIP_MAKE:-}" ]; then + exit +fi + + +# save make command for possible later reuse, bypassing this script +mkdir -p ${BUILD_DIR} +cat > ${BUILD_DIR}/make.sh << EOF +#!/bin/sh + +# Build apidiff +cd "${ROOT}/make" +make BUILDDIR="${BUILD_DIR}" \\ + BUILD_MILESTONE="${APIDIFF_BUILD_MILESTONE}" \\ + BUILD_NUMBER="${APIDIFF_BUILD_NUMBER}" \\ + BUILD_VERSION="${APIDIFF_VERSION}" \\ + BUILD_VERSION_STRING="${APIDIFF_VERSION_STRING}" \\ + DAISYDIFF_JAR="$(mixed_path "${DAISYDIFF_JAR:-}")" \\ + DAISYDIFF_SRC="$(mixed_path "${DAISYDIFF_SRC:-}")" \\ + DAISYDIFF_LICENSE="${DAISYDIFF_LICENSE}" \\ + EQUINOX_JAR="$(mixed_path "${EQUINOX_JAR:-}")" \\ + EQUINOX_LICENSE="$(mixed_path "${EQUINOX_LICENSE:-}")" \\ + HTMLCLEANER_JAR="${HTMLCLEANER_JAR}" \\ + HTMLCLEANER_LICENSE="${HTMLCLEANER_LICENSE}" \\ + JAVADIFFUTILS_JAR="$(mixed_path "${JAVADIFFUTILS_JAR}")" \\ + JAVADIFFUTILS_LICENSE="${JAVADIFFUTILS_LICENSE}" \\ + JDKHOME="${JAVA_HOME}" \\ + JUNIT_JAR="$(mixed_path "${JUNIT_JAR}")" \\ + "\$@" +EOF + +sh ${BUILD_DIR}/make.sh ${MAKE_ARGS:-} diff --git a/make/build.xml b/make/build.xml new file mode 100644 index 0000000..c2c28fb --- /dev/null +++ b/make/build.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/make/pkgsToFiles.sh b/make/pkgsToFiles.sh new file mode 100644 index 0000000..6957e98 --- /dev/null +++ b/make/pkgsToFiles.sh @@ -0,0 +1,35 @@ +#! /bin/sh +# +# Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +classdir=$1; shift +cd $classdir + +if [ "$#" -gt 0 ]; then + for i in $* ; do + dir=`echo $i | sed -e 's|\.|/|g'` + ls -F $dir | grep -v '/$' | sed -e 's|\*$||' -e "s|\(.*\)|-C $classdir $dir/\1|" + done +fi diff --git a/src/share/bin/apidiff.sh b/src/share/bin/apidiff.sh new file mode 100644 index 0000000..2824ffc --- /dev/null +++ b/src/share/bin/apidiff.sh @@ -0,0 +1,111 @@ +#!/bin/sh +# +# Copyright (c) 1998, 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +# Usage: +# apidiff ...args.... +# Run the application with the given arguments. +# The Java runtime used to run apidiff is found as follows: +# - $JAVA_HOME/bin/java is used if $JAVA_HOME is set +# (cf JDK.) +# - Otherwise, "java" is used +# +# apidiff requires a version of Java equivalent to JDK 17 or higher. +# +# You can also run the jar file directly, as in +# java -jar /lib/apidiff.jar ...args... + + +# Deduce where script is installed +# - should work on most derivatives of Bourne shell, like ash, bash, ksh, +# sh, zsh, etc, including on Windows, MKS (ksh) and Cygwin (ash or bash) +if type -p type 1>/dev/null 2>&1 && test -z "`type -p type`" ; then + myname=`type -p "$0"` +elif type type 1>/dev/null 2>&1 ; then + myname=`type "$0" | sed -e 's/^.* is a tracked alias for //' -e 's/^.* is //'` +elif whence whence 1>/dev/null 2>&1 ; then + myname=`whence "$0"` +fi +mydir=`dirname "$myname"` +p=`cd "$mydir" ; pwd` +while [ -n "$p" -a "$p" != "/" ]; do + if [ -r "$p"/lib/apidiff.jar ]; then APIDIFF_HOME="$p" ; break; fi + p=`dirname "$p"` +done +if [ -z "$APIDIFF_HOME" ]; then + echo "Cannot determine APIDIFF_HOME"; exit 1 +fi + +# Normalize APIDIFF_HOME if using Cygwin +case "`uname -s`" in + CYGWIN* ) cygwin=1 ; APIDIFF_HOME=`cygpath -a -m "$APIDIFF_HOME"` ;; +esac + + +# Separate out -J* options for the JVM= +# Unset IFS and use newline as arg separator to preserve spaces in args +DUALCASE=1 # for MKS: make case statement case-sensitive (6709498) +saveIFS="$IFS" +nl=' +' +for i in "$@" ; do + IFS= + if [ -n "$cygwin" ]; then i=`echo $i | sed -e 's|/cygdrive/\([A-Za-z]\)/|\1:/|'` ; fi + case $i in + -J* ) javaOpts=$javaOpts$nl`echo $i | sed -e 's/^-J//'` ;; + * ) apidiffOpts=$apidiffOpts$nl$i ;; + esac + IFS="$saveIFS" +done +unset DUALCASE + +# Determine java for apidiff, from JAVA_HOME, java +if [ -n "$JAVA_HOME" ]; then + APIDIFF_JAVA="$JAVA_HOME/bin/java" +else + APIDIFF_JAVA=java +fi + +# Verify java version 17 or newer used to run apidiff +version=`"$APIDIFF_JAVA" -classpath "${APIDIFF_HOME}/lib/apidiff.jar" jdk.codetools.apidiff.GetSystemProperty java.version 2>&1 | + grep 'java.version=' | sed -e 's/^.*=//' -e 's/^1\.//' -e 's/\([1-9][0-9]*\).*/\1/'` + +if [ -z "$version" ]; then + echo "Cannot determine version of java to run apidiff" + exit 1; +elif [ "$version" -lt 17 ]; then + echo "java version 17 or later is required to run apidiff" + exit 1; +fi + +# And finally ... + +IFS=$nl + +"${APIDIFF_JAVA}" \ + $javaOpts \ + -Dprogram=`basename "$0"` \ + -jar "${APIDIFF_HOME}/lib/apidiff.jar" \ + $apidiffOpts diff --git a/src/share/bin/compare-jdk-versions.sh b/src/share/bin/compare-jdk-versions.sh new file mode 100644 index 0000000..ca5757f --- /dev/null +++ b/src/share/bin/compare-jdk-versions.sh @@ -0,0 +1,259 @@ +#!/bin/bash +# +# Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +# Demonstrates how to compare the API docs for different builds or versions of JDK. +# +# The script has three parts or phases: +# 1. Determine which versions to compare +# 2. Build the docs for those versions +# 3. Compare the generated docs + +DEFAULT_LATEST_JDK= +DEFAULT_APIDIFF= +DEFAULT_REPO=. +DEFAULT_OUTDIR=build + +# Helper used to ensure the correct number of arguments is passed to bash functions +check_arguments() { + local name="$1" + local expected="$2" + local actual="$3" + + if [ ! "${expected}" = "${actual}" ]; then + echo "Incorrect number of arguments to function '${name}' (expecting ${expected} but got ${actual})" 2>&1 + exit 1 + fi +} + +ensure_arg() { + check_arguments "${FUNCNAME}" 2 $# + local option="$1" + local arg_count="$2" + if [ "$arg_count" -lt "2" ]; then + echo "The $option option requires an argument" 2>&1 + exit + fi +} + +process_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --help|-h ) HELP=1 ; shift ;; + --apidiff ) ensure_arg "$1" $# ; APIDIFF="$2" ; shift ; shift ;; + --jdk ) ensure_arg "$1" $# ; LATEST_JDK="$2" ; shift ; shift ;; + --output ) ensure_arg "$1" $# ; OUTDIR="$2" ; shift ; shift ;; + --repo ) ensure_arg "$1" $# ; REPO="$2" ; shift ; shift ;; + --latest-ga | --previous | --latest | jdk-* ) + versions="${versions} $1" ; shift ;; + * ) echo "unknown option: '$1'" 1>&2; exit 1 ;; + esac + done +} + +usage() { + echo "Usage: $0 " + echo "--help" + echo " Show this message." + echo "--apidiff " + echo " Specify location of apidiff." + echo "--jdk " + echo " Specify location of JDK used to run comparison tools." + echo "--repo " + echo " Specify location of repository in which to build docs." + echo " Default: ${DEFAULT_REPO}" + echo "--output " + echo " Specify directory for comparison reports." + echo " Default: /${DEFAULT_OUTDIR}" + echo "" + echo " 0, 1, 2 or 3 of --latest-ga, --previous, --latest, jdk-*" + echo " where jdk-* is a git tag for the repo." + echo " Default if none specified: --latest-ga --latest" + echo " Default if just one specified: --latest" +} + +process_args "$@" + +if [ -n "${HELP:-}" ]; then + usage + exit +fi + +if [ -f ~/.config/apidiff/apidiff.conf ]; then + source ~/.config/apidiff/apidiff.conf +fi + +LATEST_JDK=${LATEST_JDK:-${DEFAULT_LATEST_JDK}} +APIDIFF=${APIDIFF:-${DEFAULT_APIDIFF}} +REPO=${REPO:-${DEFAULT_REPO}} +OUTDIR=${OUTDIR:-${REPO}/${DEFAULT_OUTDIR}} + +# Sanity check args + +if [ -z "${APIDIFF}" ]; then + echo "no path specified for apidiff" 1>&2 ; exit 1 +elif [ ! -r ${APIDIFF}/lib/apidiff.jar ]; then + echo "invalid path for apidiff" 1>&2 ; exit 1 +fi + +if [ -z "${LATEST_JDK}" ]; then + echo "no path specified for latest JDK" 1>&2 ; exit 1 +elif [ ! -r ${LATEST_JDK}/bin/java ]; then + echo "invalid path for latest JDK: ${LATEST_JDK}" 1>&2 ; exit 1 +fi + +if [ ! -d ${REPO}/.git ]; then + echo "invalid path for repo: ${REPO}" 1>&2 ; exit 1 +fi + +# use echo in next line to trim excess whitespace from wc output +case $(echo $(wc -w <<< "${versions}")) in + 0 ) versions="--latest-ga --latest" ;; + 1 ) versions="${versions} --latest" ;; + 2 | 3 ) ;; + * ) echo "unexpected number of versions given: ${versions}" 1>&2 ; exit 1 ;; +esac + +# Determine whether running in a closed+open pair, or just an open repo. +if [ -d ${REPO}/open ]; then + OPEN=open +else + OPEN=. +fi + +# Phase 1: determine which versions to build and compare, +# identified by the corresponding `git` tags. +# The versions (and hence tags) are determined automatically, from +# version-numbers.conf and the output of `git tag`. +# The following tags are determined: +# PREVIOUS_GA_TAG, PREVIOUS_TAG, LATEST_TAG + +# ensure the files in the work area are up to date before reading +# version-numbers.conf; it is assumed that the `master` branch +# always has the latest version numbers +git -C ${REPO}/${OPEN} checkout master +source ${REPO}/${OPEN}/make/conf/version-numbers.conf +VERSION_FEATURE=${VERSION_FEATURE:-${DEFAULT_VERSION_FEATURE}} + +TAGS=( $(git -C ${REPO} tag --list "jdk-${VERSION_FEATURE}*" | sort --version-sort --reverse) ) +LATEST_TAG=${TAGS[0]} +PREVIOUS_TAG=${TAGS[1]} + +PREVIOUS_FEATURE=$(( ${VERSION_FEATURE} - 1)) +LATEST_GA_TAG="jdk-${PREVIOUS_FEATURE}-ga" + +tag() { + case "$1" in + --latest-ga ) echo ${LATEST_GA_TAG} ;; + --previous ) echo ${PREVIOUS_TAG} ;; + --latest ) echo ${LATEST_TAG} ;; + jdk-* ) echo $1 ;; + * ) echo "bad tag: $1" 1>&2 ; exit 1 ;; + esac +} + +check_tag() { + local tag="$1" + if ! git -C "$REPO" rev-parse "$tag" > /dev/null 2>&1 ; then + echo tag "$tag" not found in repo "$REPO" + exit 1 + fi + if [ "${OPEN}" = "open" ]; then + if ! git -C "$REPO"/$OPEN rev-parse "$tag" > /dev/null 2>&1 ; then + echo tag "$tag" not found in repo "$OPEN"/$OPEN + exit 1 + fi + fi +} + +# Phase 2: build the docs to be compared +# $1 is the tag for the version to checkout and build. +# It should be one of `--latest-ga`, `--previous`, `--latest` or an actual `jdk-*` tag. +# The build is skipped if `images/jdk` and `images/docs-reference` both exist. +# +# Configure and use the `docs-reference` target with the $LATEST_JDK. +# The same version of JDK should be used for all versions to be compared. +# `apidiff` also requires a JDK image for the comparison. +# +# Note: building the JDK image and docs for each version to be compared +# may take a while. + +configure_jdk() { + if [ -n "${APIDIFF_CONFIGURE_JDK}" ]; then + "${APIDIFF_CONFIGURE_JDK}" "$@" ; + elif [ -r jib.sh -a -r closed/bin/jib.sh ]; then + sh jib.sh configure -- "$@" + elif [ -r bin/jib.sh -a -r ../closed/make/conf/jib-install.conf ]; then + sh bin/jib.sh configure -- "$@" + elif [ -r bin/jib.sh -a -n "${JIB_SERVER}" ]; then + sh bin/jib.sh configure -- "$@" + else + sh ./configure "$@" + fi +} + +build_reference_docs() { + TAG=$(tag $1) + if [ -d ${REPO}/build/${TAG}/images/jdk -a -d ${REPO}/build/${TAG}/images/docs-reference ]; then + echo "Skipping build for ${TAG}" + return + fi + + git -C ${REPO} checkout --detach ${TAG} + if [ "${OPEN}" = "open" ]; then + git -C ${REPO}/open checkout --detach ${TAG} + fi + + ( cd $REPO + configure_jdk \ + --with-conf-name=${TAG} \ + --enable-full-docs \ + --with-docs-reference-jdk=${LATEST_JDK} \ + --quiet + make CONF_NAME=${TAG} jdk-image docs-reference + ) +} + +# Phase 3: Compare the documentation and generate reports. + +apidiff_javase() { + for t in "$@" ; do tags="$tags $(tag $t)" ; done + for t in $tags ; do check_tag $t ; done + for t in $tags ; do build_reference_docs $t ; done + title="Comparing Java SE modules for $(echo $tags | sed -e 's/ /, /g' -e 's/\(.*\),\(.*\)/\1 and\2/')" + outdir="${OUTDIR}/apidiff/javase--$(echo $tags | sed -e 's/ /--/g')" + echo "${title}" + JAVA_HOME=${LATEST_JDK} ${APIDIFF}/bin/apidiff \ + $(for t in $tags ; do echo "--api $t --jdk-build ${REPO}/build/$t" ; done) \ + --include java.*/java.** --include java.*/javax.** \ + --exclude java.smartcardio/ \ + --jdk-docs docs-reference \ + --output-directory ${outdir} \ + --title "${title}" \ + "${EXTRA_APIDIFF_OPTIONS[@]}" + echo "Results written to ${outdir}" +} + +apidiff_javase ${versions} diff --git a/src/share/classes/META-INF/services/java.util.spi.ToolProvider b/src/share/classes/META-INF/services/java.util.spi.ToolProvider new file mode 100644 index 0000000..75ba4bc --- /dev/null +++ b/src/share/classes/META-INF/services/java.util.spi.ToolProvider @@ -0,0 +1 @@ +jdk.codetools.apidiff.APIDiff diff --git a/src/share/classes/jdk/codetools/apidiff/APIDiff.java b/src/share/classes/jdk/codetools/apidiff/APIDiff.java new file mode 100644 index 0000000..272ef74 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/APIDiff.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.PrintWriter; +import java.util.spi.ToolProvider; + +/** + * An entry point for the "apidiff" utility that implements {@link ToolProvider}. + */ +public class APIDiff implements ToolProvider { + @Override + public String name() { + return "apidiff"; + } + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + return new Main(out, err).run(args).exitCode; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/Abort.java b/src/share/classes/jdk/codetools/apidiff/Abort.java new file mode 100644 index 0000000..2dd68e9 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/Abort.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +/** + * An exception to indicate that processing has been aborted. + */ +public class Abort extends Error { + private static final long serialVersionUID = 0; +} diff --git a/src/share/classes/jdk/codetools/apidiff/CommandLine.java b/src/share/classes/jdk/codetools/apidiff/CommandLine.java new file mode 100644 index 0000000..d056e1a --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/CommandLine.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Utility methods for processing command line arguments. + * + *

This is NOT part of any supported API. + * If you write code that depends on this, you do so at your own risk. + * This code and its internal interfaces are subject to change or + * deletion without notice. + */ +public class CommandLine { + /** + * Process Win32-style command files for the specified command line + * arguments and return the resulting arguments. A command file argument + * is of the form '@file' where 'file' is the name of the file whose + * contents are to be parsed for additional arguments. The contents of + * the command file are parsed using StreamTokenizer and the original + * '@file' argument replaced with the resulting tokens. Recursive command + * files are not supported. The '@' character itself can be quoted with + * the sequence '@@'. + * @param args the arguments that may contain @files + * @return the arguments, with @files expanded + * @throws IOException if there is a problem reading any of the @files + */ + public static String[] parse(String[] args) throws IOException { + List newArgs = new ArrayList<>(); + appendParsedCommandArgs(newArgs, Arrays.asList(args)); + return newArgs.toArray(new String[newArgs.size()]); + } + + /** + * Process Win32-style command files for the specified command line + * arguments and return the resulting arguments. A command file argument + * is of the form '@file' where 'file' is the name of the file whose + * contents are to be parsed for additional arguments. The contents of + * the command file are parsed using StreamTokenizer and the original + * '@file' argument replaced with the resulting tokens. Recursive command + * files are not supported. The '@' character itself can be quoted with + * the sequence '@@'. + * @param args the arguments that may contain @files + * @return the arguments, with @files expanded + * @throws IOException if there is a problem reading any of the @files + */ + public static List parse(List args) throws IOException { + List newArgs = new ArrayList<>(); + appendParsedCommandArgs(newArgs, args); + return Collections.unmodifiableList(newArgs); + } + + private static void appendParsedCommandArgs(List newArgs, List args) throws IOException { + for (String arg : args) { + if (arg.length() > 1 && arg.charAt(0) == '@') { + arg = arg.substring(1); + if (arg.charAt(0) == '@') { + newArgs.add(arg); + } else { + loadCmdFile(arg, newArgs); + } + } else { + newArgs.add(arg); + } + } + } + + /** + * Process the given environment variable and appends any Win32-style + * command files for the specified command line arguments and return + * the resulting arguments. A command file argument + * is of the form '@file' where 'file' is the name of the file whose + * contents are to be parsed for additional arguments. The contents of + * the command file are parsed using StreamTokenizer and the original + * '@file' argument replaced with the resulting tokens. Recursive command + * files are not supported. The '@' character itself can be quoted with + * the sequence '@@'. + * @param envVariable the env variable to process + * @param args the arguments that may contain @files + * @return the arguments, with environment variable's content and expansion of @files + * @throws IOException if there is a problem reading any of the @files + * @throws CommandLine.UnmatchedQuote if an unmatched quote is found + */ + public static List parse(String envVariable, List args) + throws IOException, UnmatchedQuote { + + List inArgs = new ArrayList<>(); + appendParsedEnvVariables(inArgs, envVariable); + inArgs.addAll(args); + List newArgs = new ArrayList<>(); + appendParsedCommandArgs(newArgs, inArgs); + return newArgs; + } + + /** + * Process the given environment variable and appends any Win32-style + * command files for the specified command line arguments and return + * the resulting arguments. A command file argument + * is of the form '@file' where 'file' is the name of the file whose + * contents are to be parsed for additional arguments. The contents of + * the command file are parsed using StreamTokenizer and the original + * '@file' argument replaced with the resulting tokens. Recursive command + * files are not supported. The '@' character itself can be quoted with + * the sequence '@@'. + * @param envVariable the env variable to process + * @param args the arguments that may contain @files + * @return the arguments, with environment variable's content and expansion of @files + * @throws IOException if there is a problem reading any of the @files + * @throws CommandLine.UnmatchedQuote if an unmatched quote is found + */ + public static String[] parse(String envVariable, String[] args) throws IOException, UnmatchedQuote { + List out = parse(envVariable, Arrays.asList(args)); + return out.toArray(new String[out.size()]); + } + + private static void loadCmdFile(String name, List args) throws IOException { + try (Reader r = Files.newBufferedReader(Paths.get(name), Charset.defaultCharset())) { + Tokenizer t = new Tokenizer(r); + String s; + while ((s = t.nextToken()) != null) { + args.add(s); + } + } + } + + private static class Tokenizer { + private final Reader in; + private int ch; + + public Tokenizer(Reader in) throws IOException { + this.in = in; + ch = in.read(); + } + + public String nextToken() throws IOException { + skipWhite(); + if (ch == -1) { + return null; + } + + StringBuilder sb = new StringBuilder(); + char quoteChar = 0; + + while (ch != -1) { + switch (ch) { + case ' ': + case '\t': + case '\f': + if (quoteChar == 0) { + return sb.toString(); + } + sb.append((char) ch); + break; + + case '\n': + case '\r': + return sb.toString(); + + case '\'': + case '"': + if (quoteChar == 0) { + quoteChar = (char) ch; + } else if (quoteChar == ch) { + quoteChar = 0; + } else { + sb.append((char) ch); + } + break; + + case '\\': + if (quoteChar != 0) { + ch = in.read(); + switch (ch) { + case '\n', '\r' -> { + while (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' || ch == '\f') { + ch = in.read(); + } + continue; + } + case 'n' -> ch = '\n'; + case 'r' -> ch = '\r'; + case 't' -> ch = '\t'; + case 'f' -> ch = '\f'; + } + } + sb.append((char) ch); + break; + + default: + sb.append((char) ch); + } + + ch = in.read(); + } + + return sb.toString(); + } + + void skipWhite() throws IOException { + while (ch != -1) { + switch (ch) { + case ' ': + case '\t': + case '\n': + case '\r': + case '\f': + break; + + case '#': + ch = in.read(); + while (ch != '\n' && ch != '\r' && ch != -1) { + ch = in.read(); + } + break; + + default: + return; + } + + ch = in.read(); + } + } + } + + @SuppressWarnings("fallthrough") + private static void appendParsedEnvVariables(List newArgs, String envVariable) + throws UnmatchedQuote { + + if (envVariable == null) { + return; + } + String in = System.getenv(envVariable); + if (in == null || in.trim().isEmpty()) { + return; + } + + final char NUL = (char)0; + final int len = in.length(); + + int pos = 0; + StringBuilder sb = new StringBuilder(); + char quote = NUL; + char ch; + + loop: + while (pos < len) { + ch = in.charAt(pos); + switch (ch) { + case '\"': case '\'': + if (quote == NUL) { + quote = ch; + } else if (quote == ch) { + quote = NUL; + } else { + sb.append(ch); + } + pos++; + break; + case '\f': case '\n': case '\r': case '\t': case ' ': + if (quote == NUL) { + newArgs.add(sb.toString()); + sb.setLength(0); + while (ch == '\f' || ch == '\n' || ch == '\r' || ch == '\t' || ch == ' ') { + pos++; + if (pos >= len) { + break loop; + } + ch = in.charAt(pos); + } + break; + } + // fall through + default: + sb.append(ch); + pos++; + } + } + if (sb.length() != 0) { + newArgs.add(sb.toString()); + } + if (quote != NUL) { + throw new UnmatchedQuote(envVariable); + } + } + + /** + * Thrown when an unmatched quote is found when expanding an eneviornment variable. + */ + public static class UnmatchedQuote extends Exception { + private static final long serialVersionUID = 0; + + /** + * The name of the environment variable. + */ + public final String variableName; + + UnmatchedQuote(String variable) { + this.variableName = variable; + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/DamerauLevenshteinDistance.java b/src/share/classes/jdk/codetools/apidiff/DamerauLevenshteinDistance.java new file mode 100644 index 0000000..519f8a8 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/DamerauLevenshteinDistance.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.util.HashMap; +import java.util.Map; + +// See JDK: jdk.compiler/com.sun.tools.javac.util.StringUtils.DamerauLevenshteinDistance + +/**Call {@link #of(String, String)} to calculate the distance. + * + *

Usage Examples

+ * + * Pick top three vocabulary words whose normalized distance from + * the misspelled word is no greater than one-third. + * + * {@snippet : + * record Pair(String word, int distance) { } + * + * var suggestions = vocabulary.stream() + * .map(v -> new Pair(v, DamerauLevenshteinDistance.of(v, misspelledWord))) + * .filter(p -> Double.compare(1.0 / 3, ((double) p.distance()) / p.word().length()) >= 0) + * .sorted(Comparator.comparingDouble(Pair::distance)) + * .limit(3) + * .toList(); + * } + */ +class DamerauLevenshteinDistance { + + /* + * This is a Java implementation of the algorithm from "An Extension of + * the String-to-String Correction Problem" by R. Lowrance and + * R. A. Wagner (https://dl.acm.org/doi/10.1145/321879.321880). + * That algorithm is O(|a|*|b|) in both space and time. + * + * This implementation encapsulates arrays and (most of) strings behind + * methods to accommodate for algorithm indexing schemes which are -1, + * 0, and 1 based and to offset memory and performance overhead if any + * strings in the pair contain non-ASCII symbols. + */ + + private final int INF; + private final int[][] h; + private final String a; + private final String b; + + private static final int Wi = 1; // insert + private static final int Wd = 1; // delete + private static final int Wc = 1; // change + private static final int Ws = 1; // interchange + + static { + assert 2L * Ws >= Wi + Wd; // algorithm requirement + } + + private int[] smallDA; + private Map bigDA; + + /** {@return the edit distance between two strings} + * The distance returned from this method has the following properties: + *
    + *
  1. {@code a.equals(b) && of(a, b) == 0) || (!a.equals(b) && of(a, b) > 0)} + *
  2. {@code of(a, b) == of(b, a)} + *
  3. {@code of(a, b) + of(b, c) >= of(a, c)} + *
+ * + * @implSpec + * This method is safe to be called by multiple threads. + * @throws NullPointerException if any of the two strings are null + * @throws ArithmeticException if any step of the calculation + * overflows an int + */ + public static int of(String a, String b) { + return new DamerauLevenshteinDistance(a, b).calculate(); + } + + private int calculate() { + for (int i = 0; i <= a.length(); i++) { + h(i, 0, i * Wd); + h(i, -1, INF); + } + for (int j = 0; j <= b.length(); j++) { + h(0, j, j * Wi); + h(-1, j, INF); + } + // algorithm's line #8 that initializes DA is not needed here + // because this class encapsulates DA and initializes it + // separately + for (int i = 1; i <= a.length(); i++) { + int db = 0; + for (int j = 1; j <= b.length(); j++) { + int i1 = da(characterAt(b, j)); + int j1 = db; + boolean eq = characterAt(a, i) == characterAt(b, j); + int d = eq ? 0 : Wc; + if (eq) { + db = j; + } + int m = min(h(i - 1, j - 1) + d, + h(i, j - 1) + Wi, + h(i - 1, j) + Wd, + h(i1 - 1, j1 - 1) + (i - i1 - 1) * Wd + Ws + (j - j1 - 1) * Wi); + h(i, j, m); + } + da(characterAt(a, i), i); + } + return h(a.length(), b.length()); + } + + private int characterAt(String s, int i) { + return s.charAt(i - 1); + } + + private void h(int i, int j, int value) { + h[i + 1][j + 1] = value; + } + + private int h(int i, int j) { + return h[i + 1][j + 1]; + } + + /* + * This implementation works with UTF-16 strings, but favours strings + * that comprise ASCII characters. Measuring distance between a pair + * of ASCII strings is likely to be a typical use case for this + * implementation. + * + * If a character for which the value is to be stored does not fit into + * the ASCII range, this implementation switches to a different storage + * dynamically. Since neither string lengths nor character values + * change, any state accumulated so far, including any loops and local + * variables, remains valid. + * + * Note, that if the provided character were a surrogate and this + * implementation dealt with code points, which it does not, dynamic + * switching of the storage would not be enough. The complete + * representation would need to be changed. That would entail + * discarding any accumulated state and repeating the computation. + */ + + private int da(int i) { + if (smallDA != null && i < '\u0080') { + return smallDA[i]; + } + // if a character cannot be found, it means that the character + // hasn't been updated, which means that the associated value + // is the default value, which is 0 + if (bigDA != null) { + Integer v = bigDA.get((char) i); + return v == null ? 0 : v; + } else { + return 0; + } + } + + private void da(int i, int value) { + if (bigDA == null && i < '\u0080') { + if (smallDA == null) { + smallDA = new int[127]; + } + smallDA[i] = value; + } else { + if (bigDA == null) { + bigDA = new HashMap<>(); + if (smallDA != null) { // rebuild DA accumulated so far + for (int j = 0; j < smallDA.length; j++) { + int v = smallDA[j]; + if (v != 0) + bigDA.put((char) j, v); + } + smallDA = null; // no longer needed + } + } + bigDA.put((char) i, value); + } + assert smallDA == null ^ bigDA == null; // at most one in use + } + + private static int min(int a, int b, int c, int d) { + return Math.min(a, Math.min(b, Math.min(c, d))); + } + + private DamerauLevenshteinDistance(String a, String b) { + this.a = a; + this.b = b; + this.h = new int[this.a.length() + 2][this.b.length() + 2]; + INF = this.a.length() * Wd + this.b.length() * Wi + 1; + if (INF < 0) + throw new ArithmeticException("Overflow"); + } +} \ No newline at end of file diff --git a/src/share/classes/jdk/codetools/apidiff/GetSystemProperty.java b/src/share/classes/jdk/codetools/apidiff/GetSystemProperty.java new file mode 100644 index 0000000..e082f4c --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/GetSystemProperty.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.IOException; + +/** + * A standalone utility to get one or more system properties. + * The command line arguments should either be {@code -all} + * or a series of system property names. + */ +public class GetSystemProperty +{ + /** + * The main program. + * @param args a series of property names, or {@code -all}. + */ + public static void main(String[] args) { + if (args.length == 1 && args[0].equals("-all")) { + try { + System.getProperties().store(System.out, "system properties"); + } catch (IOException e) { + System.err.println(e); + System.exit(1); + } + } else { + for (String arg : args) { + String v = System.getProperty(arg); + System.out.println(arg + "=" + (v == null ? "" : v)); + } + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/JDKBuildOption.java b/src/share/classes/jdk/codetools/apidiff/JDKBuildOption.java new file mode 100644 index 0000000..e57b2ff --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/JDKBuildOption.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2020,2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import jdk.codetools.apidiff.model.Selector; + +/** + * A class to encapsulate the functionality of the {@code --jdk-build} option, + * which provides a shorthand for the underlying primitive options when doing + * a "standard" comparison involving JDK builds. + */ +public class JDKBuildOption { + private final Path buildDir; + private final Path imagesDir; + + JDKBuildOption(Path dir) { + this.buildDir = dir; + this.imagesDir = dir.resolve("images"); + } + + void expand(Options options, Options.APIOptions apiOptions, Log log) { + apiOptions.addFileManagerOpt("--system", getSystem().toString()); + + // proactively get api dir if available, in case we want to subsequently + // set compareAPIDescriptions by default + apiOptions.apiDir = getAPIDirectory(options, log); + + if (options.compareDocComments == Boolean.TRUE) { + Set modules = new LinkedHashSet<>(); + Path tmpDir = unpackSource(options, log, modules); + for (String m : modules) { + apiOptions.addFileManagerOpt("--patch-module", + m + "=" + tmpDir.resolve(m)); + } + // since we're also setting the --system option, + // just set the --source option here + apiOptions.source = getRelease(log); + } + } + + private Path getSystem() { + return imagesDir.resolve("jdk"); + } + + private Path getAPIDirectory(Options options, Log log) { + Map dirs = new HashMap<>(); + try (DirectoryStream stream = Files.newDirectoryStream(imagesDir, + p -> Files.isDirectory(p) && p.getFileName().toString().contains("docs"))) { + for (Path entry: stream) { + dirs.put(entry.getFileName().toString(), entry); + } + } catch (DirectoryIteratorException e) { + // I/O error encountered during the iteration; the cause is an IOException + softError(log, options, "jdkbuild.ioerror-finding-docs", e.getCause()); + return null; + } catch (IOException e) { + softError(log, options, "jdkbuild.ioerror-finding-docs", e); + return null; + } + Path docsDir; + if (dirs.isEmpty()) { + softError(log, options, "jdkbuild.err.no-docs", imagesDir); + return null; + } else if (options.jdkDocs == null) { + if (dirs.size() > 1) { + softError(log, options, "jdkbuild.err.multiple-docs", + imagesDir, + String.join(", ", dirs.keySet())); + return null; + } else { + docsDir = dirs.values().iterator().next(); + } + } else { + Path dir = dirs.get(options.jdkDocs); + if (dir == null) { + softError(log, options, "jdkbuild.err.cannot-find-docs", options.jdkDocs, imagesDir); + return null; + } + docsDir = dir; + } + return docsDir.resolve("api"); + } + + /** + * Reports a hard error if comparison of API descriptions has been explicitly requested. + * Otherwise, does nothing. + * + * @param log the log + * @param options the options + * @param key the resource key + * @param args the arguments + */ + void softError(Log log, Options options, String key, Object... args) { + if (options.compareApiDescriptions == Boolean.TRUE) { + log.error(key, args); + } + } + + private String getRelease(Log log) { + Map map = getReleaseInfo(log); + return map.get("JAVA_VERSION"); + } + + private Map getReleaseInfo(Log log) { + Map map = new LinkedHashMap<>(); + Pattern p = Pattern.compile("(?[A-Z0-9_]+)=\"(?.*)\"$"); + Path releaseFile = getSystem().resolve("release"); + try { + for (String line : Files.readAllLines(releaseFile)) { + Matcher m = p.matcher(line); + if (m.matches()) { + map.put(m.group("name"), m.group("value")); + } + } + } catch (IOException e) { + log.error("jdkbuild.err.error-reading-release-file", releaseFile, e); + } + return map; + } + + private Path unpackSource(Options options, Log log, Set modules) { + Selector s = new Selector(options.includes, options.excludes); + + Path tmpSrcDir = buildDir.resolve("apidiff.tmp").resolve("src"); + Path srcZip = buildDir.resolve("support").resolve("src.zip"); + try (ZipFile zf = new ZipFile(srcZip.toFile())) { + Enumeration e = zf.entries(); + while (e.hasMoreElements()) { + ZipEntry ze = e.nextElement(); + String name = ze.getName(); + if (!name.endsWith(".java")) { + continue; + } + if (name.startsWith("/")) { + name = name.substring(1); + } + int firstSep = name.indexOf("/"); // after module name + int lastSep = name.lastIndexOf("/"); // before type name + if (lastSep > firstSep) { // ensures two or more instances + String m = name.substring(0, firstSep); + String p = name.substring(firstSep + 1, lastSep).replace("/", "."); + String t = name.substring(lastSep + 1).replace(".java", ""); + if (s.acceptsType(m, p, t)) { + try (InputStream in = zf.getInputStream(ze)) { + Path outFile = tmpSrcDir.resolve(name.replace("/", File.separator)); + Files.createDirectories(outFile.getParent()); + Files.copy(in, outFile, StandardCopyOption.REPLACE_EXISTING); + } + modules.add(m); + } + } + } + } catch (IOException e) { + log.error("jdkbuild.err.error-reading-src.zip", srcZip, e); + + } + return tmpSrcDir; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/Log.java b/src/share/classes/jdk/codetools/apidiff/Log.java new file mode 100644 index 0000000..f6a3383 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/Log.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2018,2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.PrintWriter; +import java.nio.file.Path; + +/** + * Utilities to write logging messages. + */ +public class Log { + /** + * An output stream for "expected" output. + */ + public final PrintWriter out; + + /** + * An output stream for "diagnostic" output. + */ + public final PrintWriter err; + + /** + * The messages used by this log. + */ + private final Messages messages = Messages.instance( "jdk.codetools.apidiff.resources.log"); + + private String errPrefix = messages.getString("log.err-prefix"); + private String warnPrefix = messages.getString("log.warn-prefix"); + private String notePrefix = messages.getString("log.note-prefix"); + + private int errCount = 0; + private int warnCount = 0; + + /** + * Creates an instance of a log. + * + * @param out the stream to which to write normal output + * @param err the stream to which to write error output + */ + public Log(PrintWriter out, PrintWriter err) { + this.out = out; + this.err = err; + } + + public void flush() { + out.flush(); + err.flush(); + } + + /** + * Reports an error message, based on a resource key and optional arguments. + * + * @param key the resource key + * @param args the arguments + */ + public void error(String key, Object... args) { + err.println(errPrefix + " " + messages.getString(key, args)); + errCount++; + } + + /** + * Reports a warning message, based on a resource key and optional arguments. + * + * @param key the resource key + * @param args the arguments + */ + public void warning(String key, Object... args) { + err.println(warnPrefix + " " + messages.getString(key, args)); + warnCount++; + } + + /** + * Reports a message, based on a resource key and optional arguments. + * + * @param key the resource key + * @param args the arguments + */ + public void report(String key, Object... args) { + err.println(messages.getString(key, args)); + } + + /** + * Reports an error message, with optional file position. + * + * @param file the file, or null + * @param line the line of the file, if any + * @param key the resource key for the message, or null if the first arg is a localized message + * @param args the arguments for the message + */ + public void error(Path file, long line, String key, Object... args) { + String message = (key == null) ? args[0].toString() : messages.getString(key, args); + if (file == null) { + err.println(errPrefix + " " + message); + } else { + err.println(file + ":" + line + ": " + message); + } + errCount++; + } + + /** + * Reports a warning message, with optional file position. + * + * @param file the file, or null + * @param line the line of the file, if any + * @param key the resource key for the message, or null if the first arg is a localized message + * @param args the arguments for the message + */ + public void warning(Path file, long line, String key, Object... args) { + String message = (key == null) ? args[0].toString() : messages.getString(key, args); + if (file == null) { + err.println(warnPrefix + " " + message); + } else { + err.println(file + ":" + line + ": " + warnPrefix + " " + message); + } + warnCount++; + } + + /** + * Reports a note, with optional file position. + * + * @param file the file, or null + * @param line the line of the file, if any + * @param key the resource key for the message, or null if the first arg is a localized message + * @param args the arguments for the message + */ + public void note(Path file, long line, String key, Object... args) { + String message = key == null ? args[0].toString() : messages.getString(key, args); + if (file == null) { + err.println(notePrefix + " " + message); + } else { + err.println(file + ":" + line + ": " + notePrefix + " " + message); + } + } + + /** + * Returns the number of errors that have been reported. + * + * @return the number of errors + */ + public int errorCount() { + return errCount; + } + + /** + * Returns the number of warnings that have been reported. + * + * @return the number of errors + */ + public int warningCount() { + return warnCount; + } + + /** + * Reports the number of errors and warnings that have been reported. + */ + void reportCounts() { + if (errCount > 0) { + err.println(messages.getString("log.errors", errCount)); + } + + if (warnCount > 0) { + err.println(messages.getString("log.warnings", errCount)); + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/Main.java b/src/share/classes/jdk/codetools/apidiff/Main.java new file mode 100644 index 0000000..75e6dbc --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/Main.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2018,2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Proxy; +import java.nio.file.Files; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import jdk.codetools.apidiff.Options.VerboseKind; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIComparator; +import jdk.codetools.apidiff.model.AccessKind; +import jdk.codetools.apidiff.model.Selector; +import jdk.codetools.apidiff.report.LogReporter; +import jdk.codetools.apidiff.report.MultiplexReporter; +import jdk.codetools.apidiff.report.Reporter; +import jdk.codetools.apidiff.report.html.HtmlReporter; + +/** + * Main entry point for the "apidiff" utility. + * The code can be invoked from the command line, + * or by equivalent API methods. + */ +public class Main { + /** + * An encapsulation of the exit code from running the tool. + */ + public enum Result { + OK(0), + DIFFS(1), + BAD_ARGS(2), + FAULT(3); + final int exitCode; + + Result(int exitCode) { + this.exitCode = exitCode; + } + } + + /** + * Executes the tool, configured with the given arguments. + * + * This is the main entry point when invoked from the command-line. + * It uses the standard output and error stream. + * + * @param args the arguments to configure the tool + */ + public static void main(String... args) { + Result r = new Main().run(args); + if (r != Result.OK) { + System.exit(r.exitCode); + } + } + + private final PrintWriter out; + private final PrintWriter err; + + /** + * Creates an instance of the class that uses + * the standard output and error streams. + */ + public Main() { + out = new PrintWriter(System.out); + err = new PrintWriter(System.err, true); + } + + /** + * Creates an instance of the class that uses the given stream. + * + * @param out the stream for standard output + * @param err the stream for error messages and other diagnostics. + */ + public Main(PrintWriter out, PrintWriter err) { + this.out = out; + this.err = err; + } + + /** + * Executes the tool, configured with the given arguments. + * + * @param args the arguments to configure the tool + * + * @return a value indicating the outcome of the comparison + */ + public Result run(String... args) { + return run(List.of(args)); + } + + /** + * Executes the tool, configured with the given arguments. + * + * @param args the arguments to configure the tool + * + * @return a value indicating the outcome of the comparison + */ + public Result run(List args) { + Log log = new Log(out, err); + try { + return run(args, log); + } finally { + log.flush(); + } + } + + private Result run(List args, Log log) { + try { + args = CommandLine.parse(args); + } catch (IOException e) { + log.error("main.err.bad-@file", e.getMessage()); + return Result.BAD_ARGS; + } + + Options options = new Options(log, args); + if (log.errorCount() > 0) { + return Result.BAD_ARGS; + } + + if (options.version) { + Version.getCurrent().show(log.out); + } + + if (options.help) { + options.showHelp(); + log.flush(); + } + + if ((options.version || options.help) && options.allAPIOptions.isEmpty()) { + return Result.OK; + } + + options.validate(); + if (log.errorCount() > 0) { + return Result.BAD_ARGS; + } + + Instant start = Instant.now(); + + Selector s = new Selector(options.includes, options.excludes); + AccessKind ak = options.getAccessKind(); + + Set apis = options.allAPIOptions.values().stream() + .map(a -> API.of(a, s, ak, log)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + List rList = new ArrayList<>(); + + rList.add(new LogReporter(log, options)); + if (options.getHiddenOption("trace-reporter") != null) { + rList.add(createTraceReporter(log)); + } + + if (options.getOutDir() != null) { + try { + Files.createDirectories((options.getOutDir())); + } catch (IOException e) { + log.error("main.err.cant-create-output-directory", options.getOutDir()); + return Result.FAULT; + } + Notes notes = null; + if (options.notes != null) { + try { + notes = Notes.read(options.notes, log); + } catch (IOException e) { + log.error("main.err.cant-read-notes", options.notes, e); + return Result.FAULT; + } + } + rList.add(new HtmlReporter(apis, options, notes, log)); + } + + Reporter r = (rList.size() == 1) ? rList.get(0) : new MultiplexReporter(rList); + + boolean equal; + try { + APIComparator ac = new APIComparator(apis, options, r, log); + equal = ac.compare(); + } catch (Abort ex) { + // processing aborted + equal = false; + } + + if (options.isVerbose((VerboseKind.TIME))) { + Instant now = Instant.now(); + Duration d = Duration.between(start, now); + long hours = d.toHours(); + int minutes = d.toMinutesPart(); + int seconds = d.toSecondsPart(); + log.report("main.elapsed", hours, minutes, seconds); + } + + log.reportCounts(); + log.flush(); + + return (log.errorCount() > 0) ? Result.FAULT : equal ? Result.OK : Result.DIFFS; + } + + private Reporter createTraceReporter(Log log) { + return (Reporter) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{ Reporter.class }, + (proxy, method, args) -> { + log.err.println("!! " + method.getName() + ": " + Arrays.toString(args)); + return null; + }); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/Messages.java b/src/share/classes/jdk/codetools/apidiff/Messages.java new file mode 100644 index 0000000..8e3bb5b --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/Messages.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018,2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Set; + +/** + * A utility class to provide localized messages, based on resources in a resource bundle. + */ +public class Messages { + static final Map map = new HashMap<>(); + + /** + * Returns a singleton instance of a {@code Messages} object for a given + * resource bundle name. + * + * @param name the name of the resource bundle + * @return the instance + */ + // TODO: Locale? + public static Messages instance(String name) { + synchronized (map) { + return map.computeIfAbsent(name, n -> new Messages(name)); + } + } + + /** + * Gets an entry from the resource bundle. + * If the resource cannot be found, a message is printed to the console + * and the result will be a string containing the method parameters. + * @param key the name of the entry to be returned + * @param args an array of arguments to be formatted into the result using + * {@link java.text.MessageFormat#format} + * @return the formatted string + */ + public String getString(String key, Object... args) { + try { + return MessageFormat.format(bundle.getString(key), args); + } catch (MissingResourceException e) { + System.err.println("WARNING: missing resource: " + key + " for " + name); + return key + Arrays.toString(args); + } + } + + /** + * Returns the set of keys defined in the resource bundle. + * @return the keys + */ + public Set getKeys() { + return bundle.keySet(); + } + + /** + * Creates a resource bundle for the given name. + * @param name the name of the resource bundle + */ + private Messages(String name) { + this.name = name; + bundle = ResourceBundle.getBundle(name); + } + + private final String name; + private final ResourceBundle bundle; +} + diff --git a/src/share/classes/jdk/codetools/apidiff/Notes.java b/src/share/classes/jdk/codetools/apidiff/Notes.java new file mode 100644 index 0000000..5ce2c95 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/Notes.java @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeParameterElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; +import jdk.codetools.apidiff.model.TypeMirrorKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.ArrayTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.DeclaredTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.PrimitiveTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.TypeVariableKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.WildcardTypeKey; + +/** + * A class to associate URIs and descriptions with elements. + */ +public class Notes { + /** + * A class for an individual note that may be associated with one or more elements. + */ + public static class Entry { + /** + * The name of the entry, that is used to identify the associated elements. + */ + public final String name; + + /** + * The URI for the note. + */ + public final URI uri; + + /** + * The description for the note. + */ + public final String description; + + /** + * Whether the note applies to enclosed elements as well. + */ + public final boolean recursive; + + /** + * Creates a note + * + * @param name the name of the note + * @param uri the URI for the note + * @param description the description for the note + * @param recursive whether the note applies to enclosed elements as well + */ + Entry(String name, URI uri, String description, boolean recursive) { + this.name = name; + this.uri = uri; + this.description = description; + this.recursive = recursive; + } + + @Override + public String toString() { + return "Entry[name=" + name + ",uri=" + uri + ",description=" + description + ",recursive=" + recursive + "]"; + } + } + + private final Map> entries; + + private Notes() { + entries = new HashMap<>(); + } + + /** + * Reads a file containing a description of the notes to be used. + * If there are errors in the content of the file, they will be reported to the log. + * + * @param file the file + * @param log the log + * + * @return the result of reading the file + * @throws IOException if an IO exception occurs while reading the file + */ + public static Notes read(Path file, Log log) throws IOException { + return new Reader(log).read(file); + } + + /** + * Returns the entries for a given element key. + * The entries are returned in a map, that associates a boolean value with each entry, + * which indicates whether the entry is for an enclosing element. + * + * @param k the element key + * @return a map containing the entries for the key. + */ + public Map getEntries(ElementKey k) { + GetEntriesVisitor v = new GetEntriesVisitor(); + return v.getEntries(k); + } + + private class GetEntriesVisitor implements ElementKey.Visitor { + private Map map = new LinkedHashMap<>(); + + Map getEntries(ElementKey k) { + k.accept(this, false); + return map; + } + + @Override + public Void visitModuleElement(ModuleElementKey mKey, Boolean isParent) { + add(nameVisitor.getName(mKey), isParent); + return null; + } + + @Override + public Void visitPackageElement(PackageElementKey pKey, Boolean isParent) { + if (pKey.moduleKey != null) { + pKey.moduleKey.accept(this, true); + } + add(nameVisitor.getName(pKey), isParent); + return null; + } + + @Override + public Void visitTypeElement(TypeElementKey tKey, Boolean isParent) { + if (tKey.enclosingKey != null) { + tKey.enclosingKey.accept(this, true); + } + add(nameVisitor.getName(tKey), isParent); + return null; + } + + @Override + public Void visitExecutableElement(ExecutableElementKey eKey, Boolean isParent) { + add(nameVisitor.getName(eKey), isParent); + return null; + } + + @Override + public Void visitVariableElement(VariableElementKey vKey, Boolean isParent) { + add(nameVisitor.getName(vKey), isParent); + return null; + } + + @Override + public Void visitTypeParameterElement(TypeParameterElementKey tKey, Boolean all) { + throw new IllegalArgumentException(tKey.toString()); + } + + private void add(String name, boolean isParent) { + List l = entries.get(name); + if (l != null) { + if (isParent) { + l.stream().filter(e -> e.recursive).forEach(e -> map.put(e, true)); + } else { + l.forEach(e -> map.put(e, false)); + } + } + } + } + + /** + * Returns the display name for an element key. + * + * @param eKey the element key + * + * @return the display name + */ + public static String getName(ElementKey eKey) { + return nameVisitor.getName(eKey); + } + + private static final NameVisitor nameVisitor = new NameVisitor(); + + private static class NameVisitor implements ElementKey.Visitor, TypeMirrorKey.Visitor { + String getName(ElementKey k) { + return k.accept(this, new StringBuilder()).toString(); + } + + @Override + public StringBuilder visitModuleElement(ModuleElementKey mKey, StringBuilder sb) { + return sb.append(mKey.name); + } + + @Override + public StringBuilder visitPackageElement(PackageElementKey pKey, StringBuilder sb) { + if (pKey.moduleKey != null) { + pKey.moduleKey.accept(this, sb); + sb.append("/"); + } + return sb.append(pKey.name); + } + + @Override + public StringBuilder visitTypeElement(TypeElementKey tKey, StringBuilder sb) { + if (tKey.enclosingKey != null) { + tKey.enclosingKey.accept(this, sb); + sb.append("."); + } + return sb.append(tKey.name); + } + + @Override + public StringBuilder visitExecutableElement(ExecutableElementKey eKey, StringBuilder sb) { + eKey.typeKey.accept(this, sb).append("#").append(eKey.name).append("("); + boolean first = true; + for (TypeMirrorKey t : eKey.params) { + if (first) { + first = false; + } else { + sb.append(","); + } + t.accept(this, sb); + } + return sb.append(")"); + } + + @Override + public StringBuilder visitVariableElement(VariableElementKey vKey, StringBuilder sb) { + vKey.typeKey.accept(this, sb); + return sb.append("#").append(vKey.name); + } + + @Override + public StringBuilder visitTypeParameterElement(TypeParameterElementKey k, StringBuilder sb) { + throw new IllegalArgumentException(k.toString()); + } + + @Override + public StringBuilder visitArrayType(ArrayTypeKey k, StringBuilder sb) { + return k.componentKey.accept(this, sb).append("[]"); + } + + @Override + public StringBuilder visitDeclaredType(DeclaredTypeKey k, StringBuilder sb) { + ElementKey eKey = k.elementKey; + return switch (eKey.kind) { + case TYPE -> sb.append(((TypeElementKey) eKey).name); // simple name only + case TYPE_PARAMETER -> sb.append(((TypeParameterElementKey) eKey).name); // simple name only + default -> throw new IllegalArgumentException((k.toString())); + }; + } + + @Override + public StringBuilder visitPrimitiveType(PrimitiveTypeKey k, StringBuilder sb) { + return sb.append(k.kind.toString().toLowerCase(Locale.ROOT)); + } + + @Override + public StringBuilder visitTypeVariable(TypeVariableKey k, StringBuilder sb) { + return sb.append(k.name); + } + + @Override + public StringBuilder visitWildcardType(WildcardTypeKey k, StringBuilder sb) { + throw new IllegalArgumentException((k.toString())); + } + + } + + private static class Reader { + private final Log log; + private Path file; + + Pattern p = Pattern.compile("\\s*([\\S]+)\\s*(.*?)\\s*"); + Notes notes = new Notes(); + URI uri = null; + String description = null; + int lineNumber = 0; + boolean skipLines = false; + + Reader(Log log) { + this.log = log; + } + + Notes read(Path file) throws IOException { + this.file = file; + + for (String line : Files.readAllLines(file)) { + lineNumber++; + process(line); + } + + return notes; + } + + void process(String line) { + + if (line.startsWith("#") || line.isBlank()) { + return; + } + + Matcher m = p.matcher(line); + if (!m.matches()) { + // should not happen, since we already ignored blank lines + log.error(file, lineNumber, "notes.err.bad-line", line); + return; + } + + String first = m.group(1); + String rest = m.group(2); + + if (first.contains(":")) { + try { + uri = new URI(first); + description = rest; + skipLines = false; + } catch (URISyntaxException e) { + log.error(file, lineNumber, "notes.err.bad-uri", first); + skipLines = true; + } + return; + } + + if (!rest.isEmpty()) { + log.error(file, lineNumber, "notes.err.bad-line", line); + return; + } + + boolean recursive; + if (first.endsWith("/*") || first.endsWith(".*")) { + first = first.substring(0, first.length() - 2); + recursive = true; + } else { + recursive = false; + } + + if (!isValidSignature(first)) { + log.error(file, lineNumber, "notes.err.bad-signature", first); + return; + } + + if (skipLines) { + // uri has been reported as invalid; can't create entry + return; + } + + if (uri == null) { + log.error(file, lineNumber, "notes.err.no-current-uri"); + skipLines = true; + return; + } + + notes.entries.computeIfAbsent(first, __ -> new ArrayList<>()) + .add(new Entry(first, uri, description, recursive)); + } + + boolean isValidSignature(String sig) { + int slash = sig.indexOf("/"); + if (slash != -1) { + if (!isQualifiedIdentifier(sig.substring(0, slash))) { + return false; + } + sig = sig.substring(slash + 1); + if (sig.isEmpty()) { + // signature was module/ + return true; + } + } + + int hash = sig.indexOf("#"); + if (hash == -1) { + // signature was [module/] package-or-type + return isQualifiedIdentifier(sig); + } + + String type = sig.substring(0, hash); + String member = sig.substring(hash + 1); + if (!isQualifiedIdentifier(type)) { + // bad [package .] type + return false; + } + + int lParen = member.indexOf("("); + if (lParen == -1) { + // signature looks like [module/] [package .] type # field + return isIdentifier(member); + } + + // signature looks like [module/] type # method ( param-types ) + String method = member.substring(0, lParen); + String params = member.substring(lParen + 1, member.length() - 1); + return (isIdentifier(method) || method.equals("")) + && (params.isEmpty() + || Stream.of(params.split(",", -1)).allMatch(this::isIdentifier)); + + } + + boolean isQualifiedIdentifier(String name) { + return Stream.of(name.split("\\.", -1)).allMatch(this::isIdentifier); + } + + boolean isIdentifier(String name) { + if (name.isEmpty()) { + return false; + } + + if (!Character.isJavaIdentifierStart(name.charAt(0))) { + return false; + } + + for (int i = 1; i < name.length(); i++) { + if (!Character.isUnicodeIdentifierPart(name.charAt(i))) { + return false; + } + } + + return true; + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/Options.java b/src/share/classes/jdk/codetools/apidiff/Options.java new file mode 100644 index 0000000..ce13a30 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/Options.java @@ -0,0 +1,1184 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff; + +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.lang.model.SourceVersion; + +import jdk.codetools.apidiff.model.AccessKind; + +/** + * The command-line options for the {@code apidiff} program. + */ +public class Options { + + static class BadOption extends Exception { + private static final long serialVersionUID = -1L; + final String key; + final Object[] args; + BadOption(String key, Object... args) { + super(key + Arrays.toString(args)); + this.key = key; + this.args = args; + } + } + + private final Log log; + + /** + * A container for the options to configure an API. + */ + public static class APIOptions { + /** + * The name of the API. + */ + public final String name; + + /** + * A short plain-text label for the API. + */ + public String label; + + /** + * The options to configure a file manager for the API. + */ + public Map> fileManagerOpts = new LinkedHashMap<>(); + + /** + * The value of the {@code --release} option for the API. + */ + public String release; + + /** + * The value of the {@code --source} option for the API. + */ + public String source; + + /** + * Whether or not {@code --enable-preview} has been specified. + */ + public boolean enablePreview; + + /** + * The API directory containing the documentation generated by javadoc. + */ + public Path apiDir; + + /** + * The location of a JDK build, from which to derive a series of options. + */ + public Path jdkBuildDir; + + public APIOptions(String name) { + this.name = name; + } + + void addFileManagerOpt(String opt, String arg) { + fileManagerOpts.computeIfAbsent(opt, _o -> new ArrayList<>()).add(arg); + } + + public String toString() { + return "APIOptions[name:" + name + + ",label:" + label + + ",fmOpts:" + fileManagerOpts + + ",release:" + release + + ",source:" + source + + ",enablePreview:" + enablePreview + + ",apiDir:" + apiDir + + ",jdkBuildDir:" + jdkBuildDir + + "]"; + } + } + + // env options + Map allAPIOptions = new LinkedHashMap<>(); + APIOptions currAPIOptions = null; + boolean apiOption = false; + + // selection/filtering options + List includes = new ArrayList<>(); + List excludes = new ArrayList<>(); + AccessKind access; + Boolean compareDocComments; + Boolean compareApiDescriptions; + Boolean compareApiDescriptionsAsText; + String jdkDocs; + + // output options + Path outDir; + String title; + String description; + Path notes; + Path mainStylesheet; + List extraStylesheets; + List resourceFiles; + boolean showEqual; + + /** + * The position of additional text to be included in the report. + */ + public enum InfoTextKind { + /** + * At the top of each page, above the header bar. + * Intended for warning and status messages. + */ + TOP, + + /** + * Within the main header bar, on the right hand side. + * Intended for a short name for the report. + */ + HEADER, + + /** + * Within the main footer bar, on the right hand side. + * Intended for a short name for the report. + */ + FOOTER, + + /** + * At the bottom of each page, below the footer bar. + * Intended for legal details, such as license and copyright info. + */ + BOTTOM + } + + private Map infoText = new EnumMap<>(InfoTextKind.class); + + /** + * The level for which messages should be generated in "verbose: mode. + */ + public enum VerboseKind { + /** Generate messages about comparing modules. */ + MODULE, + /** Generate messages about comparing modules and packages. */ + PACKAGE, + /** Generate messages about comparing modules, packages, and types. */ + TYPE, + /** Generate messages about different items. */ + DIFFERENCES, + /** Generate messages about missing items. */ + MISSING, + /** Generate messages about the time taken. */ + TIME + } + + private Set verboseKinds = EnumSet.noneOf(VerboseKind.class); + + // meta options + boolean help; + boolean version; + private boolean verbose; + + // hidden options + private Map hidden = new HashMap<>(); + + /** + * The comparison mode, inferred from the `--include` options. + * The mode determines the file manager locations in which to + * look for the elements to be compared. + */ + public enum Mode { + /** + * Elements are in modules, to be found on the aggregate module path. + */ + MODULE, + /** + * Elements are either in the unnamed module, or are in no module, + * to be found on the source path, class path, and so on. + */ + PACKAGE + } + + /** + * A class that defines the set of supported options. + */ + public enum Option { + /** + * {@code --api} name. + * + *

The option should be followed by any API-specific configuration options in the + * arguments that immediately follow this option. + */ + API("--api", "opt.arg.api") { + @Override + void process(String opt, String arg, Options options) { + options.currAPIOptions = options.allAPIOptions.computeIfAbsent(arg, APIOptions::new); + options.apiOption = true; + } + }, + + /** + * {@code --api-directory} directory. + * + *

This is an API-specific option. . + */ + API_DIR("--api-directory", "opt.arg.api-directory") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + Path p = asExistingPath(arg); + options.putAPIOption(opt, a -> a.apiDir = p); + } + }, + + /** + * {@code --class-path} path. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + CLASS_PATH("--class-path", "opt.arg.class-path") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.addFileManagerOpt(opt, arg)); + } + }, + + /** + * {@code --compare-api-descriptions} boolean-value + */ + COMPARE_API_DESCRIPTIONS("--compare-api-descriptions", "opt.arg.boolean") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.compareApiDescriptions = asBoolean(arg); + } + }, + + /** + * {@code --compare-api-descriptions-as-text} boolean-value + */ + COMPARE_API_DESCRIPTIONS_AS_TEXT("--compare-api-descriptions-as-text", "opt.arg.boolean") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.compareApiDescriptionsAsText = asBoolean(arg); + if (options.compareApiDescriptionsAsText) { + options.compareApiDescriptions = true; + } + } + }, + + /** + * {@code --compare-doc-comments} boolean-value + */ + COMPARE_DOC_COMMENTS("--compare-doc-comments", "opt.arg.boolean") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.compareDocComments = asBoolean(arg); + } + }, + + /** + * {@code --enable-preview}. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + ENABLE_PREVIEW("--enable-preview", null) { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.enablePreview = true); + } + }, + + /** + * {@code --label} text. + * + *

This is an API-specific option. . + */ + LABEL("--label", "opt.arg.plain-text") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.label = arg); + } + }, + + /** + * {@code --module-path} path. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + MODULE_PATH("--module-path", "opt.arg.module-path") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.addFileManagerOpt(opt, arg)); + } + }, + + /** + * {@code --module-source-path} pattern-or-module-specific-path. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + MODULE_SOURCE_PATH("--module-source-path", "opt.arg.module-source-path") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.addFileManagerOpt(opt, arg)); + } + }, + + /** + * {@code --module-path} module-specific-path. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + PATCH_MODULE("--patch-module", "opt.arg.patch-module") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.addFileManagerOpt(opt, arg)); + } + }, + + /** + * {@code --release} release. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + RELEASE("--release", "opt.arg.jdk-version") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.release = arg); + } + }, + + /** + * {@code --source} release. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + SOURCE("--source", "opt.arg.jdk-version") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.source = arg); + } + }, + + /** + * {@code --source-path} path. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + SOURCE_PATH("--source-path", "opt.arg.source-path") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.addFileManagerOpt(opt, arg)); + } + }, + + /** + * {@code --system} jdk. + * + *

This is an API-specific option. See the {@code javac} documentation for details. + */ + SYSTEM("--system", "opt.arg.jdk-home") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.putAPIOption(opt, a -> a.addFileManagerOpt(opt, arg)); + } + }, + + /** + * {@code --jdk-build} build-directory + * + * Shorthand for a series of API-specific options that can be derived from the + * location of a JDK build. + */ + JDK_BUILD("--jdk-build", "opt.arg.jdk-build") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + Path jdkBuildDir = asExistingPath(arg); + options.putAPIOption(opt, a -> a.jdkBuildDir = jdkBuildDir); + } + }, + + /** + * {@code --jdk-docs} docs-directory-name + * + * Used with {@code --jdk-build} to specify the name of the docs directory. + */ + JDK_DOCS("--jdk-docs", "opt.arg.jdk-docs") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.jdkDocs = arg; + } + }, + + // Selection options + + /** + * {@code --access} {@code }public|protected|package|private}. + * + *

Specifies the access level of items to be compared. + * The default is {@code protected}. + */ + ACCESS("--access", "opt.arg.access") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.access = switch (arg) { + case "public" -> AccessKind.PUBLIC; + case "protected" -> AccessKind.PROTECTED; + case "package" -> AccessKind.PACKAGE; + case "private" -> AccessKind.PRIVATE; + default -> throw new BadOption("options.err.bad-access", arg); + }; + } + }, + + /** + * {@code --exclude} pattern. + * + *

Specify patterns for elements to be excluded from the comparison. + */ + EXCLUDE("--exclude", "opt.arg.pattern") { + @Override + void process(String opt, String arg, Options options) { + options.excludes.add(arg); + } + }, + + /** + * {@code --include} pattern. + * + *

Specify patterns for elements to be included in the comparison. + */ + INCLUDE("--include", "opt.arg.pattern") { + @Override + void process(String opt, String arg, Options options) { + options.includes.add(arg); + } + }, + + // Output options + + /** + * {@code --output-directory} dir. + * + *

The directory in which to generate the comparison report. + */ + OUTDIR("--output-directory -d", "opt.arg.directory") { + @Override + void process(String opt, String arg, Options options) { + options.outDir = Path.of(arg); + } + }, + + /** + * {@code --title} string. + * + *

A plain-text title for the report. + */ + TITLE("--title", "opt.arg.plain-text") { + @Override + void process(String opt, String arg, Options options) { + options.title = arg; + } + }, + + /** + * {@code --description} html-text. + * + *

A description to be included in the report. + */ + DESCRIPTION("--description", "opt.arg.html-text") { + @Override + void process(String opt, String arg, Options options) { + options.description = arg; + } + }, + + /** + * {@code --info-text} place-list{@code =}html-text. + * + *

Additional text to be included in one or more of the top, header, footer or bottom + * of the page. The option may be given more than once, but at most once for any one + * position. + */ + INFO_TEXT("--info-text", "opt.arg.info-text") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + int eq = arg.indexOf('='); + if (eq == -1) { + throw new BadOption("options.err.invalid.info.text"); + } + String[] keys = arg.substring(0, eq).split(","); + for (String k : keys) { + try { + InfoTextKind kind = InfoTextKind.valueOf(k.toUpperCase(Locale.ROOT)); + options.infoText.put(kind, arg.substring(eq + 1)); + } catch (IllegalArgumentException e) { + throw new BadOption("options.err.invalid-info-text-kind", k); + } + } + } + }, + + /** + * {@code --notes} notes-file + * + *

Details of notes (links) to be associated with various individual elements.

+ */ + NOTES("--notes", "opt.arg.file") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.notes = asExistingPath(arg); + } + }, + + /** + * {@code --main-stylesheet} file + * + *

Specify an alternative default stylesheet. + */ + MAIN_STYLESHEET("--main-stylesheet", "opt.arg.file") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + options.mainStylesheet = asExistingPath(arg); + } + + }, + + /** + * {@code --extra-stylesheet} file + * + *

Specify an additional stylesheet. + */ + EXTRA_STYLESHEET("--extra-stylesheet", "opt.arg.file") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + if (options.extraStylesheets == null) { + options.extraStylesheets = new ArrayList<>(); + } + options.extraStylesheets.add(asExistingPath(arg)); + } + + }, + + /** + * {@code --resource-files} file + * + *

Specify additional resource files to be included in the output. + */ + RESOURCE_FILES("--resource-files", "opt.arg.file-or-directory") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + if (options.resourceFiles == null) { + options.resourceFiles = new ArrayList<>(); + } + options.resourceFiles.add(asPath(arg)); + } + + }, + + + // Help and information options + + /** + * {@code --help}. + * + *

Show command line help. + */ + HELP("--help -h -help -?", null) { + @Override + void process(String opt, String arg, Options options) { + options.help = true; + } + }, + + /** + * {@code --version}. + * + *

Show the version of the program. + */ + VERSION("--version -v", null) { + @Override + void process(String opt, String arg, Options options) { + options.version = true; + } + }, + + /** + * {@code --verbose [flag|-flag|all|none]*}. + * + *

Specify the verbosity of the program. + */ + VERBOSE("--verbose", "opt.arg.verbose") { + @Override + void process(String opt, String arg, Options options) throws BadOption { + for (String a : arg.split(",+")) { + switch (a) { + case "all" -> + options.verboseKinds.addAll(EnumSet.allOf(VerboseKind.class)); + + case "none" -> + options.verboseKinds.clear(); + + default -> { + String name; + boolean clear = false; + if (a.startsWith("-")) { + clear = true; + name = a.substring(1); + } else { + name = a; + } + try { + VerboseKind k = VerboseKind.valueOf(name.toUpperCase(Locale.ROOT)); + if (clear) { + options.verboseKinds.remove(k); + } else { + options.verboseKinds.add(k); + } + } catch (IllegalArgumentException e) { + throw new BadOption("options.err.invalid-arg-for-verbose", a); + } + } + } + } + } + @Override + public String getHelpDescription(Messages msgs, String key) { + String flags = Arrays.stream(VerboseKind.values()) + .map(k -> k.name().toLowerCase(Locale.ROOT)) + .collect(Collectors.joining(" ")); + return msgs.getString(key, flags); + } + }; + + final List names; + final String arg; + + Option(String names, String arg) { + this.names = Arrays.asList(names.split("\\s+")); + this.arg = arg; + } + + abstract void process(String opt, String arg, Options options) throws BadOption; + + /** + * Returns the list of names for the option. + * + * @return the names + */ + public List getNames() { + return names; + } + + /** + * Returns the resource key for the argument accepted by the option, + * or {@code null} if the option does not take an argument. + * + * @return the resource key + */ + public String getArg() { + return arg; + } + + /** + * Returns the description for an option, based on a resource key. + * + * @param msgs the messages to be used to generate the description + * @param key the resource key + * + * @return the description + */ + public String getHelpDescription(Messages msgs, String key) { + return msgs.getString(key); + } + + private static boolean asBoolean(String arg) throws BadOption { + return switch (arg.toLowerCase(Locale.ROOT)) { + case "true", "yes", "on" -> true; + case "false", "no", "off" -> false; + default -> throw new BadOption("options.err.invalid-boolean", arg); + }; + } + } + + Options(Log log, List args) { + // @files should have already been processed + + this.log = log; + + Map map = new HashMap<>(); + for (Option o : Option.values()) { + o.names.forEach(n -> map.put(n, o)); + } + + // TODO: convert to use Iterator or ListIterator + for (int i = 0; i < args.size(); i++) { + String arg = args.get(i); + // currently no support for positional args + String optName, optValue; + int eq = arg.startsWith("--") ? arg.indexOf("=") : -1; + if (eq == -1) { + optName = arg; + optValue = null; + } else { + optName = arg.substring(0, eq); + optValue = arg.substring(eq + 1); + } + if (optName.isEmpty()) { + log.error("options.err.bad.argument", arg); + } else { + Option opt = map.get(optName); + if (opt == null) { + if (optName.startsWith("-XD")) { + setHiddenOption(optName.substring(3)); + } else { + reportBadOption(optName); + } + } else { + apiOption = false; + try { + if (opt.arg == null) { + // no value for option required + if (optValue != null) { + log.error("options.err.unexpected-value-for-option", optName, optValue); + } else { + opt.process(optName, null, this); + } + } else { + // value for option required; use next arg if not found after '=' + if (optValue == null) { + if (i + 1 < args.size()) { + optValue = args.get(++i); + } else { + log.error("options.err.missing-value-for-option", optName); + continue; + } + } + opt.process(optName, optValue, this); + } + } catch (BadOption e) { + log.error(e.key, e.args); + } + // Cancel the "API options mode" when a non-API option is used. + if (!apiOption) { + currAPIOptions = null; + } + } + } + } + } + + private void reportBadOption(String name) { + log.error("options.err.unknown-option", name); + + var allOptionNames = Stream.of(Option.values()) + .flatMap(o -> o.getNames().stream()); + record Pair(String word, double similarity) { } + final double MIN_SIMILARITY = 0.7; + var suggestions = allOptionNames + .map(t -> new Pair(t, similarity(t, name))) + .sorted(Comparator.comparingDouble(Pair::similarity).reversed() /* more similar first */) + // .peek(p -> System.out.printf("%.3f, (%s ~ %s)%n", p.similarity, p.word, name)) // debug + .takeWhile(p -> Double.compare(p.similarity, MIN_SIMILARITY) >= 0) + .map(Pair::word) + .toList(); + switch (suggestions.size()) { + case 0 -> { } + case 1 -> log.report("options.did-you-mean", suggestions.get(0)); + default -> log.report("options.did-you-mean-one-of", String.join(" ", suggestions)); + } + log.report("options.for-more-details-see-usage"); + } + + // a value in [0, 1] range: the closer the value is to 1, the more similar + // the strings are + private static double similarity(String a, String b) { + // Normalize the distance so that similarity between "x" and "y" is + // less than that of "ax" and "ay". Use the greater of two lengths + // as normalizer, as it's an upper bound for the distance. + return 1.0 - ((double) DamerauLevenshteinDistance.of(a, b)) + / Math.max(a.length(), b.length()); + } + + /** + * Returns the collection of API options. + * + * @return the collection of API options + */ + public Map getAllAPIOptions() { + return allAPIOptions; + } + + /** + * Returns the mode for the comparison. + * + * @return the mode for the comparison + */ + public Mode getMode() { + return includes.isEmpty() + ? null + : includes.get(0).contains("/") ? Mode.MODULE : Mode.PACKAGE; + } + + /** + * Returns the access kind to be used to select items to be compared. + * If a value was not specified in the options provided to the constructor, + * a default value of {@code PROTECTED} is returned. + * + * @return the access + */ + public AccessKind getAccessKind() { + return access != null ? access : AccessKind.PROTECTED; + } + + /** + * Returns whether API descriptions should be compared. + * + * @return {@code true} if API descriptions should be compared + */ + public boolean compareApiDescriptions() { + return compareApiDescriptions; + } + + /** + * Returns whether API descriptions should be compared as plain text. + * + * @return {@code true} if API descriptions should be compared as plain text + */ + public boolean compareApiDescriptionsAsText() { + return compareApiDescriptionsAsText; + } + + /** + * Returns whether documentation comments should be compared. + * + * @return {@code true} if documentation comments should be compared + */ + public boolean compareDocComments() { + return compareDocComments; + } + + /** + * Returns the output directory to be used. + * + * @return the output directory + */ + public Path getOutDir() { + return outDir; + } + + /** + * Returns the title for the report. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * Returns the description for the report. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the additional text for the report. + * @param kind the kind of the text + * @return the text + */ + public String getInfoText(InfoTextKind kind) { + return infoText.get(kind); + } + + /** + * Returns the user-specified main stylesheet for the report. + * + * @return the stylesheet + */ + public Path getMainStylesheet() { + return mainStylesheet; + } + + /** + * Returns any user-specified additional stylesheets for the report. + * + * @return the stylesheets + */ + public List getExtraStylesheets() { + return (extraStylesheets == null) ? Collections.emptyList() : extraStylesheets; + } + + /** + * Returns any user-specified additional stylesheets for the report. + * + * @return the stylesheets + */ + public List getResourceFiles() { + return (resourceFiles == null) ? Collections.emptyList() : resourceFiles; + } + + public boolean showEqual() { + return showEqual; + } + + /** + * Returns the value of a "hidden" option, set by {@code -XD} or + * {@code -XD=}. + * + * @param name the name of the hidden option + * @return the value of the option, or null if not set + */ + public String getHiddenOption(String name) { + return hidden.get(name); + } + + /** + * Returns whether a kind of verbose mode is enabled. + * @param k the kind of verbose mode + * @return {@code true} if a kind of verbose mode is enabled + */ + public boolean isVerbose(VerboseKind k) { + return verboseKinds.contains(k); + } + + void putAPIOption(String optName, Consumer f) throws BadOption { + if (currAPIOptions == null) { + throw new BadOption("options.err.no-api-for-option", optName); + } + apiOption = true; + f.accept(currAPIOptions); + } + + void setHiddenOption(String s) { + int eq = s.indexOf('='); + if (eq == -1) { + hidden.put(s, s); + } else { + hidden.put(s.substring(0, eq), s.substring(eq + 1)); + } + } + + static Path asPath(String p) throws BadOption { + try { + return Path.of(p); + } catch (InvalidPathException e) { + throw new BadOption("options.err.bad-file", p); + } + } + + static Path asExistingPath(String p) throws BadOption { + try { + Path path = Path.of(p); + if (!Files.exists(path)) { + throw new BadOption("options.err.file-not-found", p); + } + return path; + } catch (InvalidPathException e) { + throw new BadOption("options.err.bad-file", p); + } + } + + void validate() { +// // fixme? +// // check not package and module paths for different APIs +// // check not mixed modes for different APIs +// + // Check at least one includes option + if (includes.isEmpty()) { + log.error("options.err.no-include-options"); + } else { + Mode mode = getMode(); + includes.forEach(s -> checkPattern(mode, s)); + excludes.forEach(s -> checkPattern(mode, s)); + } + + allAPIOptions.values().forEach(this::checkAPIOptions); + if (log.errorCount() > 0) { + return; + } + + boolean allApiHaveApiDir = allAPIOptions.values().stream().allMatch(a -> a.apiDir != null); + if (compareApiDescriptions == null) { + // if not specified explicitly, compare API descriptions if not comparing doc comments + // and if --api-directory is specified for all API instances + compareApiDescriptions = (compareDocComments != Boolean.TRUE) && allApiHaveApiDir; + } else { + // if specified, check --api-directory is specified for all API instances + if (compareApiDescriptions && !allApiHaveApiDir) { + log.error("options.err.compare-api-but-missing-dir"); + } + } + + if (compareApiDescriptionsAsText == null) { + compareApiDescriptionsAsText = false; + } + + if (compareDocComments == null) { + // if not specified explicitly, compare doc comments if not comparing API descriptions + compareDocComments = !compareApiDescriptions; + } + + if (resourceFiles != null) { + for (var resFile : resourceFiles) { + if (resFile.isAbsolute() && !Files.exists(resFile)) { + // if it is an absolute path and doesn't exist, + // report error, no need to check further + log.error("options.err.resource-file-not-found", resFile); + } else { + // otherwise check that the file is in at least one api directory + boolean found = false; + for (var apiOpts : allAPIOptions.values()) { + var apiDir = apiOpts.apiDir; + if (apiDir != null) { + var absApiDir = apiDir.toAbsolutePath(); + if (resFile.isAbsolute() && resFile.startsWith(absApiDir) + || Files.exists(absApiDir.resolve(resFile))) { + found = true; + break; + } + } + } + if (!found) { + log.error("options.err.resource-file-not-found-in-api-dirs", resFile); + } + } + } + } + } + + private void checkAPIOptions(APIOptions apiOptions) { + if (apiOptions.jdkBuildDir != null) { + Path dir = apiOptions.jdkBuildDir; + if (!Files.exists(dir.resolve("spec.gmk"))) { + log.error("options.err.bad-jdk-build-dir", apiOptions.name, dir); + } else if (!Files.exists(dir.resolve("images"))) { + log.error("options.err.no-images-in-jdk-build-dir", apiOptions.name, dir); + } else { + JDKBuildOption o = new JDKBuildOption(apiOptions.jdkBuildDir); + o.expand(this, apiOptions, log); + } + } + } + + private void checkPattern(Mode mode, String arg) { + String s = arg; + int slash = s.indexOf("/"); + if (slash == -1) { + if (mode == Mode.MODULE) { + log.error("options.err.missing-module-name", arg); + } + } else { + String mdl = s.substring(0, slash); + if (mdl.endsWith(".*")) { + mdl = mdl.substring(0, mdl.length() - 2); + } + if (mdl.isEmpty()) { + log.error("options.err.empty-module-name", arg); + } else if (!SourceVersion.isName(mdl, SourceVersion.latestSupported())) { + log.error("options.err.bad-module-name", arg); + } else if (mode == Mode.PACKAGE) { + log.error("options.err.unexpected-module-name", arg); + } + s = s.substring(slash + 1); + } + + if (s.equals("**")) { + if (mode == Mode.PACKAGE) { + log.error("options.err.bad-package-name", arg); + } + } else { + String pkg; + if (s.endsWith(".*")) { + pkg = s.substring(0, s.length() - 2); + } else if (s.endsWith(".**")) { + pkg = s.substring(0, s.length() - 3); + } else { + pkg = s; + } + if (pkg.isEmpty()) { + if (mode == Mode.PACKAGE) { + log.error("options.err.empty-package-name", arg); + } + } else if (!SourceVersion.isName(pkg, SourceVersion.latestSupported())) { + log.error("options.err.bad-package-name", arg); + } + } + } + + private SourceVersion getSourceVersion(String v) { + switch (v) { + case "1.6", "1.7", "1.8", "1.9", "1.10" -> v = v.substring(2); + } + try { + return SourceVersion.valueOf("RELEASE_" + v); + } catch (IllegalArgumentException e) { + log.error("options.err.bad-source", v); + return SourceVersion.latest(); + } + } + + void showHelp() { + Messages messages = Messages.instance("jdk.codetools.apidiff.resources.help"); + PrintWriter out = log.out; + String header = messages.getString("options.usage.header"); + header.lines().forEach(out::println); + for (Option o: Option.values()) { + String argText = o.arg == null ? null : messages.getString(o.arg); + String first = ""; + for (String name : o.names) { + if (first.isEmpty()) { + first = name; + } else { + out.print(", "); + } + out.print(" " + name); + if (argText != null) { + log.out.print(" "); + log.out.print(argText); + } + } + out.println(); + String helpTextKey = "opt.desc." + first.replaceAll("^-+", "") + .replaceAll("(?i)[^A-Z0-9-]+", "."); + String helpText = o.getHelpDescription(messages, helpTextKey); + helpText.lines().forEach(l -> out.println(" " + l)); + } + } + +} diff --git a/src/share/classes/jdk/codetools/apidiff/Version.java b/src/share/classes/jdk/codetools/apidiff/Version.java new file mode 100644 index 0000000..c1d47c6 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/Version.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2006, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.codetools.apidiff; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Properties; + +/** + * A class to access version info from the manifest info in a jar file. + */ +public class Version { + /** + * Returns the current version. + * @return the current version + */ + public static Version getCurrent() { + if (currentVersion == null) + currentVersion = new Version(); + return currentVersion; + } + + private static Version currentVersion; + + /** The name of the product. */ + public final String product; + /** The version string. */ + public final String version; + /** The milestone. */ + public final String milestone; + /** The build number. */ + public final String build; + /** The version of Java used to build the jar file. */ + public final String buildJavaVersion; + /** The date on which the jar file was built. */ + public final String buildDate; + + private Version() { + Properties manifest = getManifestForClass(getClass()); + if (manifest == null) + manifest = new Properties(); + + String prefix = "apidiff"; + product = manifest.getProperty(prefix + "-Name"); + version = manifest.getProperty(prefix + "-Version"); + milestone = manifest.getProperty(prefix + "-Milestone"); + build = manifest.getProperty(prefix + "-Build"); + buildJavaVersion = manifest.getProperty(prefix + "-BuildJavaVersion"); + buildDate = manifest.getProperty(prefix + "-BuildDate"); + } + + void show(PrintWriter out) { + String thisJavaHome = System.getProperty("java.home"); + String thisJavaVersion = System.getProperty("java.version"); + + File classPathFile = getClassPathFileForClass(Main.class); + String unknown = messages.getString("version.msg.unknown"); + String classPath = (classPathFile == null ? unknown : classPathFile.getPath()); + + Object[] versionArgs = { + product, + version, + milestone, + build, + classPath, + thisJavaVersion, + thisJavaHome, + buildJavaVersion, + buildDate + }; + + /* + * Example format string: + * + * {0}, version {1} {2} {3} + * Installed in {4} + * Running on platform version {5} from {6}. + * Built with {7} on {8}. + * + * Example output: + * + * apidiff, version 1.0 dev b00 + * Installed in /usr/local/apidiff/lib/apidiff.jar + * Running on platform version 1.8 from /opt/jdk/1.8.0. + * Built with 1.8 on 09/11/2006 07:52 PM. + */ + + out.println(messages.getString("version.msg.info", versionArgs)); + } + + private Properties getManifestForClass(Class c) { + URL classPathEntry = getClassPathEntryForClass(c); + if (classPathEntry == null) + return null; + + try { + Enumeration e = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF"); + while (e.hasMoreElements()) { + URL url = e.nextElement(); + if (url.getProtocol().equals("jar")) { + String path = url.getPath(); + int sep = path.lastIndexOf("!"); + URL u = new URL(path.substring(0, sep)); + if (u.equals(classPathEntry )) { + Properties p = new Properties(); + try (InputStream in = url.openStream()) { + p.load(in); + } + return p; + } + } + } + } catch (IOException ignore) { + } + return null; + } + + private URL getClassPathEntryForClass(Class c) { + try { + URL url = c.getResource("/" + c.getName().replace('.', '/') + ".class"); + if (url != null && url.getProtocol().equals("jar")) { + String path = url.getPath(); + int sep = path.lastIndexOf("!"); + return new URL(path.substring(0, sep)); + } + } catch (MalformedURLException ignore) { + } + return null; + } + + private File getClassPathFileForClass(Class c) { + URL url = getClassPathEntryForClass(c); + if (url.getProtocol().equals("file")) + return new File(url.getPath()); + return null; + } + + private final Messages messages = Messages.instance("jdk.codetools.apidiff.resources.log"); + +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/Content.java b/src/share/classes/jdk/codetools/apidiff/html/Content.java new file mode 100644 index 0000000..a5e5a22 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/Content.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.io.IOException; +import java.io.Writer; + +/** + * Superclass for all items in an HTML tree. + */ +public abstract class Content { + /** + * An empty item, which does not generate any output when written to a stream. + */ + public static final Content empty = new Content() { + @Override + protected void write(Writer out) throws IOException { } + }; + + /** + * Writes this object as a fragment of HTML. + * + * @param out the writer + * + * @throws IOException if an IO exception occurs + */ + protected abstract void write(Writer out) throws IOException; + + /** + * Writes a string, escaping characters {@code <}, {@code >}, {@code &}. + * + * @param out the writer + * @param s the string + * + * @throws IOException if an IO exception occurs + */ + protected void writeEscaped(Writer out, String s) throws IOException { + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + switch (ch) { + case '<' -> out.write("<"); + case '>' -> out.write(">"); + case '&' -> out.write("&"); + default -> out.write(ch); + } + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/Entity.java b/src/share/classes/jdk/codetools/apidiff/html/Entity.java new file mode 100644 index 0000000..9bce85d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/Entity.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.io.IOException; +import java.io.Writer; + +/** + * An HTML entity. + * + * @see Unicode + * @see FileFormat.info Unicode + */ +public class Entity extends Content { + /** Unicode CHECK MARK. */ + public static final Entity CHECK = new Entity("check", 0x2713); + /** Unicode CIRCLED DIGIT ONE. */ + public static final Entity CIRCLED_DIGIT_ONE = new Entity(null, 0x2460); + /** Unicode DINGBAT NEGATIVE CIRCLED DIGIT ONE. */ + public static final Entity NEGATIVE_CIRCLED_DIGIT_ONE = new Entity(null, 0x2776); + /** Unicode BALLOT X. */ + public static final Entity CROSS = new Entity("cross", 0x2717); + /** Unicode EQUALS SIGN. */ + public static final Entity EQUALS = new Entity("equals", 0x3d); + /** Unicode NOT EQUAL TO. */ + public static final Entity NE = new Entity("ne", 0x2260); + /** Unicode NO-BREAK SPACE. */ + public static final Entity NBSP = new Entity("nbsp", 0xa0); + + private static final boolean useNumericEntities = Boolean.getBoolean("useNumericEntities"); + + private final String name; + private final int value; + + private Entity(String name, int value) { + this.name = name; + this.value = value; + } + + @Override + protected void write(Writer out) throws IOException { + out.write("&"); + out.write(name == null || useNumericEntities ? String.format("#x%x", value) : name); + out.write(";"); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/HtmlAttr.java b/src/share/classes/jdk/codetools/apidiff/html/HtmlAttr.java new file mode 100644 index 0000000..c51bba7 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/HtmlAttr.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.util.Locale; + +/** + * HTML attributes for {@code HtmlTree} nodes. + * + * This should be a superset of the attributes that may be generated by the standard doclet. + * However, we still need to be able to cope with unknown attributes, such as may be found + * in user doc-comments. + */ +public enum HtmlAttr { + ALT, + ARIA_CONTROLS("aria-controls"), + ARIA_EXPANDED("aria-expanded"), + ARIA_LABEL("aria-label"), + ARIA_LABELLEDBY("aria-labelledby"), + ARIA_ORIENTATION("aria-orientation"), + ARIA_SELECTED("aria-selected"), + CHANGETYPE, + CHARSET, + CHECKED, + CLASS, + CLEAR, + COLS, + COLSPAN, + CONTENT, + DATA_COPIED("data-copied"), // custom HTML5 data attribute + DISABLED, + DOWNLOAD, + FOR, + HEADERS, + HEIGHT, + HREF, + HTTP_EQUIV("http-equiv"), + ID, + LANG, + NAME, + ONCLICK, + ONKEYDOWN, + ONLOAD, + PLACEHOLDER, + REL, + ROLE, + ROWS, + ROWSPAN, + SCOPE, + SCROLLING, + SIZES, + SRC, + STYLE, + SUMMARY, + TABINDEX, + TARGET, + TITLE, + TYPE, + VALUE, + WIDTH; + + private final String value; + + HtmlAttr() { + this.value = name().toLowerCase(Locale.ROOT); + } + + HtmlAttr(String name) { + this.value = name; + } + + public String toString() { + return value; + } + + public static HtmlAttr of(String name) { + return valueOf(name.toUpperCase(Locale.ROOT).replace("-", "_")); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/HtmlTree.java b/src/share/classes/jdk/codetools/apidiff/html/HtmlTree.java new file mode 100644 index 0000000..2e4e5a9 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/HtmlTree.java @@ -0,0 +1,588 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.io.IOException; +import java.io.Writer; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * An HTML element. + */ +public final class HtmlTree extends Content { + + private static final boolean coalesceText = Boolean.getBoolean("coalesceText"); + + + //----------------------------------------------------- + // + // Head items + + /** + * Creates a {@code } with a given charset and title. + * + * @param charset the charset + * @param title the title + * @return the {@code } element + */ + public static HtmlTree HEAD(String charset, String title) { + return new HtmlTree(TagName.HEAD) + .add(new HtmlTree(TagName.META) + .set(HtmlAttr.CHARSET, charset)) + .add(new HtmlTree(TagName.TITLE).add(title)); + } + + /** + * Creates a {@code } with a given {@code href} attribute. + * + * @param href the value for the {@code href} attribute + * @return the {@code } element + */ + public static HtmlTree BASE(String href) { + return new HtmlTree(TagName.BASE) + .set(HtmlAttr.HREF, encodeURL(href)); + } + + /** + * Creates a {@code } with a given {@code rel} and {@code href} attributes. + * + * @param rel the value for the {@code rel} attribute + * @param href the value for the {@code href} attribute + * @return the {@code } element + */ + public static HtmlTree LINK(String rel, String href) { + return new HtmlTree(TagName.LINK) + .set(HtmlAttr.REL, rel) + .set(HtmlAttr.TYPE, MediaType.forPath(href).contentType) + .set(HtmlAttr.HREF, encodeURL(href)); + } + + /** + * Creates a {@code } for an icon for the page. + * + * @param iconURI the value for the {@code href} attribute + * @return the {@code } element + */ + public static HtmlTree LINK_ICON(String iconURI) { + // see https://stackoverflow.com/questions/48956465/favicon-standard-2018-svg-ico-png-and-dimensions + // for suggestion that IE expects "shortcut icon" + HtmlTree link = LINK("icon", iconURI); + Pattern p = Pattern.compile("[0-9]+x[0-9]+"); + Matcher m = p.matcher(iconURI); + if (m.find()) { + link.set(HtmlAttr.SIZES, m.group(0)); + } + return link; + } + + /** + * Creates a {@code } for a stylesheet for the page. + * + * @param stylesheetURI the value for the {@code href} attribute + * @return the {@code } element + */ + public static HtmlTree LINK_STYLESHEET(String stylesheetURI) { + return LINK("stylesheet", stylesheetURI); + } + + /** + * Creates a {@code } element with given name and content attributes. + * + * @param name the value for the {@code name} attribute + * @param content the value for the {@code content} attribute + * @return the {@code } element + */ + public static HtmlTree META(String name, String content) { + return new HtmlTree(TagName.META) + .set(HtmlAttr.NAME, name) + .set(HtmlAttr.CONTENT, content); + } + + /** + * Creates a {@code } element for a default viewport. + * + * @return the {@code } element + */ + public static HtmlTree META_VIEWPORT() { + return META_VIEWPORT("width=device-width, initial-scale=1.0"); + } + + /** + * Creates a {@code } element for a specific viewport. + * + * @param content the value for the {@code content} attribute + * @return the {@code } element + */ + public static HtmlTree META_VIEWPORT(String content) { + return META("viewport", content); + } + + //----------------------------------------------------- + // + // Body, regions, div + + + public static HtmlTree BODY() { + return new HtmlTree(TagName.BODY); + } + + public static HtmlTree BODY(List contents) { + return new HtmlTree(TagName.BODY, contents); + } + + public static HtmlTree HEADER(Content... contents) { + return new HtmlTree(TagName.HEADER, contents); + } + + public static HtmlTree MAIN(Content... contents) { + return new HtmlTree(TagName.MAIN, contents); + } + + public static HtmlTree NAV(Content... contents) { + return new HtmlTree(TagName.NAV, contents); + } + + public static HtmlTree FOOTER(Content... contents) { + return new HtmlTree(TagName.FOOTER, contents); + } + + public static HtmlTree DIV(Content... contents) { + return new HtmlTree(TagName.DIV, contents); + } + + public static HtmlTree DIV(List contents) { + return new HtmlTree(TagName.DIV, contents); + } + public static HtmlTree SECTION(Content... contents) { + return new HtmlTree(TagName.SECTION, contents); + } + + public static HtmlTree SECTION(List contents) { + return new HtmlTree(TagName.SECTION, contents); + } + + public static HtmlTree PRE(Content... contents) { + return new HtmlTree(TagName.PRE, contents); + } + + public static HtmlTree PRE(List contents) { + return new HtmlTree(TagName.PRE, contents); + } + + public static HtmlTree DETAILS(Content... contents) { + return new HtmlTree(TagName.DETAILS, contents); + } + + public static HtmlTree SUMMARY(Content... contents) { + return new HtmlTree(TagName.SUMMARY, contents); + } + + //----------------------------------------------------- + // + // Headers + + public static HtmlTree H1(Content... contents) { + return new HtmlTree(TagName.H1, contents); + } + + public static HtmlTree H2(Content... contents) { + return new HtmlTree(TagName.H2, contents); + } + + public static HtmlTree H3(Content... contents) { + return new HtmlTree(TagName.H3, contents); + } + + //----------------------------------------------------- + // + // Table items + + public static HtmlTree TABLE(Content... contents) { + return new HtmlTree(TagName.TABLE, contents); + } + + public static HtmlTree CAPTION(Content... contents) { + return new HtmlTree(TagName.CAPTION, contents); + } + + public static HtmlTree THEAD(Content... contents) { + return new HtmlTree(TagName.THEAD, contents); + } + + public static HtmlTree TBODY(Content... contents) { + return new HtmlTree(TagName.TBODY, contents); + } + + public static HtmlTree TFOOT(Content... contents) { + return new HtmlTree(TagName.TFOOT, contents); + } + + public static HtmlTree TR(Content... contents) { + return new HtmlTree(TagName.TR, contents); + } + + public static HtmlTree TH(Content... contents) { + return new HtmlTree(TagName.TH, contents); + } + + public static HtmlTree TD(Content... contents) { + return new HtmlTree(TagName.TD, contents); + } + + //----------------------------------------------------- + // + // List items + + public static HtmlTree UL(Content... contents) { + return new HtmlTree(TagName.UL, contents); + } + + public static HtmlTree UL(List contents) { + return new HtmlTree(TagName.UL, contents); + } + + public static HtmlTree OL(Content... contents) { + return new HtmlTree(TagName.OL, contents); + } + + public static HtmlTree LI(Content... contents) { + return new HtmlTree(TagName.LI, contents); + } + + public static HtmlTree LI(List contents) { + return new HtmlTree(TagName.LI, contents); + } + + public static HtmlTree DL(Content... contents) { + return new HtmlTree(TagName.DL, contents); + } + + public static HtmlTree DT(Content... contents) { + return new HtmlTree(TagName.DT, contents); + } + + public static HtmlTree DD(Content... contents) { + return new HtmlTree(TagName.DD, contents); + } + + //----------------------------------------------------- + // + // Basic text items + + public static HtmlTree A(String href, Content... contents) { + return new HtmlTree(TagName.A) + .set(HtmlAttr.HREF, encodeURL(href)) + .add(contents); + } + + public static HtmlTree A(URI href, Content... contents) { + return A(href.toString(), contents); + } + + public static HtmlTree B(Content... contents) { + return new HtmlTree(TagName.B, contents); + } + + public static HtmlTree P(Content... contents) { + return new HtmlTree(TagName.P, contents); + } + + public static HtmlTree SPAN(Content... contents) { + return new HtmlTree(TagName.SPAN, contents); + } + + public static HtmlTree SPAN(List contents) { + return new HtmlTree(TagName.SPAN, contents); + } + + //----------------------------------------------------- + // + // An HTMLTree is a tag name, a collection of attributes and a sequence of contents. + // The tag name is normally represented by a TagName, but we need to model the + // HTML being compared in HtmlDiffBuilder, which may use tag names that are not + // present in the TagName enum. Likewise, the attribute names are normally + // represented by HtmlAttr, but we need to model attributes that are not present + // in the enum. Therefore, both the tag name and attribute name are represented + // by an Object, which is either the relevant enum, or a normalized (upper-case) + // string. + + private final Object tag; // TagName or String + private final Map attrs; // key is HtmlAttr or String + private final List contents; + + public HtmlTree(TagName tag) { + this.tag = tag; + attrs = new LinkedHashMap<>(); + contents = new ArrayList<>(); + } + + public HtmlTree(String tag) { + this.tag = tag.toUpperCase(Locale.ROOT); + attrs = new LinkedHashMap<>(); + contents = new ArrayList<>(); + } + + public HtmlTree(TagName tag, Content... contents) { + this(tag); + add(contents); + } + + public HtmlTree(TagName tag, List contents) { + this(tag); + add(contents); + } + + public HtmlTree add(Content content) { + if (coalesceText && !contents.isEmpty()) { + if (content instanceof Text) { + return add(((Text) content).s); + } else { + Content c = contents.get(contents.size() - 1); + if (c instanceof TextBuilder) { + ((TextBuilder) c).trimToSize(); + } + } + } + contents.add(content); + return this; + } + + public HtmlTree add(Content... contents) { + List.of(contents).forEach(this::add); + return this; + } + + public HtmlTree add(List contents) { + contents.forEach(this::add); + return this; + } + + public HtmlTree add(Stream contents) { + contents.forEach(this::add); + return this; + } + + public HtmlTree add(CharSequence text) { + if (text.length() == 0) { + return this; + } + + if (coalesceText && !contents.isEmpty()) { + Content c = contents.get(contents.size() - 1); + if (c instanceof TextBuilder) { + ((TextBuilder) c).append(text); + return this; + } else if (c instanceof Text) { + TextBuilder mt = new TextBuilder(((Text) c).s).append(text); + contents.set(contents.size() - 1, mt); + return this; + } + } + + contents.add(new Text(text)); + return this; + } + + public String getTagString() { + return (tag instanceof TagName) ? ((TagName) tag).name() : ((String) tag); + } + + public boolean hasTag(TagName t) { + return tag == t; + } + + public boolean hasTag(String t) { + return getTagString().equalsIgnoreCase(t); + } + + public String get(HtmlAttr attr) { + return attrs.get(attr); + } + + public HtmlTree set(HtmlAttr attr, String value) { + attrs.put(attr, value); + return this; + } + + public HtmlTree set(String unknownAttr, String value) { + attrs.put(unknownAttr, value); + return this; + } + + + public HtmlTree setId(String id) { + return set(HtmlAttr.ID, id); + } + + public HtmlTree setClass(String id) { + return set(HtmlAttr.CLASS, id); + } + + public HtmlTree setTitle(String text) { + return set(HtmlAttr.TITLE, text); + } + + public List contents() { + return contents; + } + + @Override + public void write(Writer out) throws IOException { + if (tag == TagName.HTML) { + out.write("\n"); + } + out.write("<"); + out.write(toLowerCase(tag)); + for (Map.Entry e : attrs.entrySet()) { + out.write(" "); + out.write(toLowerCase(e.getKey())); + if (e.getValue() != null) { + out.write("=\""); + writeEscaped(out, e.getValue()); // should also escape " as " + out.write("\""); + } + } + out.write(">"); + for (Content c : contents) { + c.write(out); + } + + if (tag instanceof TagName) { + var tn = (TagName) tag; + switch (tn) { + case BASE: + case LINK: + case META: + case BR: + case HR: + break; + default: + out.write(""); + } + + switch (tn) { + case A: + case B: + case I: + case LI: + case SPAN: + break; + default: + out.write("\n"); + } + } else { + out.write(""); + } + } + + private String toLowerCase(Object o) { + return (o instanceof Enum ? ((Enum) o).name() : o.toString()).toLowerCase(Locale.ROOT); + } + + + + /* + * The sets of ASCII URI characters to be left unencoded. + * See "Uniform Resource Identifier (URI): Generic Syntax" + * IETF RFC 3986. https://tools.ietf.org/html/rfc3986 + */ + public static final BitSet MAIN_CHARS; + public static final BitSet QUERY_FRAGMENT_CHARS; + + static { + BitSet alphaDigit = bitSet(bitSet('A', 'Z'), bitSet('a', 'z'), bitSet('0', '9')); + BitSet unreserved = bitSet(alphaDigit, bitSet("-._~")); + BitSet genDelims = bitSet(":/?#[]@"); + BitSet subDelims = bitSet("!$&'()*+,;="); + MAIN_CHARS = bitSet(unreserved, genDelims, subDelims); + BitSet pchar = bitSet(unreserved, subDelims, bitSet(":@")); + QUERY_FRAGMENT_CHARS = bitSet(pchar, bitSet("/?")); + } + + private static BitSet bitSet(String s) { + BitSet result = new BitSet(); + for (int i = 0; i < s.length(); i++) { + result.set(s.charAt(i)); + } + return result; + } + + private static BitSet bitSet(char from, char to) { + BitSet result = new BitSet(); + result.set(from, to + 1); + return result; + } + + private static BitSet bitSet(BitSet... sets) { + BitSet result = new BitSet(); + for (BitSet set : sets) { + result.or(set); + } + return result; + } + + /** + * Apply percent-encoding to a URL. + * This is similar to {@link java.net.URLEncoder} but + * is less aggressive about encoding some characters, + * like '(', ')', ',' which are used in the anchor + * names for Java methods in HTML5 mode. + * + * @param url the url to be percent-encoded. + * @return a percent-encoded string. + */ + public static String encodeURL(String url) { + BitSet nonEncodingChars = MAIN_CHARS; + StringBuilder sb = new StringBuilder(); + for (byte c : url.getBytes(StandardCharsets.UTF_8)) { + if (c == '?' || c == '#') { + sb.append((char) c); + // switch to the more restrictive set inside + // the query and/or fragment + nonEncodingChars = QUERY_FRAGMENT_CHARS; + } else if (nonEncodingChars.get(c & 0xFF)) { + sb.append((char) c); + } else { + sb.append(String.format("%%%02X", c & 0xFF)); + } + } + return sb.toString(); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/MediaType.java b/src/share/classes/jdk/codetools/apidiff/html/MediaType.java new file mode 100644 index 0000000..74a7e3a --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/MediaType.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +/** + * A media-type, for use in {@code } nodes. + */ + public enum MediaType { + IMAGE_GIF("image/gif"), + // see https://stackoverflow.com/questions/13827325/correct-mime-type-for-favicon-ico + IMAGE_ICON("image/vnd.microsoft.icon"), + IMAGE_JPEG("image/jpeg"), + IMAGE_PNG("image/png"), + TEXT_CSS("text/css; charset=UTF-8"), + TEXT_HTML("text/html; charset=UTF-8"), + TEXT_PLAIN("text/plain; charset=UTF-8"); + + public final String contentType; + + MediaType(String contentType) { + this.contentType = contentType; + } + + /** + * Infers the media type for a file, based on the filename extension. + * + * @param path the path for the file + * + * @return the media type + */ + public static MediaType forPath(String path){ + if (path.endsWith(".css")) { + return MediaType.TEXT_CSS; + } else if (path.endsWith(".html")) { + return MediaType.TEXT_HTML; + } else if (path.endsWith(".txt")) { + return MediaType.TEXT_PLAIN; + } else if (path.endsWith(".png")) { + return MediaType.IMAGE_PNG; + } else if (path.endsWith(".jpg")) { + return MediaType.IMAGE_JPEG; + } else if (path.endsWith(".ico")) { + return MediaType.IMAGE_ICON; + } else if (path.endsWith(".gif")) { + return MediaType.IMAGE_GIF; + } else { + throw new IllegalArgumentException(path); + } + } + +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/RawHtml.java b/src/share/classes/jdk/codetools/apidiff/html/RawHtml.java new file mode 100644 index 0000000..4eb26d7 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/RawHtml.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.io.IOException; +import java.io.Writer; + +/** + * A fragment of raw HTML. + */ +public class RawHtml extends Content { + private final String html; + + /** + * Creates an object containing raw HTML. No additional escaping is provided, + * @param html the HTML + */ + public RawHtml(String html) { + this.html = html; + } + + @Override + protected void write(Writer out) throws IOException { + out.write(html); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/TagName.java b/src/share/classes/jdk/codetools/apidiff/html/TagName.java new file mode 100644 index 0000000..0eb2b2d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/TagName.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.util.Locale; + +/** + * Tags for {@code HtmlTree} nodes. + * + * This should be a superset of the tags that may be generated by the standard doclet. + * However, we still need to be able to cope with unknown tags, such as may be found + * in user doc-comments. + */ +public enum TagName { + A, + B, + BASE, + BLOCKQUOTE, + BODY, + BR, + BUTTON, + CAPTION, + CITE, + CODE, + COL, + COLGROUP, + DD, + DETAILS, + DFN, + DIV, + DL, + DT, + EM, + FOOTER, + FORM, + H1, H2, H3, H4, H5, H6, + HEAD, + HEADER, + HR, + HTML, + I, + IMG, + INPUT, + LABEL, + LI, + LISTING, + LINK, + MAIN, + MENU, + META, + NAV, + NOSCRIPT, + OL, + P, + PRE, + SAMP, + SCRIPT, + SECTION, + SMALL, + SPAN, + STRONG, + STYLE, + SUB, + SUMMARY, + SUP, + TABLE, + TBODY, + TD, + TFOOT, + TH, + THEAD, + TITLE, + TR, + U, + UL, + VAR, + WBR; + + public static TagName of(String name) { + return valueOf(name.toUpperCase(Locale.ROOT)); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/Text.java b/src/share/classes/jdk/codetools/apidiff/html/Text.java new file mode 100644 index 0000000..3c98ab9 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/Text.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.io.IOException; +import java.io.Writer; + +/** + * Textual content. + * + * @see TextBuilder + */ +public class Text extends Content { + // TODO: provide additional constants (somewhere?) for common strings + + /** + * An object providing a single space character. + * + * @see Entity#NBSP + */ + public static final Text SPACE = new Text(" "); + + final String s; + + /** + * Creates textual content from a sequence of characters. + * + * @param text the characters + * @return the textual content + */ + public static Text of(CharSequence text) { + return new Text(text); + } + + /** + * Creates textual content from a sequence of characters. + * + * @param text the characters + */ + public Text(String text) { + s = text; + } + + /** + * Creates textual content from a sequence of characters. + * + * @param text the characters + */ + public Text(CharSequence text) { + s = text.toString(); + } + + /** + * Writes the content. + * Special characters ('{@code <}', '{@code &}' and '{@code >}') will be escaped. + * + * @param out the stream to which to write the content + * @throws IOException if an IO exception occurs. + */ + @Override + public void write(Writer out) throws IOException { + writeEscaped(out, s); + } + +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/TextBuilder.java b/src/share/classes/jdk/codetools/apidiff/html/TextBuilder.java new file mode 100644 index 0000000..05e2d4d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/TextBuilder.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.html; + +import java.io.IOException; +import java.io.Writer; + +/** + * Class for generating string content for {@code HTMLTree} nodes. + * The content is mutable to the extent that additional content may be added. + * + * @see Text + */ +public class TextBuilder extends Content { + + private final StringBuilder sb; + + /** + * Creates a mutable container for textual content. + * + * @param text the initial content + */ + public TextBuilder(CharSequence text) { + sb = new StringBuilder(text); + } + + /** + * Append additional characters to the content. + * + * @param text the characters + * + * @return this object + */ + public TextBuilder append(CharSequence text) { + sb.append(text); + return this; + } + + /** + * Trims the size of the internal string builder. + * + * @return this object + */ + TextBuilder trimToSize() { + sb.trimToSize(); + return this; + } + + /** + * Writes the content. + * Special characters ('{@code <}', '{@code &}' and '{@code >}') will be escaped. + * + * @param out the stream to which to write the content + * + * @throws IOException if an IO exception occurs. + */ + @Override + public void write(Writer out) throws IOException { + writeEscaped(out, sb.toString()); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/html/package-info.java b/src/share/classes/jdk/codetools/apidiff/html/package-info.java new file mode 100644 index 0000000..f26f26d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/html/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Utilities for building HTML trees and writing them to files. + */ +package jdk.codetools.apidiff.html; \ No newline at end of file diff --git a/src/share/classes/jdk/codetools/apidiff/model/API.java b/src/share/classes/jdk/codetools/apidiff/model/API.java new file mode 100644 index 0000000..730d5cd --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/API.java @@ -0,0 +1,1128 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.type.UnionType; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.util.DocTrees; +import com.sun.source.util.JavacTask; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Options.APIOptions; + +/** + * An abstraction of an API, as represented by some combination of source files, + * class files, and generated documentation. + */ +public abstract class API { + private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + + /** + * Creates an API object for the given parameters. + * + * @param opts the options to configure the API + * @param s a selector to filter the set of modules, packages and types to be compared + * @param ak the access kind, to filter the set of elements to be compared according + * their declared access + * @param log a log, to which any problems will be reported + * + * @return the API + */ + public static API of(APIOptions opts, Selector s, AccessKind ak, Log log) { + return new JavacAPI(opts, s, ak, log); + } + + /** + * The name of the API, as provided in the API options. + */ + public final String name; + + /** + * A short plain text label for the API, as provided in the API options. + */ + public final String label; + + /** + * The selector to filter the set of modules, packages and types to be compared. + */ + protected final Selector selector; + + /** + * The access kind, to filter the set of elements to be compared according + * their declared access. + */ + protected final AccessKind accessKind; + + /** + * The log, to which any problems will be reported. + */ + protected final Log log; + + /** + * The file manager used to read files, derived from the API options. + */ + protected final StandardJavaFileManager fileManager; + + /** + * Creates an instance of an API. + * + * @param opts the options for the API + * @param s the selector for the elements to be compared + * @param ak the access kind for the elements to be compared + * @param log the log, to which any any problems will be reported + */ + protected API(APIOptions opts, Selector s, AccessKind ak, Log log) { + this.name = opts.name; + this.label = opts.label; + this.selector = s; + this.accessKind = ak; + this.log = log; + + fileManager = compiler.getStandardFileManager(null, null, null); + } + + /** + * Returns the set of packages to be compared that are defined in this API. + * + * @return the package to be compared + */ + public abstract Set getPackageElements(); + + /** + * Returns the set of modules to be compared that are defined in this API. + * + * @return the modules to be compared + */ + public abstract Set getModuleElements(); + + /** + * Returns the set of packages to be compared that are defined in this API in a given module. + * + * @param m the module + * + * @return the packages to be compared + */ + public abstract Set getPackageElements(ModuleElement m); + + /** + * Returns the set of packages to be compared that are defined in this API + * and exported to all modules by a given module. + * + * @param m the module + * + * @return the packages to be compared + */ + public abstract Set getExportedPackageElements(ModuleElement m); + + /** + * Returns the set of types to be compared that are defined in this API in a given package. + * + * @param p the module + * + * @return the packages to be compared + */ + public abstract Set getTypeElements(PackageElement p); + + /** + * Returns the collection of annotation values, including defaults, for a given annotation mirror. + * + * @param am the annotation mirror + * + * @return the collection of annotation values + */ + public abstract Map getAnnotationValuesWithDefaults(AnnotationMirror am); + + /** + * Returns whether the annotation type of an annotation is {@code @Documented}. + * + * @param am the annotation + * + * @return {@code true} if and only if the type of the annotation is {@code @Documented} + */ + public boolean isDocumented(AnnotationMirror am) { + TypeElement te = (TypeElement) am.getAnnotationType().asElement(); + for (AnnotationMirror a : te.getAnnotationMirrors()) { + Name n = ((TypeElement) a.getAnnotationType().asElement()).getQualifiedName(); + if (n.contentEquals("java.lang.annotation.Documented")) { + return true; + } + } + return false; + } + + /** + * Returns the serialized form for a type element, or null if none. + * + * @param e the type element + * + * @return the serialized form + */ + public abstract SerializedForm getSerializedForm(TypeElement e); + + /** + * Returns the parsed documentation comment for a given element. + * + * @param e the element + * + * @return the doc comment tree + */ + public abstract DocCommentTree getDocComment(Element e); + + /** + * Returns the parsed documentation comment in a given file. + * The file must be an HTML file. + * + * @param fo the file object + * + * @return the doc comment tree + */ + public abstract DocCommentTree getDocComment(JavaFileObject fo); + + /** + * Returns the API description for a given element, extracted from the API documentation. + * + * @param e the element + * + * @return the API description + */ + public abstract String getApiDescription(Element e); + + /** + * Returns the API description in a given file. + * The file must be an HTML file. + * + * @param fo the file object + * + * @return the API description + */ + public abstract String getApiDescription(JavaFileObject fo); + + /** + * Returns the content of a file as an array of bytes, or null if there is + * an error reading the file. + * + * @param fo the file + * + * @return the bytes + */ + public abstract byte[] getAllBytes(JavaFileObject fo); + + /** + * The kind of location for which to list files. + * + * @see #listFiles + */ + public enum LocationKind { + /** Source files, as found on the module source path, source path or class path, as appropriate. */ + SOURCE, + /** API files, as found in the API directory. */ + API + } + + /** + * Returns a list of the files found in a subdirectory of the source directories or API directory + * for a module or package. + * + * @param kind the kind of location to search + * @param e the module or package + * @param subdirectory the optional subdirectory + * @param kinds the kinds of files + * @param recurse whether to recurse into subdirectories + * + * @return the list of files + */ + public abstract List listFiles(LocationKind kind, Element e, String subdirectory, + Set kinds, boolean recurse); + + /** + * Returns the {@code Elements Elements} utility class for this API. + * + * @return the {@code Elements} utility class + */ + public abstract Elements getElements(); + + /** + * Returns the {@code Types Types} utility class for this API. + * + * @return the {@code Types} utility class + */ + public abstract Types getTypes(); + + /** + * Returns the {@code DocTrees DocTrees} utility class for this API. + * + * @return the {@code DocTrees} utility class + */ + public abstract DocTrees getTrees(); + + static class JavacAPI extends API { + private List javacOpts; + private int platformVersion; + private Elements elements; + private Types types; + private DocTrees docTrees; + private SerializedFormFactory serializedFormFactory; + private Map serializedFormDocsMap; + private final Path apiDir; + private final APIReader apiReader; + private final boolean apiModuleDirectories; + + /** + * A tuple containing a location and the kinds of files that may be read from that location. + */ + private class LocationAndKinds { + final Location locn; + final Set kinds; + LocationAndKinds(Location locn, Set kinds) { + this.locn = locn; + this.kinds = kinds; + } + } + + private Map moduleLocationAndKinds; + private Set modules; + private Map> modulePackages; + + private Set packages; + + /** Map of recently accessed APIDocs, organized as an LRU cache. */ + private static final int MAX_APIDOCS = 20; + private LinkedHashMap apiDocs = new LinkedHashMap<>(MAX_APIDOCS, 0.9f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_APIDOCS; + } + }; + + JavacAPI(APIOptions opts, Selector s, AccessKind ak, Log log) { + super(opts, s, ak, log); + + for (Map.Entry> e : opts.fileManagerOpts.entrySet()) { + String opt = e.getKey(); + List args = e.getValue(); + for (String arg : args) { + Iterator argIter = arg == null + ? Collections.emptyIterator() + : Collections.singleton(arg).iterator(); + boolean ok = fileManager.handleOption(opt, argIter); + if (!ok) { + throw new IllegalArgumentException(opt); + } + } + } + + javacOpts = new ArrayList<>(); + + if (opts.release != null && opts.source != null) { + throw new IllegalArgumentException("both --release and --source"); + } else if (opts.release != null) { + javacOpts.addAll(List.of("--release", opts.release)); + platformVersion = Integer.parseInt(opts.release); + } else if (opts.source != null) { + javacOpts.addAll(List.of("--source", opts.source)); + platformVersion = Integer.parseInt(opts.source); + } else { + platformVersion = Runtime.version().feature(); + } + + if (opts.enablePreview) { + if (opts.release == null && opts.source == null) { + throw new IllegalArgumentException("either --release or --source must be specified with --enable-preview"); + } + javacOpts.add("--enable-preview"); + } + + apiDir = opts.apiDir; + apiReader = new APIReader(log); + + if (apiDir == null) { + apiModuleDirectories = false; + } else { + boolean foundModuleSummary = false; + try (Stream ds = Files.walk(apiDir, 1)) { + foundModuleSummary = ds.anyMatch(p -> Files.isDirectory(p) && Files.exists(p.resolve("module-summary.html"))); + } catch (IOException e) { + // TODO: report error and exit + } + apiModuleDirectories = foundModuleSummary; + } + } + + void initJavac(Set selectedModules) { + if (!selectedModules.isEmpty()) { + javacOpts.add("--add-modules"); + javacOpts.add(String.join(",", selectedModules)); + } + javacOpts.add("-proc:only"); + JavacTask javacTask = (JavacTask) compiler.getTask(log.err, fileManager, this::reportDiagnostic, javacOpts, null, null); + elements = javacTask.getElements(); + elements.getModuleElement("java.base"); // forces module graph to be instantiated, etc + types = javacTask.getTypes(); + docTrees = DocTrees.instance(javacTask); + serializedFormFactory = new SerializedFormFactory(this) { + @Override + public SerializedFormDocs getSerializedFormDocs(TypeElement te) { + // TODO: should we stop if there is a problem reading the file, other than "file not found" + // related: should this file be read proactively, if it exists, and if comparing API descriptions + if (serializedFormDocsMap == null) { + if (apiDir == null) { + serializedFormDocsMap = Collections.emptyMap(); + } else { + Path file = apiDir.resolve("serialized-form.html"); + serializedFormDocsMap = SerializedFormDocs.read(log, file); + } + } + String name = te.getQualifiedName().toString(); + return serializedFormDocsMap.get(name); + } + }; + } + + @Override + public Set getPackageElements() { + if (packages == null) { + initJavac(Collections.emptySet()); + + packages = new HashSet<>(); + + List locationAndKinds = new ArrayList<>(); + if (fileManager.hasLocation(StandardLocation.SOURCE_PATH)) { + locationAndKinds.add(new LocationAndKinds(StandardLocation.SOURCE_PATH, + EnumSet.of(JavaFileObject.Kind.SOURCE, JavaFileObject.Kind.HTML, JavaFileObject.Kind.OTHER))); + locationAndKinds.add(new LocationAndKinds(StandardLocation.CLASS_PATH, + EnumSet.of(JavaFileObject.Kind.CLASS))); + } else { + locationAndKinds.add(new LocationAndKinds(StandardLocation.CLASS_PATH, + EnumSet.allOf(JavaFileObject.Kind.class))); + } + + if (platformVersion <= 8) { + locationAndKinds.add(new LocationAndKinds(StandardLocation.PLATFORM_CLASS_PATH, + EnumSet.of(JavaFileObject.Kind.CLASS))); + } + + packages = getPackageElements(null, locationAndKinds); + } + return packages; + } + + @Override + public Set getModuleElements() { + if (modules == null) { + // Note: the following code does not support module source code on the source path. + // If necessary, use the module-specific form of --module-source-path to specify + // the source path for a single module. + // While it would be reasonable to check the source path, and even look for + // module-info.java, determining the module name would require reading and parsing + // the file. Not impossible, but ... + List modulePaths = List.of( + StandardLocation.MODULE_SOURCE_PATH, + StandardLocation.UPGRADE_MODULE_PATH, + StandardLocation.SYSTEM_MODULES, + StandardLocation.MODULE_PATH); + + moduleLocationAndKinds = new HashMap<>(); + for (Location mp : modulePaths) { + Set kinds = (mp == StandardLocation.MODULE_SOURCE_PATH) + ? EnumSet.of(JavaFileObject.Kind.SOURCE, JavaFileObject.Kind.HTML, JavaFileObject.Kind.OTHER) + : EnumSet.of(JavaFileObject.Kind.CLASS); + try { + for (Set locns : fileManager.listLocationsForModules(mp)) { + for (Location locn : locns) { + String mdlName = fileManager.inferModuleName(locn); + moduleLocationAndKinds.putIfAbsent(mdlName, new LocationAndKinds(locn, kinds)); + } + } + } catch (IOException e) { + // ignore for now; eventually save first and suppress the rest + } + } + + Set selectedModules = moduleLocationAndKinds.keySet().stream() + .filter(selector::acceptsModule) + .collect(Collectors.toSet()); + + initJavac(selectedModules); + + modules = selectedModules.stream() + .map(elements::getModuleElement) + .collect(Collectors.toSet()); + } + + return modules; + } + + /** + * {@inheritDoc} + * + * @see #getExportedPackageElements(ModuleElement) + */ + @Override + public Set getPackageElements(ModuleElement m) { + if (modulePackages == null) { + modulePackages = new LinkedHashMap<>(); + } + + Set packages = modulePackages.get(m); + if (packages == null) { + String moduleName = m.getQualifiedName().toString(); + LocationAndKinds lk = moduleLocationAndKinds.get(moduleName); + packages = (lk == null) ? Collections.emptySet() : getPackageElements(m, List.of(lk)); + modulePackages.put(m, packages); + } + return packages; + } + + @Override + public Set getExportedPackageElements(ModuleElement m) { + Set allExported = m.getDirectives().stream() + .filter(d -> d.getKind() == ModuleElement.DirectiveKind.EXPORTS) + .map(d -> (ModuleElement.ExportsDirective) d) + .filter(d -> d.getTargetModules() == null) + .map(d -> d.getPackage()) + .collect(Collectors.toSet()); + + return getPackageElements(m).stream() + .filter(allExported::contains) + .collect(Collectors.toSet()); + } + + /** + * Returns the packages found for a given module (or no module) in a series of locations. + * The Language Model API does not provided an explicit way to obtain the collection of packages + * for a module (or no module.) And so, the packages are determined by listing files in combinations + * of the selected packages and given locations. The package names are inferred from the file names, + * and the packages are then obtained by using {@link Elements#getPackageElement(CharSequence)} or + * {@link Elements#getPackageElement(ModuleElement, CharSequence)} as appropriate. + * + * @param me the module, or {@code null} for "no module" or the unnamed module + * @param locationAndKinds the locations and the kinds of files to check in those locations + * + * @return the packages + */ + private Set getPackageElements(ModuleElement me, List locationAndKinds) { + String moduleName = (me == null) ? null : me.getQualifiedName().toString(); + Map selectedPackages = new LinkedHashMap<>(); + for (Selector.Entry entry : selector.includes) { + if (!entry.includeModule.test(moduleName)) { + continue; + } + String packagePart = entry.packagePart; + boolean recurse = entry.typePart.equals("**"); + for (LocationAndKinds lk : locationAndKinds) { + try { + for (JavaFileObject f : fileManager.list(lk.locn, packagePart, lk.kinds, recurse)) { + String binaryName = fileManager.inferBinaryName(lk.locn, f); + int lastDot = binaryName.lastIndexOf("."); + if (lastDot != -1) { + String packageName = binaryName.substring(0, lastDot); + if (!selectedPackages.containsKey(packageName)) { + selectedPackages.put(packageName, selector.acceptsPackage(moduleName, packageName)); + } + } + } + } catch (IOException e) { + // TODO: ignore for now; eventually save first and suppress the rest, and abort? + } + } + } + + Function getPackageElement = (me == null) + ? pkgName -> elements.getPackageElement(pkgName) + : pkgName -> elements.getPackageElement(me, pkgName); + + return selectedPackages.entrySet().stream() + .filter(Entry::getValue) + .map(e -> getPackageElement.apply(e.getKey())) + .collect(Collectors.toSet()); + } + + @Override + public Set getTypeElements(PackageElement p) { + ModuleElement me = elements.getModuleOf(p); + String mn = (me == null) ? null : me.getQualifiedName().toString(); + String pn = p.getQualifiedName().toString(); + Set types = new HashSet<>(); + for (Element e : p.getEnclosedElements()) { + if (accessKind.accepts(e)) { + TypeElement te = (TypeElement) e; + String tn = te.getSimpleName().toString(); + if (selector.acceptsType(mn, pn, tn)) { + types.add(te); + } + } + } + return types; + } + + @Override + public Map getAnnotationValuesWithDefaults(AnnotationMirror am) { + return elements.getElementValuesWithDefaults(am); + } + + @Override + public SerializedForm getSerializedForm(TypeElement e) { + return serializedFormFactory.get(e); + } + + @Override + public DocCommentTree getDocComment(Element e) { + return docTrees.getDocCommentTree(e); + } + + private final ApiDescriptionVisitor apiDescriptionVisitor = new ApiDescriptionVisitor(); + + @Override + public String getApiDescription(Element e) { + if (apiDir == null) { + return null; + } + + APIDocs d = apiDocs.computeIfAbsent(getSpecFile(e), f -> APIDocs.read(apiReader, f)); + return apiDescriptionVisitor.visit(e, d); + } + + private Path getSpecFile(Element e) { + switch (e.getKind()) { + case MODULE: + return getSpecDir(e).resolve("module-summary.html"); + + case PACKAGE: + return getSpecDir(e).resolve("package-summary.html"); + + default: + var eKind = e.getKind(); + if (eKind.isClass() || eKind.isInterface()) { + StringBuilder typeFile = new StringBuilder(e.getSimpleName() + ".html"); + while (e.getEnclosingElement().getKind() != ElementKind.PACKAGE) { + e = e.getEnclosingElement(); + typeFile.insert(0, e.getSimpleName() + "."); + } + return getSpecDir(e).resolve(typeFile.toString()); + } else { + return getSpecFile(e.getEnclosingElement()); + } + } + } + + private Path getSpecDir(Element e) { + switch (e.getKind()) { + case MODULE: { + ModuleElement me = (ModuleElement) e; + return apiDir.resolve(me.getQualifiedName().toString()); + } + + case PACKAGE: { + PackageElement pe = (PackageElement) e; + ModuleElement me = (ModuleElement) pe.getEnclosingElement(); + Path dir = (me == null) ? apiDir : getSpecDir(me); + String sep = dir.getFileSystem().getSeparator(); + return dir.resolve(pe.getQualifiedName().toString().replace(".", sep)); + } + + default: + return getSpecDir(e.getEnclosingElement()); + } + } + + @Override + public DocCommentTree getDocComment(JavaFileObject fo) { + if (fo.getKind() != JavaFileObject.Kind.HTML) { + throw new IllegalArgumentException(fo.getName()); + } + + return docTrees.getDocCommentTree(fo); + } + + @Override + public String getApiDescription(JavaFileObject fo) { + if (fo.getKind() != JavaFileObject.Kind.HTML) { + throw new IllegalArgumentException(fo.getName()); + } + + Path p = fileManager.asPath(fo); + if (p == null || !Files.exists(p)) { + return null; + } + + try { + // TODO: consider using a new reader, DocFileReader, possibly returning a new object, + // containing title and body, or a single more versatile pattern + String s = Files.readString(p); + Matcher startMain = Pattern.compile("(?i)]*>").matcher(s); + if (startMain.find()) { + int start = startMain.end(); + Matcher endMain = Pattern.compile("(?i)").matcher(s); + if (endMain.find(start)) { + int end = endMain.start(); + return s.substring(start, end); + } + } + Matcher startBody = Pattern.compile("(?i)]*>").matcher(s); + if (startBody.find()) { + int start = startBody.end(); + Matcher endBody = Pattern.compile("(?i)").matcher(s); + if (endBody.find(start)) { + int end = endBody.start(); + return s.substring(start, end); + } + } + // TODO: report cannot find content + return null; + } catch (IOException e) { + // TODO: should report + return null; + } + } + + @Override + public byte[] getAllBytes(JavaFileObject fo) { + Path p = fileManager.asPath(fo); + if (p == null) { + return null; + } + try { + return Files.readAllBytes(p); + } catch (IOException e) { + // TODO: report error, or propagate + return null; + } + } + + @Override + public List listFiles(LocationKind kind, Element e, String subdirectory, + Set kinds, boolean recurse) { + return switch (kind) { + case SOURCE -> listSourceFiles(e, subdirectory, kinds, recurse); + case API -> listApiFiles(e, subdirectory, kinds, recurse); + }; + } + + private List listSourceFiles(Element e, String subdirectory, + Set kinds, boolean recurse) { + Location locn; + ModuleElement me = elements.getModuleOf(e); + if (me != null && !me.isUnnamed()) { + LocationAndKinds lk = moduleLocationAndKinds.get(me.getQualifiedName().toString()); + locn = (lk == null) ? null : lk.locn; + } else if (fileManager.hasLocation(StandardLocation.SOURCE_PATH)) { + locn = StandardLocation.SOURCE_PATH; + } else if (fileManager.hasLocation(StandardLocation.CLASS_PATH)) { + locn = StandardLocation.CLASS_PATH; + } else { + locn = null; + } + + if (locn == null) { + return Collections.emptyList(); + } + + String dirName = switch (e.getKind()) { + case MODULE -> ""; + case PACKAGE -> ((PackageElement) e).getQualifiedName().toString().replaceAll("\\.", "/"); + default -> throw new IllegalArgumentException(e.getKind() + " " + e); + }; + + if (subdirectory != null && !subdirectory.isEmpty()) { + dirName = dirName.isEmpty() ? subdirectory : dirName + "/" + subdirectory; + } + + List files = new ArrayList<>(); + try { + for (JavaFileObject f : fileManager.list(locn, dirName, kinds, recurse)) { + files.add(f); + } + } catch (IOException ex) { + // TODO: ignore for now; eventually save first and suppress the rest, and abort? + // or simply throw up to caller, which can fail the comparison + } + return files; + + } + + private List listApiFiles(Element e, String subdirectory, + Set kinds, boolean recurse) { + if (apiDir == null) { + return Collections.emptyList(); + } + + Path dir = apiDir; + ModuleElement me = elements.getModuleOf(e); + if (me != null && !me.isUnnamed()) { + // handle module directories anomaly from JDK 9 + if (apiModuleDirectories) { + dir = dir.resolve(me.getQualifiedName().toString()); + } else { + // doc files for named modules are not supported if there is no + // module subdirectory. + return Collections.emptyList(); + } + } + if (e instanceof PackageElement) { + PackageElement pe = (PackageElement) e; + if (!pe.isUnnamed()) { + dir = dir.resolve(pe.getQualifiedName().toString().replace(".", File.separator)); + } + } + if (subdirectory != null && !subdirectory.isEmpty()) { + dir = dir.resolve(subdirectory); + } + + List files = new ArrayList<>(); + try { + boolean allKinds = kinds.equals(EnumSet.allOf(JavaFileObject.Kind.class)); + Files.walkFileTree(dir, Set.of(), recurse ? Integer.MAX_VALUE : 1, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (allKinds || kinds.contains(getKind(file))) { + files.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException ex) { + // TODO: ignore for now; eventually save first and suppress the rest, and abort? + // or simply throw up to caller, which can fail the comparison + } + return asFileObjects(files); + } + + private JavaFileObject.Kind getKind(Path file) { + String name = file.getFileName().toString(); + int lastDot = name.lastIndexOf(name); + String extn = lastDot == -1 ? "" : name.substring(lastDot + 1); + return switch (extn) { + case "java" -> JavaFileObject.Kind.SOURCE; + case "class" -> JavaFileObject.Kind.CLASS; + case "html" -> JavaFileObject.Kind.HTML; + default -> JavaFileObject.Kind.OTHER; + }; + } + + private List asFileObjects(List files) { + List fileObjects = new ArrayList<>(); + for (JavaFileObject fo : fileManager.getJavaFileObjectsFromPaths(files)) { + fileObjects.add(fo); + } + return fileObjects; + } + + @Override + public Elements getElements() { + return elements; + } + + @Override + public Types getTypes() { + return types; + } + + @Override + public DocTrees getTrees() { + return docTrees; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + name + "]"; + } + + private void reportDiagnostic(Diagnostic d) { + JavaFileObject fo = d.getSource(); + Path file = fo == null ? null : fileManager.asPath(fo); + long line = d.getLineNumber(); + String message = d.getMessage(Locale.getDefault()); + + switch (d.getKind()) { + case ERROR -> log.error(file, line, null, message); + case WARNING -> log.warning(file, line, null, message); + case NOTE -> log.note(file, line, null, message); + } + } + + /** + * A visitor to access the API description for an element from the collection + * of API docs read from the appropriate file. + * The visitor determines the appropriate method and/or signature to use to + * access the description. + */ + private static class ApiDescriptionVisitor implements ElementVisitor { + private ExecutableSignatureVisitor signatureVisitor = new ExecutableSignatureVisitor(); + + @Override + public String visit(Element e, APIDocs d) { + return e.accept(this, d); + } + + @Override + public String visitModule(ModuleElement me, APIDocs d) { + return d.getDescription(); + } + + @Override + public String visitPackage(PackageElement pe, APIDocs d) { + return d.getDescription(); + } + + @Override // CLASS, INTERFACE, ANNOTATION_TYPE, ENUM, etc (RECORD, SEALED_TYPE...) + public String visitType(TypeElement te, APIDocs d) { + return d.getDescription(); + } + + @Override // FIELD, ENUM_CONSTANT + public String visitVariable(VariableElement ve, APIDocs d) { + return d.getDescription(ve.getSimpleName().toString()); + } + + @Override // METHOD, CONSTRUCTOR + public String visitExecutable(ExecutableElement ee, APIDocs d) { + return d.getDescription(signatureVisitor.getSignature(ee)); + } + + @Override + public String visitTypeParameter(TypeParameterElement tpe, APIDocs d) { + throw new IllegalArgumentException(tpe.getKind() + " " + tpe.getSimpleName()); + } + + @Override + public String visitUnknown(Element e, APIDocs d) { + throw new IllegalArgumentException(e.getKind() + " " + e.getSimpleName()); + } + } + + /** + * A visitor to determine the signature for an executable element, as used by + * {@code javadoc} to identify the description for the element in the page + * for the enclosing type, and thereby used to identify the description in + * the appropriate {@code APIDocs} object. + */ + private static class ExecutableSignatureVisitor + implements ElementVisitor, TypeVisitor { + String getSignature(ExecutableElement ee) { + StringBuilder sb = new StringBuilder(); + sb.append(ee.getSimpleName()); // automatically uses for constructor + List params = ee.getParameters(); + if (params.isEmpty()) { + sb.append("()"); + } else { + String sep = "("; + for (VariableElement ve : ee.getParameters()) { + sb.append(sep); + ve.asType().accept(this, sb); + sep = ","; + } + sb.append(")"); + } + return sb.toString(); + } + + @Override + public Void visit(Element e, StringBuilder sb) { + return e.accept(this, sb); + } + + @Override + public Void visitPackage(PackageElement e, StringBuilder sb) { + error(e); + return null; + } + + @Override + public Void visitType(TypeElement e, StringBuilder sb) { + sb.append(e.getQualifiedName()); + return null; + } + + @Override + public Void visitVariable(VariableElement e, StringBuilder sb) { + error(e); + return null; + } + + @Override + public Void visitExecutable(ExecutableElement e, StringBuilder sb) { + error(e); + return null; + } + + @Override + public Void visitTypeParameter(TypeParameterElement e, StringBuilder sb) { + sb.append(e.getSimpleName()); + return null; + } + + @Override + public Void visitUnknown(Element e, StringBuilder sb) { + error(e); + return null; + } + + private void error(Element e) { + throw new IllegalArgumentException(e.getKind() + "[" + e + "]"); + } + + @Override + public Void visit(TypeMirror t, StringBuilder sb) { + return t.accept(this, sb); + } + + @Override + public Void visitPrimitive(PrimitiveType t, StringBuilder sb) { + sb.append(t.getKind().toString().toLowerCase(Locale.ROOT)); + return null; + } + + @Override + public Void visitNull(NullType t, StringBuilder sb) { + throw new IllegalArgumentException(t.getKind() + " " + t.toString()); + } + + @Override + public Void visitArray(ArrayType t, StringBuilder sb) { + visit(t.getComponentType(), sb); + sb.append("[]"); + return null; + } + + @Override + public Void visitDeclared(DeclaredType t, StringBuilder sb) { + visit(t.asElement(), sb); + return null; + } + + @Override + public Void visitError(ErrorType t, StringBuilder sb) { + error(t); + return null; + } + + @Override + public Void visitTypeVariable(TypeVariable t, StringBuilder sb) { + visit(t.asElement(), sb); + return null; + } + + @Override + public Void visitWildcard(WildcardType t, StringBuilder sb) { + visit(t.getSuperBound(), sb); + return null; + } + + @Override + public Void visitExecutable(ExecutableType t, StringBuilder sb) { + error(t); + return null; + } + + @Override + public Void visitNoType(NoType t, StringBuilder sb) { + error(t); + return null; + } + + @Override + public Void visitUnknown(TypeMirror t, StringBuilder sb) { + error(t); + return null; + } + + @Override + public Void visitUnion(UnionType t, StringBuilder sb) { + error(t); // TODO? should not happen in args of ExecutableElement + return null; + } + + @Override + public Void visitIntersection(IntersectionType t, StringBuilder sb) { + error(t); // TODO? should not happen in args of ExecutableElement + return null; + } + + private void error(TypeMirror t) { + throw new IllegalArgumentException(t.getKind() + "[" + t + "]"); + } + + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/APIComparator.java b/src/share/classes/jdk/codetools/apidiff/model/APIComparator.java new file mode 100644 index 0000000..62cb89d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/APIComparator.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Set; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.PackageElement; + +import jdk.codetools.apidiff.Abort; +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for APIs. + */ +public class APIComparator { + private final Set apis; + private final Options options; + private final Options.Mode mode; + private final Reporter reporter; + private final Log log; + + /** + * Creates an comparator to compare the specifications of the declared elements + * in a series of APIs, using a reporter to generate a report of the results. + * Depending on the mode of the comparison, the items to be compared will be + * read from the overall module path or from the source and class path. + * + * @param apis the APIs + * @param options the command-line options + * @param reporter the reporter + * @param log the log + */ + public APIComparator(Set apis, Options options, Reporter reporter, Log log) { + this.apis = apis; + this.options = options; + this.mode = options.getMode(); + this.reporter = reporter; + this.log = log; + } + + /** + * Compares the collection of APIs specified for this comparator. + * + * @return {@code true} if the APIs are equivalent according to the configured settings + */ + public boolean compare() { + boolean equal = switch (mode) { + case MODULE -> compareModules(); + case PACKAGE -> comparePackages(); + }; + if (log.errorCount() > 0) { + throw new Abort(); + } + reporter.completed(equal); + return equal; + } + + private boolean compareModules() { + KeyTable allModules = new KeyTable<>(); + + for (API api: apis) { + for (ModuleElement me : api.getModuleElements()) { + allModules.put(ElementKey.of(me), api, me); + } + } + + ModuleComparator mc = new ModuleComparator(apis, options, reporter); + return mc.compareAll(allModules); + } + + private boolean comparePackages() { + KeyTable allPackages = new KeyTable<>(); + + for (API api: apis) { + for (PackageElement pe : api.getPackageElements()) { + allPackages.put(ElementKey.of(pe), api, pe); + } + } + + PackageComparator pc = new PackageComparator(apis, options, reporter); + return pc.compareAll(allPackages); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/APIDocs.java b/src/share/classes/jdk/codetools/apidiff/model/APIDocs.java new file mode 100644 index 0000000..bcc66f1 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/APIDocs.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import jdk.codetools.apidiff.Log; + +/** + * A class that encapsulates the descriptions generated by javadoc in a file + * for a module, package or type. + */ +public class APIDocs { + /** + * Reads a file generated by javadoc, and extracts the descriptions for + * the declarations contained therein. + * + *

If the file does not exist, an empty object will be returned, + * which returns {@code null} for {@link #getDescription()} and {@link #getDescription(String)}, + * and an empty map for {@link #getMemberDescriptions()}. + * + * @param log a log to which any errors will be reported + * @param file the file to be read + * + * @return an instance of {@code APIDocs} that contains the descriptions + * found in the file. + */ + public static APIDocs read(Log log, Path file) { + return read(new APIReader(log), file); + + } + + /** + * Reads a file generated by javadoc, and extracts the descriptions for + * the declarations contained therein. + * + *

If the file does not exist, an empty object will be returned, + * which returns {@code null} for {@link #getDescription()} and {@link #getDescription(String)}, + * and an empty map for {@link #getMemberDescriptions()}. + * + * @param r an API reader to read the file + * @param file the file to be read + * + * @return an instance of {@code APIDocs} that contains the descriptions + * found in the file. + */ + public static APIDocs read(APIReader r, Path file) { + if (!Files.exists(file)) { + return EMPTY; + } + + r.read(file); + return new APIDocs(r.getDeclarationNames(), r.getDescription(), r.getMemberDescriptions()); + } + + private final Map declNames; + private final String description; + private final Map memberDescriptions; + + private static final APIDocs EMPTY = new APIDocs(Collections.emptyMap(), null, Collections.emptyMap()); + + private APIDocs(Map declNames, String description, Map memberDescriptions) { + this.declNames = declNames; + this.description = description; + this.memberDescriptions = memberDescriptions; + } + + /** + * Returns the parts of the names for the top-level element defined in the file. + * The keys for the names are {@code module}, {@code package} and {@code class}. + * + * @return a map containing the parts of the names + */ + public Map getDeclarationNames() { + return declNames; + } + + /** + * Returns the description for the primary element declared in the file: + * the module, page or type. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the description for a member declared in the file, or null if the member is not found. + * + * Only types contain members; modules and packages do not. + * Members are identified by their signature. + * The signature is either the name of the member, or {@code } for a constructor, + * followed by a comma-separated list of argument types enclosed in parentheses if the member + * is a constructor or method. + * + * @param memberSignature the signature of the member + * @return the description of the member + */ + public String getDescription(String memberSignature) { + return memberDescriptions.get(memberSignature); + } + + /** + * Returns a map containing the descriptions of all the members found in the file. + * + * @return a map of descriptions, indexed by signature + */ + public Map getMemberDescriptions() { + return memberDescriptions; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/APIMap.java b/src/share/classes/jdk/codetools/apidiff/model/APIMap.java new file mode 100644 index 0000000..01887fd --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/APIMap.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * A map in which to store the instances of corresponding items in different + * instances of an API. + * + *

Null values are not permitted in the map. + * + *

The representation of the map is intended to be opaque. + * + * @param the type of the items in this map + */ +public class APIMap extends LinkedHashMap { + private static final long serialVersionUID = 0; + + private APIMap() { } + + /** + * Creates an instance of an {@code APIMap}. + * + * @param the type of the instances in the map. + * @return the map + */ + public static APIMap of() { + return new APIMap<>(); + } + + /** + * Creates an instance of an {@code APIMap} containing an initial entry. + * + * @param the type of the instances in the map. + * @param api the API + * @param t the instance of the item for the given API + * @return the map + */ + public static APIMap of(API api, T t) { + APIMap map = new APIMap<>(); + map.put(api, t); + return map; + } + + @Override + public T put(API api, T t) { + Objects.requireNonNull(api); + Objects.requireNonNull(t); + return super.put(api, t); + } + + /** + * Creates a new map by applying a function to each of the values in this map. + * If the function returns {@code null} for an entry, no corresponding entry + * is put in the new map. + * + * @param f the function + * @param the type of entries in the new map + * + * @return the new map + */ + public APIMap map(Function f) { + APIMap result = APIMap.of(); + for (Map.Entry e : entrySet()) { + R r = f.apply(e.getValue()); + if (r != null) { + result.put(e.getKey(), r); + } + } + return result; + } + + /** + * Creates a new map by applying a bi-function to each of the entries in this map. + * If the function returns {@code null} for an entry, no corresponding entry + * is put in the new map. + * + * @param f the function + * @param the type of entries in the new map + * + * @return the new map + */ + public APIMap map(BiFunction f) { + APIMap result = APIMap.of(); + for (Map.Entry e : entrySet()) { + R r = f.apply(e.getKey(), e.getValue()); + if (r != null) { + result.put(e.getKey(), r); + } + } + return result; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/APIReader.java b/src/share/classes/jdk/codetools/apidiff/model/APIReader.java new file mode 100644 index 0000000..df7fae8 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/APIReader.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jdk.codetools.apidiff.Log; + +/** + * A reader that reads descriptions for program elements from HTML files generated by javadoc. + * Only files in HTML5 format are supported: the primary reason being the lossy-encoding + * of member signatures into {@code name} attributes in HTML4 files. + * + * The output generated by javadoc is not well-specified and varies across releases. + * The following rules allow fuzzy parsing for at least JDK version 11 and later. + * + *

    + *
  • For a module, the description is the smallest enclosing {@code
    } containing + * {@code id="module-description"} or {@code id="module.description"}, or the {@code
    } + * within that section. It may be followed by an additional {@code
    } containing + * the block tags for the description. + * + *
  • For a package, the description is the smallest enclosing {@code
    } containing + * {@code id="package-description"} or {@code id="package.description"}, or the {@code
    } + * within that section. + * + *
  • For a type, the description is the enclosing + * {@code
    } or + * {@code
    }, + * or the {@code
    } within that section. + * + *
  • For a member, the description follows the "...Detail" heading, then an + * {@code id="signature"}, then a heading and a subsequent {@code
    }. + * There may be enclosing {@code
      } and {@code
    • } nodes to be taken into + * account, for older versions of javadoc. There is an enclosing {@code
      } + * we could select as well. Note the heading has an incomplete signature; + * the preceding {@code id} is more accurate. + *
    + */ +public class APIReader extends HtmlParser { + private enum Kind { MODULE, PACKAGE, TYPE } + + private final Log log; + private Kind kind; + private String description; + private Map memberDescriptions; + private Map declarationNames; + + APIReader(Log log) { + this.log = log; + } + + // TEMPORARY! + boolean debug = false; + private void debugPrintln(Supplier s) { + if (debug) { + System.err.println(s.get()); + } + } + + @Override + public void read(Path file) { + debug = Objects.equals(file.getFileName().toString(), System.getProperty("debug.APIReader")); + kind = getKind(file); + description = null; + memberDescriptions = new HashMap<>(); + debugPrintln(() -> "**************************************** read " + file + " " + kind); + super.read(file); + } + + /** + * Returns the parts of the names for the top-level element defined on this page. + * The keys for the names are {@code module}, {@code package} and {@code class}. + * + * @return a map containing the parts of the names + */ + public Map getDeclarationNames() { + return (declarationNames == null) ? Collections.emptyMap() : declarationNames; + } + + /** + * Returns the main description of the primary element declared on this page. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns a map containing the descriptions of the member elements enclosed by the + * primary element declared on the page. + * The keys of the map are the signatures of the individual members. + * + * @return the map + */ + public Map getMemberDescriptions() { + return memberDescriptions; + } + + @Override + protected void error(Path file, int lineNumber, String message) { + log.err.println(file + ":" + lineNumber + ": " + message); + } + + @Override + protected void error(Path file, int lineNumber, Throwable t) { + log.err.println(file + ":" + lineNumber + ": " + t); + } + + private StringBuilder contentBuffer; + private boolean inMain; + private int startDescriptionIndex; + private boolean inDescription; + private String descriptionId; + private int blockDepth; + private boolean inDetails; + private boolean inHeader; + private boolean inSubTitle; + private String detailHeading; + private boolean allowMoreModuleSections; + + @Override + protected void content(Supplier content) { + if (contentBuffer != null) { + contentBuffer.append(content.get()); + } + } + + @Override + protected void html() { + if (startDescriptionIndex == -1) { + startDescriptionIndex = getBufferIndex() - 1; + } + super.html(); + if (!inDescription) { + startDescriptionIndex = -1; + } + } + + @Override + protected void startElement(String name, Map attrs, boolean isSelfClosing) { + // skip almost everything not in the `
    ` element + if (!inMain) { + switch (name) { + case "meta" -> { + String nameAttr = attrs.get("name"); + String contentAttr = attrs.get("content"); + if (Objects.equals(nameAttr, "description") + && contentAttr != null + && contentAttr.startsWith("declaration: ")) { + Map names = new LinkedHashMap<>(); + Pattern p = Pattern.compile("(?[a-z]+):\\s+(?[A-Za-z0-9_$.]+)"); + Matcher m = p.matcher(contentAttr.substring(contentAttr.indexOf(' '))); + while (m.find()) { + names.put(m.group("key"), m.group("value")); + } + declarationNames = names; + } + } + case "main" -> + inMain = true; + } + return; + } + + debugPrintln(() -> "<" + name + " " + attrs + ">"); + String classAttr = attrs.get("class"); + switch (name) { + case "div": + // For some older versions of javadoc, the description for a type declaration + // is contained in `
    ...
    ` + if ((kind == Kind.TYPE) && isClassDescription(attrs)) { + debugPrintln(() -> "!! start description for TYPE"); + inDescription = true; + blockDepth = 1; + } else if (Objects.equals(classAttr, "header")) { + inHeader = true; + } else if (Objects.equals(classAttr, "subTitle") + || Objects.equals(classAttr, "sub-title") ) { + inSubTitle = true; + contentBuffer = new StringBuilder(); + } else { + if (inDescription) { + blockDepth++; + } + } + break; + + case "section": + // For some versions of javadoc, the description for a module or package + // is contained in a section containing an element with `id` set to + // `module.description` or `package.description`. For these, + // proactively set inDescription, but cancel if the first id is incorrect. + // For some more recent versions of javadoc, the description for a type + // is contained in `
    ...
    `, or + // `
    ...
    ` but + // note that the section is poorly formed for not having a heading. + // For some more recent versions of javadoc, the description for a member + // is contained in `
    ...
    `. + if ((kind == Kind.MODULE || kind == Kind.PACKAGE) && description == null + || kind == Kind.MODULE && allowMoreModuleSections) { + if (inDescription) { + blockDepth++; + } else { + debugPrintln(() -> "?? start description for MODULE or PACKAGE"); + inDescription = true; + blockDepth = 1; + } + } else if ((kind == Kind.TYPE) && isClassDescription(attrs)) { + debugPrintln(() -> "!! start description for TYPE"); + inDescription = true; + blockDepth = 1; + } else if ((kind == Kind.TYPE) && Objects.equals(classAttr, "detail")) { + debugPrintln(() -> "!! start description for TYPE member"); + inDescription = true; + blockDepth = 1; + } + debugPrintln(() -> ">>>>>> in section " + inDescription + " " + blockDepth); + break; + + // For some versions of javadoc, the description for a member starts in the + // details section, with a heading of the appropriate rank. By itself, + // the content of the heading does not fully identify the member; + // an `id` attribute in the heading or in an element that follows provides + // the information. + case "h1": case "h2": case "h3": case "h4": + contentBuffer = new StringBuilder(); + if (inDetails && !inDescription && name.equals(detailHeading)) { + debugPrintln(() -> "!! start description because inDetails but !inDescription"); + inDescription = true; + descriptionId = null; + } + break; + + // Some versions of javadoc use two sections for the description, instead of one. + // This is handled by reading sections until the first `
      ` element that is not + // part of the description. For these versions of javadoc, this `
        ` introduces + // the summary sections. + case "ul": + if (inDescription) { + blockDepth++; + } else { + allowMoreModuleSections = false; + } + break; + } + + // id attributes are used to help identify the beginning of descriptions + // and (for members) the basic signature of the member itself. + String id = attrs.get("id"); + switch (kind) { + // For a module, the main description is a section containing + // `id="module-description"` or `id="module.description"` + case MODULE: + if (inDescription && descriptionId == null && id != null) { + if (isModuleDescription(attrs)) { + debugPrintln(() -> "!! commit description for MODULE"); + descriptionId = id; + allowMoreModuleSections = true; + } else { + debugPrintln(() -> "XX cancel description for MODULE"); + inDescription = false; + } + } + break; + + // For a package, the main description is a section containing + // `id="package-description"` or `id="package.description"` + case PACKAGE: + if (inDescription && descriptionId == null && id != null) { + if (isPackageDescription(attrs)) { + debugPrintln(() -> "!! commit description for PACKAGE"); + descriptionId = id; + } else { + debugPrintln(() -> "XX cancel description for PACKAGE"); + inDescription = false; + } + } + break; + + // For a type, identifying the main description does not depend on + // any `id` attribute (it depends on the element and class). + // But, an `id` in the details part of the file may indicate the + // beginning of the description of a member, or may provide the + // `declarationId` if the description has already been started ... + // for example, by a heading. + case TYPE: + if (inDetails && descriptionId == null && (id != null) && !id.matches("[a-z]+[-.]detail[s]?")) { + if (!inDescription) { + debugPrintln(() -> "!! start description for TYPE " + id); + inDescription = true; + blockDepth = 0; + } + debugPrintln(() -> "!! set id for TYPE member " + id); + descriptionId = id; + } + break; + } + + debugPrintln(() -> " inDesc:" + inDescription + " depth:" + blockDepth + " descId:" + descriptionId); + } + + /* + * Match the following strings: + * Constructor Detail + * Element Detail + * Enum Constant Detail + * Field Detail + * Method Detail + * + * For now, it's a weak match. + * If we get false positives, we can strengthen the match. + * Ideally, we should not need to rely on javadoc's marker comments. + * + * The headings would be more grammatically correct if they were + * plural, and ended in "s". The pattern proactively anticipates this + * possibility. + */ + private final Pattern detail = Pattern.compile("(?i)[a-z ]+ detail[s]?"); + + @Override + protected void endElement(String name) { + // skip everything not in the `
        ` element + if (!inMain) { + return; + } + + debugPrintln(() -> "" + blockDepth); + switch (name) { + case "div": + if (inSubTitle) { + String subTitle = contentBuffer.toString().replace(" ", " "); + debugPrintln(() -> "subTitle: " + subTitle); + setDeclarationName(subTitle); + inSubTitle = false; + } + break; + + case "h1": case "h2": case "h3": case "h4": + String content = contentBuffer.toString(); + if (inHeader) { + String heading = content.replace(" ", " "); + debugPrintln(() -> "heading: " + heading); + setDeclarationName(heading); + inHeader = false; + } else if (detail.matcher(content).matches()) { + debugPrintln(() -> "START inDetails"); + inDetails = true; + detailHeading = "h" + (char)(name.charAt(1) + 1); + debugPrintln(() -> "detailHeading: " + detailHeading); + } + break; + + // nothing more to do + case "main": + inDetails = false; + inMain = false; + break; + } + + // Once a description has been started, in general the description is ended + // when the block nesting level drops to zero. + if (inDescription) { + switch (name) { + case "section": + case "div": + case "ul": + if (--blockDepth == 0) { + String d = getBufferString(startDescriptionIndex, getBufferIndex()); + switch (kind) { + case MODULE: + // Some versions of javadoc use multiple section elements for the + // description of a module, so we concatenate them here. + debugPrintln(() -> ("*** " + kind + ":\n" + d).replace("\n", "\n*** ")); + description = (description == null) ? d : description + d; + break; + + case PACKAGE: + debugPrintln(() -> ("*** " + kind + ":\n" + d).replace("\n", "\n*** ")); + description = d; + break; + + case TYPE: + // Determine whether this is the end of the main description, or the end of the description + // of a member. + if (descriptionId == null) { + debugPrintln(() -> ("*** " + kind + ":\n" + d).replace("\n", "\n*** ")); + description = d; + } else { + debugPrintln(() -> ("*** " + kind + ": " + descriptionId + "\n" + d).replace("\n", "\n*** ")); + memberDescriptions.put(descriptionId, d); + } + break; + } + inDescription = false; + descriptionId = null; + } + } + } + } + + /** + * Module descriptions are recognised as one of the following: + * {@code }} + * {@code }} + * + * @param attrs the attributes + * @return whether the attributes indicate a class description + */ + private boolean isModuleDescription(Map attrs) { + String id = attrs.get("id"); + if (id == null) { + return false; + } + + return switch (id) { + case "module-description", // new style + "module.description" -> // old style + true; + + default -> + false; + }; + } + + /** + * Package descriptions are recognised as one of the following: + * {@code }} + * {@code }} + * + * @param attrs the attributes + * @return whether the attributes indicate a class description + */ + private boolean isPackageDescription(Map attrs) { + String id = attrs.get("id"); + if (id == null) { + return false; + } + + return switch (id) { + case "package-description", // new style + "package.description" -> // old style + true; + + default -> + false; + }; + } + + /** + * Class descriptions are recognised as one of the following: + * {@code }} + * + * @param attrs the attributes + * @return whether the attributes indicate a class description + */ + private boolean isClassDescription(Map attrs) { + String id = attrs.get("id"); + if (id != null) { + return id.equals("class-description"); // new style + } else { + return Objects.equals(attrs.get("class"), "description"); // old style + } + } + + private void setDeclarationName(String keyValue) { + Pattern p = Pattern.compile("(?[A-Za-z]+):?\\s+(?[A-Za-z0-9_$.]+)"); + Matcher m = p.matcher(keyValue); + if (m.matches()) { + if (declarationNames == null) { + declarationNames = new LinkedHashMap<>(); + } + declarationNames.putIfAbsent( + m.group("key").toLowerCase(Locale.ROOT), + m.group("value")); + } + } + + private Kind getKind(Path file) { + return switch (file.getFileName().toString()) { + case "module-summary.html" -> Kind.MODULE; + case "package-summary.html" -> Kind.PACKAGE; + default -> Kind.TYPE; + }; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/AccessKind.java b/src/share/classes/jdk/codetools/apidiff/model/AccessKind.java new file mode 100644 index 0000000..6e15cb2 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/AccessKind.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; + +/** + * The kind of access filters. + */ +public enum AccessKind { + /** + * A filter for items declared to be {@code public}. + */ + PUBLIC { + @Override + public boolean accepts(Set modifiers) { + return modifiers.contains(Modifier.PUBLIC); + } + + @Override + public boolean allModuleDetails() { + return false; + } + }, + + /** + * A filter for items declared to be {@code public} or {@code protected}. + */ + PROTECTED { + @Override + public boolean accepts(Set modifiers) { + return modifiers.contains(Modifier.PUBLIC) || modifiers.contains(Modifier.PROTECTED); + } + + @Override + public boolean allModuleDetails() { + return false; + } + }, + + /** + * A filter for items declared to be {@code public}, {@code protected} or package-private. + */ + PACKAGE { + @Override + public boolean accepts(Set modifiers) { + return !modifiers.contains(Modifier.PRIVATE); + } + + @Override + public boolean allModuleDetails() { + return true; + } + }, + + /** + * A filter for all items. + */ + PRIVATE { + @Override + public boolean accepts(Set modifiers) { + return true; + } + + @Override + public boolean allModuleDetails() { + return true; + } + }; + + /** + * Returns whether the filter accepts an item with the given set of modifiers. + * + * @param modifiers the modifiers + * @return {@code true} if the item is accepted by the filter + */ + public abstract boolean accepts(Set modifiers); + + /** + * Returns whether the filter accepts an element according to its modifiers. + * + * @param e the element + * @return {@code true} if the element is accepted by the filter + */ + public boolean accepts(Element e) { + return accepts(e.getModifiers()); + } + + /** + * Returns whether all module details should be compared and displayed. + * + * @return {@code true} if and only if all module details should be compared and displayed + */ + public abstract boolean allModuleDetails(); +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/AnnotationComparator.java b/src/share/classes/jdk/codetools/apidiff/model/AnnotationComparator.java new file mode 100644 index 0000000..763e542 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/AnnotationComparator.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import javax.lang.model.AnnotatedConstruct; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.AbstractAnnotationValueVisitor14; + +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for the {@link AnnotationMirror annotations} on {@link AnnotatedConstruct annotated constructs}. + * + *

        Annotations are compared according to their structure ("deep equals") down to + * the level of element names, which are compared using {@link ElementKey#equals ElementKey.equals}. + */ +public class AnnotationComparator { + /** The APIs to be compared. */ + protected final Set apis; + /** The access kind. */ + private final AccessKind accessKind; + /** The reporter to which to report any differences. */ + protected final Reporter reporter; + + /** + * Creates an instance of comparator for annotations. + * + * @param apis the APIs being compared + * @param accessKind the access kind + * @param reporter the reporter to which to report any differences. + */ + protected AnnotationComparator(Set apis, AccessKind accessKind, Reporter reporter) { + this.apis = apis; + this.accessKind = accessKind; + this.reporter = reporter; + } + + /** + * Compares all the annotations at a given position within instances of an API. + * + * @param pos the position of the annotations to be compared + * @param acMap the annotated constructs whose annotations are being compared + * @return {@code true} if and only if all the annotations are equal + */ + public boolean compareAll(Position pos, APIMap acMap) { + Map> annoMap = extractAnnotations(acMap); + + boolean allEqual = true; + for (Map.Entry> e : annoMap.entrySet()) { + ElementKey k = e.getKey(); + APIMap v = e.getValue(); + boolean equal = compare(pos.annotation(k), v); + allEqual &= equal; + } + return allEqual; + } + + private Map> extractAnnotations(APIMap acMap) { + Map> annoMap = new HashMap<>(); + for (Map.Entry entry : acMap.entrySet()) { + API api = entry.getKey(); + AnnotatedConstruct c = entry.getValue(); + for (AnnotationMirror am : c.getAnnotationMirrors()) { + if (isIncluded(api, am)) { + annoMap.computeIfAbsent(ElementKey.of(am.getAnnotationType().asElement()), e -> APIMap.of()) + .put(api, am); + } + } + } + return annoMap; + } + + private boolean isIncluded(API api, AnnotationMirror am) { + return switch (accessKind) { + case PUBLIC, PROTECTED -> api.isDocumented(am); + default -> true; + }; + } + + /** + * Compares the annotations found at a given position in the instances of + * the APIs being compared. + * + * @param pos the position + * @param map the annotations + * @return {@code true} if and only if all the annotations are equal + */ + public boolean compare(Position pos, APIMap map) { + Map> valueMap = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + API api = entry.getKey(); + AnnotationMirror am = entry.getValue(); + Map values = api.getAnnotationValuesWithDefaults(am); + for (Map.Entry e : values.entrySet()) { + ExecutableElement ee = e.getKey(); + AnnotationValue av = e.getValue(); + valueMap.computeIfAbsent(ElementKey.of(ee), e_ -> APIMap.of()).put(api, av); + } + } + + boolean allEqual = true; + reporter.comparing(pos, map); + try { + for (Map.Entry> entry : valueMap.entrySet()) { + ElementKey key = entry.getKey(); + APIMap avMap = entry.getValue(); + allEqual &= new AnnotationValueComparator(pos.annotationValue(key)).compare(avMap); + } + } finally { + reporter.completed(pos, allEqual); + } + + return allEqual; + } + + class AnnotationValueComparator extends AbstractAnnotationValueVisitor14> { + + private final Position pos; + + AnnotationValueComparator(Position pos) { + this.pos = pos; + } + + boolean compare(APIMap avMap) { + boolean equal = false; + reporter.comparing(pos, avMap); + try { + AnnotationValue baseline = avMap.values().stream() + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (baseline == null) { + // no non-null value found, so all must be null, and hence equal + return true; + } + equal = visit(baseline, avMap); + } finally { + reporter.completed(pos, equal); + } + return equal; + } + + @Override + public Boolean visitAnnotation(AnnotationMirror a, APIMap avMap) { + APIMap amMap = APIMap.of(); + for (Map.Entry entry : avMap.entrySet()) { + API api = entry.getKey(); + AnnotationValue av = entry.getValue(); + Object avo = av.getValue(); + if (avo instanceof AnnotationMirror) { + amMap.put(api, (AnnotationMirror) avo); + } else { + reporter.reportDifferentAnnotationValues(pos, avMap); + return false; + } + } + return AnnotationComparator.this.compare(pos, amMap); + } + + @Override + public Boolean visitArray(List l, APIMap avMap) { + IntTable t = new IntTable<>(); + for (Map.Entry entry : avMap.entrySet()) { + API api = entry.getKey(); + AnnotationValue av = entry.getValue(); + if (av.getValue() instanceof List) { + @SuppressWarnings("unchecked") + List avList = (List) av.getValue(); + t.put(api, avList); + } else { + reporter.reportDifferentAnnotationValues(pos, avMap); + return false; + } + } + + boolean allEqual = true; + for (int i = 0; i < t.size(); i++) { + APIMap map = t.entries(i); + allEqual &= new AnnotationValueComparator(pos.annotationArrayIndex(i)).compare(map); + } + return allEqual; + } + + @Override + public Boolean visitBoolean(boolean b, APIMap avMap) { + return compare(b, avMap); + } + + @Override + public Boolean visitByte(byte b, APIMap avMap) { + return compare(b, avMap); + } + + @Override + public Boolean visitChar(char c, APIMap avMap) { + return compare(c, avMap); + } + + @Override + public Boolean visitDouble(double d, APIMap avMap) { + return compare(d, avMap); + } + + @Override + public Boolean visitEnumConstant(VariableElement ve, APIMap avMap) { + return compare(ElementKey.of(ve), avMap, o -> { + if (!(o instanceof VariableElement)) + return false; + return ElementKey.of((VariableElement) o); + }); + } + + @Override + public Boolean visitFloat(float f, APIMap avMap) { + return compare(f, avMap); + } + + @Override + public Boolean visitInt(int i, APIMap avMap) { + return compare(i, avMap); + } + + @Override + public Boolean visitLong(long l, APIMap avMap) { + return compare(l, avMap); + } + + @Override + public Boolean visitShort(short s, APIMap avMap) { + return compare(s, avMap); + } + + @Override + public Boolean visitString(String s, APIMap avMap) { + return compare(s, avMap); + } + + @Override + public Boolean visitType(TypeMirror t, APIMap avMap) { + return compare(TypeMirrorKey.of(t), avMap, o -> { + if (!(o instanceof TypeMirror)) + return false; + return TypeMirrorKey.of((TypeMirror) o); + }); + } + + @Override + public Boolean visitUnknown(AnnotationValue av, APIMap avMap) { + // should not happen! + reporter.reportDifferentAnnotationValues(pos, avMap); + return false; + } + + /** + * Compares an object against all the given annotation values, using {@code Object.equals}. + * @param o the object + * @param avMap the collection of annotation values + * @return {@code true} if the object equals all the annotation values + */ + private boolean compare(Object o, APIMap avMap) { + for (AnnotationValue v : avMap.values()) { + if (v == null || !o.equals(v.getValue())) { + reporter.reportDifferentAnnotationValues(pos, avMap); + return false; + } + } + return true; + } + + /** + * Compares an object against all the given annotation values, a function to derive a value + * to be compare with using {@code Object.equals}. + * @param o the object + * @param avMap the collection of annotation values + * @return {@code true} if the object equals all the transformed annotation values + */ + private boolean compare(Object o, APIMap avMap, Function f) { + for (AnnotationValue v : avMap.values()) { + if (v == null || !o.equals(f.apply(v.getValue()))) { + reporter.reportDifferentAnnotationValues(pos, avMap); + return false; + } + } + return true; + } + } + +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/DocCommentComparator.java b/src/share/classes/jdk/codetools/apidiff/model/DocCommentComparator.java new file mode 100644 index 0000000..5b38262 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/DocCommentComparator.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Set; + +import com.sun.source.doctree.DocCommentTree; + +/** + * A comparator for {@link DocCommentTree documentation comments}. + */ +public class DocCommentComparator { + private final Set apis; + + /** + * Creates a comparator for instances of documentation comments found in + * different APIs. + * + * @param apis the APIs + */ + public DocCommentComparator(Set apis) { + this.apis = apis; + } + + /** + * Compare instances of a documentation comment for an element in different APIs. + * + * @param dPos the position of the element + * @param dMap the map giving the instance of the comment in different APIs + * @return {@code true} if all the instances are equivalent + */ + public boolean compare(Position dPos, APIMap dMap) { + // TODO: compare first sentence, body, sorted block tags + return true; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/DocFile.java b/src/share/classes/jdk/codetools/apidiff/model/DocFile.java new file mode 100644 index 0000000..92a009d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/DocFile.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import javax.lang.model.element.Element; +import javax.tools.JavaFileObject; + +/** + * An object representing a file in a {@code doc-files} subdirectory + * of the source directory or generated API directory for a package. + */ +public class DocFile { + /** + * The standard name of the subdirectory used for doc files. + */ + public static final String DOC_FILES = "doc-files"; + + /** + * The API for the doc file. + */ + public final API api; + + /** + * The element that "contains" the doc file. It may be a module element or a package element. + */ + public final Element element; + + /** + * The source and/or API file. + * One or the other (but not both) may be null if no such file is found. + */ + public final Map files; + + /** + * Returns a table built by listing all the files in the {@code doc-files} + * subdirectory of the source and generated API directories for a module or package. + * + * @param pMap the map of corresponding packages in the APIs being compared + * + * @return the table + */ + static Map> listDocFiles(APIMap pMap) { + Set allKinds = EnumSet.allOf(JavaFileObject.Kind.class); + Map> fMap = new TreeMap<>(); + pMap.forEach((api, e) -> { + for (API.LocationKind lk : API.LocationKind.values()) { + for (JavaFileObject fo : api.listFiles(lk, e, DOC_FILES, allKinds, true)) { + + // There is no supported way to get the name of the file relative to the package in which + // the search was done. JavaFileManager.inferBinaryName comes close, but is not ideal. + // The following assumes that "doc-files" only appears once in the path name. + // A more rigorous check would be to include the module name or package name, + // but even that is not guaranteed to be unique. + + String name = fo.getName(); + int index = name.indexOf(DOC_FILES); + if (index == -1) { + throw new IllegalArgumentException(fo.getName()); + } + String path = name.substring(index + DOC_FILES.length() + 1); + + APIMap dMap = fMap.computeIfAbsent(path, __ -> APIMap.of()); + DocFile df = dMap.computeIfAbsent(api, __ -> new DocFile(api, e)); + df.files.put(lk, fo); + } + } + }); + return fMap; + } + + private DocFile(API api, Element element) { + this.api = api; + this.element = element; + this.files = new EnumMap<>(API.LocationKind.class); + } + + /** + * Returns the kind of these doc files. + * By construction, they all have the same file name, and hence all have the + * same kind, so it is sufficient to just pick one. + * + * @return the kind + */ + public JavaFileObject.Kind getKind() { + return files.values().iterator().next().getKind(); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/DocFilesComparator.java b/src/share/classes/jdk/codetools/apidiff/model/DocFilesComparator.java new file mode 100644 index 0000000..d4d1d9b --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/DocFilesComparator.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.tools.JavaFileObject; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.model.API.LocationKind; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for the "doc files" for a package. + * These are the files in the "doc-files" subdirectory of the + * source directories for the package, or in the "doc-files" + * subdirectory of the directory for the package in the generated + * API. + * + *

        The comparison includes: + *

          + *
        • the documentation comment for HTML files found in the source directory + *
        • the API description for HTML files found in the generated API directory + *
        • other files in the source and generated API directories + *
        + */ +public class DocFilesComparator { + /** The APIs to be compared. */ + protected final Set apis; + /** The command-line options. */ + protected final Options options; + /** The reporter to which to report any differences. */ + protected final Reporter reporter; + + /** + * Creates a comparator to compare "doc files" across a set of APIs. + * + * @param apis the set of APIs + * @param reporter the reporter to which to report differences + */ + public DocFilesComparator(Set apis, Options options, Reporter reporter) { + this.apis = apis; + this.options = options; + this.reporter = reporter; + } + + /** + * Compares all the doc files with the same name within instances of an API. + * + * @param pPos the position for the package containing the doc files + * @param table the table containing the doc files to be compared + * + * @return {@code true} if and only if all the elements are equal + */ + public boolean compareAll(Position pPos, Map> table) { + boolean allEqual = true; + for (Map.Entry> e : table.entrySet()) { + String name = e.getKey(); + APIMap fMap = e.getValue(); + boolean equal = compare(pPos.docFile(name), fMap); + allEqual &= equal; + } + return allEqual; + } + + private boolean compare(Position fPos, APIMap fMap) { + boolean allEqual = false; + reporter.comparing(fPos, fMap); + try { + allEqual = checkMissing(fPos, fMap); + if (fMap.size() > 1) { + // compare doc comments and API description for HTML files; + // compare file contents for all other files (typically images) + if (fMap.values().iterator().next().getKind() == JavaFileObject.Kind.HTML) { + allEqual &= compareDocComments(fPos, fMap); + allEqual &= compareApiDescriptions(fPos, fMap); + } else { + allEqual &= compareFiles(fPos, fMap); + } + } + } finally { + reporter.completed(fPos, allEqual); + } + return allEqual; + } + + /** + * Checks whether any expected doc files are missing in any APIs. + * Missing files will be reported to the comparator's reporter. + * + * @param fPos the position of the file + * @param fMap the map giving the files in the different APIs + * @return {@code true} if all the expected files are found + */ + private boolean checkMissing(Position fPos, APIMap fMap) { + Set missing = apis.stream() + .filter(a -> !fMap.containsKey(a)) + .collect(Collectors.toSet()); // warning: unordered + + if (missing.isEmpty()) { + return true; + } else { + reporter.reportMissing(fPos, missing); + return false; + } + } + + + /** + * Compares the documentation for the doc files at a given position in + * different instances of an API. + * + * @param fPos the position + * @param fMap the map of file files + * @return {@code true} if and only if all the instances of the documentation are equal + */ + protected boolean compareDocComments(Position fPos, APIMap fMap) { + if (!options.compareDocComments()) { + return true; + } + + // TODO: make this depend on command-line options to compare some combination of + // raw doc comments, (parsed) doc comments. +// APIMap docComments = APIMap.of(); +// for (Map.Entry e : eMap.entrySet()) { +// API api = e.getKey(); +// Element te = e.getValue(); +// DocCommentTree dct = api.getDocComment(te); +// if (dct != null) { +// docComments.put(api, dct); +// } +// } +// DocCommentComparator dc = new DocCommentComparator(eMap.keySet()); +// return dc.compare(ePos, docComments); + + APIMap rawDocComments = fMap.map((api, df) -> { + JavaFileObject fo = df.files.get(LocationKind.SOURCE); + return fo == null ? "" : api.getTrees().getDocCommentTree(fo).toString(); + }); + + // raw doc comments are equal if none of the doc-files has a doc comment, + // or if they all have the same doc comment. + boolean allEqual = rawDocComments.isEmpty() + || rawDocComments.size() == fMap.size() && rawDocComments.values().stream().distinct().count() == 1; + if (!allEqual) { + reporter.reportDifferentRawDocComments(fPos, rawDocComments); + } + return allEqual; + } + + /** + * Compares the API descriptions for the doc files at a given position + * in different instances of an API. + * + * @param fPos the position + * @param fMap the map of doc files + * + * @return {@code true} if and only if all the instances of the API description are equal + */ + protected boolean compareApiDescriptions(Position fPos, APIMap fMap) { + if (!options.compareApiDescriptions()) { + return true; + } + + APIMap apiDescriptions = fMap.map((api, df) -> { + JavaFileObject fo = df.files.get(LocationKind.API); + return fo == null ? "" : api.getApiDescription(fo); + }); + + // API descriptions are equal if none of the doc-files has a description, + // or if they all have the same description. + boolean allEqual = apiDescriptions.isEmpty() + || apiDescriptions.size() == fMap.size() && apiDescriptions.values().stream().distinct().count() == 1; + if (!allEqual) { + reporter.reportDifferentApiDescriptions(fPos, apiDescriptions); + } + return allEqual; + } + + private boolean compareFiles(Position fPos, APIMap fMap) { + return compareFiles(fPos, fMap, API.LocationKind.SOURCE) + && compareFiles(fPos, fMap, API.LocationKind.API); + } + + private boolean compareFiles(Position fPos, APIMap fMap, API.LocationKind kind) { + // If there are no files at all in this location-kind, that's OK, + // and they are vacuously equal. + boolean noFiles = fMap.values().stream().allMatch(df -> df.files.get(kind) == null); + if (noFiles) { + return true; + } + + // But if some files are present and some are missing, that's an automatic difference. + boolean missingFiles = fMap.values().stream().anyMatch(df -> df.files.get(kind) == null); + if (missingFiles) { + return false; + } + + // Otherwise, compare the contents of each file (other than the first) against the first. + // While it would be possible to open streams on each file, and read/compare the streams + // in parallel, that seems overall. And note, for reference, we do read the full contents + // of HTML files into memory. + byte[] ref = null; + for (Map.Entry entry : fMap.entrySet()) { + API api = entry.getKey(); + DocFile df = entry.getValue(); + byte[] bytes = api.getAllBytes(df.files.get(kind)); + if (bytes == null) { + return false; + } + if (ref == null) { + ref = bytes; + } else if (!Arrays.equals(ref, bytes)) { + return false; + } + } + return true; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/ElementComparator.java b/src/share/classes/jdk/codetools/apidiff/model/ElementComparator.java new file mode 100644 index 0000000..f50f7a9 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/ElementComparator.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A base class for comparators that compare elements. + * + * @param the type of element to be compared + */ +public abstract class ElementComparator { + /** The APIs to be compared. */ + protected final Set apis; + /** The command-line options. */ + protected final Options options; + /** The access kind. */ + protected final AccessKind accessKind; + /** The reporter to which to report any differences. */ + protected final Reporter reporter; + + /** + * Creates an instance of comparator for elements. + * + * @param apis the APIs being compared + * @param options the command line options + * @param reporter the reporter to which to report any differences. + */ + protected ElementComparator(Set apis, Options options, Reporter reporter) { + this.apis = apis; + this.options = options; + this.accessKind = options.getAccessKind(); + this.reporter = reporter; + } + + /** + * Compares all the elements with equivalent keys within instances of an API. + * + * @param table the table containing the elements to be compared + * @return {@code true} if and only if all the elements are equal + */ + public boolean compareAll(KeyTable table) { + boolean allEqual = true; + for (Map.Entry> e : table.entries()) { + ElementKey k = e.getKey(); + APIMap v = e.getValue(); + boolean equal = compare(k, v); + allEqual &= equal; + } + return allEqual; + } + + /** + * Compares all the elements at equivalent positions within instances of an API. + * + * @param f a function to provide the position of each row in the table + * @param table the table containing the elements to be compared + * @return {@code true} if and only if all the elements are equal + */ + public boolean compareAll(Function f, IntTable table) { + boolean allEqual = true; + for (int i = 0; i < table.size(); i++) { + allEqual &= compare(f.apply(i), table.entries(i)); + } + return allEqual; + } + + /** + * Compares the elements found at the given position in different APIs. + * + *

        This implementation delegates to {@link #compare(Position, APIMap)}. + * + * @param eKey the key for the element + * @param eMap the map giving the elements in the different APIs + * @return {@code true} if all the elements are equal + */ + public boolean compare(ElementKey eKey, APIMap eMap) { + return compare(Position.of(eKey), eMap); + } + + /** + * Compares the elements found at the given position in different APIs. + * + * @param ePos the position of the element + * @param eMap the map giving the elements in the different APIs + * @return {@code true} if all the elements are equal + */ + public abstract boolean compare(Position ePos, APIMap eMap); + + /** + * Checks whether any expected elements are missing in any APIs. + * Missing elements will be reported to the comparator's reporter. + * + * @param ePos the position of the element + * @param eMap the map giving the elements in the different APIs + * @return {@code true} if all the expected elements are found + */ + public boolean checkMissing(Position ePos, APIMap eMap) { + Set missing = apis.stream() + .filter(a -> !eMap.containsKey(a)) + .collect(Collectors.toSet()); // warning: unordered + + if (missing.isEmpty()) { + return true; + } else { + reporter.reportMissing(ePos, missing); + return false; + } + } + + /** + * Compares the annotations for elements at a given position in + * different instances of an API. + * + * @param ePos the position + * @param eMap the map of elements + * @return {@code true} if and only if all the annotations are equal + */ + protected boolean compareAnnotations(Position ePos, APIMap eMap) { + AnnotationComparator ac = new AnnotationComparator(eMap.keySet(), accessKind, reporter); + return ac.compareAll(ePos, eMap); + } + + /** + * Compares the documentation for the elements at a given position in + * different instances of an API. + * + * @param ePos the position + * @param eMap the map of elements + * @return {@code true} if and only if all the instances of the documentation are equal + */ + protected boolean compareDocComments(Position ePos, APIMap eMap) { + if (!options.compareDocComments()) { + return true; + } + +// APIMap docComments = APIMap.of(); +// for (Map.Entry e : eMap.entrySet()) { +// API api = e.getKey(); +// Element te = e.getValue(); +// DocCommentTree dct = api.getDocComment(te); +// if (dct != null) { +// docComments.put(api, dct); +// } +// } +// DocCommentComparator dc = new DocCommentComparator(eMap.keySet()); +// return dc.compare(ePos, docComments); + + APIMap rawDocComments = APIMap.of(); + for (Map.Entry entry : eMap.entrySet()) { + API api = entry.getKey(); + Element e = entry.getValue(); + String c = getDocComment(api, e); + if (c != null) { + rawDocComments.put(api, c); + } + } + // raw doc comments are equal if none of the elements has a doc comment, + // or if they all have the same doc comment. + boolean allEqual = rawDocComments.isEmpty() + || rawDocComments.size() == eMap.size() && rawDocComments.values().stream().distinct().count() == 1; + if (!allEqual) { + reporter.reportDifferentRawDocComments(ePos, rawDocComments); + } + return allEqual; + } + + /** + * Returns the doc comment for an element in a given API. + * + *

        This implementation uses {@link API#getElements()}.getDocComment(Element)}. + * + * @param api the API + * @param e the element + * + * @return the doc comment + */ + protected String getDocComment(API api, Element e) { + return api.getElements().getDocComment(e); + } + + /** + * Compares the API descriptions for the elements at a given position + * in different instances of an API. + * + * @param ePos the position + * @param eMap the map of elements + * + * @return {@code true} if and only if all the instances of the API description are equal + */ + protected boolean compareApiDescriptions(Position ePos, APIMap eMap) { + if (!options.compareApiDescriptions()) { + return true; + } + + APIMap apiDescriptions = APIMap.of(); + for (Map.Entry entry : eMap.entrySet()) { + API api = entry.getKey(); + Element e = entry.getValue(); + String c = getApiDescription(api, e); + if (c != null) { + apiDescriptions.put(api, c); + } + } + // API descriptions are equal if none of the elements has a description, + // or if they all have the same doc comment. + boolean allEqual = apiDescriptions.isEmpty() + || apiDescriptions.size() == eMap.size() && apiDescriptions.values().stream().distinct().count() == 1; + if (!allEqual) { + reporter.reportDifferentApiDescriptions(ePos, apiDescriptions); + } + return allEqual; + } + + + /** + * Returns the API description for an element in a given API. + * + *

        This implementation uses {@link API#getApiDescription(Element)}. + * + * @param api the API + * @param e the element + * + * @return the API description + */ + protected String getApiDescription(API api, Element e) { + return api.getApiDescription(e); + } + + /** + * Compares the doc files for a module or package at a given position + * in different instance of an API. + * + * @param ePos the position + * @param eMap the map of elements + * + * @return {@code true} if and only if all the doc files are equal + */ + protected boolean compareDocFiles(Position ePos, APIMap eMap) { + Map> fMap = DocFile.listDocFiles(eMap); + DocFilesComparator dfc = new DocFilesComparator(apis, options, reporter); + return dfc.compareAll(ePos, fMap); + } + + /** + * Compares the modifiers for the elements at a given position in + * different instances of an API. + * + * @param ePos the position + * @param eMap the map of elements + * @return {@code true} if and only if all the modifiers are equal + */ + protected boolean compareModifiers(Position ePos, APIMap eMap) { + if (eMap.size() == 1) + return true; + + Set baseline = null; + for (E e : eMap.values()) { + Set modifiers = e.getModifiers(); + if (modifiers.contains(Modifier.NATIVE) || modifiers.contains(Modifier.SYNCHRONIZED)) { + modifiers = EnumSet.copyOf(modifiers); + modifiers.removeAll(EnumSet.of(Modifier.NATIVE, Modifier.SYNCHRONIZED)); + } + if (baseline == null) { + baseline = modifiers; + } else if (!baseline.equals(modifiers)) { + reporter.reportDifferentModifiers(ePos, eMap); + return false; + } + } + + return true; + } + +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/ElementExtras.java b/src/share/classes/jdk/codetools/apidiff/model/ElementExtras.java new file mode 100644 index 0000000..ef103e9 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/ElementExtras.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** + * A class to encapsulate reflective access to recent API, thereby allowing the + * tool to run on older versions of the platform when such API is not required. + */ +public class ElementExtras { + public class Fault extends Error { + Fault(Throwable cause) { + super(cause); + } + } + + private static final ElementExtras instance = new ElementExtras(); + + private final Runtime.Version version = Runtime.version(); + private final Method getRecordComponentsMethod; + private final Method getPermittedSubclassesMethod; + + /** + * Returns the instance of this class. + * + * @return the instance + */ + public static ElementExtras instance() { + return instance; + } + + /** + * Creates the one instance of this class. + */ + private ElementExtras() { + getRecordComponentsMethod = lookup(14, TypeElement.class, "getRecordComponents"); + getPermittedSubclassesMethod = lookup(15, TypeElement.class, "getPermittedSubclasses"); + } + + private Method lookup(int since, Class c, String name, Class... paramTypes) { + try { + if (version.feature() >= since) { + return c.getDeclaredMethod(name, paramTypes); + } + } catch (ReflectiveOperationException e) { + System.err.println("Cannot find " + c + "." + name + "(" + + Stream.of(paramTypes).map(Class::getSimpleName).collect(Collectors.joining(",")) + + ")"); + } + return null; + } + + /** + * Invokes {@code TypeElement.getRecordComponents()} for a type element, if the method is available, + * and returns the result of calling that method. + * If the method is not available, {@code null} is returned. + * + * @param e the element + * @return the result of calling {@code TypeElement.getRecordComponent()}, or {@code null} + */ + public List getRecordComponents(TypeElement e) { + if (getRecordComponentsMethod == null) { + return Collections.emptyList(); + } + + try { + return (List) getRecordComponentsMethod.invoke(e); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new Fault(ite); + } + } catch (ReflectiveOperationException roe) { + throw new Fault(roe); + } + } + + /** + * Invokes {@code TypeElement.getPermittedSubclasses()} for a type element, if the method is available, + * and returns the result of calling that method. + * If the method is not available, {@code null} is returned. + * + * @param e the element + * @return the result of calling {@code TypeElement.getPermittedSubclasses()}, or {@code null} + */ + public List getPermittedSubclasses(TypeElement e) { + if (getPermittedSubclassesMethod == null) { + return Collections.emptyList(); + } + + try { + return (List) getPermittedSubclassesMethod.invoke(e); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new Fault(ite); + } + } catch (ReflectiveOperationException roe) { + throw new Fault(roe); + } + } + +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/ElementKey.java b/src/share/classes/jdk/codetools/apidiff/model/ElementKey.java new file mode 100644 index 0000000..5691948 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/ElementKey.java @@ -0,0 +1,825 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.lang.ref.SoftReference; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.WeakHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.SimpleElementVisitor14; + +/** + * A lightweight wrapper for an element that is independent of any API environment and + * that can be used to associate corresponding elements in different instances of an API. + * It can also be used to check for nominal equality of reference types in a type mirror. + * + *

        Values are cached in a memory-sensitive cache. + * + * @see KeyTable + */ +public abstract sealed class ElementKey implements Comparable { + /** + * The {@code kind} of an element key. + */ + public enum Kind { + /** A module. */ + MODULE, + /** A package. */ + PACKAGE, + /** A class or interface. */ + TYPE, + /** An executable element. */ + EXECUTABLE, + /** A variable (field) element. */ + VARIABLE, + /** A type parameter element. */ + TYPE_PARAMETER + } + + private static final ElementVisitor factory = new SimpleElementVisitor14<>() { + @Override + public ElementKey visitModule(ModuleElement e, Void _p) { + return new ModuleElementKey(e); + } + @Override + public ElementKey visitPackage(PackageElement e, Void _p) { + return new PackageElementKey(e); + } + @Override + public ElementKey visitType(TypeElement e, Void _p) { + return new TypeElementKey(e); + } + @Override + public ElementKey visitExecutable(ExecutableElement e, Void _p) { + return new ExecutableElementKey(e); + } + @Override + public ElementKey visitVariable(VariableElement e, Void _p) { + return new VariableElementKey(e); + } + @Override + public ElementKey visitTypeParameter(TypeParameterElement e, Void _p) { + return new TypeParameterElementKey(e); + } + @Override + public ElementKey visitRecordComponent(RecordComponentElement e, Void _p) { + return new VariableElementKey((VariableElement) e); + } + @Override + public ElementKey defaultAction(Element e, Void _p) { + throw new UnsupportedOperationException(e.getKind() + " " + e); + } + }; + + // TODO: handle javax.lang.model.element.UnknownElementException; maybe handle visitUnknown in the factory visitor; + // the error may arise for unknown annotation types + private static final Cache cache = new Cache<>(e -> factory.visit(e, null)); + + /** + * Returns the key for an element. + * + * @param e the element + * + * @return they key + */ + public static ElementKey of(Element e) { + return cache.get(e); + } + + public final Kind kind; + + /** + * Creates a key with a given kind. + * + * @param kind the kind. + */ + protected ElementKey(Kind kind) { + this.kind = kind; + } + + /** + * Returns the enclosing element key, or {@code null} if none. + * + * @return the enclosing element key + */ + public abstract ElementKey getEnclosingKey(); + + /** + * Applies a visitor to this key. + * + * @param v the visitor + * @param p a visitor-specified parameter + * @param the type of the result + * @param

        the type of the parameter + * + * @return a visitor-specified result + */ + public abstract R accept(Visitor v, P p); + + /** + * Checks whether this key is of a given kind. + * + * @param kind the kind + * + * @return {@code true} if and only if the key is of the given kind + */ + public boolean is(Kind kind) { + return kind == this.kind; + } + + /** + * Checks whether this key is of a given kind. + * + * @param kind the kind + * + * @return {@code true} if and only if the key is of the given kind + */ + public abstract boolean is(ElementKind kind); + + /** + * Compares two names. + * + * @param n1 the first name + * @param n2 the second name + * + * @return the result of the comparison + */ + protected static int compare(Name n1, Name n2) { + // For now, just do a simple lexicographic compare. + // We may want to upgrade this to do a case-ignore comparison first, + // and only do a case-significant comparison if the case-ignore reports equal. + // This would be to get the following example sort order: + // double, Double, float, Float, int, Integer, etc + return CharSequence.compare(n1, n2); + } + + /** + * Compares two lists of items. + * + * @param the type of items in the list + * @param l1 the first list + * @param l2 the second list + * + * @return the result of the comparison + */ + protected static > int compare(List l1, List l2) { + if (l1.isEmpty() && l2.isEmpty()) { + return 0; + } + Iterator iter1 = l1.iterator(); + Iterator iter2 = l2.iterator(); + while (iter1.hasNext() && iter2.hasNext()) { + int i = iter1.next().compareTo(iter2.next()); + if (i != 0) { + return i; + } + } + return iter1.hasNext() ? +1 : iter2.hasNext() ? -1 : 0; + } + + /** + * Compares two names for equality. + * + * @param n1 the first name + * @param n2 the second name + * + * @return {@code true} if and only if the two names have the exact same contents + */ + protected static boolean equals(Name n1, Name n2) { + return n1.contentEquals(n2); + } + + /** + * Returns a non-zero hash code for a name. + * + * @param n the name + * + * @return the hash code + */ + protected static int hashCode(Name n) { + int hashCode = n.toString().hashCode(); + return (hashCode == 0) ? 1 : hashCode; + } + + /** + * A visitor of element keys, in the style of the visitor design pattern. + * Classes implementing this interface are used to operate on an element key + * when the kind of key is unknown at compile time. + * When a visitor is passed to an element key's {@code accept} method, + * the visitXyz method applicable to that key is invoked. + * + * @param the return type of this visitor's methods. + * Use Void for visitors that do not need to return results. + * @param

        the type of the additional parameter to this visitor's methods. + * Use Void for visitors that do not need an additional parameter. + */ + public interface Visitor { + /** + * Visits a key for a module element. + * + * @param k the key to visit + * @param p a visitor-specified parameter + * + * @return a visitor-specified result + */ + R visitModuleElement(ModuleElementKey k, P p); + + /** + * Visits a key for a package element. + * + * @param k the key to visit + * @param p a visitor-specified parameter + * + * @return a visitor-specified result + */ + R visitPackageElement(PackageElementKey k, P p); + + /** + * Visits a key for a type element. + * + * @param k the key to visit + * @param p a visitor-specified parameter + * + * @return a visitor-specified result + */ + R visitTypeElement(TypeElementKey k, P p); + + /** + * Visits a key for an executable element. + * + * @param k the key to visit + * @param p a visitor-specified parameter + * + * @return a visitor-specified result + */ + R visitExecutableElement(ExecutableElementKey k, P p); + + /** + * Visits a key for a variable element. + * + * @param k the key to visit + * @param p a visitor-specified parameter + * + * @return a visitor-specified result + */ + R visitVariableElement(VariableElementKey k, P p); + + /** + * Visits a key for a type parameter element. + * + * @param k the key to visit + * @param p a visitor-specified parameter + * + * @return a visitor-specified result + */ + R visitTypeParameterElement(TypeParameterElementKey k, P p); + } + + /** + * A memory-sensitive cache of values generated by a factory. + * The cache is a {@link WeakHashMap} of {@link SoftReference} values. + * + * @param the type of keys for the cache + * @param the type of values stored in the cache + */ + static class Cache { + private final Function factory; + private final WeakHashMap> map = new WeakHashMap<>(); + + /** + * Creates a cache for values generated by a factory. + * + * @param factory the factory for values to be cached + */ + Cache(Function factory) { + this.factory = factory; + } + + /** + * Gets the value for a key, creating it if it does not already exist. + * Because values are stored in the cache with soft references, there + * is no guarantee that the same value will be returned for the same key. + * + * @param k the key + * + * @return the value + */ + synchronized V get(K k) { + SoftReference vr = map.get(k); + V v = (vr == null) ? null : vr.get(); + if (v == null) { + v = factory.apply(k); + map.put(k, new SoftReference<>(v)); + } + return v; + } + } + + /** + * An element key for a module element. + */ + public static final class ModuleElementKey extends ElementKey { + + public final Name name; + private int hashCode; + + ModuleElementKey(ModuleElement me) { + super(Kind.MODULE); + name = me.getQualifiedName(); + } + + @Override + public ElementKey getEnclosingKey() { + return null; + } + + @Override + public int compareTo(ElementKey other) { + int ck = kind.compareTo(other.kind); + return (ck != 0) ? ck : compare(name, ((ModuleElementKey) other).name); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + return equals(name, ((ModuleElementKey) other).name); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = hashCode(name); + } + return hashCode; + } + + @Override + public String toString() { + return "ModuleKey[" + name + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitModuleElement(this, p); + } + + @Override + public boolean is(ElementKind kind) { + return (kind == ElementKind.MODULE); + } + + } + + /** + * An element key for a package element. + */ + public static final class PackageElementKey extends ElementKey { + public final ElementKey moduleKey; + public final Name name; + private int hashCode; + + PackageElementKey(PackageElement pe) { + super(Kind.PACKAGE); + ModuleElement me = (ModuleElement) pe.getEnclosingElement(); + moduleKey = (me == null || me.isUnnamed()) ? null : ElementKey.of(me); + name = pe.getQualifiedName(); + } + + @Override + public ElementKey getEnclosingKey() { + return moduleKey; + } + + @Override + public int compareTo(ElementKey other) { + int ck = kind.compareTo(other.kind); + if (ck != 0) { + return ck; + } + PackageElementKey otherPackage = (PackageElementKey) other; + int cm; + if (moduleKey == null) { + cm = otherPackage.moduleKey == null ? 0 : -1; + } else if (otherPackage.moduleKey == null) { + cm = 1; + } else { + cm = moduleKey.compareTo(otherPackage.moduleKey); + } + return (cm != 0) ? cm : compare(name, otherPackage.name); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + PackageElementKey otherPackage = (PackageElementKey) other; + return Objects.equals(moduleKey, otherPackage.moduleKey) + && equals(name, otherPackage.name); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = (moduleKey == null ? 0 : moduleKey.hashCode() * 37) + hashCode(name); + } + return hashCode; + } + + @Override + public String toString() { + return "PackageKey[" + (moduleKey == null ? "" : moduleKey + ",") + name + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitPackageElement(this, p); + } + + @Override + public boolean is(ElementKind kind) { + return (kind == ElementKind.PACKAGE); + } + + } + + /** + * An element key for a type element. + */ + public static final class TypeElementKey extends ElementKey { + public final ElementKey enclosingKey; // A type can be enclosed in a package or another type + public final Name name; + private int hashCode; + + TypeElementKey(TypeElement te) { + super(Kind.TYPE); + enclosingKey = ElementKey.of(te.getEnclosingElement()); + name = te.getSimpleName(); + } + + @Override + public ElementKey getEnclosingKey() { + return enclosingKey; + } + + @Override + public int compareTo(ElementKey other) { + int ck = kind.compareTo(other.kind); + if (ck != 0) { + return ck; + } + TypeElementKey otherType = (TypeElementKey) other; + int ce = enclosingKey.compareTo(otherType.enclosingKey); + return (ce != 0) ? ce : compare(name, otherType.name); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + TypeElementKey otherType = (TypeElementKey) other; + return enclosingKey.equals(otherType.enclosingKey) + && equals(name, otherType.name); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = enclosingKey.hashCode() * 37 + hashCode(name); + } + return hashCode; + } + + @Override + public String toString() { + return "TypeKey[" + enclosingKey + "," + name + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitTypeElement(this, p); + } + + @Override + public boolean is(Kind kind) { + return (kind == Kind.TYPE); + } + + /** + * {@inheritDoc} + * By design, {@code TypeElementKey} does not include details about the element kind, + * and so always throws {@code UnsupportedOperationException} for any element kind + * that is a type. + * + * @param kind the kind + * + * @return {@code false} for all element kinds that are not the kind of a type + * @throws UnsupportedOperationException if the element kind is the kind of a type + */ + @Override + public boolean is(ElementKind kind) { + return switch (kind) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE -> throw new UnsupportedOperationException(); + default -> false; + }; + } + + } + + public static abstract sealed class MemberElementKey extends ElementKey { + public final ElementKey typeKey; + public final ElementKind elementKind; + public final Name name; + + protected MemberElementKey(Kind kind, Element e) { + super(kind); + this.typeKey = ElementKey.of(e.getEnclosingElement()); + this.elementKind = e.getKind(); + this.name = e.getSimpleName(); + } + + @Override + public ElementKey getEnclosingKey() { + return typeKey; + } + + @Override + public boolean is(ElementKind kind) { + return kind == elementKind; + } + + } + + /** + * An element key for an executable element. + */ + public static final class ExecutableElementKey extends MemberElementKey { + + public final List params; + private int hashCode; + + ExecutableElementKey(ExecutableElement ee) { + super(Kind.EXECUTABLE, ee); + params = ee.getParameters().stream() + .map(e -> TypeMirrorKey.of(e.asType())) + .collect(Collectors.toList()); + } + + @Override + public int compareTo(ElementKey other) { + int ck = kind.compareTo(other.kind); + if (ck != 0) { + return ck; + } + + ExecutableElementKey otherExecutable = (ExecutableElementKey) other; + + int ct = typeKey.compareTo(otherExecutable.typeKey); + if (ct != 0) { + return ct; + } + + int cek = elementKind.compareTo(otherExecutable.elementKind); + if (cek != 0) { + return cek; + } + + int cn = CharSequence.compare(name, otherExecutable.name); + if (cn != 0) { + return cn; + } + + return compare(params, otherExecutable.params); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + ExecutableElementKey otherExecutable = (ExecutableElementKey) other; + return kind == otherExecutable.kind + && elementKind == otherExecutable.elementKind + && name.contentEquals(otherExecutable.name) + && params.equals(otherExecutable.params); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = kind.ordinal() * 37 + typeKey.hashCode(); + hashCode = hashCode * 37 + elementKind.hashCode(); + hashCode = hashCode * 37 + hashCode(name); + hashCode = hashCode * 37 + params.hashCode(); + } + return hashCode; + } + + @Override + public String toString() { + String p = params.stream().map(Object::toString).collect(Collectors.joining(",")); + return "ExecutableKey[" + elementKind + ":" + name + "(" + p + ")]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitExecutableElement(this, p); + } + } + + + /** + * An element key for a variable element. + */ + public static final class VariableElementKey extends MemberElementKey { + + private int hashCode; + + VariableElementKey(VariableElement ve) { + super(Kind.VARIABLE, ve); + } + + @Override + public int compareTo(ElementKey other) { + int ck = kind.compareTo(other.kind); + if (ck != 0) { + return ck; + } + + VariableElementKey otherVariable = (VariableElementKey) other; + + int ct = typeKey.compareTo(otherVariable.typeKey); + if (ct != 0) { + return ct; + } + + int cek = elementKind.compareTo(otherVariable.elementKind); + if (cek != 0) { + return cek; + } + + return CharSequence.compare(name, otherVariable.name); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + VariableElementKey otherVariable = (VariableElementKey) other; + return kind == otherVariable.kind + && elementKind == otherVariable.elementKind + && name.contentEquals(otherVariable.name); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = typeKey.hashCode() * 37 + hashCode(name); + hashCode = hashCode * 37 + elementKind.hashCode(); + } + return hashCode; + } + + @Override + public String toString() { + return "VariableKey[" + elementKind + ":" + name + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitVariableElement(this, p); + } + } + + /** + * An element key for a type parameter element. + */ + public static final class TypeParameterElementKey extends ElementKey { + + public final ElementKey typeKey; + public final Name name; + private int hashCode; + + TypeParameterElementKey(TypeParameterElement ve) { + super(Kind.TYPE_PARAMETER); + typeKey = ElementKey.of(ve.getEnclosingElement()); + name = ve.getSimpleName(); + } + + @Override + public ElementKey getEnclosingKey() { + return typeKey; + } + + @Override + public int compareTo(ElementKey other) { + int ck = kind.compareTo(other.kind); + if (ck != 0) { + return ck; + } + + TypeParameterElementKey otherTypeParameter = (TypeParameterElementKey) other; + + int ct = typeKey.compareTo(otherTypeParameter.typeKey); + if (ct != 0) { + return ct; + } + + return CharSequence.compare(name, otherTypeParameter.name); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + TypeParameterElementKey otherVariable = (TypeParameterElementKey) other; + return kind == otherVariable.kind + && name.contentEquals(otherVariable.name); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = typeKey.hashCode() * 37 + hashCode(name); + } + return hashCode; + } + + @Override + public String toString() { + return "TypeParameterElementKey[" + name + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitTypeParameterElement(this, p); + } + + @Override + public boolean is(ElementKind kind) { + return (kind == ElementKind.TYPE_PARAMETER); + } + } + + +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/ExecutableComparator.java b/src/share/classes/jdk/codetools/apidiff/model/ExecutableComparator.java new file mode 100644 index 0000000..5997623 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/ExecutableComparator.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.model.Position.RelativePosition; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link ExecutableElement executable elements}: constructors, methods, and members of annotation types. + * + *

        The comparison includes: + *

          + *
        • the "signature" of the element: + * its annotations, modifiers, type parameters, parameters, return type (if appropriate), throws + * and default value (if appropriate) + *
        • the documentation comment for the element + *
        + */ +public class ExecutableComparator extends ElementComparator { + + /** + * Creates a comparator to compare executable elements across a set of APIs. + * + * @param apis the set of APIs + * @param options the command-line options + * @param reporter the reporter to which to report differences + */ + public ExecutableComparator(Set apis, Options options, Reporter reporter) { + super(apis, options, reporter); + } + + /** + * Compare instances of an executable element found at a given position in different APIs. + * + * @param ePos the position of the element + * @param eMap the map giving the instance of the type element in different APIs + * @return {@code true} if all the instances are equivalent + */ + @Override + public boolean compare(Position ePos, APIMap eMap) { + boolean allEqual = false; + reporter.comparing(ePos, eMap); + try { + allEqual = checkMissing(ePos, eMap); + if (eMap.size() > 1) { + allEqual &= compareSignatures(ePos, eMap); + allEqual &= compareDocComments(ePos, eMap); + allEqual &= compareApiDescriptions(ePos, eMap); + } + } finally { + reporter.completed(ePos, allEqual); + } + return allEqual; + } + + private boolean compareSignatures(Position ePos, APIMap eMap) { + // TODO: compare signature: documented annotations, modifiers, kind, type parameters, + // return type if method, parameter types, throws, default values if annotation type + // By construction, the kind (method or constructor) should always be the same, + // and does not need to be compared. Also by construction, the basic parameter + // types will be the same, but the documented annotations on the types might differ + // and so need to be compared. + return compareAnnotations(ePos, eMap) + & compareModifiers(ePos, eMap) + & compareTypeParameters(ePos, eMap) + & compareReturnType(ePos, eMap) + & compareReceiverType(ePos, eMap) + & compareParameters(ePos, eMap) + & compareThrows(ePos, eMap) + & compareDefaultValue(ePos, eMap); + } + + private boolean compareTypeParameters(Position ePos, APIMap eMap) { + TypeParameterComparator tc = new TypeParameterComparator(eMap.keySet(), options, reporter); + IntTable typarams = IntTable.of(eMap, ExecutableElement::getTypeParameters); + return tc.compareAll(ePos, typarams); + } + + private boolean compareParameters(Position ePos, APIMap eMap) { + VariableComparator vc = new VariableComparator(eMap.keySet(), options, reporter); + IntTable params = IntTable.of(eMap, ExecutableElement::getParameters); + return vc.compareAll(ePos::parameter, params); + } + + private boolean compareReceiverType(Position ePos, APIMap eMap) { + if (ePos.is(ElementKind.CONSTRUCTOR)) + return true; + + TypeMirrorComparator tc = new TypeMirrorComparator(eMap.keySet(), reporter); + APIMap rMap = eMap.map((api, e) -> { + TypeMirror t = e.getReceiverType(); + if (t == null) { + // TODO: make this print optional + // System.err.println("unexpected null for getReceiverType " + ePos + " " + e); + t = api.getTypes().getNoType(TypeKind.NONE); + } + return t; + }); + return tc.compare(ePos.receiverType(), rMap); + } + + private boolean compareReturnType(Position ePos, APIMap eMap) { + if (ePos.is(ElementKind.CONSTRUCTOR)) + return true; + + TypeMirrorComparator tc = new TypeMirrorComparator(eMap.keySet(), reporter); + APIMap rMap = eMap.map(ExecutableElement::getReturnType); + return tc.compare(ePos.returnType(), rMap); + } + + private boolean compareThrows(Position ePos, APIMap eMap) { + Map> map = extractThrownTypes(eMap); + + // compare the groups of thrown types + Set first = null; + boolean allEqual = true; + for (Map.Entry> entry : map.entrySet()) { + TypeMirrorKey tk = entry.getKey(); + APIMap tMap = entry.getValue(); + Position pos = ePos.exception(tk); + if (tMap.size() < eMap.size()) { + // Note: using reportDifferentTypes even if some of the thrown types are missing + eMap.keySet().forEach(a -> tMap.putIfAbsent(a, null)); + reporter.reportDifferentTypes(pos, tMap); + allEqual = false; + } else { + TypeMirrorComparator tmc = new TypeMirrorComparator(eMap.keySet(), reporter); + allEqual = allEqual & tmc.compare(pos, tMap); + } + } + + if (allEqual) { + return true; + } else { + APIMap> thrownTypes = APIMap.of(); + eMap.forEach((api, ee) -> thrownTypes.put(api, ee.getThrownTypes())); + reporter.reportDifferentThrownTypes(ePos, thrownTypes); + return false; + } + } + + private Map> extractThrownTypes(APIMap eMap) { + // The order in which thrown types may be listed is not significant, + // so group the thrown types by their TypeMirrorKey. + // Note that thrown types can be type variables, and even annotated + Map> map = new TreeMap<>(); + for (Map.Entry entry : eMap.entrySet()) { + API api = entry.getKey(); + ExecutableElement ee = entry.getValue(); + for (TypeMirror tm : ee.getThrownTypes()) { + map.computeIfAbsent(TypeMirrorKey.of(tm), _k -> APIMap.of()).put(api, tm); + } + } + return map; + } + + private boolean compareDefaultValue(Position ePos, APIMap eMap) { + boolean noDefaultValues = eMap.values().stream() + .map(ExecutableElement::getDefaultValue) + .allMatch(Objects::isNull); + if (noDefaultValues) { + return true; + } + + APIMap defaultValues = APIMap.of(); + eMap.forEach((api, ee) -> { + AnnotationValue dv = ee.getDefaultValue(); + if (dv != null) { + defaultValues.put(api, dv); + } + }); + + Position dvPos = new RelativePosition(ePos, RelativePosition.Kind.DEFAULT_VALUE); + + return new AnnotationComparator(apis, accessKind, reporter) + .new AnnotationValueComparator(dvPos).compare(defaultValues); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/HtmlParser.java b/src/share/classes/jdk/codetools/apidiff/model/HtmlParser.java new file mode 100644 index 0000000..0a6619a --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/HtmlParser.java @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2002, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +/** + * A basic HTML parser. + * + * Override the protected methods as needed to get notified of significant items + * in any file that is read. + */ +public abstract class HtmlParser { + + private Path file; + private Reader in; + private int ch; + private long charNumber; + private int lineNumber; + private boolean inScript; + private boolean xml; + + /** + * Creates an instance of an HTML parser. + */ + public HtmlParser() { } + + /** + * Read a file. + * + *

        Ideally, we should honor a charset found in a {@code } element in the head of the document, + * but the reality is that all documents use one of ASCII, ISO-8859-1 or UTF-8, and generally + * specify either ISO-8859-1 or UTF-8. UTF-8 is backwards compatible with both ASCII and ISO-8859-1, + * and so we assume the use of UTF-8, and use a decoder to replace bad values with the + * standard Unicode REPLACEMENT CHARACTER U+FFFD.

        + * + *

        As alternatives, we could initially assume an 8-bit encoding (e.g. ASCII or ISO-8859-1, + * and switch to UTF-8 if needed (but note {@link java.io.InputStreamReader} may read ahead + * some bytes for efficiency, making it hard to know the state of the stream if and when we + * need to switch. Or, we could read ahead some amount looking for a charset, and then + * reset the stream and start over with the specified charset.

        + * + * @param file the file to be read + */ + public void read(Path file) { + try { + readBuffer(file); + } catch (IOException e) { + error(file, -1, e); + } + + this.file = file; + startFile(file); + try { + int startContentIndex = 0; + charNumber = 0; + lineNumber = 1; + xml = false; + nextChar(); + + while (ch != -1) { + switch (ch) { + case '<' -> { + if (bufferIndex > startContentIndex + 1) { + int from = startContentIndex; + int to = bufferIndex - 1; + content(() -> getBufferString(from, to)); + } + html(); + startContentIndex = bufferIndex - 1; + } + + case '\n' -> { + int from = startContentIndex; + int to = bufferIndex; + content(() -> getBufferString(from, to)); + startContentIndex = bufferIndex; + nextChar(); + } + + default -> { + nextChar(); + } + } + } + } finally { + endFile(); + } + } + + /** + * The contents of the file being processed. + */ + private char[] buffer; + + /** + * The position of the next character to be read. + */ + private int bufferIndex; + + /** + * The position of the last character in the buffer. + */ + private int maxBufferIndex; + + /** + * Read a file into the buffer. + * Bad characters in the input are simply replaced with U+FFFD. + * + * @param file the file + * @throws IOException if an IO exception occurs + */ + private void readBuffer(Path file) throws IOException { + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .replaceWith("\ufffd"); + float factor = decoder.averageCharsPerByte() * 0.8f + + decoder.maxCharsPerByte() * 0.2f; + + // overestimate buffer size, to avoid reallocation + long byteSize = Files.size(file); + int bufferSize = (int) (byteSize * factor) + 128; + + if (buffer == null || buffer.length < bufferSize) { + buffer = new char[bufferSize]; + } + + try (InputStream is = Files.newInputStream(file); + Reader r = new BufferedReader(new InputStreamReader(is, decoder))) { + int offset = 0; + int n; + while ((n = r.read(buffer, offset, buffer.length - offset)) != -1) { + offset += n; + if (offset == buffer.length) { + // should not happen, but just in case... + char[] newBuffer = new char[buffer.length + buffer.length / 4]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + buffer = newBuffer; + } + } + + bufferIndex = 0; + maxBufferIndex = offset; + } + } + + /** + * Returns the position in the file of the next character to be read. + * + * @return the position + */ + protected int getBufferIndex() { + return bufferIndex; + } + + /** + * Returns a substring of content in the buffer. + * + * @param from the position of the first character of the substring + * @param to the position of the first character after the substring + * + * @return the substring + */ + protected String getBufferString(int from, int to) { + return new String(buffer, from, to - from); + } + + /** + * Returns a substring of content in the buffer, excluding leading and trailing whitespace. + * + * @param from the position of the first character of the substring + * @param to the position of the first character after the substring + * + * @return the substring + */ + protected String getTrimBufferString(int from, int to) { + while (from < to && Character.isWhitespace(buffer[from])) { + from++; + } + while (to > from && Character.isWhitespace(buffer[to - 1])) { + to--; + } + return getBufferString(from, to); + } + + /** + * Returns the position in the file of the most recently read character. + * + * @return the position + */ + protected long charNumber() { + return charNumber; + } + + /** + * Returns the line number in the file of the most recently read character. + * + * @return the line number + */ + protected int getLineNumber() { + return lineNumber; + } + + /** + * Called when a file has been opened, before parsing begins. + * This is always the first notification when reading a file. + * This implementation does nothing. + * + * @param file the file + */ + protected void startFile(Path file) { } + + /** + * Called when the parser has finished reading a file. + * This is always the last notification when reading a file, + * unless any errors occur while closing the file. + * This implementation does nothing. + */ + protected void endFile() { } + + /** + * Called when a doctype declaration is found, at the beginning of the file. + * This implementation does nothing. + * @param s a supplier for the doctype declaration + */ + protected void doctype(Supplier s) { } + + /** + * Called when the opening tag of an HTML element is encountered. + * This implementation does nothing. + * @param name the name of the tag + * @param attrs the attribute + * @param selfClosing whether this is a self-closing tag + */ + protected void startElement(String name, Map attrs, boolean selfClosing) { } + + /** + * Called when the closing tag of an HTML tag is encountered. + * This implementation does nothing. + * @param name the name of the tag + */ + protected void endElement(String name) { } + + /** + * Called for sequences of character content. + * @param content a supplier for the character content + */ + protected void content(Supplier content) { } + + /** + * Called for sequences of comment. + * @param comment a supplier for the comment + */ + protected void comment(Supplier comment) { } + + /** + * Called when an error has been encountered. + * @param file the file being read + * @param lineNumber the line number of line containing the error + * @param message a description of the error + */ + protected abstract void error(Path file, int lineNumber, String message); + + /** + * Called when an exception has been encountered. + * @param file the file being read + * @param lineNumber the line number of the line being read when the exception was found + * @param t the exception + */ + protected abstract void error(Path file, int lineNumber, Throwable t); + + /** + * Reads the next character from the buffer and returns it. + * + * @return the character + */ + protected int nextChar() { + if (bufferIndex == maxBufferIndex) { + ch = -1; + } else { + ch = buffer[bufferIndex++]; + charNumber++; + if (ch == '\n') + lineNumber++; + } + return ch; + } + + /** + * Read the start or end of an HTML tag, or the doctype declaration, + * skipping any HTML comments. + * + * Syntax: + * {@literal } or {@literal } + */ + protected void html() { + nextChar(); + if (isIdentifierStart((char) ch)) { + String name = readIdentifier().toLowerCase(Locale.ROOT); + Map attrs = htmlAttrs(); + if (attrs != null) { + boolean selfClosing = false; + if (ch == '/') { + nextChar(); + selfClosing = true; + } + if (ch == '>') { + nextChar(); + startElement(name, attrs, selfClosing); + if (name.equals("script")) { + inScript = true; + } + return; + } + } + } else if (ch == '/') { + nextChar(); + if (isIdentifierStart((char) ch)) { + String name = readIdentifier().toLowerCase(Locale.ROOT); + skipWhitespace(); + if (ch == '>') { + nextChar(); + endElement(name); + if (name.equals("script")) { + inScript = false; + } + return; + } + } + } else if (ch == '!') { + nextChar(); + if (ch == '-') { + nextChar(); + if (ch == '-') { + nextChar(); + int startCommentIndex = bufferIndex - 1; + while (ch != -1) { + int dash = 0; + while (ch == '-') { + dash++; + nextChar(); + } + // Strictly speaking, a comment should not contain "--" + // so dash > 2 is an error, dash == 2 implies ch == '>' + // See http://www.w3.org/TR/html-markup/syntax.html#syntax-comments + // for more details. + if (dash >= 2 && ch == '>') { + int to = bufferIndex - 3; + comment(() -> getBufferString(startCommentIndex, to)); + nextChar(); + return; + } + + nextChar(); + } + } + } else if (ch == '[') { + nextChar(); + if (ch == 'C') { + nextChar(); + if (ch == 'D') { + nextChar(); + if (ch == 'A') { + nextChar(); + if (ch == 'T') { + nextChar(); + if (ch == 'A') { + nextChar(); + if (ch == '[') { + while (true) { + nextChar(); + if (ch == ']') { + nextChar(); + if (ch == ']') { + nextChar(); + if (ch == '>') { + nextChar(); + return; + } + } + } + } + + } + } + } + } + } + } + } else { + int startDocTypeIndex = bufferIndex - 1; + while (ch != -1 && ch != '>') { + nextChar(); + } + nextChar(); + Pattern p = Pattern.compile("(?is)doctype\\s+html\\s?.*"); + String s = getBufferString(startDocTypeIndex, bufferIndex - 2); + if (p.matcher(s).matches()) { + doctype(() -> s); + return; + } + } + } else if (ch == '?') { + nextChar(); + if (ch == 'x') { + nextChar(); + if (ch == 'm') { + nextChar(); + if (ch == 'l') { + Map attrs = htmlAttrs(); + if (ch == '?') { + nextChar(); + if (ch == '>') { + nextChar(); + xml = true; + return; + } + } + } + } + + } + } + + if (!inScript) { + error(file, lineNumber, "bad html"); + } + } + + /** + * Read a series of HTML attributes, terminated by {@literal > }. + * Each attribute is of the form {@literal identifier[=value] }. + * "value" may be unquoted, single-quoted, or double-quoted. + */ + private Map htmlAttrs() { + Map map = Collections.emptyMap(); // default, for common case + skipWhitespace(); + + while (isIdentifierStart((char) ch)) { + String name = readAttributeName().toLowerCase(Locale.ROOT); + skipWhitespace(); + String value = null; + if (ch == '=') { + nextChar(); + skipWhitespace(); + if (ch == '\'' || ch == '"') { + char quote = (char) ch; + nextChar(); + int startValueIndex = bufferIndex - 1; + while (ch != -1 && ch != quote) { + nextChar(); + } + value = replaceSimpleEntities(getBufferString(startValueIndex, bufferIndex - 1)); + nextChar(); + } else { + int startValueIndex = bufferIndex - 1; + while (ch != -1 && !isUnquotedAttrValueTerminator((char) ch)) { + nextChar(); + } + value = getBufferString(startValueIndex, bufferIndex - 1); + } + skipWhitespace(); + } + if (map.isEmpty()) { + // change to a mutable map + map = new LinkedHashMap<>(); + } + map.put(name, value); + } + + return map; + } + + private boolean isIdentifierStart(char ch) { + return Character.isUnicodeIdentifierStart(ch); + } + + private String readIdentifier() { + int startIndex = bufferIndex - 1; + nextChar(); + while (ch != -1 && Character.isUnicodeIdentifierPart(ch)) { + nextChar(); + } + return getBufferString(startIndex, bufferIndex - 1); + } + + private String readAttributeName() { + int startIndex = bufferIndex - 1; + nextChar(); + while (ch != -1 && Character.isUnicodeIdentifierPart(ch) + || ch == '-' + || xml && ch == ':') { + nextChar(); + } + return getBufferString(startIndex, bufferIndex - 1); + } + + private boolean isWhitespace(char ch) { + return Character.isWhitespace(ch); + } + + private void skipWhitespace() { + while (isWhitespace((char) ch)) { + nextChar(); + } + } + + private String replaceSimpleEntities(String s) { + return s.replace("<", "<") + .replace(">", ">") + .replace("&", "&"); + } + + private boolean isUnquotedAttrValueTerminator(char ch) { + return switch (ch) { + case '\f', '\n', '\r', '\t', ' ', '"', '\'', '`', '=', '<', '>' -> true; + default -> false; + }; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/IntTable.java b/src/share/classes/jdk/codetools/apidiff/model/IntTable.java new file mode 100644 index 0000000..3c66f3d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/IntTable.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * A table of {@code (int x API) -> T}. + * + * The table can be used associate elements from different APIs + * that are associated by their position in their enclosing element. + * + * @param the type of item stored in this table + */ +public class IntTable { + private List> list = new ArrayList<>(); + + /** + * Creates a table from an API map and a function to provide a list of related items to populate the table. + * + * @param map the map + * @param f the function + * @param the type of values in the map + * @param the type of values in the list returned by the function + * + * @return the table + * + * @see KeyTable#of + */ + // Used to get type parameters (of type and executable), parameters, and bounds of type parameters + static IntTable of(APIMap map, Function> f) { + IntTable result = new IntTable<>(); + for (Map.Entry e : map.entrySet()) { + result.put(e.getKey(), f.apply(e.getValue())); + } + return result; + } + + /** + * Updates the table with a series of values for a given API. + * The effect is to set the values in a column of the table, + * extending the number of rows if necessary. + * + * @param api the api + * @param items the items + */ + public void put(API api, List items) { + int i = 0; + for (T item : items) { + APIMap m; + if (i < list.size()) { + m = list.get(i); + } else { + m = APIMap.of(); + list.add(m); + } + m.put(api, item); + i++; + } + } + + /** + * Adds a new entry for a given API. + * + * @param api the API + * @param item the item + */ + public void add(API api, T item) { + for (APIMap m : list) { + if (!m.containsKey(api)) { + m.put(api, item); + return; + } + } + APIMap m = APIMap.of(); + list.add(m); + m.put(api, item); + } + + /** + * Returns the number of rows in the table. + * + * @return the number of rows in the table. + */ + public int size() { + return list.size(); + } + + /** + * Returns the entries for a given row in the table. + * + * @param index the index of the row. + * @return the entries + */ + public APIMap entries(int index) { + return list.get(index); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/KeyTable.java b/src/share/classes/jdk/codetools/apidiff/model/KeyTable.java new file mode 100644 index 0000000..1aa3ef3 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/KeyTable.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.BiFunction; +import javax.lang.model.element.Element; + +/** + * A table of {@code (ElementKey x API) -> T}. + * + * The table can be used associate items from different APIs that are associated + * by means of a key representing the item. + * + * @param the type of item stored in this table + */ +public class KeyTable { + private Map> map = new TreeMap<>(); + + /** + * Creates a table from an API map and a function to provide a set of related items to populate the table. + * + * @param map the map + * @param f the function + * @param the type of values in the map + * @param the type of values in the set returned by the function + * + * @return the table + * + * @see IntTable#of + */ + // Used to get packages from a module, and types from a package + static KeyTable of(APIMap map, BiFunction> f) { + KeyTable result = new KeyTable<>(); + for (Map.Entry entry : map.entrySet()) { + API api = entry.getKey(); + for (R e : f.apply(api, entry.getValue())) { + result.put(ElementKey.of(e), api, e); + } + } + return result; + } + + /** + * Puts an item into the table, according to a key and the api in which it is + * an instance. + * + * @param key the key + * @param api the api + * @param item the item + * @return the previous value, if any + */ + public T put(ElementKey key, API api, T item) { + return map.computeIfAbsent(key, _k -> APIMap.of()).put(api, item); + } + + /** + * Returns an iterator for the collections of items within the table + * for a given key. + * + * @return an iterable for the collections of items associated with a given key + */ + public Iterable>> entries() { + return map.entrySet(); + } + + @Override + public String toString() { + return map.toString(); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/ModuleComparator.java b/src/share/classes/jdk/codetools/apidiff/model/ModuleComparator.java new file mode 100644 index 0000000..a7fa9b6 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/ModuleComparator.java @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.ModuleElement.DirectiveKind; +import javax.lang.model.element.ModuleElement.ExportsDirective; +import javax.lang.model.element.ModuleElement.OpensDirective; +import javax.lang.model.element.ModuleElement.ProvidesDirective; +import javax.lang.model.element.ModuleElement.RequiresDirective; +import javax.lang.model.element.ModuleElement.UsesDirective; +import javax.lang.model.element.PackageElement; + +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTreeVisitor; +import com.sun.source.doctree.ProvidesTree; +import com.sun.source.doctree.UsesTree; +import com.sun.source.util.DocTreeScanner; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link ModuleElement module elements}. + * + *

        The comparison includes: + *

          + *
        • the annotations of the module + *
        • the modifiers of the module + *
        • the directives in the module + *
        • the documentation comment for the module + *
        • the selected packages in the module + *
        + */ +public class ModuleComparator extends ElementComparator { + + /** + * Creates a comparator to compare module elements across a set of APIs. + * + * @param apis the set of APIs + * @param options the command-line options + * @param reporter the reporter to which to report differences + */ + public ModuleComparator(Set apis, Options options, Reporter reporter) { + super(apis, options, reporter); + } + + /** + * Compares instances of a module element found in different APIs. + * + * @param mPos the position of the element + * @param mMap the map giving the instance of the module element in different APIs + * @return {@code true} if all the instances are equivalent + */ + @Override + public boolean compare(Position mPos, APIMap mMap) { + boolean allEqual = false; + reporter.comparing(mPos, mMap); + try { + allEqual = checkMissing(mPos, mMap); + if (mMap.size() > 1) { + allEqual &= compareAnnotations(mPos, mMap); + allEqual &= compareModifiers(mPos, mMap); + allEqual &= compareDirectives(mPos, mMap); + allEqual &= compareDocComments(mPos, mMap); + allEqual &= compareApiDescriptions(mPos, mMap); + allEqual &= comparePackages(mPos, mMap); + allEqual &= compareDocFiles(mPos, mMap); + } + } finally { + reporter.completed(mPos, allEqual); + } + return allEqual; + } + + private boolean comparePackages(Position mPos, APIMap mMap) { + PackageComparator pc = new PackageComparator(mMap.keySet(), options, reporter); + KeyTable packages = options.getAccessKind().compareTo(AccessKind.PROTECTED) <= 0 + ? KeyTable.of(mMap, API::getExportedPackageElements) + : KeyTable.of(mMap, API::getPackageElements); + return pc.compareAll(packages); + } + + /** + * Compares the modifiers for instances of a module element found in different APIs. + * + *

        For a module element, the only modifier is whether it is an open module or not. + */ + @Override + protected boolean compareModifiers(Position mPos, APIMap mMap) { + if (mMap.size() == 1) + return true; + + boolean first = true; + boolean baselineIsOpen = false; + for (ModuleElement me : mMap.values()) { + if (first) { + baselineIsOpen = me.isOpen(); + first = false; + } else if (me.isOpen() != baselineIsOpen) { + reporter.reportDifferentModifiers(mPos, mMap); + return false; + } + } + + return true; + } + + private boolean compareDirectives(Position mPos, APIMap mMap) { + KeyTable requires = new KeyTable<>(); + KeyTable exports = new KeyTable<>(); + KeyTable opens = new KeyTable<>(); + KeyTable provides = new KeyTable<>(); + KeyTable uses = new KeyTable<>(); + + boolean allDirectiveDetails = accessKind.allModuleDetails(); + + for (Map.Entry e : mMap.entrySet()) { + API api = e.getKey(); + ModuleElement me = e.getValue(); + + for (Directive d : me.getDirectives()) { + switch (d.getKind()) { + case REQUIRES -> { + RequiresDirective rd = (RequiresDirective) d; + if (allDirectiveDetails || rd.isTransitive()) { + requires.put(ElementKey.of(rd.getDependency()), api, rd); + } + } + case EXPORTS -> { + ExportsDirective ed = (ExportsDirective) d; + if (allDirectiveDetails || ed.getTargetModules() == null) { + exports.put(ElementKey.of(ed.getPackage()), api, ed); + } + } + case OPENS -> { + OpensDirective od = (OpensDirective) d; + if (allDirectiveDetails || od.getTargetModules() == null) { + opens.put(ElementKey.of(od.getPackage()), api, od); + } + } + case PROVIDES -> { + ProvidesDirective pd = (ProvidesDirective) d; + if (allDirectiveDetails || isDocumented(api, me, pd)) { + provides.put(ElementKey.of(pd.getService()), api, pd); + } + } + case USES -> { + UsesDirective ud = (UsesDirective) d; + if (allDirectiveDetails || isDocumented(api, me, ud)) { + uses.put(ElementKey.of(ud.getService()), api, ud); + } + } + } + } + } + + Set mAPIs = mMap.keySet(); + boolean allEqual = new RequiresComparator(mAPIs).compareAll(mPos, requires); + allEqual &= new ExportsComparator(mAPIs).compareAll(mPos, exports); + allEqual &= new OpensComparator(mAPIs).compareAll(mPos, opens); + allEqual &= new ProvidesComparator(mAPIs, allDirectiveDetails).compareAll(mPos, provides); + allEqual &= new UsesComparator(mAPIs).compareAll(mPos, uses); + return allEqual; + } + + /** + * A cache of the service information derived from doc comments. + */ + private Map>> documentedServices = new HashMap<>(); + + /** + * Determines if the service type in a "provides" or "uses" directive is documented or not. + * A service type is documented if it is the subject of a corresponding {@code @provides} + * or {@code @uses} tag identifying the service type. + * + * @param api the API containing the module element + * @param me the module element + * @param d the directive containing the service type + * + * @return {@code true} if the service type is documented in the doc comment for the module + */ + private boolean isDocumented(API api, ModuleElement me, Directive d) { + Map> servicesForModule = + documentedServices.computeIfAbsent(me, __ -> getDocumentedServices(api, me)); + String serviceName = switch (d.getKind()) { + case USES -> ((UsesDirective) d).getService().getQualifiedName().toString(); + case PROVIDES -> ((ProvidesDirective) d).getService().getQualifiedName().toString(); + default -> throw new IllegalArgumentException(); + }; + return servicesForModule.get(d.getKind()).contains(serviceName); + } + + /** + * Returns details about which services are documented for a module. + * This requires the source file for the module to be available; + * if it is not available, an empty map is returned. + * + * @param api the API containing the module element + * @param me the module element + * + * @return a map which identifies which services are documented for a module. + */ + private Map> getDocumentedServices(API api, ModuleElement me) { + Map> map = new EnumMap<>(DirectiveKind.class); + map.put(DirectiveKind.PROVIDES, new HashSet<>()); + map.put(DirectiveKind.USES, new HashSet<>()); + DocCommentTree dct = api.getDocComment(me); + if (dct != null) { + DocTreeVisitor>> serviceScanner = new DocTreeScanner<>() { + @Override + public Void visitProvides(ProvidesTree pt, Map> map) { + map.get(DirectiveKind.PROVIDES).add(pt.getServiceType().getSignature()); + return null; + } + @Override + public Void visitUses(UsesTree ut, Map> map) { + map.get(DirectiveKind.USES).add(ut.getServiceType().toString()); + return null; + } + }; + dct.accept(serviceScanner, map); + } + return map; + } + + private abstract class DirectiveComparator { + final Set apis; + final DirectiveKind kind; + + DirectiveComparator(Set apis, DirectiveKind kind) { + this.apis = apis; + this.kind = kind; + } + + boolean compareAll(Position mPos, KeyTable table) { + boolean allEqual = true; + // TODO? use comparing... try { ... } finally { compared... } + for (Map.Entry> e : table.entries()) { + ElementKey ek = e.getKey(); + APIMap v = e.getValue(); + boolean equal = compare(mPos.directive(kind, ek), v); + allEqual &= equal; + } + return allEqual; + } + + abstract boolean compare(Position pos, APIMap map); + + protected boolean checkMissing(Position dPos, APIMap dMap) { + Set missing = apis.stream() + .filter(a -> !dMap.containsKey(a)) + .collect(Collectors.toSet()); // warning: unordered + + if (missing.isEmpty()) { + return true; + } else { + reporter.reportMissing(dPos, missing); + return false; + } + } + + protected boolean compare(Position dPos, APIMap dMap, Function> f) { + boolean allEqual = false; + reporter.comparing(dPos, dMap); + try { + allEqual = checkMissing(dPos, dMap); + if (dMap.size() > 1) { + Set baseline = null; + for (D d : dMap.values()) { + List elements = f.apply(d); + Set set = (elements == null) ? Collections.emptySet() + : elements.stream().map(ElementKey::of).collect(Collectors.toSet()); + if (baseline == null) { + baseline = set; + } else if (!set.equals(baseline)) { + allEqual = false; + break; + } + } + if (!allEqual) { + reporter.reportDifferentDirectives(dPos, dMap); + } + } + } finally { + reporter.completed(dPos, allEqual); + } + + return allEqual; + } + } + + private class RequiresComparator extends DirectiveComparator { + RequiresComparator(Set apis) { + super(apis, DirectiveKind.REQUIRES); + } + + @Override + boolean compare(Position rPos, APIMap rMap) { + boolean allEqual = false; + reporter.comparing(rPos, rMap); + try { + allEqual = checkMissing(rPos, rMap); + if (rMap.size() > 1) { + boolean first = true; + boolean baselineIsStatic = false; + boolean baselineIsTransitive = false; + for (RequiresDirective rd : rMap.values()) { + if (first) { + baselineIsStatic = rd.isStatic(); + baselineIsTransitive = rd.isTransitive(); + first = false; + } else if (rd.isStatic() != baselineIsStatic || rd.isTransitive() != baselineIsTransitive) { + allEqual = false; + break; + } + } + if (!allEqual) { + reporter.reportDifferentDirectives(rPos, rMap); + } + } + } finally { + reporter.completed(rPos, allEqual); + } + + return allEqual; + } + } + + private class ExportsComparator extends DirectiveComparator { + ExportsComparator(Set apis) { + super(apis, DirectiveKind.EXPORTS); + } + + @Override + boolean compare(Position ePos, APIMap eMap) { + return compare(ePos, eMap, ExportsDirective::getTargetModules); + } + } + + private class OpensComparator extends DirectiveComparator { + OpensComparator(Set apis) { + super(apis, DirectiveKind.OPENS); + } + + @Override + boolean compare(Position oPos, APIMap oMap) { + return compare(oPos, oMap, OpensDirective::getTargetModules); + } + } + + private class ProvidesComparator extends DirectiveComparator { + private final boolean allDirectiveDetails; + + ProvidesComparator(Set apis, boolean allDirectiveDetails) { + super(apis, DirectiveKind.PROVIDES); + this.allDirectiveDetails = allDirectiveDetails; + } + + @Override + boolean compare(Position pPos, APIMap pMap) { + // The implementations listed in the directive are not considered + // part of the public API, so do not compare (i.e. ignore) + // that part of the API unless allDirectiveDetails is set. + return compare(pPos, pMap, + d -> allDirectiveDetails ? d.getImplementations() : Collections.emptyList()); + } + } + + private class UsesComparator extends DirectiveComparator { + UsesComparator(Set apis) { + super(apis, DirectiveKind.USES); + } + + @Override + boolean compare(Position uPos, APIMap uMap) { + return checkMissing(uPos, uMap); + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/PackageComparator.java b/src/share/classes/jdk/codetools/apidiff/model/PackageComparator.java new file mode 100644 index 0000000..848bf42 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/PackageComparator.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.EnumSet; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileObject; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.model.API.LocationKind; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link PackageElement package elements}. + * + *

        The comparison includes: + *

          + *
        • the documentation comment for the package + *
        • the selected types in the package + *
        • additional documentation files for the package + *
        + */ +public class PackageComparator extends ElementComparator { + + /** + * Creates a comparator to compare package elements across a set of APIs. + * + * @param apis the set of APIs + * @param options the command-line options + * @param reporter the reporter to which to report differences + */ + public PackageComparator(Set apis, Options options, Reporter reporter) { + super(apis, options, reporter); + } + + /** + * Compares instances of a package element found in different APIs. + * + * @param pPos the position of the element + * @param pMap the map giving the instance of the package element in different APIs + * @return {@code true} if all the instances are equivalent + */ + @Override + public boolean compare(Position pPos, APIMap pMap) { + boolean allEqual = false; + reporter.comparing(pPos, pMap); + try { + allEqual = checkMissing(pPos, pMap); + if (pMap.size() > 1) { + allEqual &= compareAnnotations(pPos, pMap); + allEqual &= compareDocComments(pPos, pMap); + allEqual &= compareApiDescriptions(pPos, pMap); + allEqual &= compareTypes(pPos, pMap); + allEqual &= compareDocFiles(pPos, pMap); + } + } finally { + reporter.completed(pPos, allEqual); + } + return allEqual; + } + + private boolean compareTypes(Position pPos, APIMap pMap) { + TypeComparator tc = new TypeComparator(pMap.keySet(), options, reporter); + KeyTable types = KeyTable.of(pMap, API::getTypeElements); + return tc.compareAll(types); + } + + /** + * Returns the doc comment for an element in a given API. + * + * If the element is a package element, and no comment is found in the + * package's {@code package-info.java} file, the methods looks for + * a {@code package.html} file as a fallback. + * + * @param api the API + * @param e the element + * + * @return the doc comment + */ + @Override + protected String getDocComment(API api, Element e) { + String s = super.getDocComment(api, e); + if (s != null) { + return s; + } + + if (e.getKind() == ElementKind.PACKAGE) { + // There is no easy API to get the package.html file directly. + // Trees.getDocCommentTree(Element e, String relativeName) only looks on + // the source path; ideally, it should realize the package is in a module + // and use the correct entry from the module source path. But even so, + // this method is somewhat lower-level, and just wants the raw text, + // and not the parsed doc comment tree. + for (JavaFileObject fo : api.listFiles(LocationKind.SOURCE, e, "", + EnumSet.of(JavaFileObject.Kind.HTML), false)) { + if (fo.getName().endsWith("/package.html")) { + return api.getApiDescription(fo); + } + } + } + + return null; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/Position.java b/src/share/classes/jdk/codetools/apidiff/model/Position.java new file mode 100644 index 0000000..b5dd493 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/Position.java @@ -0,0 +1,861 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Comparator; +import java.util.Objects; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ModuleElement.DirectiveKind; + +/** + * An abstract description of the position in an API of some item + * to be compared. + * + *

        Positions are initially created from element keys; + * additional positions describing positions within an item + * can be created by various factory methods. These methods + * may throw {@code UnsupportedOperationException} if the + * factory method is not applicable for the parent position. + */ +public abstract class Position { + /** + * Creates a position for a given element key, which must be + * for a module, package, type, constructor, method, enum + * constant or field. + * + *

        Positions for parameters and type parameters should + * be created using the appropriate factory method, providing + * the necessary index value. + * + * @param k the key for the element + * @return the position + */ + public static Position of(ElementKey k) { + return switch (k.kind) { + case MODULE, PACKAGE, TYPE, VARIABLE, EXECUTABLE -> new ElementPosition(k); + default -> throw new IllegalArgumentException(k.toString()); + }; + } + + /** + * Returns whether this position is the position of an element. + * + * @return {@code true} if and ony if the position is the position of an element + */ + public boolean isElement() { + return false; + } + + /** + * Returns whether this position represents the given kind of element. + * + * @param kind the kind + * + * @return {@code true} if and only if the position represents the given kind of element + */ + public boolean is(ElementKind kind) { + return false; + } + + /** + * Returns whether this position is a relative position. + * + * @return {@code true} if and only if the position is a relative position + */ + public boolean isRelative() { + return false; + } + + /** + * Returns whether this position represents the given kind of relative position. + * + * @param kind the kind + * + * @return {@code true} if and only if the position represents the given kind of relative position + */ + public boolean is(RelativePosition.Kind kind) { + return false; + } + + /** + * Creates a position for an annotation on an element, + * identified by the annotation type. + * + *

        Repeated annotations are assumed to be enclosed within + * a container annotation. + * + * @param key the key of the annotation type + * + * @return the position + */ + public abstract RelativePosition annotation(ElementKey key); + + /** + * Creates a position for a value within an array of annotation values. + * + * @param index the position within the array + * + * @return the position + */ + public RelativePosition annotationArrayIndex(int index) { + throw unsupported(); + } + + /** + * Creates a position for value within an annotation, + * identified by the annotation type element. + * + * @param key the key of the annotation type member + * + * @return the position + */ + public RelativePosition annotationValue(ElementKey key) { + throw unsupported(); + } + + /** + * Creates a position for a bound of a type parameter element. + * + * @param index the index of the bound within the list of bounds + * + * @return the position + */ + public RelativePosition bound(int index) { + throw unsupported(); + } + + /** + * Creates a position for the default value of an annotation element. + * + * @return the position + */ + public RelativePosition defaultValue() { + throw unsupported(); + } + + /** + * Creates a position for a directive within a module declaration, + * based on the kind of the directive and the primary type described + * by the directive. The kind of the primary type depends on the + * kind of directive. + * + * @param kind the kind of directive + * @param key the key for the primary type of the directory + * + * @return the position + */ + public RelativePosition directive(DirectiveKind kind, ElementKey key) { + throw unsupported(); + } + + /** + * Creates a position for a doc file of a module or package, identified by + * the name relative to the enclosing {@code doc-files} directory. + * The name always uses {@code /} as the internal file separator + * (and not the platform file separator. + * + * @param name the name + * + * @return the position + */ + public RelativePosition docFile(String name) { + throw unsupported(); + } + + /** + * Creates a position for an exception that is thrown from an executable element. + * + * @param key the key of the exception that is thrown + * + * @return the position + */ + public RelativePosition exception(TypeMirrorKey key) { + throw unsupported(); + } + + /** + * Creates a position for a parameter of an executable element. + * + * @param index the index of the parameter within the list of parameters + * + * @return the position + */ + public RelativePosition parameter(int index) { + throw unsupported(); + } + + /** + * Creates a position for a permitted subtype of a sealed class. + * + * @param key the element key for the subtype + * + * @return the position + */ + public RelativePosition permittedSubclass(ElementKey key) { + throw unsupported(); + } + + /** + * Creates a position for the receiver type of a method. + * + * @return the position + */ + public RelativePosition receiverType() { + throw unsupported(); + } + + /** + * Creates a position for a record component of a record. + * + * @param index the index of the component within the list of components + * + * @return the position + */ + public RelativePosition recordComponent(int index) { + throw unsupported(); + } + + /** + * Creates a position for the return type of a method. + * + * @return the position + */ + public RelativePosition returnType() { + throw unsupported(); + } + + /** + * Creates a position for the {@code serialVersionUID} of a type element or its serialized form. + * + * @return the position + */ + public RelativePosition serialVersionUID() { + throw unsupported(); + } + + /** + * Creates a position for a serialization method of a type element or its serialized form. + * + * @param name the name of the method + * + * @return the position + */ + public RelativePosition serializationMethod(String name) { + throw unsupported(); + } + + /** + * Creates a position for a serialization overview of a type element or its serialized form. + * + * @return the position + */ + public RelativePosition serializationOverview() { + throw unsupported(); + } + + /** + * Creates a position for a serialized field of a type element or its serialized form. + * + * @param name the name of the field + * + * @return the position + */ + public RelativePosition serializedField(String name) { + throw unsupported(); + } + + /** + * Creates a position for the serialized form of a type element. + * + * @return the position + */ + public RelativePosition serializedForm() { + throw unsupported(); + } + + /** + * Creates a position for the superclass of a class type. + * + * @return the position + */ + public RelativePosition superclass() { + throw unsupported(); + } + + /** + * Creates a position for a superinterface of a class or interface type. + * + * @param eKey the index of the parameter within the list of parameters + * @return the position + */ + public RelativePosition superinterface(ElementKey eKey) { + throw unsupported(); + } + + /** + * Creates a position for a type parameter of a type or executable element. + * + * @param index the index of the type parameter within the list of type parameters + * @return the position + */ + public RelativePosition typeParameter(int index) { + throw unsupported(); + } + + /** + * Returns the element key, if this position directly represents an element. + * @return the element key + * @throws UnsupportedOperationException if this position does not directly represent an element + */ + public ElementKey asElementKey() { + throw unsupported(); + } + + /** + * Returns the position as one with the given index class. + * The method provides an easy safe way to downcast a {@code RelativePosition} + * to a {@code RelativePosition} where {@code T} is the kind of the index + * for the expected kind of the position. + * + * @param kind the expected kind of the position + * @param c the expected class of the index + * @param the expected type of the index + * + * @return the position + */ + public RelativePosition as(RelativePosition.Kind kind, Class c) { + throw unsupported(); + } + + /** + * Returns the element key for a position. + * + * The element key for an element position is its key. + * The element key for a relative position is the element key of its parent. + * + * @return the element key for a position + */ + public abstract ElementKey getElementKey(); + + // Abstract, to force all subtypes to implement this method. + @Override + public abstract boolean equals(Object other); + + // Abstract, to force all subtypes to implement this method. + @Override + public abstract int hashCode(); + + /** + * Applies a visitor to this position. + * @param v the visitor + * @param p a visitor-specified parameter + * @param the type of the result + * @param

        the type of the parameter + * @return a visitor-specified result + */ + public abstract R accept(Visitor v, P p); + + /** + * Throws an {@code UnsupportedOperationException} if a given condition is {@code false}. + * + * @param cond the condition + */ + protected void check(boolean cond) { + if (!cond) { + throw unsupported(); + } + } + + /** + * Creates an {@code UnsupportedOperationException} for this position. + * @return the exception + */ + protected UnsupportedOperationException unsupported() { + return new UnsupportedOperationException(getClass().getSimpleName() + " " + toString()); + } + + /** + * A visitor of positions, in the style of the visitor design pattern. + * Classes implementing this interface are used to operate on a position + * when the kind of position is unknown at compile time. + * When a visitor is passed to a position's {@code accept} method, + * the visitXyz method applicable to that position is invoked. + * + * @param the return type of this visitor's methods. + * Use Void for visitors that do not need to return results. + * @param

        the type of the additional parameter to this visitor's methods. + * Use Void for visitors that do not need an additional parameter. + */ + public interface Visitor { + /** + * Visits a position identified by an element key. + * @param kp the position to visit + * @param p a visitor-specified parameter + * @return a visitor-specified result + */ + R visitElementPosition(ElementPosition kp, P p); + + /** + * Visits a relative position. + * @param ip the position to visit + * @param p a visitor-specified parameter + * @return a visitor-specified result + */ + R visitRelativePosition(RelativePosition ip, P p); + } + + /** + * A position identified by an element key. + */ + public static class ElementPosition extends Position { + /** The element key. */ + public final ElementKey key; + + ElementPosition(ElementKey key) { + this.key = key; + } + + @Override + public boolean is(ElementKind kind) { + return key.is(kind); + } + + @Override + public boolean isElement() { + return true; + } + + @Override + public RelativePosition annotation(ElementKey key) { + return new RelativePosition<>(this, RelativePosition.Kind.ANNOTATION, key); + } + + @Override + public RelativePosition defaultValue() { + check(key.is(ElementKind.METHOD)); + return new RelativePosition<>(this, RelativePosition.Kind.DEFAULT_VALUE); + } + + @Override + public RelativePosition directive(DirectiveKind kind, ElementKey typeKey) { + check(key.is(ElementKey.Kind.MODULE)); + return new RelativePosition<>(this, kind, typeKey); + } + + @Override + public RelativePosition docFile(String name) { + check(key.is(ElementKey.Kind.MODULE) || key.is(ElementKey.Kind.PACKAGE)); + return new RelativePosition<>(this, RelativePosition.Kind.DOC_FILE, name); + } + + @Override + public RelativePosition exception(TypeMirrorKey key) { + return new RelativePosition<>(this, RelativePosition.Kind.EXCEPTION, key); + } + + @Override + public RelativePosition parameter(int index) { + check(key.is(ElementKey.Kind.EXECUTABLE)); + return new RelativePosition<>(this, RelativePosition.Kind.PARAMETER, index); + } + + @Override + public RelativePosition permittedSubclass(ElementKey key) { + check(key.is(ElementKey.Kind.TYPE)); + return new RelativePosition<>(this, RelativePosition.Kind.PERMITTED_SUBCLASS, key); + } + + @Override + public RelativePosition receiverType() { + check(key.is(ElementKind.METHOD)); + return new RelativePosition<>(this, RelativePosition.Kind.RECEIVER_TYPE); + } + + @Override + public RelativePosition recordComponent(int index) { + //check(key.is(ElementKey.Kind.RECORD)); + return new RelativePosition<>(this, RelativePosition.Kind.RECORD_COMPONENT, index); + } + + @Override + public RelativePosition returnType() { + check(key.is(ElementKind.METHOD)); + return new RelativePosition<>(this, RelativePosition.Kind.RETURN_TYPE); + } + + @Override + public RelativePosition serialVersionUID() { + check(key.kind == ElementKey.Kind.TYPE); + return new RelativePosition<>(this, RelativePosition.Kind.SERIAL_VERSION_UID); + } + + @Override + public RelativePosition serializationMethod(String name) { + check(key.kind == ElementKey.Kind.TYPE); + return new RelativePosition<>(this, RelativePosition.Kind.SERIALIZATION_METHOD, name); + } + + @Override + public RelativePosition serializationOverview() { + check(key.kind == ElementKey.Kind.TYPE); + return new RelativePosition<>(this, RelativePosition.Kind.SERIALIZATION_OVERVIEW); + } + + @Override + public RelativePosition serializedField(String name) { + check(key.kind == ElementKey.Kind.TYPE); + return new RelativePosition<>(this, RelativePosition.Kind.SERIALIZED_FIELD, name); + } + + @Override + public RelativePosition serializedForm() { + check(key.kind == ElementKey.Kind.TYPE); + return new RelativePosition<>(this, RelativePosition.Kind.SERIALIZED_FORM); + } + + @Override + public RelativePosition superclass() { + // Although superclass is only meaningful for elementKind CLASS, + // we have to tolerate requesting a position for the superclass + // on ANNOTATION_TYPE, ENUM and INTERFACE, because we might be + // comparing elements that changed kind, such as from CLASS to INTERFACE. + check(key.kind == ElementKey.Kind.TYPE); + return new RelativePosition<>(this, RelativePosition.Kind.SUPERCLASS); + } + + @Override + public RelativePosition superinterface(ElementKey eKey) { + check(key.kind == ElementKey.Kind.TYPE); + return new RelativePosition<>(this, RelativePosition.Kind.SUPERINTERFACE, eKey); + } + + @Override + public RelativePosition typeParameter(int index) { + // Strictly speaking, enum types and annotation types cannot be generic + // but that will never arise in a well-formed program. + check(key.kind == ElementKey.Kind.TYPE || key.kind == ElementKey.Kind.EXECUTABLE); + return new RelativePosition<>(this, RelativePosition.Kind.TYPE_PARAMETER, index); + } + + @Override + public ElementKey asElementKey() { + return key; + } + + @Override + public ElementKey getElementKey() { + return key; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + return key.equals(((ElementPosition) other).key); + } + + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public String toString() { + return "{" + key + "}"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitElementPosition(this, p); + } + } + + /** + * A position within an parent position, specified by its kind + * and possible index. + * + *

        Depending on the kind, the index should be an integer, + * a name (element key) or null (void). + */ + public static class RelativePosition extends Position { + /** + * The kind of index for an {@code RelativePosition}. + */ + public enum Kind { + /** The index of an annotation on an annotated construct. */ + ANNOTATION, + /** The index of an item in an annotation array value. */ + ANNOTATION_ARRAY_INDEX, + /** The index of a value in an annotation. */ + ANNOTATION_VALUE, + /** The index of a bound in a type parameter. */ + BOUND, + /** The default value of an annotation element. */ + DEFAULT_VALUE, + /** A doc file in a module or package. */ + DOC_FILE, + /** The index of a exception that is thrown. */ + EXCEPTION, + /** The index of an {@code exports} directive in a module declaration. */ + MODULE_EXPORTS, + /** The index of an {@code requires} directive in a module declaration. */ + MODULE_REQUIRES, + /** The index of an {@code opens} directive in a module declaration. */ + MODULE_OPENS, + /** The index of an {@code provides} directive in a module declaration. */ + MODULE_PROVIDES, + /** The index of an {@code uses} directive in a module declaration. */ + MODULE_USES, + /** The index of a parameter. */ + PARAMETER, + /** The index of a permitted subclass of a type element. */ + PERMITTED_SUBCLASS, + /** The receiver type of a method. */ + RECEIVER_TYPE, + /** The index of a record component. */ + RECORD_COMPONENT, + /** The return type of a method. */ + RETURN_TYPE, + /** The {@code serialVersionUID} of a serializable class. */ + SERIAL_VERSION_UID, + /** A serialization method in a serializable class. */ + SERIALIZATION_METHOD, + /** The serialization overview of a serializable class. */ + SERIALIZATION_OVERVIEW, + /** A serialized field in a serializable class. */ + SERIALIZED_FIELD, + /** The serialized form of a serializable class. */ + SERIALIZED_FORM, + /** The superclass of a type. */ + SUPERCLASS, + /** A superinterface of a type. */ + SUPERINTERFACE, + /** The index of a type parameter. */ + TYPE_PARAMETER + } + + /** The enclosing position. */ + public final Position parent; + /** The kind of item given by the relative position. */ + public final Kind kind; + /** The index given by the relative position. */ + public final T index; + + RelativePosition(Position parent, Kind kind) { + this.parent = Objects.requireNonNull(parent); + this.kind = Objects.requireNonNull(kind); + this.index = null; + } + + RelativePosition(Position parent, Kind kind, T index) { + this.parent = Objects.requireNonNull(parent); + this.kind = Objects.requireNonNull(kind); + this.index = index; + } + + RelativePosition(Position parent, DirectiveKind kind, T index) { + this(parent, getKind(kind), index); + } + + private static Kind getKind(DirectiveKind k) { + return switch (k) { + case EXPORTS -> Kind.MODULE_EXPORTS; + case OPENS -> Kind.MODULE_OPENS; + case PROVIDES -> Kind.MODULE_PROVIDES; + case REQUIRES -> Kind.MODULE_REQUIRES; + case USES -> Kind.MODULE_USES; + }; + } + + @Override + public boolean isRelative() { + return true; + } + + @Override + public boolean is(RelativePosition.Kind kind) { + return this.kind == kind; + } + + @Override + public RelativePosition annotation(ElementKey key) { + return new RelativePosition<>(this, Kind.ANNOTATION, key); + } + + @Override + public RelativePosition annotationArrayIndex(int i) { + return new RelativePosition<>(this, Kind.ANNOTATION_ARRAY_INDEX, i); + } + + @Override + public RelativePosition annotationValue(ElementKey key) { + return new RelativePosition<>(this, Kind.ANNOTATION_VALUE, key); + } + + @Override + public RelativePosition bound(int i) { + return new RelativePosition<>(this, Kind.BOUND, i); + } + + @Override + public RelativePosition exception(TypeMirrorKey type) { + check(kind == Kind.SERIALIZATION_METHOD); + return new RelativePosition<>(parent, Kind.EXCEPTION, type); + } + + @Override + public RelativePosition parameter(int index) { + check(kind == Kind.SERIALIZATION_METHOD); + return new RelativePosition<>(this, Kind.PARAMETER, index); + } + + @Override + public RelativePosition receiverType() { + check(kind == Kind.SERIALIZATION_METHOD); + return new RelativePosition<>(this, Kind.RECEIVER_TYPE); + } + + @Override + public RelativePosition returnType() { + check(kind == Kind.SERIALIZATION_METHOD); + return new RelativePosition<>(this, Kind.RETURN_TYPE); + } + + @Override + public RelativePosition serialVersionUID() { + check(kind == Kind.SERIALIZED_FORM); + return new RelativePosition<>(parent, RelativePosition.Kind.SERIAL_VERSION_UID); + } + + @Override + public RelativePosition serializationMethod(String name) { + check(kind == Kind.SERIALIZED_FORM); + return new RelativePosition<>(parent, RelativePosition.Kind.SERIALIZATION_METHOD, name); + } + + @Override + public RelativePosition serializationOverview() { + check(kind == Kind.SERIALIZED_FORM); + return new RelativePosition<>(parent, RelativePosition.Kind.SERIALIZATION_OVERVIEW); + } + + @Override + public RelativePosition serializedField(String name) { + check(kind == Kind.SERIALIZED_FORM); + return new RelativePosition<>(parent, RelativePosition.Kind.SERIALIZED_FIELD, name); + } + + @Override + public RelativePosition serializedForm() { + return switch (kind) { + case SERIAL_VERSION_UID, SERIALIZATION_METHOD, SERIALIZATION_OVERVIEW, SERIALIZED_FIELD -> + new RelativePosition<>(parent, Kind.SERIALIZED_FORM); + default -> throw unsupported(); + }; + } + + @Override + public ElementKey getElementKey() { + return parent.getElementKey(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + RelativePosition ip = (RelativePosition) other; + return parent.equals(ip.parent) + && kind == ip.kind + && Objects.equals(index, ip.index); + } + + } + + @Override + public RelativePosition as(Kind kind, Class c) { + @SuppressWarnings("unchecked") + RelativePosition p = (RelativePosition) this; + return p; + } + + @Override + public int hashCode() { + int hashCode = parent.hashCode(); + hashCode = hashCode * 37 + kind.hashCode(); + hashCode = hashCode * 37 + Objects.hashCode(index); + return hashCode; + } + + @Override + public String toString() { + // TODO: convert to a simple debug print, and use SignatureVisitor or pretty print. + return "{" + parent + ": " + kind + " #" + index + "}"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitRelativePosition(this, p); + } + } + + /** + * A comparator for relative positions whose index is an element key. + */ + public static final Comparator> elementKeyIndexComparator = (rp1, rp2) -> { + if (rp1.parent != rp2.parent) { + throw new IllegalArgumentException("parents not equal"); + } + if (rp1.kind != rp2.kind) { + throw new IllegalArgumentException("kinds not equal"); + } + ElementKey i1 = (ElementKey) rp1.index; + ElementKey i2 = (ElementKey) rp2.index; + return i1.compareTo(i2); + }; + + /** + * A comparator for relative positions whose index is a string. + */ + public static final Comparator> stringIndexComparator = (rp1, rp2) -> { + if (rp1.parent != rp2.parent) { + throw new IllegalArgumentException("parents not equal"); + } + if (rp1.kind != rp2.kind) { + throw new IllegalArgumentException("kinds not equal"); + } + String i1 = (String) rp1.index; + String i2 = (String) rp2.index; + return i1.compareTo(i2); + }; +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/RecordComponentComparator.java b/src/share/classes/jdk/codetools/apidiff/model/RecordComponentComparator.java new file mode 100644 index 0000000..f246ad5 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/RecordComponentComparator.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Objects; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.Name; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for record component elements. + * + *

        The comparison includes: + *

          + *
        • the "signature" of the element: its annotations, name and type + *
        + */ +public class RecordComponentComparator extends ElementComparator { + + /** + * Creates a comparator to compare record component elements across a set of APIs. + * + * @param apis the set of APIs + * @param options the command-line options + * @param reporter the reporter to which to report differences + */ + public RecordComponentComparator(Set apis, Options options, Reporter reporter) { + super(apis, options, reporter); + } + + /** + * Compares instances of a record component element found in different APIs. + * + * @param rcPos the position of the element + * @param rcMap the map giving the instance of the variable element in different APIs + * @return {@code true} if all the instances are equivalent + */ + @Override + public boolean compare(Position rcPos, APIMap rcMap) { + boolean allEqual = false; + reporter.comparing(rcPos, rcMap); + try { + allEqual = checkMissing(rcPos, rcMap); + if (rcMap.size() > 1) { + allEqual &= compareSignatures(rcPos, rcMap); + allEqual &= compareDocComments(rcPos, rcMap); + allEqual &= compareApiDescriptions(rcPos, rcMap); + } + } finally { + reporter.completed(rcPos, allEqual); + } + return allEqual; + } + + private boolean compareSignatures(Position rcPos, APIMap rcMap) { + return compareAnnotations(rcPos, rcMap) + & compareNames(rcPos, rcMap) + & compareType(rcPos, rcMap); + } + + private boolean compareNames(Position rcPos, APIMap rcMap) { + + if (rcMap.size() == 1) + return true; + + Name archetype = rcMap.values().stream() + .filter(Objects::nonNull) + .findFirst() + .get() + .getSimpleName(); + + for (Element e : rcMap.values()) { + if (!e.getSimpleName().contentEquals(archetype)) { + reporter.reportDifferentNames(rcPos, rcMap); + return false; + } + } + return true; + } + + private boolean compareType(Position rcPos, APIMap rcMap) { + TypeMirrorComparator tmc = new TypeMirrorComparator(rcMap.keySet(), reporter); + APIMap tMap = rcMap.map(Element::asType); + return tmc.compare(rcPos, tMap); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/Selector.java b/src/share/classes/jdk/codetools/apidiff/model/Selector.java new file mode 100644 index 0000000..78f8b6c --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/Selector.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.List; + +/** + * A filter to determine whether modules, packages and types within + * an API are "selected", according to a series of "include" and "exclude" + * options. + * + *

        The following patterns are accepted: + *

          + *
        • module/** all types in a module + *
        • module/package.** all types in a package and its subpackages + *
        • module/package.* all types in a package + *
        • module/package.Type a specific type + *
        • package.** all types in a package and its subpackages + * that are not in a named module + *
        • package.* all types in a package is not in a named module + *
        • package.Type a specific type in a package that is not in a named module + *
        + * where: + *
          + *
        • "module" is a qualified identifier, optionally ending in '.*' + * to indicate all modules beginning with the given prefix, + *
        • "package" is a qualified identifier indicating a package of that name + *
        • "not in a named module" is case when using versions of the platform + * prior to the introduction of the module system, or in the unnamed module + * thereafter. + *
        + */ +public class Selector { + static class Entry { + final String pattern; + + final String modulePart; + final String packagePart; + final String typePart; + + final Predicate includeModule; + final Predicate includePackage; + final Predicate includeType; + + final Predicate excludeModule; + final Predicate excludePackage; + final Predicate excludeType; + + static final Predicate ALWAYS = s -> true; + static final Predicate NEVER = s -> false; + + Entry(String pattern) { + this.pattern = pattern; + int sep = pattern.indexOf("/"); + String head, tail; + if (sep == -1) { + head = null; + tail = pattern; + } else { + head = pattern.substring(0, sep); + tail = pattern.substring(sep + 1); + } + + if (head == null) { + includeModule = s -> s == null || s.isEmpty(); + } else if (head.endsWith(".*")) { + String mdlPrefix = head.substring(0, head.length() - 1); + includeModule = s -> s != null && s.startsWith(mdlPrefix); + } else { + includeModule = s -> s != null && s.equals(head); + } + modulePart = head; + + if (tail.isEmpty() || tail.equals("**")) { + includePackage = ALWAYS; + includeType = ALWAYS; + excludeModule = includeModule; + excludePackage = ALWAYS; + excludeType = ALWAYS; + packagePart = ""; + typePart = "**"; + } else if (tail.equals("*")) { + includePackage = String::isEmpty; + includeType = s -> true; + excludeModule = NEVER; + excludePackage = includePackage; + excludeType = ALWAYS; + packagePart = ""; + typePart = "*"; + } else if (tail.endsWith(".*")) { + String pkgName = tail.substring(0, tail.length() - 2); + includePackage = s -> s.equals(pkgName); + includeType = s -> true; + excludeModule = NEVER; + excludePackage = includePackage; + excludeType = ALWAYS; + packagePart = pkgName; + typePart = "*"; + } else if (tail.endsWith(".**")) { + String pkgName = tail.substring(0, tail.length() - 3); + String pkgPrefix = tail.substring(0, tail.length() - 2); + includePackage = s -> s.equals(pkgName) || s.startsWith(pkgPrefix); + includeType = s -> true; + excludeModule = NEVER; + excludePackage = includePackage; + excludeType = ALWAYS; + packagePart = pkgName; + typePart = "**"; + } else { + int lastDot = tail.lastIndexOf("."); + if (lastDot == -1) { + includePackage = String::isEmpty; + includeType = s -> s.equals(tail); + packagePart = ""; + typePart = tail; + } else { + String pkgName = tail.substring(0, lastDot); + String typeName = tail.substring(lastDot + 1); + includePackage = s -> s.equals(pkgName); + includeType = s -> s.equals(typeName); + packagePart = pkgName; + typePart = typeName; + } + + excludeModule = NEVER; + excludePackage = NEVER; + excludeType = includeType; + } + } + + boolean includesModule(String moduleName) { + return includeModule.test(moduleName); + } + + boolean excludesModule(String moduleName) { + return excludeModule.test(moduleName); + } + + boolean includesPackage(String moduleName, String packageName) { + return includeModule.test(moduleName) && includePackage.test(packageName); + } + + boolean excludesPackage(String moduleName, String packageName) { + return excludesModule(moduleName) + || includesModule(moduleName) && excludePackage.test(packageName); + } + + boolean includesType(String moduleName, String packageName, String typeName) { + return includesPackage(moduleName, packageName) && includeType.test(typeName); + } + + boolean excludesType(String moduleName, String packageName, String typeName) { + return excludesPackage(moduleName, packageName) + || includesPackage(moduleName, packageName) && excludeType.test(typeName); + } + } + + final List includes; + final List excludes; + + /** + * Creates a selector based upon a series of "includes" and "excludes" options. + * + * @param includes the list of patterns describing the elements to be included + * @param excludes the list of patterns describing the elements to be excluded + */ + public Selector(List includes, List excludes) { + this.includes = includes.stream().map(Entry::new).collect(Collectors.toList()); + this.excludes = excludes.stream().map(Entry::new).collect(Collectors.toList()); + } + + /** + * Returns whether a module is selected, according to the configured options. + * + * @param name the module name + * @return {@code true} if the module is selected + */ + public boolean acceptsModule(String name) { + return (includes.isEmpty() || includes.stream().anyMatch(e -> e.includesModule(name))) + && excludes.stream().noneMatch(e -> e.excludesModule(name)); + } + + /** + * Returns whether a package is selected, according to the configured options. + * + * @param moduleName the name of the module enclosing the package + * @param packageName the package name + * @return {@code true} if the package is selected + */ + public boolean acceptsPackage(String moduleName, String packageName) { + return (includes.isEmpty() || includes.stream().anyMatch(e -> e.includesPackage(moduleName, packageName))) + && excludes.stream().noneMatch(e -> e.excludesPackage(moduleName, packageName)); + } + + /** + * Returns whether a top-level type is selected, according to the configured options. + * + * @param moduleName the name of the module enclosing the type + * @param packageName the name of the package enclosing the type + * @param typeName the simple name of the type + * @return {@code true} if the type is selected + */ + public boolean acceptsType(String moduleName, String packageName, String typeName) { + return (includes.isEmpty() || includes.stream().anyMatch(e -> e.includesType(moduleName, packageName, typeName))) + && excludes.stream().noneMatch(e -> e.excludesType(moduleName, packageName, typeName)); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/SerializedForm.java b/src/share/classes/jdk/codetools/apidiff/model/SerializedForm.java new file mode 100644 index 0000000..8564be5 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/SerializedForm.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.List; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +import com.sun.source.doctree.DocTree; + +/** + * A "serialized form" is a container for the constants, fields and methods + * of a class related to Java object serialization, as defined in the + * "Java Object Serialization Specification". + * + * In the context of apidiff, it provides access to fields and methods + * which were declared as private (and so not included in the primary + * access-based selection) but which nevertheless may contribute to + * the serialized form of a type element. But, note that the actual + * serialized form may be specified narratively by the {@code serialData} + * tag on one of a number of serialization methods, or by a series of + * {@code @serialField} tags on the {@code serialPersistentFields} + * member of a serializable class. + * + * Finally, note that classes may of may not be included in the documentation + * by using {@code @serial include | exclude} on the class or package + * documentation. + */ +public class SerializedForm { + + private final long serialVersionUID; + private final List fields; + private final List methods; + private final SerializedFormDocs docs; + + SerializedForm(long serialVersionUID, List fields, List methods, + SerializedFormDocs docs) { + this.serialVersionUID = serialVersionUID; + this.fields = fields; + this.methods = methods; + this.docs = docs; + } + + /** + * Returns the serial version UID for the element. + * + * @return the serial version UID + */ + public long getSerialVersionUID() { + return serialVersionUID; + } + + /** + * Returns the list of fields in the serialized form. + * + * @return the list of fields in the serialized form. + */ + public List getFields() { + return fields; + } + + public Field getField(CharSequence name) { + return getFields().stream() + .filter(f -> f.getName().contentEquals(name)) + .findFirst() + .orElse(null); + } + + /** + * Returns the list of serialization methods. + * + * @return the list of serialization. + */ + public List getMethods() { + return methods; + } + + /** + * Returns the descriptions for the items in the serialized form. + * + * @return the descriptions + */ + public SerializedFormDocs getDocs() { + return docs; + } + + /** + * A serialized field. + * + * Instances are created by information in the {@code @serialField} tags of the + * {@code serialPersistentFields} member, or from the default set of serializable fields. + */ + public interface Field { + /** + * Returns the type element enclosing this field. + * + * @return the type element + */ + TypeElement getEnclosingTypeElement(); + + /** + * Returns the name of the field. + * + * @return the name of the field + */ + Name getName(); + + /** + * Returns the type of the field, or a type with of kind {@code NONE} if the type + * could not be determined. The value may have kind {@code} if the field is + * described by information in a {@code @serialField} tag, and the type specification + * could not be resolved. + * + * Note: the use of {@code NONE} is non-standard in this context. It would be better + * if the type were of kind {@code ERROR} if the signature cannot be resolved, + * but that is not possible with the current API. {@code null} is not used because + * that is generally used to mean "missing" instead of "error". + * + * @return the type of the field, or a type of kind {@code NONE}. + */ + TypeMirror getType(); + + /** + * Returns the documentation comment of the field. + * + * If the field is described by information in a {@code @serialField} tag, + * the comment is taken from that tag. + * If the field is a default serializable field, the comment is the + * full comment of that field. + * + * @return the documentation comment of the field. + */ + List getDocComment(); + + /** + * Returns the signature of the type of the field. + * The signature is always available, even if {@link #getType()} returns a + * type of kind {@code NONE}. + * + * If the field is described by information in a {@code @serialField} tag, + * the signature is as found in that tag. + * If the field is a default serializable field, the signature is the + * result of {@code getType().toString()}. + * + * @return the type signature of the field + */ + String getSignature(); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/SerializedFormComparator.java b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormComparator.java new file mode 100644 index 0000000..655fcbd --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormComparator.java @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import com.sun.source.doctree.DocTree; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.model.Position.RelativePosition; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link SerializedForm serialized forms}. + */ +public class SerializedFormComparator { + /** The APIs to be compared. */ + protected final Set apis; + /** The command-line options. */ + protected final Options options; + /** The reporter to which to report any differences. */ + protected final Reporter reporter; + + /** + * Creates a comparator to compare the serialized forms for an element across a set of APIs. + * + * @param apis the set of APIs + * @param options the command-line options + * @param reporter the reporter to which to report differences + */ + public SerializedFormComparator(Set apis, Options options, Reporter reporter) { + this.apis = apis; + this.options = options; + this.reporter = reporter; + } + + /** + * Compares the serialized forms for a type element found at the given position in different APIs. + * + * @param sfPos the position for the serialized form + * @param forms the map giving the serialized forms in the different APIs + * + * @return {@code true} if all the elements are equal + */ + public boolean compare(Position sfPos, APIMap forms) { + if (forms.isEmpty()) { + return true; + } + + boolean allEqual = false; + reporter.comparing(sfPos, forms); + try { + allEqual = checkMissing(sfPos, forms); + //if (forms.size() > 1) { + allEqual &= compareSerialVersionUIDs(sfPos, forms); + allEqual &= compareSerializationOverviews(sfPos, forms); + allEqual &= compareSerializedFields(sfPos, forms); + allEqual &= compareSerializationMethods(sfPos, forms); + //} + } finally { + reporter.completed(sfPos, allEqual); + } + return allEqual; + } + + private boolean checkMissing(Position pos, Map map) { + Set missing = apis.stream() + .filter(a -> !map.containsKey(a)) + .collect(Collectors.toSet()); // warning: unordered + + if (missing.isEmpty()) { + return true; + } else { + reporter.reportMissing(pos, missing); + return false; + } + } + + private boolean compareSerialVersionUIDs(Position sfPos, APIMap fMap) { + // Just direct comparison of long values, explicit or default; + // there is no doc comment, but there may be an item in the serialized-form.html file + Position uPos = sfPos.serialVersionUID(); + APIMap uMap = APIMap.of(); + fMap.forEach((api, sf) -> uMap.put(api, sf.getSerialVersionUID())); + + boolean allEqual = false; + reporter.comparing(uPos, uMap); + try { + allEqual = checkMissing(uPos, uMap); + if (fMap.size() > 1) { + long archetype = uMap.values().iterator().next(); + boolean eq = uMap.values().stream() + .allMatch(u -> u == archetype); + if (!eq) { + reporter.reportDifferentValues(uPos, uMap); + } + allEqual &= eq; + } + allEqual &= compareDescriptions(uPos, fMap, SerializedFormDocs::getSerialVersionUID); + } finally { + reporter.completed(uPos, allEqual); + } + return allEqual; + } + + private boolean compareSerializationOverviews(Position sfPos, APIMap fMap) { + Position oPos = sfPos.serializationOverview(); + + boolean allEqual = false; + reporter.comparing(oPos, fMap); + try { + // there no derivative map other than the map of descriptions created in compareDescriptions + allEqual = compareDescriptions(oPos, fMap, SerializedFormDocs::getOverview); + } finally { + reporter.completed(oPos, allEqual); + } + return allEqual; + } + + private boolean compareDescriptions(Position pos, APIMap fMap, + Function f) { + APIMap descriptions = fMap.map((api, sf) -> { + SerializedFormDocs docs = sf.getDocs(); + return (docs == null) ? null : f.apply(docs); + }); + + // Descriptions are equal if none of the serialized forms has a description, + // or if they all have the same description. + // TODO: checkMissing? + boolean allEqual = descriptions.isEmpty() + || descriptions.size() == fMap.size() && descriptions.values().stream().distinct().count() == 1; + if (!allEqual) { + reporter.reportDifferentApiDescriptions(pos, descriptions); + } + return allEqual; + } + + private boolean compareSerializedFields(Position sfPos, APIMap forms) { + Map> fieldsMap = new TreeMap<>(); + forms.forEach((api, sf) -> { + for (SerializedForm.Field f : sf.getFields()) { + fieldsMap.computeIfAbsent(f.getName().toString(), __ -> APIMap.of()).put(api, f); + } + }); + + boolean allEqual = true; + for (Map.Entry> e : fieldsMap.entrySet()) { + String name = e.getKey(); + APIMap fMap = e.getValue(); + boolean equal = compare(sfPos, name, fMap); + allEqual &= equal; + } + return allEqual; + } + + private boolean compare(Position sfPos, String name, APIMap fMap) { + RelativePosition fPos = sfPos.serializedField(name); + boolean allEqual = false; + reporter.comparing(fPos, fMap); + try { + allEqual = checkMissing(fPos, fMap); + if (fMap.size() > 1) { + allEqual &= compareSignatures(fPos, fMap); + allEqual &= compareDocComments(fPos, fMap); + allEqual &= compareSerializedFieldDescriptions(fPos, fMap); + } + } finally { + reporter.completed(fPos, allEqual); + } + return allEqual; + + } + + private boolean compareSignatures(Position fPos, APIMap fMap) { + // If any field being compared has type of NONE, the field's type could not be resolved. + // (Ideally, the representation would use ERROR instead of NONE, but that is not possible.) + // Treat all such instances are automatically different to anything else. + APIMap tMap = APIMap.of(); + fMap.forEach((api, f) -> tMap.put(api, f.getType())); + if (fMap.values().stream().anyMatch(f -> f.getType().getKind() == TypeKind.NONE)) { + reporter.reportDifferentTypes(fPos, tMap); + return false; + } else { + TypeMirrorComparator tmc = new TypeMirrorComparator(fMap.keySet(), reporter); + return tmc.compare(fPos, tMap); + } + } + + private boolean compareDocComments(Position fPos, APIMap fMap) { + APIMap rawDocComments = APIMap.of(); + for (Map.Entry entry : fMap.entrySet()) { + API api = entry.getKey(); + SerializedForm.Field f = entry.getValue(); + String c = getDocComment(f); + if (c != null) { + rawDocComments.put(api, c); + } + } + // raw doc comments are equal if none of the elements has a doc comment, + // or if they all have the same doc comment. + // TODO: checkMissing? + boolean allEqual = rawDocComments.isEmpty() + || rawDocComments.size() == fMap.size() && rawDocComments.values().stream().distinct().count() == 1; + if (!allEqual) { + reporter.reportDifferentRawDocComments(fPos, rawDocComments); + } + return allEqual; + } + + private String getDocComment(SerializedForm.Field f) { + List trees = f.getDocComment(); + if (trees == null) { + return null; + } + return trees.stream().map(Object::toString).collect(Collectors.joining()); + } + + private boolean compareSerializedFieldDescriptions(Position fPos, APIMap fMap) { + APIMap descriptions = fMap.map((api, f) -> { + SerializedForm form = api.getSerializedForm(f.getEnclosingTypeElement()); + if (form == null) return null; + SerializedFormDocs docs = form.getDocs(); + if (docs == null) return null; + return docs.getFieldDescription(f.getName().toString()); + }); + + // Descriptions are equal if none of the fields has a description, + // or if they all have the same description. + // TODO: checkMissing? + boolean allEqual = descriptions.isEmpty() + || descriptions.size() == fMap.size() && descriptions.values().stream().distinct().count() == 1; + if (!allEqual) { + reporter.reportDifferentApiDescriptions(fPos, descriptions); + } + return allEqual; + } + + private boolean compareSerializationMethods(Position sfPos, APIMap forms) { + Map, APIMap> mMap = new TreeMap<>(RelativePosition.stringIndexComparator); + forms.forEach((api, sf) -> { + for (ExecutableElement m : sf.getMethods()) { + RelativePosition mPos = sfPos.serializationMethod(m.getSimpleName().toString()); + mMap.computeIfAbsent(mPos, p -> APIMap.of()).put(api, m); + } + }); + + ExecutableComparator ec = new ExecutableComparator(apis, options, reporter) { + /** + * Returns the API description found in the serialized-form.html file. + */ + @Override + protected String getApiDescription(API api, Element e) { + SerializedFormDocs docs = forms.get(api).getDocs(); + return docs == null ? null : docs.getMethodDescription(e.getSimpleName().toString()); + } + }; + + boolean allEqual = true; + for (Map.Entry, APIMap> entry : mMap.entrySet()) { + allEqual &= ec.compare(entry.getKey(), entry.getValue()); + } + return allEqual; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/SerializedFormDocs.java b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormDocs.java new file mode 100644 index 0000000..826c980 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormDocs.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import jdk.codetools.apidiff.Log; + +/** + * A class containing the descriptions for the serialized form of a type, + * as found in the appropriate file generated by javadoc. + */ +public class SerializedFormDocs { + private final String serialVersionUID; + private final String overview; + private final Map fields; + private final Map methods; + + /** + * Reads a file generated by javadoc, and extracts the descriptions for + * the serialized forms of the types contained therein. + * + *

        If the file does not exist, an empty map will be returned. + * + * @param log a log to which any errors will be reported + * @param file the file to be read + * + * @return an instance of {@code APIDocs} that contains the descriptions + * found in the file. + */ + public static Map read(Log log, Path file) { + if (Files.exists(file)) { + SerializedFormReader r = new SerializedFormReader(log); + r.read(file); + return r.getSerializedFormDocs(); + } else { + return Collections.emptyMap(); + } + } + + /** + * Creates an instance of {@code SerializedFormDocs}. + * + * @param serialVersionUID the serial version UID, or {@code null} + * @param overview the overview, or {@code null} + * @param fields the collection of descriptions for the serialized fields + * @param methods the collection of descriptions for the serialization methods + */ + SerializedFormDocs(String serialVersionUID, String overview, + Map fields, Map methods) { + this.serialVersionUID = serialVersionUID; + this.overview = overview; + this.fields = fields; + this.methods = methods; + } + + /** + * Returns the serial version UID for the type, or {@code null} if not known. + * + * @return the serial version UID + */ + public String getSerialVersionUID() { + return serialVersionUID; + } + + /** + * Returns the serialization overview for the type, or {@code null} if not given. + * The overview comes from the documentation comment for the + * {@code serialPersistentFields} member. + * + * @return the serialization overview + */ + public String getOverview() { + return overview; + } + + /** + * Returns a map containing the descriptions of all the serialized fields + * for this type that were found in the file. + * + * @return a map of descriptions, indexed by the name of the field + */ + public Map getFieldDescriptions() { return fields; } + + /** + * Returns the description for a serialized field, or {@code null} if not found. + * + * @param name the name of the field + * + * @return the description + */ + public String getFieldDescription(String name) { + return fields.get(name); + } + + /** + * Returns a map containing the descriptions of all the serialization methods + * for this type that were found in the file. + * + * @return a map of descriptions, indexed by the name of the method + */ + public Map getMethodDescriptions() { return methods; } + + /** + * Returns the description for a serialization method, or {@code null} if not found. + * No serialization methods are overloaded, and so it is not necessary to include + * the list of parameter types. + * + * @param name the name of the method + * + * @return the description + */ + public String getMethodDescription(String name) { + return methods.get(name); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/SerializedFormFactory.java b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormFactory.java new file mode 100644 index 0000000..68dcb2e --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormFactory.java @@ -0,0 +1,959 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; +import javax.lang.model.util.SimpleTypeVisitor14; +import javax.lang.model.util.Types; + +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.ReferenceTree; +import com.sun.source.doctree.SerialFieldTree; +import com.sun.source.doctree.SerialTree; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.DocTreePath; +import com.sun.source.util.DocTreePathScanner; +import com.sun.source.util.DocTrees; +import com.sun.source.util.TreePath; + +/** + * A factory to create the {@link SerializedForm} object for a + * type element if appropriate. + * + * A serialized form is only created for a type element if + * it is {@code Serializable} but not an enum, and if it + * is marked with {@code @serial include}, or the enclosing + * package is not marked with {@code serial exclude} and + * the type is {@code public} or {@code protected}. + */ +public class SerializedFormFactory { + + private final Map excludedPackages; + + private final Elements elements; + private final Types types; + private final DocTrees trees; + + private final TypeMirror serializable; + private final TypeMirror externalizable; + private final TypeMirror objectInput; + private final TypeMirror objectInputStream; + private final TypeMirror objectOutput; + private final TypeMirror objectOutputStream; + private final TypeMirror objectStreamField; + + private final Name readExternal; + private final Name writeExternal; + private final Name readObject; + private final Name readObjectNoData; + private final Name writeObject; + private final Name readResolve; + private final Name writeReplace; + private final Name serialPersistentFields; + private final Name serialVersionUID; + + private Set privateStaticFinal = Set.of(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL); + + /** + * Creates an instance of {@code SerializedFormFactory} using the utility objects for + * {@code Elements}, {@code Types} and {@code Trees} available from an API. + * + * @param api the API + */ + public SerializedFormFactory(API api) { + this(api.getElements(), api.getTypes(), api.getTrees()); + } + + /** + * Creates an instance of {@code SerializedFormFactory} using the given utility objects. + * + * @param elements the {@code Elements} utility class to be used + * @param types the {@code Types} utility class to be used + * @param trees the {@code DocTrees} utility class to be used + */ + public SerializedFormFactory(Elements elements, Types types, DocTrees trees) { + this.elements = elements; + this.types = types; + this.trees = trees; + + excludedPackages = new HashMap<>(); + + ModuleElement javaBase = elements.getModuleElement("java.base"); + + serializable = getType(javaBase, "java.io.Serializable"); + externalizable = getType(javaBase, "java.io.Externalizable"); + objectInput = getType(javaBase, "java.io.ObjectInput"); + objectInputStream = getType(javaBase, "java.io.ObjectInputStream"); + objectOutput = getType(javaBase, "java.io.ObjectOutput"); + objectOutputStream = getType(javaBase, "java.io.ObjectOutputStream"); + objectStreamField = getType(javaBase, "java.io.ObjectStreamField"); + + readExternal = elements.getName("readExternal"); + writeExternal = elements.getName("writeExternal"); + readObject = elements.getName("readObject"); + readObjectNoData = elements.getName("readObjectNoData"); + writeObject = elements.getName("writeObject"); + readResolve = elements.getName("readResolve"); + writeReplace = elements.getName("writeReplace"); + serialPersistentFields = elements.getName("serialPersistentFields"); + serialVersionUID = elements.getName("serialVersionUID"); + } + + /** + * Returns the instance of {@code SerializedFormDocs} containing the information + * related to a given type element, or {@code null} if no such information is available. + * + * This implementation returns {@code null}. + * + * @param te the type element + * + * @return the instance of {@code SerializedFormDocs} containing the information + */ + public SerializedFormDocs getSerializedFormDocs(TypeElement te) { + return null; + } + + /** + * Returns a type of an element with a given canonical name, as seen from the given module. + * + * @param me the module + * @param name the name + * + * @return the type of the element + */ + private TypeMirror getType(ModuleElement me, String name) { + return elements.getTypeElement(me, name).asType(); + } + + /** + * Returns the {@code SerializedForm} object for a type element, or null if it does not have one. + * + * @param te the type element + * + * @return the {@code SerializedForm} object + */ + public SerializedForm get(TypeElement te) { + if (!isIncluded(te)) { + return null; + } + + long serialVersionUID = getSerialVersionUID(te); + + List fields; + List methods; + + if (types.isAssignable(te.asType(), externalizable)) { + fields = List.of(); + methods = getExternalizableMethods(te); + } else { + fields = getSerializableFields(te); + methods = getSerializableMethods(te); + } + + SerializedFormDocs docs = getSerializedFormDocs(te); + + return new SerializedForm(serialVersionUID, fields, methods, docs); + } + + // + + /** + * Determines if a type element has a specific serialized form. + * + * A type element has a specific serialized form if + * it is {@code Serializable} but not an enum, and if it + * is marked with {@code @serial include}, or the enclosing + * package is not marked with {@code serial exclude} and + * the type is {@code public} or {@code protected}. + * + * @param te the type element + * + * @return {@code true} if and only if the type element has a specific + * serialized form + */ + private boolean isIncluded(TypeElement te) { + if (te.getKind() == ElementKind.ENUM + || !types.isAssignable(te.asType(), serializable)) { + return false; + } + + Optional serialTrees = getSerialTrees(te); + if (matches(serialTrees, "include")) { + return true; + } + + if (matches(serialTrees, "exclude") + || excludedPackages.computeIfAbsent(elements.getPackageOf(te), + p -> matches(getSerialTrees(p), "exclude"))) { + return false; + } + + Set modifiers = te.getModifiers(); + return modifiers.contains(Modifier.PUBLIC) + || modifiers.contains(Modifier.PROTECTED); + } + + /** + * Returns whether an optional {@code SerialTree} object matches the given kind. + * The kind is typically "include" or "exclude". + * + * @param optSerial the optional {@code SerialTree} + * @param kind the kind + * + * @return {@code true} if and only if a match is found + */ + private boolean matches(Optional optSerial, String kind) { + return optSerial.isPresent() && optSerial.get().getDescription().toString().equals(kind); + } + + /** + * Returns the {@code {@serial ...}} tag, if any, in the doc comment for an element. + * + * @param e the element + * + * @return the tag + */ + private Optional getSerialTrees(Element e) { + DocCommentTree dct = trees.getDocCommentTree(e); + if (dct == null) { + return Optional.empty(); + } + + return dct.getBlockTags().stream() + .filter(t -> t.getKind() == DocTree.Kind.SERIAL) + .map(t -> (SerialTree) t) + .findFirst(); + } + + // + + // + + /** + * Returns the {@code serialVersionUID} for a type element. + * If the type element defines an appropriate field, the constant value + * of the field is returned; otherwise, the default value is computed. + * + * @param te the type element + * + * @return the serial version UID + */ + private long getSerialVersionUID(TypeElement te) { + VariableElement ve = te.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.FIELD + && e.getSimpleName() == serialVersionUID) + .map(e -> (VariableElement) e) + .findFirst() + .orElse(null); + + if (ve != null + && ve.getModifiers().contains(Modifier.STATIC) + && ve.getModifiers().contains(Modifier.FINAL) + && types.isSameType(ve.asType(), types.getPrimitiveType(TypeKind.LONG))) { + Object o = ve.getConstantValue(); + if (o instanceof Long) { + return (Long) o; + } + } + + return computeDefaultSUID(te); + } + + /** + * Computes the default serial version UID value for the given class. + * + * This code is translated from the corresponding code in {@code java.io.ObjectStreamClass}, + * converting it from using runtime reflection to compile-time reflection. + */ + private long computeDefaultSUID(TypeElement te) { + try { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + DataOutputStream dout = new DataOutputStream(bout); + + dout.writeUTF(te.getQualifiedName().toString()); + + int classMods = IntModifier.getModifiers(te) & + (IntModifier.PUBLIC | IntModifier.FINAL | + IntModifier.INTERFACE | IntModifier.ABSTRACT); + + /* + * compensate for javac bug in which ABSTRACT bit was set for an + * interface only if the interface declared methods + */ + List methods = ElementFilter.methodsIn(te.getEnclosedElements()); + if ((classMods & IntModifier.INTERFACE) != 0) { + classMods = (methods.size() > 0) ? + (classMods | IntModifier.ABSTRACT) : + (classMods & ~IntModifier.ABSTRACT); + } + dout.writeInt(classMods); + + if (te.asType().getKind() != TypeKind.ARRAY) { + /* + * compensate for change in 1.2FCS in which + * Class.getInterfaces() was modified to return Cloneable and + * Serializable for array classes. + */ + List interfaces = te.getInterfaces(); + List ifaceNames = interfaces.stream() + .map(SerializedFormFactory::getInterfaceName) + .sorted() + .collect(Collectors.toList()); + for (String n : ifaceNames) { + dout.writeUTF(n); + } + } + + List fields = ElementFilter.fieldsIn(te.getEnclosedElements()); + List fieldSigs = fields.stream() + .map(MemberSignature::new) + .sorted(Comparator.comparing(ms -> ms.name)) + .collect(Collectors.toList()); + for (MemberSignature sig : fieldSigs) { + int mods = IntModifier.getModifiers(sig.member) & + (IntModifier.PUBLIC | IntModifier.PRIVATE | IntModifier.PROTECTED | + IntModifier.STATIC | IntModifier.FINAL | IntModifier.VOLATILE | + IntModifier.TRANSIENT); + if (((mods & IntModifier.PRIVATE) == 0) || + ((mods & (IntModifier.STATIC | IntModifier.TRANSIENT)) == 0)) + { + dout.writeUTF(sig.name); + dout.writeInt(mods); + dout.writeUTF(sig.signature); + } + } + + if (hasStaticInitializer(te)) { + dout.writeUTF(""); + dout.writeInt(IntModifier.STATIC); + dout.writeUTF("()V"); + } + + List cons = ElementFilter.constructorsIn(te.getEnclosedElements()); + List consSigs = cons.stream() + .map(MemberSignature::new) + .sorted(Comparator.comparing(ms -> ms.signature)) + .collect(Collectors.toList()); + for (MemberSignature sig : consSigs) { + int mods = IntModifier.getModifiers(sig.member) & + (IntModifier.PUBLIC | IntModifier.PRIVATE | IntModifier.PROTECTED | + IntModifier.STATIC | IntModifier.FINAL | + IntModifier.SYNCHRONIZED | IntModifier.NATIVE | + IntModifier.ABSTRACT | IntModifier.STRICT); + if ((mods & IntModifier.PRIVATE) == 0) { + dout.writeUTF(""); + dout.writeInt(mods); + dout.writeUTF(sig.signature.replace('/', '.')); + } + } + + List methSigs = methods.stream() + .map(MemberSignature::new) + .sorted(Comparator.comparing((MemberSignature ms) -> ms.name) + .thenComparing(ms -> ms.signature)) + .collect(Collectors.toList()); + for (MemberSignature sig : methSigs) { + int mods = IntModifier.getModifiers(sig.member) & + (IntModifier.PUBLIC | IntModifier.PRIVATE | IntModifier.PROTECTED | + IntModifier.STATIC | IntModifier.FINAL | + IntModifier.SYNCHRONIZED | IntModifier.NATIVE | + IntModifier.ABSTRACT | IntModifier.STRICT); + if ((mods & IntModifier.PRIVATE) == 0) { + dout.writeUTF(sig.name); + dout.writeInt(mods); + dout.writeUTF(sig.signature.replace('/', '.')); + } + } + + dout.flush(); + + MessageDigest md = MessageDigest.getInstance("SHA"); + byte[] hashBytes = md.digest(bout.toByteArray()); + long hash = 0; + for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) { + hash = (hash << 8) | (hashBytes[i] & 0xFF); + } + return hash; + } catch (IOException ex) { + throw new InternalError(ex); + } catch (NoSuchAlgorithmException ex) { + throw new InternalError(ex.getMessage()); + } + } + + /** + * Returns the fully qualified name for a type mirror representing an interface, + * such as found in the superinterfaces of a class. + * + * @param t the type mirror + * + * @return the name + */ + private static String getInterfaceName(TypeMirror t) { + Element e = ((DeclaredType) t).asElement(); + return ((TypeElement) e).getQualifiedName().toString(); + } + + /** + * Returns whether a type element has, or will have, a static initializer. + * A type has a static initializer if it has an executable member named {@code }. + * This may arise due to explicit presence of {@code static { ... }} in source code, + * or to hold the initialization of static fields with a non-constant value. + * + * @param te the type element + * + * @return if the type element has or will have a static initializer + */ + private boolean hasStaticInitializer(TypeElement te) { + if (te.getEnclosedElements().stream().anyMatch(e -> e.getKind() == ElementKind.STATIC_INIT)) { + return true; + } + + // if the source is available, scan the AST for the element, looking for + // either 'static { ... }' or static variables with non-constant initializers + TreePath p = trees.getPath(te); + if (p != null && p.getLeaf() instanceof ClassTree) { + ClassTree ct = (ClassTree) p.getLeaf(); + for (Tree t : ct.getMembers()) { + switch (t.getKind()) { + case BLOCK -> { + BlockTree bt = (BlockTree) t; + if (bt.isStatic()) { + // found an explicit static initializer block + return true; + } + } + case VARIABLE -> { + VariableTree vt = (VariableTree) t; + if (vt.getModifiers().getFlags().contains(Modifier.STATIC) + && vt.getInitializer() != null) { + Element e = trees.getElement(new TreePath(p, vt)); + if (e != null && e.getKind() == ElementKind.FIELD) { + Object cv = ((VariableElement) e).getConstantValue(); + if (cv == null) { + // found field with an initializer that is not a constant + // expression, and so will require an implicit static initializer block + return true; + } + } + } + } + } + } + } + + return false; + } + + /** + * A wrapper around runtime modifiers. + * These are distinct from {@link javax.lang.model.element.Modifier}, + * and while they are similar, there is not a direct one-to-one correspondence. + * For example, {@code javax.lang.model} models interfaces differently, + * and runtime reflection has not explicit value equivalent for DEFAULT. + * + * Note, the spec for the computing the default serialVersionUID is defined + * in terms of the runtime kind of modifiers. + */ + private static class IntModifier { + + static final int ABSTRACT = java.lang.reflect.Modifier.ABSTRACT; + static final int FINAL = java.lang.reflect.Modifier.FINAL; + static final int INTERFACE = java.lang.reflect.Modifier.INTERFACE; + static final int NATIVE = java.lang.reflect.Modifier.NATIVE; + static final int PRIVATE = java.lang.reflect.Modifier.PRIVATE; + static final int PROTECTED = java.lang.reflect.Modifier.PROTECTED; + static final int PUBLIC = java.lang.reflect.Modifier.PUBLIC; + static final int STATIC = java.lang.reflect.Modifier.STATIC; + static final int STRICT = java.lang.reflect.Modifier.STRICT; + static final int SYNCHRONIZED = java.lang.reflect.Modifier.SYNCHRONIZED; + static final int TRANSIENT = java.lang.reflect.Modifier.TRANSIENT; + static final int VOLATILE = java.lang.reflect.Modifier.VOLATILE; + + static int getModifiers(Element e) { + int mods = 0; + for (Modifier m : e.getModifiers()) { + switch (m) { + case ABSTRACT: mods |= ABSTRACT; break; + case DEFAULT: /* no equivalent */ break; + case FINAL: mods |= FINAL; break; + case NATIVE: mods |= NATIVE; break; + case PRIVATE: mods |= PRIVATE; break; + case PROTECTED: mods |= PROTECTED; break; + case PUBLIC: mods |= PUBLIC; break; + case STATIC: mods |= STATIC; break; + case STRICTFP: mods |= STRICT; break; + case SYNCHRONIZED: mods |= SYNCHRONIZED; break; + case TRANSIENT: mods |= TRANSIENT; break; + case VOLATILE: mods |= VOLATILE; break; + } + } + + if (e.getKind().isInterface()) { + mods |= INTERFACE; + } + + return mods; + } + } + + /** + * A simple container for a field or executable member of a type element, + * providing the information that will be used to computer the default serialVersionUID. + */ + private static class MemberSignature { + String name; + Element member; + String signature; + + MemberSignature(Element ve) { + name = ve.getSimpleName().toString(); + member = ve; + signature = descriptorVisitor.visit(ve.asType(), new StringBuilder()).toString(); + } + } + + /** + * A visitor to compute the signature (descriptor) for members of a type element. + */ + private static TypeVisitor descriptorVisitor = new SimpleTypeVisitor14<>() { + @Override + public StringBuilder defaultAction(TypeMirror t, StringBuilder sb) { + throw new Error(t.getKind() + ": " + t.toString()); + } + + @Override + public StringBuilder visitArray(ArrayType t, StringBuilder sb) { + sb.append("["); + return t.getComponentType().accept(this, sb); + } + + @Override + public StringBuilder visitDeclared(DeclaredType t, StringBuilder sb) { + return sb.append("L") + .append(((TypeElement) t.asElement()).getQualifiedName().toString().replace(".", "/")) + .append(";"); + } + + @Override + public StringBuilder visitExecutable(ExecutableType t, StringBuilder sb) { + sb.append('('); + for (TypeMirror p : t.getParameterTypes()) { + p.accept(this, sb); + } + sb.append(')'); + return t.getReturnType().accept(this, sb); + } + + @Override + public StringBuilder visitTypeVariable(TypeVariable t, StringBuilder sb) { + return sb.append("Ljava/lang/Object;"); // TODO: use bounds? types.erasure(t).accept(this, sb) ? + } + + @Override + public StringBuilder visitNoType(NoType t, StringBuilder sb) { + + if (t.getKind() != TypeKind.VOID) { + throw new IllegalArgumentException((t.toString())); + } + return sb.append('V'); + } + + @Override + public StringBuilder visitPrimitive(PrimitiveType t, StringBuilder sb) { + char ch = switch (t.getKind()) { + case BYTE -> 'B'; + case CHAR -> 'C'; + case DOUBLE -> 'D'; + case FLOAT -> 'F'; + case INT -> 'I'; + case LONG -> 'L'; + case SHORT -> 'S'; + case BOOLEAN -> 'Z'; + default -> throw new IllegalArgumentException(t.toString()); + }; + return sb.append(ch); + } + }; + + // + + // + + /** + * Returns the list of methods related to the serialization in a type element + * that is externalizable. + * + * The list includes: {@code readExternal}, {@code writeExternal}, {@code readResolve} + * and {@code writeReplace}. + * + * @param te the type element + * + * @return the list + */ + private List getExternalizableMethods(TypeElement te) { + return getMethods(te, ee -> + isMethod(ee, readExternal, objectInput) + || isMethod(ee, writeExternal, objectOutput) + || isMethod(ee, readResolve) + || isMethod(ee, writeReplace)); + } + + /** + * Returns the list of methods related to the serialization in a type element + * that is serializable (but not externalizable). + * + * The list includes: {@code readObject}, {@code readObjectNoData}, {@code writeObject}, + * {@code readResolve} and {@code writeReplace}. + * + * @param te the type element + * + * @return the list + */ + private List getSerializableMethods(TypeElement te) { + return getMethods(te, ee -> + isMethod(ee, readObject, objectInputStream) + || isMethod(ee, readObjectNoData) + || isMethod(ee, writeObject, objectOutputStream) + || isMethod(ee, readResolve) + || isMethod(ee, writeReplace)); + } + + /** + * Returns the list of methods in a type element that match a given predicate. + * + * @param te the type element + * @param filter the predicate + * + * @return the list + */ + private List getMethods(TypeElement te, Predicate filter) { + Map map = new HashMap<>(); + for (Element e : elements.getAllMembers(te)) { + if (e.getKind() != ElementKind.METHOD) { + continue; + } + + ExecutableElement ee = (ExecutableElement) e; + if (filter.test(ee)) { + ExecutableElement prev = map.get(ee.getSimpleName()); + if (prev == null || elements.overrides(ee, prev, te)) { + map.put(ee.getSimpleName(), ee); + } + } + } + List list = new ArrayList<>(map.values()); + list.sort((e1, e2) -> CharSequence.compare(e1.getSimpleName(), e2.getSimpleName())); + return list; + } + + /** + * Returns whether an executable element has a given name and no parameters. + * + * @param ee the element + * @param name the name + * + * @return true if the element has the given name and no parameters + */ + private boolean isMethod(ExecutableElement ee, Name name) { + return ee.getSimpleName() == name + && ee.getParameters().isEmpty(); + } + + /** + * Returns whether an executable element has a given name and a single parameter + * of a given type. + * + * @param ee the element + * @param name the name + * @param param the parameter type + * + * @return true if the element has the given name and no parameters + */ + private boolean isMethod(ExecutableElement ee, Name name, TypeMirror param) { + return ee.getSimpleName() == name + && ee.getParameters().size() == 1 + && types.isSameType(ee.getParameters().get(0).asType(), param); + } + // + + // + + /** + * Returns the list of fields related to the serialization in a type element + * that is serializable (but not externalizable). + * + * The list contains the default set of fields to be serialized. + * This set is determined from the {@code @serialField} tags on the {@code persistentSerialFields} + * (if defined), or the list of non-static non-transient fields declared + * in the type element. + * + * The list also contains the fields for {@code serialVersionUID} and {@code persistentSerialFields}, + * if present. They can be distinguished from the default set of fields to be serialized + * by name and by being declared to be {@code static}. + * + * + * @param te the type element + * + * @return the list of fields in the serialized form + */ + private List getSerializableFields(TypeElement te) { + List list = new ArrayList<>(); + + VariableElement spf = te.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.FIELD) + .map(e -> (VariableElement) e) + .filter(this::isSerialPersistentFields) + .findFirst() + .orElse(null); + + if (spf != null) { + DocCommentTree dct = trees.getDocCommentTree(spf); + if (dct != null) { + DocTreePathScanner> scanner = new DocTreePathScanner<>() { + @Override + public Void visitSerialField(SerialFieldTree tree, List list) { + list.add(new DocumentedField(te, getCurrentPath())); + return null; + } + }; + scanner.scan(new DocTreePath(trees.getPath(te), dct), list); + } + } else { + for (VariableElement ve : ElementFilter.fieldsIn(te.getEnclosedElements())) { + Set modifiers = ve.getModifiers(); + if (modifiers.contains(Modifier.STATIC) || modifiers.contains(Modifier.TRANSIENT)) { + continue; + } + list.add(new VariableElementField(ve)); + } + } + + return list; + } + + /** + * Returns whether a field is a valid declaration of {@code serialPersistentFields}. + * + * @param ve the field + * + * @return {@code true} if and only if this is a valid declaration of {@code serialPersistentFields} + */ + private boolean isSerialPersistentFields(VariableElement ve) { + return ve.getSimpleName() == serialPersistentFields + && ve.getModifiers().equals(privateStaticFinal) + && types.isSameType(ve.asType(), types.getArrayType(objectStreamField)); + } + + /** + * Details for a field in a serialized form, that is derived from information + * in {@code @serialField} tags on the {@code serialPersistentFields} field. + */ + private class DocumentedField implements SerializedForm.Field { + private final TypeElement enclosingTypeElement; + private final Name name; + private final TypeMirror type; + private final List description; + private final String signature; + + DocumentedField(TypeElement te, DocTreePath p) { + enclosingTypeElement = te; + DocTree t = p.getLeaf(); + if (t.getKind() != DocTree.Kind.SERIAL_FIELD) { + throw new IllegalArgumentException(t.getKind().toString()); + } + SerialFieldTree sft = (SerialFieldTree) t; + name = sft.getName().getName(); + type = getType(p, sft.getType()); + description = sft.getDescription(); + signature = sft.getType().toString(); + } + + @Override + public TypeElement getEnclosingTypeElement() { + return enclosingTypeElement; + } + + /** + * Returns the type for the signature found in a {@code @serialField} tag, + * or a type of kind {@code NONE} if the type cannot be resolved. + * + * Note: it would be better if it was possible to use a type of kind ERROR + * instead of NONE, but that cannot be done with the current API. + * + * javac does not directly support array signatures, so count and remove + * the trailing '[]' characters, look up the base type, and then convert + * to the appropriate number of levels of array. + * + * @param serialFieldPath the path for {@code serialField} tag + * @param refTree the reference tree within the {@code serialField} tag + * + * @return the type + */ + private TypeMirror getType(DocTreePath serialFieldPath, ReferenceTree refTree) { + + String sig = refTree.getSignature(); + int dims = 0; + int index = sig.length(); + while (index > 2) { + if (sig.charAt(index - 2) == '[' && sig.charAt(index - 1) == ']') { + dims++; + index -= 2; + } else { + break; + } + } + + String baseSig = sig.substring(0, index); + TypeMirror t; + switch (baseSig) { + case "boolean" -> t = types.getPrimitiveType(TypeKind.BOOLEAN); + case "byte" -> t = types.getPrimitiveType(TypeKind.BYTE); + case "char" -> t = types.getPrimitiveType(TypeKind.CHAR); + case "double" -> t = types.getPrimitiveType(TypeKind.DOUBLE); + case "float" -> t = types.getPrimitiveType(TypeKind.FLOAT); + case "int" -> t = types.getPrimitiveType(TypeKind.INT); + case "long" -> t = types.getPrimitiveType(TypeKind.LONG); + case "short" -> t = types.getPrimitiveType(TypeKind.SHORT); + case "void" -> t = types.getPrimitiveType(TypeKind.VOID); + default -> { + DocTreePath refPath = new DocTreePath(serialFieldPath, + dims == 0 ? refTree : trees.getDocTreeFactory().newReferenceTree(baseSig)); + Element e = trees.getElement(refPath); + if (e == null) { + // ideally, we would be able to use an instance of an ERROR type, + // but that is not available in the API, so use NONE as a marker value instead. + return types.getNoType(TypeKind.NONE); + } + t = e.asType(); + } + } + + while (dims > 0) { + t = types.getArrayType(t); + dims--; + } + + return t; + } + + @Override + public Name getName() { + return name; + } + + @Override + public TypeMirror getType() { + return type; + } + + @Override + public List getDocComment() { + return description; + } + + @Override + public String getSignature() { + return signature; + } + } + + /** + * Details for a field in a serialized form, that is derived from a field + * in the type element. + */ + private class VariableElementField implements SerializedForm.Field { + VariableElement ve; + + VariableElementField(VariableElement ve) { + this.ve = ve; + } + + @Override + public TypeElement getEnclosingTypeElement() { + return (TypeElement) ve.getEnclosingElement(); + } + + @Override + public Name getName() { + return ve.getSimpleName(); + } + + @Override + public TypeMirror getType() { + return ve.asType(); + } + + @Override + public List getDocComment() { + DocCommentTree dct = trees.getDocCommentTree(ve); + return dct == null ? null : List.of(dct); + } + + @Override + public String getSignature() { + return ve.asType().toString(); + } + } + // +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/SerializedFormReader.java b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormReader.java new file mode 100644 index 0000000..dfb7c97 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/SerializedFormReader.java @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jdk.codetools.apidiff.Log; + +/** + * A reader that reads the descriptions for the serialized forms of types, + * from the serialized-form.html file generated by javadoc. + * + *

        The output generated by javadoc is not well-specified and varies across releases. + * The following rules allow fuzzy parsing for at least JDK version 11 and later. + * + *

          + *
        • The details for a type are introduced by a heading beginning "Class name". + * + *
        • The serialVersionUID is contained in a definition list: + * {@code
          serialVersionUID:
          the-value
          } + * + *
        • The serialization overview is introduced by a heading "Serialization Overview". + * The description begins after the following {@code
        • } and ends at the + * corresponding {@code
        • }. + * + *
        • The serialized fields are introduced by a heading "Serialized Fields". + * The individual fields are introduced by headings containing the name of the field. + * The description of the field begins after the {@code
          } containing the
          + *     signature, and ends before the enclosing {@code 
        • }. + * + *
        • The serialization methods are introduced by a heading "Serialization Methods". + * The individual methods are introduced by headings containing the name of the method. + * The description of the method begins after the {@code
          } containing the
          + *     signature, and ends before the enclosing {@code 
        • }. + *
        + * + * The descriptions for a type end when the heading for the next type is seen, + * or when {@code
        } is read. + */ +public class SerializedFormReader extends HtmlParser { + private final Log log; + private Map allDocs; + + /** + * Creates a reader. + * + * @param log a log to which to report any errors. + */ + SerializedFormReader(Log log) { + this.log = log; + } + + // TEMPORARY! + boolean debug = false; + private void debugPrintln(Supplier s) { + if (debug) { + System.err.println(s.get()); + } + } + + @Override + public void read(Path file) { + allDocs = new LinkedHashMap<>(); + super.read(file); + } + + /** + * Returns the collection of descriptions found during a preceding call + * of {@code read}. + * The descriptions are indexed by the name of the corresponding type. + * + * @return the descriptions. + */ + Map getSerializedFormDocs() { + return allDocs; + } + + @Override + protected void error(Path file, int lineNumber, String message) { + log.err.println(file + ":" + lineNumber + ": " + message); + } + + @Override + protected void error(Path file, int lineNumber, Throwable t) { + log.err.println(file + ":" + lineNumber + ": " + t); + } + + private StringBuilder contentBuffer; + private boolean inMain; + private int startElementIndex; + private int startDescriptionIndex; + private boolean inDescription; + private boolean inMemberSignature; + private int ulDepth; + + /** + * An indication of the position of the parser. + */ + private enum State { + /** + * Before any types have been detected. + * It transitions to {@code TYPE} when the heading for a type is found. + */ + INIT, + /** + * Within the details for a type, but not in any specific part of the type. + * The state is entered when the heading for a type is found. + */ + TYPE, + /** + * Within the details for the serial version UID. + * The state is entered when the heading for the {@code serialVersionUID} is found, + * and reverts to {@code TYPE} when the description has been read. + */ + SERIAL_VERSION_UID, + /** + * Within the details for the serialization overview. + * The state is entered when the heading for the serialization overview is found, + * and reverts to {@code TYPE} when the description has been read. + */ + OVERVIEW, + /** + * Within the details for the serialized fields, but not in any specific field. + * The state is entered when the heading for the serialized fields is found, + * and will typically progress to progress to {@code FIELD}, {@code METHODS}, + * or {@code TYPE}, depending on which kind of heading is found. + */ + FIELDS, + /** + * Within the details for a specific serialized field. + * The state is entered when the heading for a specific serialized field is found, + * and reverts to {@code FIELDS} when the description has been read. + */ + FIELD, + /** + * Within the details for the serialization methods, but not in any specific method. + * The state is entered when the heading for the serialization methods is found, + * and will typically progress to {@code METHOD} or {@code TYPE}, depending on + * which kind of heading is found. In practice, + */ + METHODS, + /** + * Within the details for a specific serialization method. + * The state is entered when the heading for a specific serialization method is found, + * and reverts to {@code METHODS} when the description has been read. + */ + METHOD + } + private State state; + + /** + * The name of the current type, determined by the heading beginning {@code Class }. + */ + private String currType; + + /** + * The name of the current field or method. + */ + private String currMember; + + /** + * The serialVersionUID. + */ + private String serialVersionUID; + + /** + * The serialization overview. + */ + private String overview; + + /** + * The collection of descriptions for the serialized fields of the current type. + */ + private Map fieldDescriptions; + + /** + * The collection of descriptions for the serialization methods of the current type. + */ + private Map methodDescriptions; + + @Override + protected void content(Supplier content) { + if (contentBuffer != null) { + contentBuffer.append(content.get()); + } + } + + @Override + protected void html() { + startElementIndex = getBufferIndex() - 1; + super.html(); + } + + @Override + protected void startElement(String name, Map attrs, boolean selfClosing) { + // skip everything not in the `
        ` element + if (!inMain) { + if (name.equals("main")) { + inMain = true; + state = State.INIT; + } + return; + } + + debugPrintln(() -> " <" + name + " " + attrs + "> " + state + " " + inDescription + " " + ulDepth); + + switch (name) { + case "dt": + case "dd": + case "h2": case "h3": case "h4": case "h5": + if (!inDescription) { + contentBuffer = new StringBuilder(); + } + break; + + case "div": + String cssClass = attrs.get("class"); + if (cssClass != null && cssClass.matches("member(S|-s)ignature")) { + inMemberSignature = true; + } + break; + + case "li": + if (state == State.OVERVIEW && !inDescription) { + debugPrintln(() -> "start description for " + state + " after start " + name); + inDescription = true; + startDescriptionIndex = getBufferIndex(); + ulDepth = 0; + } + break; + + case "ul": + if (inDescription) { + ulDepth++; + } + break; + } + } + + private final Pattern classPtn = Pattern.compile("(?i)(Class|Exception|Record)(\\s| )++(?\\S+).*"); + + @Override + protected void endElement(String name) { + // skip everything not in the `
        ` element + if (!inMain) { + return; + } + + debugPrintln(() -> " " + state + " " + inDescription + " " + ulDepth); + + switch (name) { + case "dt": + if (!inDescription && contentBuffer.toString().equals("serialVersionUID:")) { + state = State.SERIAL_VERSION_UID; + } + break; + + case "dd": + if (!inDescription && state == State.SERIAL_VERSION_UID) { + serialVersionUID = contentBuffer.toString(); + state = State.TYPE; + } + break; + + case "h2": case "h3": case "h4": case "h5": + if (inDescription) { + break; + } + + String content = contentBuffer.toString(); + contentBuffer = null; + + switch (content) { + case "Serialized Fields": + state = State.FIELDS; + break; + + case "Serialization Methods": + state = State.METHODS; + break; + + case "Serialization Overview": + state = State.OVERVIEW; + break; + + default: + if (ulDepth == 0) { + Matcher m = classPtn.matcher(content); + if (m.matches()) { + if (currType != null) { + saveCurrentDocs(); + } + debugPrintln(() -> "START TYPE " + m.group("name")); + state = State.TYPE; + currType = m.group(1); + serialVersionUID = null; + overview = null; + fieldDescriptions = Collections.emptyMap(); + methodDescriptions = Collections.emptyMap(); + } else { + switch (state) { + case FIELDS -> { + debugPrintln(() -> "START " + state + " " + content); + currMember = content; + state = State.FIELD; + } + case METHODS -> { + debugPrintln(() -> "START " + state + " " + content); + currMember = content; + state = State.METHOD; + } + } + } + } + } + debugPrintln(() -> "Finished heading " + name + " " + state + ": " + content); + break; + + case "li": + if (inDescription && ulDepth == 0) { + String d = getTrimBufferString(startDescriptionIndex, startElementIndex); + switch (state) { + case FIELD -> { + fieldDescriptions = saveMember(fieldDescriptions, d); + state = State.FIELDS; + } + case METHOD -> { + methodDescriptions = saveMember(methodDescriptions, d); + state = State.METHODS; + } + case OVERVIEW -> { + overview = d; + state = State.TYPE; + } + } + debugPrintln(() -> "end description " + state + " " + currMember + " " + d); + inDescription = false; + } + break; + + case "main": + saveCurrentDocs(); + debugPrintln(() -> "Docs for " + allDocs.keySet()); + inMain = false; + break; + + case "pre": + switch (state) { + case FIELD: + case METHOD: + if (!inDescription) { + debugPrintln(() -> "start description for " + state + " after end " + name); + inDescription = true; + startDescriptionIndex = getBufferIndex(); + ulDepth = 0; + } + } + break; + + case "div": + if (inMemberSignature) { + debugPrintln(() -> "start description for " + state + " after end " + name); + inMemberSignature = false; + inDescription = true; + startDescriptionIndex = getBufferIndex(); + ulDepth = 0; + } + break; + + case "ul": + if (inDescription) { + ulDepth--; + } + break; + } + } + + private Map saveMember(Map descriptions, String d) { + if (descriptions.isEmpty()) { + // replace the shared empty map with an unshared mutable map + descriptions = new HashMap<>(); + } + + descriptions.put(currMember, d); + + return descriptions; + } + + private void saveCurrentDocs() { + if (currType != null) { + if (debug) { + debugPrintln(() -> "SAVE type " + currType); + fieldDescriptions.forEach((f, d) -> debugPrintln(() -> "SAVE field " + f + ": " + d)); + methodDescriptions.forEach((m, d) -> debugPrintln(() -> "SAVE method " + m + ": " + d)); + debugPrintln(() -> "SAVE overview " + overview); + } + allDocs.put(currType, + new SerializedFormDocs(serialVersionUID, overview, fieldDescriptions, methodDescriptions)); + currType = null; + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/TypeComparator.java b/src/share/classes/jdk/codetools/apidiff/model/TypeComparator.java new file mode 100644 index 0000000..0185f2d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/TypeComparator.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link TypeElement type elements}: classes, interfaces, enums and annotation types. + * + *

        The comparison includes: + *

          + *
        • the "signature" of the type: its annotations, modifiers, kind, type parameters, supertypes + *
        • the documentation comment for the type + *
        • the selected members in the type + *
        + */ +public class TypeComparator extends ElementComparator { + + private final ElementExtras elementExtras = ElementExtras.instance(); + + /** + * Creates a comparator to compare type elements across a set of APIs. + * + * @param apis the set of APIs + * @param options the command-line options + * @param reporter the reporter to which to report differences + */ + public TypeComparator(Set apis, Options options, Reporter reporter) { + super(apis, options, reporter); + } + + /** + * Compare instances of a type element found at a given position in different APIs. + * + * @param tPos the position of the element + * @param tMap the map giving the instance of the type element in different APIs + * @return {@code true} if all the instances are equivalent + */ + @Override + public boolean compare(Position tPos, APIMap tMap) { + boolean allEqual = false; + reporter.comparing(tPos, tMap); + try { + allEqual = checkMissing(tPos, tMap); + if (tMap.size() > 1) { + allEqual &= compareSignatures(tPos, tMap); + allEqual &= compareDocComments(tPos, tMap); + allEqual &= compareApiDescriptions(tPos, tMap); + allEqual &= compareMembers(tPos, tMap); + allEqual &= compareSerializedForms(tPos, tMap); + } + } finally { + reporter.completed(tPos, allEqual); + } + return allEqual; + } + + private boolean compareSerializedForms(Position tPos, APIMap tMap) { + APIMap forms = APIMap.of(); + tMap.forEach((api, te) -> { + SerializedForm sf = api.getSerializedForm(te); + if (sf != null) { + forms.put(api, sf); + } + }); + + if (forms.isEmpty()) { + return true; + } else { + SerializedFormComparator sfc = new SerializedFormComparator(tMap.keySet(), options, reporter); + return sfc.compare(tPos.serializedForm(), forms); + } + } + + // TODO: in time, the members of a record type may include record components + private boolean compareMembers(Position ePos, APIMap tMap) { + KeyTable nestedTypes = new KeyTable<>(); + KeyTable constructors = new KeyTable<>(); + KeyTable methods = new KeyTable<>(); + KeyTable enumConstants = new KeyTable<>(); + KeyTable fields = new KeyTable<>(); + IntTable recordComponents = new IntTable<>(); + + for (Map.Entry e : tMap.entrySet()) { + API api = e.getKey(); + TypeElement te = e.getValue(); + for (Element member : te.getEnclosedElements()) { + if (!accessKind.accepts(member)) { + continue; + } + ElementKey key = ElementKey.of(member); + switch (member.getKind()) { + case ENUM, RECORD, CLASS, INTERFACE, ANNOTATION_TYPE -> + nestedTypes.put(key, api, (TypeElement) member); + + case ENUM_CONSTANT -> + enumConstants.put(key, api, (VariableElement) member); + + case FIELD -> + fields.put(key, api, (VariableElement) member); + + case CONSTRUCTOR -> + constructors.put(key, api, (ExecutableElement) member); + + case METHOD -> + methods.put(key, api, (ExecutableElement) member); + + case RECORD_COMPONENT -> + recordComponents.add(api, (VariableElement) member); + + case STATIC_INIT, INSTANCE_INIT -> { + // expected but ignored, since it is never part of any API + } + + default -> throw new Error("unexpected element: " + member.getKind() + " " + member); + } + } + } + + Set tMapApis = tMap.keySet(); + TypeComparator tc = new TypeComparator(tMapApis, options, reporter); // can we use "this"? + VariableComparator vc = new VariableComparator(tMapApis, options, reporter); + ExecutableComparator ec = new ExecutableComparator(tMapApis, options, reporter); + RecordComponentComparator rc = new RecordComponentComparator(tMapApis, options, reporter); + + return tc.compareAll(nestedTypes) + & rc.compareAll(ePos::recordComponent, recordComponents) + & vc.compareAll(enumConstants) + & vc.compareAll(fields) + & ec.compareAll(constructors) + & ec.compareAll(methods); + } + + // TODO: in time, the signature of a sealed type may include the sealed modifier + // and its permits list; likewise the signature of a non-sealed subtype of + // a sealed type may include the non-sealed modifier + private boolean compareSignatures(Position tPos, APIMap tMap) { + return compareAnnotations(tPos, tMap) + & compareModifiers(tPos, tMap) + & compareKinds(tPos, tMap) + & compareTypeParameters(tPos, tMap) + & compareSuperclass(tPos, tMap) + & compareInterfaces(tPos, tMap) + & comparePermittedSubclasses(tPos, tMap); + } + + private boolean compareKinds(Position tPos, APIMap tMap) { + if (tMap.size() == 1) + return true; + + ElementKind baseline = null; + for (TypeElement te : tMap.values()) { + if (baseline == null) { + baseline = te.getKind(); + } else if (te.getKind() != baseline) { + reporter.reportDifferentKinds(tPos, tMap); + return false; + } + } + return true; + } + + private boolean compareTypeParameters(Position tPos, APIMap tMap) { + TypeParameterComparator tc = new TypeParameterComparator(tMap.keySet(), options, reporter); + IntTable typarams = IntTable.of(tMap, TypeElement::getTypeParameters); + return tc.compareAll(tPos, typarams); + } + + private boolean compareSuperclass(Position pos, APIMap eMap) { + TypeMirrorComparator tc = new TypeMirrorComparator(eMap.keySet(), reporter); + APIMap sMap = eMap.map(TypeElement::getSuperclass); + return tc.compare(pos.superclass(), sMap); // null-friendly comparison + } + + private boolean compareInterfaces(Position ePos, APIMap tMap) { + Map> map = extractInterfaces(tMap); + + // TODO: the following could be a variant of TypeMirrorComparator::compareAll + // and shared with compareThrownTypes + // compare the groups of superinterfaces + Set first = null; + boolean allEqual = true; + for (Map.Entry> entry : map.entrySet()) { + ElementKey ik = entry.getKey(); + APIMap iMap = entry.getValue(); + Position pos = ePos.superinterface(ik); + if (iMap.size() < tMap.size()) { + // Note: using reportDifferentTypes even if some of the superinterfaces are missing + tMap.keySet().forEach(a -> iMap.putIfAbsent(a, null)); + reporter.reportDifferentTypes(pos, iMap); + allEqual = false; + } else { + TypeMirrorComparator tmc = new TypeMirrorComparator(tMap.keySet(), reporter); + allEqual = allEqual & tmc.compare(pos, iMap); + } + } + + if (allEqual) { + return true; + } else { + APIMap> superinterfaces = APIMap.of(); + tMap.forEach((api, te) -> superinterfaces.put(api, te.getInterfaces())); + reporter.reportDifferentSuperinterfaces(ePos, superinterfaces); + return false; + } + } + + // TODO: share with extractThrownTypes? + private Map> extractInterfaces(APIMap tMap) { + // The order in which superinterfaces may be listed is not significant, + // so group the superinterfaces by their ElementKey. + // Note that thrown types can be type variables, and even annotated + Map> map = new TreeMap<>(); + for (Map.Entry entry : tMap.entrySet()) { + API api = entry.getKey(); + TypeElement ee = entry.getValue(); + for (TypeMirror tm : ee.getInterfaces()) { + Element e = api.getTypes().asElement(tm); + map.computeIfAbsent(ElementKey.of(e), _k -> APIMap.of()).put(api, tm); + } + } + return map; + } + + private boolean comparePermittedSubclasses(Position ePos, APIMap tMap) { + Map> map = extractPermittedSubclasses(tMap); + + // TODO: the following could be a variant of TypeMirrorComparator::compareAll + // and shared with compareInterfaces, compareThrownTypes + // compare the groups of permitted subtypes + Set first = null; + boolean allEqual = true; + for (Map.Entry> entry : map.entrySet()) { + ElementKey sk = entry.getKey(); + APIMap sMap = entry.getValue(); + Position pos = ePos.permittedSubclass(sk); + if (sMap.size() < tMap.size()) { + // Note: using reportDifferentTypes even if some of the permitted subtypes are missing + tMap.keySet().forEach(a -> sMap.putIfAbsent(a, null)); // TODO: is null OK here? + reporter.reportDifferentTypes(pos, sMap); + allEqual = false; + } else { + TypeMirrorComparator tmc = new TypeMirrorComparator(tMap.keySet(), reporter); + allEqual = allEqual & tmc.compare(pos, sMap); + } + } + + if (allEqual) { + return true; + } else { + APIMap> subclasses = APIMap.of(); + tMap.forEach((api, te) -> subclasses.put(api, elementExtras.getPermittedSubclasses(te))); + reporter.reportDifferentPermittedSubclasses(ePos, subclasses); + return false; + } + } + + // TODO: share with extractThrownTypes? + private Map> extractPermittedSubclasses(APIMap tMap) { + // The order in which permitted subtypes may be listed is not significant, + // so group the permitted subtypes by their ElementKey. + // Note that permitted subtypes can be type variables, and even annotated + Map> map = new TreeMap<>(); + for (Map.Entry entry : tMap.entrySet()) { + API api = entry.getKey(); + TypeElement ee = entry.getValue(); + for (TypeMirror tm : elementExtras.getPermittedSubclasses(ee)) { + Element e = api.getTypes().asElement(tm); + map.computeIfAbsent(ElementKey.of(e), _k -> APIMap.of()).put(api, tm); + } + } + return map; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/TypeMirrorComparator.java b/src/share/classes/jdk/codetools/apidiff/model/TypeMirrorComparator.java new file mode 100644 index 0000000..55c4394 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/TypeMirrorComparator.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import javax.lang.model.element.Element; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.SimpleTypeVisitor14; + +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link TypeMirror type mirrors}. + * + * Type mirrors often occur in collections, such as the superinterfaces of a type + * or the set of checked exceptions that may be thrown by an executable element. + * + *

        Type mirrors are compared according to their structure ("deep equals") down to + * the level of element names, which are compared using {@link ElementKey#equals ElementKey.equals}. + */ +public class TypeMirrorComparator { + private final Set apis; + private final Reporter reporter; + + /** + * Creates a comparator to compare type mirrors across a set of APIs. + * + * @param apis the set of APIs + * @param reporter the reporter to which to report differences + */ + protected TypeMirrorComparator(Set apis, Reporter reporter) { + this.apis = apis; + this.reporter = reporter; + } + + /** + * Compares all the series of type mirrors at a given position in an API. + * + * @param pos the position + * @param table the table for the type mirrors at that position + * @return {@code true} if all the type mirrors are equivalent + */ + boolean compareAll(Function pos, IntTable table) { + boolean allEqual = true; + for (int i = 0; i < table.size(); i++) { + // TODO: check size of table.entries(i) against api.size() + allEqual &= compare(pos.apply(i), table.entries(i)); + } + return allEqual; + } + + /** + * Compares all the series of type mirrors at a given position in an API. + * + * @param pos the position + * @param table the table for the type mirrors for that element + * @return {@code true} if all the type mirrors are equivalent + */ + boolean compareAll(Function pos, KeyTable table) { + boolean allEqual = true; + for (Map.Entry> entry : table.entries()) { + ElementKey key = entry.getKey(); + APIMap map = entry.getValue(); + allEqual &= compare(pos.apply(key), map); + } + return allEqual; + } + + boolean compare(Position pos, APIMap map) { + if (map.size() == 1) + return true; + + TypeMirror archetype = map.values().stream().filter(Objects::nonNull).findFirst().orElse(null); + for (TypeMirror t : map.values()) { + if (!equal(archetype, t)) { + reporter.reportDifferentTypes(pos, map); + return false; + } + } + return true; + } + + private static boolean equal(Element e1, Element e2) { + return ElementKey.of(e1).equals(ElementKey.of(e2)); + } + + private static boolean equal(TypeMirror t1, TypeMirror t2) { + if (t1 == t2) + return true; + if (t1 == null || t2 == null) + return false; + if (t1.getKind() != t2.getKind()) + return false; + // TODO: compare type annotations + return equalVisitor.visit(t1, t2); + } + + private static boolean equal(List l1, List l2) { + if (l1 == l2) + return true; + if (l1 == null || l2 == null) + return false; + if (l1.size() != l2.size()) + return false; + Iterator iter1 = l1.iterator(); + Iterator iter2 = l2.iterator(); + while (iter1.hasNext() && iter2.hasNext()) { + boolean eq = equal(iter1.next(), iter2.next()); + if (!eq) { + return false; + } + } + return true; + } + + static TypeVisitor equalVisitor = new SimpleTypeVisitor14<>() { + + @Override + public Boolean visitArray(ArrayType at1, TypeMirror t2) { + ArrayType at2 = (ArrayType) t2; + return equal(at1.getComponentType(), at2.getComponentType()); + } + + @Override + public Boolean visitDeclared(DeclaredType dt1, TypeMirror t2) { + DeclaredType dt2 = (DeclaredType) t2; + return equal(dt1.asElement(), dt2.asElement()) + && equal(dt1.getTypeArguments(), dt2.getTypeArguments()); + } + + @Override + public Boolean visitNoType(NoType pt, TypeMirror t) { + return true; + } + + @Override + public Boolean visitPrimitive(PrimitiveType pt, TypeMirror t) { + return true; + } + + @Override + public Boolean visitTypeVariable(TypeVariable vt1, TypeMirror t2) { + TypeVariable vt2 = (TypeVariable) t2; + return equal(vt1.asElement(), vt2.asElement()); + } + + @Override + public Boolean visitWildcard(WildcardType wt1, TypeMirror t2) { + WildcardType wt2 = (WildcardType) t2; + return equal(wt1.getExtendsBound(), wt2.getExtendsBound()) + && equal(wt1.getSuperBound(), wt2.getSuperBound()); + } + + @Override + public Boolean defaultAction(TypeMirror e, TypeMirror t) { + throw new UnsupportedOperationException(e.getKind() + " " + e); + } + }; +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/TypeMirrorKey.java b/src/share/classes/jdk/codetools/apidiff/model/TypeMirrorKey.java new file mode 100644 index 0000000..fc9a200 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/TypeMirrorKey.java @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.lang.model.element.Name; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.SimpleTypeVisitor14; + +import jdk.codetools.apidiff.model.ElementKey.Cache; + +/** + * A wrapper for instances of type mirror that is independent of any API environment and + * that can be used to associate corresponding type mirrors in different instances of an API. + */ +public abstract sealed class TypeMirrorKey implements Comparable { + static TypeVisitor factory = new SimpleTypeVisitor14<>() { + + @Override + public TypeMirrorKey visitArray(ArrayType t, Void _p) { + return new ArrayTypeKey(t); + } + + @Override + public TypeMirrorKey visitDeclared(DeclaredType t, Void _p) { + return new DeclaredTypeKey(t); + } + + @Override + public TypeMirrorKey visitPrimitive(PrimitiveType t, Void _p) { + return new PrimitiveTypeKey(t); + } + + @Override + public TypeMirrorKey visitTypeVariable(TypeVariable t, Void _p) { + return new TypeVariableKey(t); + } + + @Override + public TypeMirrorKey visitWildcard(WildcardType t, Void _p) { + return new WildcardTypeKey(t); + } + + @Override + public TypeMirrorKey defaultAction(TypeMirror e, Void _p) { + throw new UnsupportedOperationException(e.getKind() + " " + e); + } + }; + + private static final Cache cache = new Cache<>(t -> factory.visit(t, null)); + + /** + * Returns a key for a type mirror. + * @param t the type mirror + * @return the key + */ + public static TypeMirrorKey of(TypeMirror t) { + return (t == null) ? null : cache.get(t); + } + + /** + * The kind of the type mirror used to create this key. + */ + public final TypeKind kind; + + TypeMirrorKey(TypeMirror t) { + kind = t.getKind(); + } + + /** + * Applies a visitor to this key. + * @param v the visitor + * @param p a visitor-specified parameter + * @param the type of the result + * @param

        the type of the parameter + * @return a visitor-specified result + */ + public abstract R accept(Visitor v, P p); + + /** + * A visitor of type mirror keys, in the style of the visitor design pattern. + * Classes implementing this interface are used to operate on a type mirror key + * when the kind of key is unknown at compile time. + * When a visitor is passed to a key's {@code accept} method, + * the visitXyz method applicable to that key is invoked. + * + * @param the return type of this visitor's methods. + * Use Void for visitors that do not need to return results. + * @param

        the type of the additional parameter to this visitor's methods. + * Use Void for visitors that do not need an additional parameter. + */ + public interface Visitor { + /** + * Visits a key for an array type. + * @param k the key to visit + * @param p a visitor-specified parameter + * @return a visitor-specified result + */ + R visitArrayType(ArrayTypeKey k, P p); + + /** + * Visits a key for a declared type. + * @param k the key to visit + * @param p a visitor-specified parameter + * @return a visitor-specified result + */ + R visitDeclaredType(DeclaredTypeKey k, P p); + + /** + * Visits a key for a primitive type. + * @param k the key to visit + * @param p a visitor-specified parameter + * @return a visitor-specified result + */ + R visitPrimitiveType(PrimitiveTypeKey k, P p); + + /** + * Visits a key for a type variable. + * @param k the key to visit + * @param p a visitor-specified parameter + * @return a visitor-specified result + */ + R visitTypeVariable(TypeVariableKey k, P p); + + /** + * Visits a key for a wildcard type. + * @param k the key to visit + * @param p a visitor-specified parameter + * @return a visitor-specified result + */ + R visitWildcardType(WildcardTypeKey k, P p); + } + + /** + * A key for an array type. + */ + public static final class ArrayTypeKey extends TypeMirrorKey { + /** + * A key for the component type of the array. + */ + public final TypeMirrorKey componentKey; + private int hashCode; + + ArrayTypeKey(ArrayType t) { + super(t); + componentKey = Objects.requireNonNull(TypeMirrorKey.of(t.getComponentType())); + } + + @Override + public int compareTo(TypeMirrorKey other) { + int ck = kind.compareTo(other.kind); + return (ck != 0) ? ck : componentKey.compareTo(((ArrayTypeKey) other).componentKey); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + return componentKey.equals(((ArrayTypeKey) other).componentKey); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = componentKey.hashCode(); + } + return hashCode; + } + + @Override + public String toString() { + return "ArrayKey[" + componentKey + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitArrayType(this, p); + } + } + + /** + * A key for a declared type. + */ + public static final class DeclaredTypeKey extends TypeMirrorKey { + /** + * The key for the element corresponding to this type. + */ + public final ElementKey elementKey; + /** + * The keys for any type arguments. + */ + public final List typeArgKeys; + private int hashCode; + + DeclaredTypeKey(DeclaredType t) { + super(t); + elementKey = ElementKey.of(t.asElement()); + typeArgKeys = t.getTypeArguments().stream() + .map(TypeMirrorKey::of) + .collect(Collectors.toList()); + } + + @Override + public int compareTo(TypeMirrorKey other) { + int ck = kind.compareTo(other.kind); + if (ck != 0) { + return ck; + } + DeclaredTypeKey otherKey = (DeclaredTypeKey) other; + int ce = elementKey.compareTo(otherKey.elementKey); + if (ce != 0) { + return ce; + } + return ElementKey.compare(typeArgKeys, otherKey.typeArgKeys); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + DeclaredTypeKey otherKey = (DeclaredTypeKey) other; + return elementKey.equals(otherKey.elementKey) && typeArgKeys.equals(otherKey.typeArgKeys); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = kind.hashCode() * 37 + elementKey.hashCode() * 5 + typeArgKeys.hashCode(); + } + return hashCode; + } + + @Override + public String toString() { + String typeArgs = typeArgKeys.isEmpty() ? "" + : typeArgKeys.stream().map(Object::toString).collect(Collectors.joining(",", "<", ">")); + return "DeclaredTypeKey[" + elementKey + typeArgs + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitDeclaredType(this, p); + } + } + + /** + * A key for a primitive type: + * {@code boolean}, {@code byte}, {@code char}, {@code double}, + * {@code float}, {@code int}, {@code long}, {@code short}. + * + * The kind of primitive type is identified by the {@link TypeMirrorKey#kind kind}. + */ + public static final class PrimitiveTypeKey extends TypeMirrorKey { + PrimitiveTypeKey(PrimitiveType t) { + super(t); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + return kind.equals(((PrimitiveTypeKey) other).kind); + } + } + + @Override + public int hashCode() { + return kind.hashCode(); + } + + @Override + public int compareTo(TypeMirrorKey other) { + // TODO: compare by group? + return kind.compareTo(other.kind); + } + + @Override + public String toString() { + return "PrimitiveTypeKey[" + kind + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitPrimitiveType(this, p); + } + } + + /** + * A key for a type variable. + */ + public static final class TypeVariableKey extends TypeMirrorKey { + /** + * The name of the type variable. + */ + public final Name name; // TODO: should this be the ElementKey? + private int hashCode; + + TypeVariableKey(TypeVariable t) { + super(t); + this.name = t.asElement().getSimpleName(); + } + + @Override + public int compareTo(TypeMirrorKey other) { + int ck = kind.compareTo(other.kind); + return (ck != 0) ? ck : ElementKey.compare(name, ((TypeVariableKey) other).name); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + return name.equals(((TypeVariableKey) other).name); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = ElementKey.hashCode(name); + } + return hashCode; + } + + @Override + public String toString() { + return "TypeVariableTypeMirrorKey[" + name + "]"; + } + + @Override + public R accept(Visitor v, P p) { + return v.visitTypeVariable(this, p); + } + } + + /** + * A key for a wildcard type. + */ + public static final class WildcardTypeKey extends TypeMirrorKey { + /** + * A key for the {@code extends} bound, if any, or null. + */ + public final TypeMirrorKey extendsBoundKey; + /** + * A key for the {@code super} bound, if any, or null. + */ + public final TypeMirrorKey superBoundKey; + private int hashCode; + + WildcardTypeKey(WildcardType t) { + super(t); + extendsBoundKey = TypeMirrorKey.of(t.getExtendsBound()); + superBoundKey = TypeMirrorKey.of(t.getSuperBound()); + } + + @Override + public int compareTo(TypeMirrorKey other) { + int ck = kind.compareTo(other.kind); + if (ck != 0) { + return ck; + } + WildcardTypeKey otherWildcardTypeKey = (WildcardTypeKey) other; + int ce = compare(extendsBoundKey, otherWildcardTypeKey.extendsBoundKey); + if (ce != 0) { + return ce; + } + return compare(superBoundKey, otherWildcardTypeKey.superBoundKey); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other == null || other.getClass() != getClass()) { + return false; + } else { + WildcardTypeKey otherWildcardTypeKey = (WildcardTypeKey) other; + return Objects.equals(extendsBoundKey, otherWildcardTypeKey.extendsBoundKey) + && Objects.equals(superBoundKey, otherWildcardTypeKey.superBoundKey); + } + } + + @Override + public int hashCode() { + if (hashCode == 0) { + if (extendsBoundKey != null) { + hashCode = extendsBoundKey.hashCode(); + } + if (superBoundKey != null) { + hashCode = hashCode * 37 + superBoundKey.hashCode(); + } + return hashCode; + } + return hashCode; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("WildcardTypeKey["); + String sep = ""; + if (extendsBoundKey != null) { + sb.append("extends:").append(extendsBoundKey); + sep = ","; + } + if (superBoundKey != null) { + sb.append(sep).append("super:").append(superBoundKey); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public R accept(Visitor v, P p) { + return v.visitWildcardType(this, p); + } + + private static int compare(TypeMirrorKey k1, TypeMirrorKey k2) { + if (k1 == null && k2 == null) { + return 0; + } + if (k1 == null) { + return -1; + } + if (k2 == null) { + return 1; + } + return k1.compareTo(k2); + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/TypeParameterComparator.java b/src/share/classes/jdk/codetools/apidiff/model/TypeParameterComparator.java new file mode 100644 index 0000000..fcc4d67 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/TypeParameterComparator.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Set; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link TypeParameterElement type parameters}. + * + *

        Type parameters are declared in a list on the declaration of a type or + * executable element. Type parameters in different APIs are associated + * according to their position in the list (and not by their name). + * + *

        The comparison includes: + *

          + *
        • the name + *
        • any annotations + *
        • any bounds + *
        + * + */ +public class TypeParameterComparator extends ElementComparator { + /** + * Creates an instance of a comparator for type parameters. + * + * @param apis the APIs to be compared + * @param options the command-line options + * @param reporter the reporter to which to report any differences. + */ + public TypeParameterComparator(Set apis, Options options, Reporter reporter) { + super(apis, options, reporter); + } + + /** + * Compares all the type parameters for a given element in an API. + * + * @param ePos the position of the element + * @param typarams the table for the type parameters for that element + * @return {@code true} if all the type parameters are equivalent + */ + public boolean compareAll(Position ePos, IntTable typarams) { + return compareAll(ePos::typeParameter, typarams); + } + + /** + * Compare instances of a type parameter found at a given position in different APIs. + * + * @param pos the position of the type parameter + * @param map the map giving the instance of the type parameter in different APIs + * @return {@code true} if all the instances are equivalent + */ + @Override + public boolean compare(Position pos, APIMap map) { + boolean allEquals = true; + + if (map.size() < apis.size()) { + reporter.reportDifferentTypeParameters(pos, map); + allEquals = false; + } + + allEquals &= compareNames(pos, map) + & compareBounds(pos, map) + & new AnnotationComparator(map.keySet(), accessKind, reporter).compareAll(pos, map); + + return allEquals; + } + + private boolean compareNames(Position pos, APIMap map) { + CharSequence name = null; + for (TypeParameterElement e : map.values()) { + if (name == null) { + name = e.getSimpleName(); + } else { + if (!e.getSimpleName().contentEquals(name)) { + reporter.reportDifferentTypeParameters(pos, map); + return false; + } + } + } + return true; + } + + private boolean compareBounds(Position pos, APIMap map) { + IntTable bounds = IntTable.of(map, TypeParameterElement::getBounds); + return new TypeMirrorComparator(apis, reporter).compareAll(pos::bound, bounds); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/VariableComparator.java b/src/share/classes/jdk/codetools/apidiff/model/VariableComparator.java new file mode 100644 index 0000000..e79f4d7 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/VariableComparator.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.model; + +import java.util.Set; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.report.Reporter; + +/** + * A comparator for {@link VariableElement variable elements}: fields, enum constants, and + * parameters for executable elements. + * + *

        The comparison includes: + *

          + *
        • the "signature" of the element: its annotations, modifiers and type + *
        • the documentation comment for the element + *
        + */ +public class VariableComparator extends ElementComparator { + + /** + * Creates a comparator to compare variable elements across a set of APIs. + * + * @param apis the set of APIs + * @param options the command-line options + * @param reporter the reporter to which to report differences + */ + public VariableComparator(Set apis, Options options, Reporter reporter) { + super(apis, options, reporter); + } + + /** + * Compares instances of a variable element found in different APIs. + * + * @param vPos the position of the element + * @param vMap the map giving the instance of the variable element in different APIs + * @return {@code true} if all the instances are equivalent + */ + // TODO: should the comparison include the names for parameters? + // they're not significant in the binary API but may be important in the reflective API + @Override + public boolean compare(Position vPos, APIMap vMap) { + boolean allEqual = false; + reporter.comparing(vPos, vMap); + try { + allEqual = checkMissing(vPos, vMap); + if (vMap.size() > 1) { + allEqual &= compareSignatures(vPos, vMap); + allEqual &= compareDocComments(vPos, vMap); + allEqual &= compareApiDescriptions(vPos, vMap); + } + } finally { + reporter.completed(vPos, allEqual); + } + return allEqual; + } + + private boolean compareSignatures(Position vPos, APIMap vMap) { + boolean parameters = vMap.values().iterator().next().getKind() == ElementKind.PARAMETER; + return compareAnnotations(vPos, vMap) + & (parameters || compareModifiers(vPos, vMap)) // ignore modifiers for parameters + & compareType(vPos, vMap); + } + + private boolean compareType(Position vPos, APIMap vMap) { + TypeMirrorComparator tmc = new TypeMirrorComparator(vMap.keySet(), reporter); + APIMap tMap = vMap.map(VariableElement::asType); + return tmc.compare(vPos, tMap); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/model/package-info.java b/src/share/classes/jdk/codetools/apidiff/model/package-info.java new file mode 100644 index 0000000..d6e3e6b --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/model/package-info.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Classes used to model the comparison of APIs in different compilation + * environments. + * + *

        Each {@link jdk.codetools.apidiff.model.API API} is modelled as + * a hierarchy of modules, packages, types, variables, executable elements, + * annotations and documentation comments. Each element has a corresponding + * {@link jdk.codetools.apidiff.model.ElementKey key} which is used to associate + * corresponding elements in different API instances. + * + *

        Collections of different kinds of elements existing in different APIs + * are compared using "comparators". + */ +package jdk.codetools.apidiff.model; \ No newline at end of file diff --git a/src/share/classes/jdk/codetools/apidiff/package-info.java b/src/share/classes/jdk/codetools/apidiff/package-info.java new file mode 100644 index 0000000..0d070a2 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Classes for the high-level operation of the tool, such as the command-line + * entry point, options, and other general utility classes. + */ +package jdk.codetools.apidiff; diff --git a/src/share/classes/jdk/codetools/apidiff/report/LogReporter.java b/src/share/classes/jdk/codetools/apidiff/report/LogReporter.java new file mode 100644 index 0000000..a867c2d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/LogReporter.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2018,2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.Options.VerboseKind; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.DocFile; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.Position; + +/** + * A reporter that reports messages to a log. + */ +public class LogReporter implements Reporter { + private final Log log; + private final Set shouldReport; + private final boolean shouldReportDifferences; + private final boolean shouldReportMissing; + + /** + * Creates a reporter that reports messages to a log. + * + * @param log the log + * @param options the command-line options + */ + public LogReporter(Log log, Options options) { + this.log = log; + + shouldReport = EnumSet.noneOf(ElementKey.Kind.class); + if (options.isVerbose(VerboseKind.MODULE)) { + shouldReport.add(ElementKey.Kind.MODULE); + } + if (options.isVerbose(VerboseKind.PACKAGE)) { + shouldReport.add(ElementKey.Kind.MODULE); + shouldReport.add(ElementKey.Kind.PACKAGE); + } + if (options.isVerbose(VerboseKind.TYPE)) { + shouldReport.add(ElementKey.Kind.MODULE); + shouldReport.add(ElementKey.Kind.PACKAGE); + shouldReport.add(ElementKey.Kind.TYPE); + } + shouldReportDifferences = options.isVerbose(VerboseKind.DIFFERENCES); + shouldReportMissing = options.isVerbose(VerboseKind.MISSING); + } + + private final Map> apiMaps = new HashMap<>(); + + @Override + public void comparing(Position pos, APIMap map) { + apiMaps.put(pos, map); + + if (shouldReport(pos)) { + log.report("logReport.comparing", pos); + } + } + + @Override + public void completed(Position pos, boolean equal) { + if (shouldReport(pos)) { + log.report("logReport.completed", pos, asInt(equal)); + } + + if (pos.isElement()) { + apiMaps.remove(pos); + } + } + + @Override + public void completed(boolean equal) { + log.report("logReport.finished", asInt(equal)); + } + + private static int asInt(boolean b) { + return b ? 1 : 0; + } + + private boolean shouldReport(Position pos) { + return pos.isElement() && shouldReport.contains(pos.asElementKey().kind); + } + + @Override + public void reportMissing(Position pos, Set apis) { + if (shouldReportMissing) { + for (API api : apis) { + log.report("logReport.item-not-found", api.name, toString(pos)); + } + } + } + + @Override + public void reportDifferentAnnotations(Position pos, APIMap amMap) { + if (shouldReportDifferences) { + log.report("logReport.different-annotations", toString(pos)); + } + } + + @Override + public void reportDifferentAnnotationValues(Position pos, APIMap dMap) { + if (shouldReportDifferences) { + log.report("logReport.different-annotation-values", toString(pos)); + } + } + + @Override + public void reportDifferentDirectives(Position pos, APIMap eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-directives", toString(pos)); + } + } + + @Override + public void reportDifferentKinds(Position pos, APIMap eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-kinds", toString(pos)); + } + } + + @Override + public void reportDifferentNames(Position pos, APIMap eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-names", toString(pos)); + } + } + + @Override + public void reportDifferentTypeParameters(Position pos, APIMap eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-type-parameters", toString(pos)); + } + } + + @Override + public void reportDifferentModifiers(Position pos, APIMap eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-modifiers", toString(pos)); + } + } + + @Override + public void reportDifferentTypes(Position pos, APIMap tMap) { + if (shouldReportDifferences) { + log.report("logReport.different-types", toString(pos)); + } + } + + @Override + public void reportDifferentThrownTypes(Position pos, APIMap> eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-thrown-types", toString(pos)); + } + } + + @Override + public void reportDifferentSuperinterfaces(Position pos, APIMap> eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-superinterfaces", toString(pos)); + } + } + + @Override + public void reportDifferentPermittedSubclasses(Position pos, APIMap> eMap) { + if (shouldReportDifferences) { + log.report("logReport.different-permitted-subclasses", toString(pos)); + } + } + + @Override + public void reportDifferentValues(Position pos, APIMap vMap) { + if (shouldReportDifferences) { + log.report("logReport.different-values", toString(pos)); + } + } + + @Override + public void reportDifferentRawDocComments(Position pos, APIMap cMap) { + if (shouldReportDifferences) { + log.report("logReport.different-raw-doc-comments", toString(pos)); + } + } + + @Override + public void reportDifferentApiDescriptions(Position pos, APIMap dMap) { + if (shouldReportDifferences) { + log.report("logReport.different-api-descriptions", toString(pos)); + } + } + + @Override + public void reportDifferentDocFiles(Position pos, APIMap fMap) { + if (shouldReportDifferences) { + log.report("logReport.different-doc-files", toString(pos)); + } + } + + String toString(Position pos) { + StringBuilder sb = new StringBuilder(); + pos.accept(new SignatureVisitor(apiMaps), sb); + return sb.toString(); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/MultiplexReporter.java b/src/share/classes/jdk/codetools/apidiff/report/MultiplexReporter.java new file mode 100644 index 0000000..261afd1 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/MultiplexReporter.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2018,2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report; + +import java.util.List; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.DocFile; +import jdk.codetools.apidiff.model.Position; + +/** + * A class to broadcast messages to a series of reporters. + */ +public class MultiplexReporter implements Reporter { + private final List reporters; + + /** + * Creates a reporter to broadcast messages to a series of reporters. + * + * @param reporters the reporters + */ + public MultiplexReporter(List reporters) { + this.reporters = reporters; + } + + @Override + public void comparing(Position pos, APIMap apiMap) { + reporters.forEach(r -> r.comparing(pos, apiMap)); + } + + @Override + public void completed(boolean equal) { + reporters.forEach(r -> r.completed(equal)); + } + + @Override + public void completed(Position pos, boolean equal) { + reporters.forEach(r -> r.completed(pos, equal)); + } + + @Override + public void reportMissing(Position pos, Set apis) { + reporters.forEach(r -> r.reportMissing(pos, apis)); + } + + @Override + public void reportDifferentAnnotations(Position pos, APIMap amMap) { + reporters.forEach(r -> r.reportDifferentAnnotations(pos, amMap)); + } + + @Override + public void reportDifferentAnnotationValues(Position pos, APIMap avMap) { + reporters.forEach(r -> r.reportDifferentAnnotationValues(pos, avMap)); + } + + @Override + public void reportDifferentDirectives(Position pos, APIMap dMap) { + reporters.forEach(r -> r.reportDifferentDirectives(pos, dMap)); + } + + @Override + public void reportDifferentKinds(Position pos, APIMap eMap) { + reporters.forEach(r -> r.reportDifferentKinds(pos, eMap)); + } + + @Override + public void reportDifferentNames(Position pos, APIMap eMap) { + reporters.forEach(r -> r.reportDifferentNames(pos, eMap)); + } + + @Override + public void reportDifferentTypeParameters(Position pos, APIMap eMap) { + reporters.forEach(r -> r.reportDifferentTypeParameters(pos, eMap)); + } + + @Override + public void reportDifferentModifiers(Position pos, APIMap eMap) { + reporters.forEach(r -> r.reportDifferentModifiers(pos, eMap)); + } + + @Override + public void reportDifferentTypes(Position pos, APIMap tMap) { + reporters.forEach(r -> r.reportDifferentTypes(pos, tMap)); + } + + @Override + public void reportDifferentThrownTypes(Position pos, APIMap> tMap) { + reporters.forEach(r -> r.reportDifferentThrownTypes(pos, tMap)); + } + + @Override + public void reportDifferentSuperinterfaces(Position pos, APIMap> tMap) { + reporters.forEach(r -> r.reportDifferentSuperinterfaces(pos, tMap)); + } + + @Override + public void reportDifferentPermittedSubclasses(Position pos, APIMap> tMap) { + reporters.forEach(r -> r.reportDifferentPermittedSubclasses(pos, tMap)); + } + + @Override + public void reportDifferentValues(Position pos, APIMap vMap) { + reporters.forEach(r -> r.reportDifferentValues(pos, vMap)); + } + + @Override + public void reportDifferentRawDocComments(Position pos, APIMap cMap) { + reporters.forEach(r -> r.reportDifferentRawDocComments(pos, cMap)); + } + + @Override + public void reportDifferentApiDescriptions(Position pos, APIMap cMap) { + reporters.forEach(r -> r.reportDifferentApiDescriptions(pos, cMap)); + } + + @Override + public void reportDifferentDocFiles(Position pos, APIMap fMap) { + reporters.forEach(r -> r.reportDifferentDocFiles(pos, fMap)); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/Reporter.java b/src/share/classes/jdk/codetools/apidiff/report/Reporter.java new file mode 100644 index 0000000..57310c4 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/Reporter.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2018,2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report; + +import java.util.List; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.DocFile; +import jdk.codetools.apidiff.model.Position; + +/** + * A base class for reporting differences found in different instances of an API. + */ +public interface Reporter { + /** + * Reports the elements being compared. + * + * @param pos the position of an item being compared + * @param map the map identifying the instances to be compared + * @see #completed(Position, boolean) + */ + void comparing(Position pos, APIMap map); + + /** + * Reports that a comparison has completed. + * + * @param ePos the position of an item being compared + * @param equal {@code true} if the items being compared are equivalent + * @see #comparing(Position, APIMap) + */ + void completed(Position ePos, boolean equal); + + /** + * Reports that the overall comparison has been completed. + * + * @param equal {@code true} if the APIs being compared are equivalent + */ + default void completed(boolean equal) { } + + /** + * Reports that items were not found at a given position within instances of an API. + * + * @param ePos the position of the item being compared + * @param apis the APIs + */ + void reportMissing(Position ePos, Set apis); + + + /** + * Reports that different annotations were found at a given position + * within instances of an API. + * + * @param amPos the position of the annotations + * @param amMap the map identifying the instances to be compared + */ + void reportDifferentAnnotations(Position amPos, APIMap amMap); + + /** + * Reports that different annotation values were found at a given position + * within instances of an API. + * + * @param avPos the position of the annotations + * @param avMap the map identifying the instances that were compared + */ + void reportDifferentAnnotationValues(Position avPos, APIMap avMap); + + /** + * Reports that different directives were found at a position within a module element + * within instances of an API. + * + * @param dPos the position of the directive + * @param dMap the map identifying the instances that were compared + */ + void reportDifferentDirectives(Position dPos, APIMap dMap); + + /** + * Reports that different modifiers were found at a position for an element + * within instances of an API. + * + * @param ePos the position of the directive + * @param eMap the map identifying the instances that were compared + */ + void reportDifferentModifiers(Position ePos, APIMap eMap); + + /** + * Reports that different kinds of an element were found at a position + * within instances of an API. For example, a class in one API may be + * declared an interface in another. + * + * @param ePos the position of the element + * @param eMap the map identifying the instances that were compared + */ + void reportDifferentKinds(Position ePos, APIMap eMap); + + /** + * Reports that different names for an element were found at a position + * within instances of an API. For example, record components may be + * named differently. + * + * @param ePos the position of the element + * @param eMap the map identifying the instances that were compared + */ + void reportDifferentNames(Position ePos, APIMap eMap); + + /** + * Reports that different values were found at a position + * within instances of an API. For example, serializable objects may + * have different serial version UIDs. + * + * @param vPos the position of the value + * @param vMap the map identifying the instances that were compared + */ + void reportDifferentValues(Position vPos, APIMap vMap); + + /** + * Reports that different type parameters were found at a position within + * instances of an API. + * + * @param ePos the position of the directive + * @param eMap the map identifying the instances that were compared + */ + void reportDifferentTypeParameters(Position ePos, APIMap eMap); + + /** + * Reports that different type mirrors were found at a position + * within instances of an API. + * + * @param tPos the position of the type + * @param tMap the map identifying the instances that were compared + */ + void reportDifferentTypes(Position tPos, APIMap tMap); + + /** + * Reports that different sets of thrown types were found at a position + * within instances of an API. + * + * @param ePos the position of the executable element + * @param eMap the map identifying the instances that were compared + */ + void reportDifferentThrownTypes(Position ePos, APIMap> eMap); + + /** + * Reports that different sets of superinterfaces were found at a position + * within instances of an API. + * + * @param ePos the position of the type element + * @param eMap the map identifying the instances that were compared + */ + void reportDifferentSuperinterfaces(Position ePos, APIMap> eMap); + + /** + * Reports that different sets of permitted subclasses were found at a position + * within instances of an API. + * + * @param ePos the position of the type element + * @param eMap the map identifying the instances that were compared + */ + void reportDifferentPermittedSubclasses(Position ePos, APIMap> eMap); + + /** + * Reports that different raw doc comments were found at a position + * within instances of an API. + * + * @param ePos the position of the doc comment + * @param cMap the map identifying the instances that were compared + */ + void reportDifferentRawDocComments(Position ePos, APIMap cMap); + + /** + * Reports that different API descriptions were found at a position + * within instances of an API. + * + * @param ePos the position of the doc comment + * @param cMap the map identifying the instances that were compared + */ + void reportDifferentApiDescriptions(Position ePos, APIMap cMap); + + /** + * Reports that different doc files were found for a package + * within instances of an API. + * + * @param fPos the position of the doc file + * @param fMap the map identifying the instances that were compared + */ + void reportDifferentDocFiles(Position fPos, APIMap fMap); +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/SignatureVisitor.java b/src/share/classes/jdk/codetools/apidiff/report/SignatureVisitor.java new file mode 100644 index 0000000..e252dc0 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/SignatureVisitor.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.type.TypeKind; + +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.MemberElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeParameterElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; +import jdk.codetools.apidiff.model.Position; +import jdk.codetools.apidiff.model.Position.ElementPosition; +import jdk.codetools.apidiff.model.Position.RelativePosition; +import jdk.codetools.apidiff.model.TypeMirrorKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.ArrayTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.DeclaredTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.PrimitiveTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.TypeVariableKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.WildcardTypeKey; + +/** + * A utility class to obtain the signature for an element key. + */ +public class SignatureVisitor implements + ElementKey.Visitor, + TypeMirrorKey.Visitor, + Position.Visitor { + + private final Map> apiMaps; + + /** + * Creates an instance of a signature visitor. + * + * @param apiMaps the map containing the API elements to be used in the signature + */ + public SignatureVisitor(Map> apiMaps) { + this.apiMaps = apiMaps; + } + + /** + * Returns the signature for the elements identified by an element key. + * + * @param key the key + * @return the signature + */ + public String getSignature(ElementKey key) { + return getSignature(key, new StringBuilder()).toString(); + } + + /** + * Returns the signature for the elements at a given position. + * + * @param pos the position + * @return the signature + */ + public String getSignature(Position pos) { + return pos.accept(this, new StringBuilder()).toString(); + } + + private StringBuilder getSignature(ElementKey key, StringBuilder sb) { + sb.append(getPrefix(key)).append(" "); + key.accept(this, sb); + return sb; + } + + private String getPrefix(ElementKey key) { + return switch (key.kind) { + case MODULE -> + "module"; + + case PACKAGE -> + "package"; + + case TYPE -> { + @SuppressWarnings("unchecked") + APIMap map = (APIMap) apiMaps.get(Position.of(key)); + Set eKinds = (map == null) ? Collections.emptySet() + : map.values().stream().map(Element::getKind).collect(Collectors.toSet()); + yield switch (eKinds.size()) { + case 0 -> "(unknown)"; + case 1 -> getPrefix(eKinds.iterator().next()); + default -> "(various)"; + }; + } + + case EXECUTABLE, VARIABLE -> + getPrefix(((MemberElementKey) key).elementKind); + + case TYPE_PARAMETER -> + "type parameter"; + }; + } + + private String getPrefix(ElementKind kind) { + return switch (kind) { + case ANNOTATION_TYPE -> "@interface"; + case CLASS -> "class"; + case CONSTRUCTOR -> "constructor"; + case ENUM -> "enum"; + case ENUM_CONSTANT -> "enum constant"; + case FIELD -> "field"; + case INTERFACE -> "interface"; + case METHOD -> "method"; + case MODULE -> "module"; + case PACKAGE -> "package"; + case PARAMETER -> "parameter"; + case RECORD -> "record"; + case RECORD_COMPONENT -> "record component"; + case TYPE_PARAMETER -> "type parameter"; + default -> throw new IllegalArgumentException(kind.toString()); + }; + + } + + @Override + public StringBuilder visitModuleElement(ModuleElementKey k, StringBuilder sb) { + return sb.append(k.name); + } + + @Override + public StringBuilder visitPackageElement(PackageElementKey k, StringBuilder sb) { + if (k.moduleKey != null) { + k.moduleKey.accept(this, sb).append("/"); + } + return sb.append(k.name); + } + + @Override + public StringBuilder visitTypeElement(TypeElementKey tek, StringBuilder sb) { + return tek.enclosingKey.accept(this, sb).append(".").append(tek.name); + } + + @Override + public StringBuilder visitExecutableElement(ExecutableElementKey k, StringBuilder sb) { + k.typeKey.accept(this, sb); + sb.append("#"); + if (k.elementKind == ElementKind.CONSTRUCTOR) { + TypeElementKey tek = (TypeElementKey) k.typeKey; + sb.append(tek.name); + } else { + sb.append(k.name); + } + sb.append("("); + boolean first = true; + for (TypeMirrorKey tmk : k.params) { + if (first) { + first = false; + } else { + sb.append(","); + } + tmk.accept(this, sb); + } + sb.append(")"); + return sb; + } + + @Override + public StringBuilder visitVariableElement(VariableElementKey k, StringBuilder sb) { + return k.typeKey.accept(this, sb).append("#").append(k.name); + } + + @Override + public StringBuilder visitTypeParameterElement(TypeParameterElementKey k, StringBuilder sb) { + return k.typeKey.accept(this, sb).append("<").append(k.name).append(">"); + } + + @Override + public StringBuilder visitArrayType(ArrayTypeKey k, StringBuilder sb) { + return k.componentKey.accept(this, sb).append("[]"); + } + + @Override + public StringBuilder visitDeclaredType(DeclaredTypeKey k, StringBuilder sb) { + k.elementKey.accept(this, sb); + if (!k.typeArgKeys.isEmpty()) { + sb.append("<"); + boolean first = true; + for (TypeMirrorKey tmk : k.typeArgKeys) { + if (first) { + first = false; + } else { + sb.append(","); + } + tmk.accept(this, sb); + } + sb.append(">"); + } + return sb; + } + + @Override + public StringBuilder visitPrimitiveType(PrimitiveTypeKey k, StringBuilder sb) { + return sb.append(toString(k.kind)); + } + + private String toString(TypeKind kind) { + return switch (kind) { + case BOOLEAN -> "boolean"; + case BYTE -> "byte"; + case CHAR -> "char"; + case DOUBLE -> "double"; + case FLOAT -> "float"; + case INT -> "int"; + case LONG -> "long"; + case SHORT -> "short"; + case VOID -> "void"; + default -> throw new IllegalStateException(kind.toString()); + }; + + } + + @Override + public StringBuilder visitTypeVariable(TypeVariableKey k, StringBuilder sb) { + return sb.append(k.name); + } + + @Override + public StringBuilder visitWildcardType(WildcardTypeKey k, StringBuilder sb) { + sb.append("?"); + if (k.extendsBoundKey != null) { + sb.append(" extends "); + k.extendsBoundKey.accept(this, sb); + } + if (k.superBoundKey != null) { + sb.append(" super "); + k.superBoundKey.accept(this, sb); + } + return sb; + } + + @Override + public StringBuilder visitElementPosition(ElementPosition kp, StringBuilder sb) { + return sb.append(getSignature(kp.key)); + } + + @Override + public StringBuilder visitRelativePosition(RelativePosition ip, StringBuilder sb) { + // TODO: improve for non-integer indexes, perhaps by resolving String.format in each branch + String suffix = switch (ip.kind) { + case ANNOTATION -> " @%s"; + case ANNOTATION_ARRAY_INDEX -> "[%d]"; + case ANNOTATION_VALUE -> ", value %s"; + case BOUND -> ", bound %d"; + case DEFAULT_VALUE -> ", default value"; + case DOC_FILE -> ", doc-file %s"; + case EXCEPTION -> ", throws %s"; + case MODULE_EXPORTS -> ", exports %s"; + case MODULE_REQUIRES -> ", requires %s"; + case MODULE_OPENS -> ", opens %s"; + case MODULE_PROVIDES -> ", provides %s"; + case MODULE_USES -> ", uses %s"; + case PARAMETER -> ", parameter %d"; + case PERMITTED_SUBCLASS -> ", permitted subclass %s"; + case RECEIVER_TYPE -> " receiver type"; + case RECORD_COMPONENT -> ", record component %d"; + case RETURN_TYPE -> " return type"; + case SERIAL_VERSION_UID -> ", serial version UID"; + case SERIALIZATION_METHOD -> ", serialization method %s"; + case SERIALIZATION_OVERVIEW -> ", serialization overview"; + case SERIALIZED_FIELD -> ", serialized field %s"; + case SERIALIZED_FORM -> " serialized form"; + case SUPERCLASS -> " superclass"; + case SUPERINTERFACE -> " superinterface %s"; + case TYPE_PARAMETER -> ", type parameter %d"; + }; + return ip.parent.accept(this, sb).append(String.format(suffix, ip.index)); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/DiffBuilder.java b/src/share/classes/jdk/codetools/apidiff/report/html/DiffBuilder.java new file mode 100644 index 0000000..101d477 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/DiffBuilder.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.Entity; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; + +/** + * A class to build a list for the different versions of an API fragment + * in different instances of an API. + * + *

        The various {@code build} methods all take a collection providing + * a list of alternatives. While we could use a {@code

          } list to display + * the alternatives, in general it is expected that the alternatives will + * be presented inline, and not as a typical bulleted list. In addition, + * using {@code
            } would require that all enclosing elements are able to + * accept flow content. Therefore, instead of using {@code
              } and a series + * of {@code
            • nodes}, the output is a {@code } with class "diffs" + * containing a series of {@code } elements for the alternatives. + */ +public class DiffBuilder { + Content build(List list) { + HtmlTree outerSpan = newDiffsSpan(); + list.stream() + .map(HtmlTree::SPAN) + .forEach(outerSpan::add); + return outerSpan; + } + + Content build(APIMap alternatives) { + HtmlTree outerSpan = newDiffsSpan(); + alternatives.forEach((api, c) -> outerSpan.add(buildItem(api, c))); + return outerSpan; + } + + Content build(APIMap alternatives, Function f) { + HtmlTree outerSpan = newDiffsSpan(); + alternatives.forEach((api, t) -> outerSpan.add(buildItem(api, f.apply(t)))); + return outerSpan; + } + + Content build(Set apis, APIMap alternatives, Function f) { + HtmlTree outerSpan = newDiffsSpan(); + for (API api : apis) { + T item = alternatives.get(api); + outerSpan.add(buildItem(api, item == null ? Entity.NBSP : f.apply(item))); + } + return outerSpan; + } + + private HtmlTree newDiffsSpan() { + return HtmlTree.SPAN().setClass("diffs"); + } + + private Content buildItem(API api, Content c) { + return HtmlTree.SPAN(c).setClass("api").setTitle(api.name); // TODO: improve class with api name/index + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/DocLink.java b/src/share/classes/jdk/codetools/apidiff/report/html/DocLink.java new file mode 100644 index 0000000..0912291 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/DocLink.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +/** + * Abstraction for simple relative URIs, consisting of a path, + * an optional query, and an optional fragment. {@code DocLink} objects can + * be created by the constructors below or from a {@code DocPath} using the + * convenience methods {@link DocPath#fragment fragment} and + * {@link DocPath#query query}. + */ +public class DocLink { + final DocPath path; + final String query; + final String fragment; + + /** + * Creates a DocLink representing the URI {@code #fragment}. + * + * @param fragment the fragment + * @return the DocLink + */ + public static DocLink fragment(String fragment) { + return new DocLink((DocPath) null, null, fragment); + } + + /** + * Creates a DocLink representing the URI {@code path}. + * @param path the path + */ + public DocLink(DocPath path) { + this(path, null, null); + } + + /** + * Creates a DocLink representing the URI {@code path?query#fragment}. + * Any of the component parts may be null. + * + * @param path the path + * @param query the query + * @param fragment the fragment + */ + public DocLink(DocPath path, String query, String fragment) { + this.path = path; + this.query = query; + this.fragment = fragment; + } + + /** + * Creates a DocLink representing the URI {@code path?query#fragment}. + * Any of the component parts may be null. + * + * @param path the path + * @param query the query + * @param fragment the fragment + */ + public DocLink(String path, String query, String fragment) { + this(DocPath.create(path), query, fragment); + } + + /** + * Creates a DocLink formed by relativizing the path against a given base. + * + * @param base the base + * @return the DocLink + */ + public DocLink relativizeAgainst(DocPath base) { + if (base.isEmpty() || path == null) { + return this; + } + + // The following guards against the (ugly) use-case of using DocPath to contain a URL + if (isAbsoluteURL(path)) { + return this; + } + + DocPath newPath = base.relativize(path); + // avoid generating an empty link by using the basename of the path if necessary + if (newPath.isEmpty() && isEmpty(query) && isEmpty(fragment)) { + newPath = path.basename(); + } + return new DocLink(newPath, query, fragment); + } + + // return true if the path begins :// + private boolean isAbsoluteURL(DocPath path) { + String s = path.getPath(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isLetter(c)) { + continue; + } + return (c == ':' && i + 2 < s.length() && s.charAt(i + 1)== '/' && s.charAt(i + 2)== '/'); + } + return false; + } + + /** + * Returns the link in the form "path?query#fragment", omitting any empty + * components. + * + * @return the string + */ + @Override + public String toString() { + // common fast path + if (path != null && isEmpty(query) && isEmpty(fragment)) + return path.getPath(); + + StringBuilder sb = new StringBuilder(); + if (path != null) + sb.append(path.getPath()); + if (!isEmpty(query)) + sb.append("?").append(query); + if (!isEmpty(fragment)) + sb.append("#").append(fragment); + return sb.toString(); + } + + private static boolean isEmpty(String s) { + return (s == null) || s.isEmpty(); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/DocPath.java b/src/share/classes/jdk/codetools/apidiff/report/html/DocPath.java new file mode 100644 index 0000000..6de35b6 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/DocPath.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 1998, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Abstraction for immutable relative paths. + * Paths always use '/' as a separator, and never begin or end with '/'. + */ +public class DocPath { + private final String path; + + /** The empty path. */ + public static final DocPath empty = new DocPath(""); + + /** The empty path. */ + public static final DocPath parent = new DocPath(".."); + + /** + * Creates a path from a string. + * @param p the string + * @return the path + */ + public static DocPath create(String p) { + return (p == null) || p.isEmpty() ? empty : new DocPath(p); + } + + /** + * Creates a path from a string. + * @param p the string + */ + protected DocPath(String p) { + path = (p.endsWith("/") ? p.substring(0, p.length() - 1) : p); + } + + @Override + public boolean equals(Object other) { + return (other instanceof DocPath) && path.equals(((DocPath)other).path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + /** + * Returns the basename of this path. + * @return the basename + */ + public DocPath basename() { + int sep = path.lastIndexOf("/"); + return (sep == -1) ? this : new DocPath(path.substring(sep + 1)); + } + + /** + * Returns the parent directory of this path. + * @return the parent directory + */ + public DocPath parent() { + int sep = path.lastIndexOf("/"); + return (sep == -1) ? empty : new DocPath(path.substring(0, sep)); + } + + /** + * Returns the path formed by appending the specified string to the current path. + * @param p the string + * @return the path + */ + public DocPath resolve(String p) { + if (p == null || p.isEmpty()) + return this; + if (path.isEmpty()) + return new DocPath(p); + return new DocPath(path + "/" + p); + } + + /** + * Returns the path by appending the specified path to the current path. + * @param p the path + * @return the path + */ + public DocPath resolve(DocPath p) { + if (p == null || p.isEmpty()) + return this; + if (path.isEmpty()) + return p; + return new DocPath(path + "/" + p.getPath()); + } + + /** + * Returns the file path obtained by resolving this path against a base directory. + * + * @param dir the directory + * + * @return the resolved path + */ + public Path resolveAgainst(Path dir) { + return dir.resolve(path.replace('/', File.separatorChar)).normalize(); + } + + /** + * Return the inverse path for this path. + * For example, if the path is a/b/c, the inverse path is ../../.. + * @return the path + */ + public DocPath invert() { + return new DocPath(path.replaceAll("[^/]+", "..")); + } + + /** + * Returns the path formed by eliminating empty components, + * '.' components, and redundant name/.. components. + * @return the path + */ + public DocPath normalize() { + return path.isEmpty() + ? this + : new DocPath(String.join("/", normalize(path))); + } + + private static List normalize(String path) { + return normalize(Arrays.asList(path.split("/"))); + } + + private static List normalize(List parts) { + if (parts.stream().noneMatch(s -> s.isEmpty() || s.equals(".") || s.equals(".."))) { + return parts; + } + List normalized = new ArrayList<>(); + for (String part : parts) { + switch (part) { + case "": + case ".": + break; + case "..": + int n = normalized.size(); + if (n > 0 && !normalized.get(n - 1).equals("..")) { + normalized.remove(n - 1); + } else { + normalized.add(part); + } + break; + default: + normalized.add(part); + } + } + return normalized; + } + + /** + * Normalize and make relative a given path against this path, + * assuming that this path is for a file (not a directory), + * in which the other path will appear. + * + * @param other the path to be made relative to this path + * @return the simplified path + */ + public DocPath relativize(DocPath other) { + if (other == null || other.path.isEmpty()) { + return this; + } + + if (path.isEmpty()) { + return other; + } + + List originParts = normalize(path); + int sep = path.lastIndexOf("/"); + List destParts = sep == -1 + ? normalize(other.path) + : normalize(path.substring(0, sep + 1) + other.path); + int common = 0; + while (common < originParts.size() + && common < destParts.size() + && originParts.get(common).equals(destParts.get(common))) { + common++; + } + + List newParts; + if (common == originParts.size()) { + newParts = destParts.subList(common, destParts.size()); + } else { + newParts = new ArrayList<>(); + newParts.addAll(Collections.nCopies(originParts.size() - common - 1, "..")); + newParts.addAll(destParts.subList(common, destParts.size())); + } + return new DocPath(String.join("/", newParts)); + } + + /** + * Return true if this path is empty. + * @return true if this path is empty + */ + public boolean isEmpty() { + return path.isEmpty(); + } + + /** + * Creates a DocLink formed from this path and a fragment identifier. + * @param fragment the fragment + * @return the link + */ + public DocLink fragment(String fragment) { + return new DocLink(path, null, fragment); + } + + /** + * Creates a DocLink formed from this path and a query string. + * @param query the query string + * @return the link + */ + public DocLink query(String query) { + return new DocLink(path, query, null); + } + + /** + * Returns this path as a string. + * @return the path + */ + // This is provided instead of using toString() to help catch + // unintended use of toString() in string concatenation sequences. + public String getPath() { + return path; + } + + @Override + public String toString() { + return "DocPath[" + path + "]"; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/GetFileVisitor.java b/src/share/classes/jdk/codetools/apidiff/report/html/GetFileVisitor.java new file mode 100644 index 0000000..19fa0da --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/GetFileVisitor.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.io.File; + +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeParameterElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; + +class GetFileVisitor implements ElementKey.Visitor { + + DocPath getFile(ElementKey k) { + return k.accept(this, null); + } + + @Override + public DocPath visitModuleElement(ModuleElementKey k, Void _p) { + return getModuleDir(k).resolve("module-summary.html"); + } + + private DocPath getModuleDir(ModuleElementKey k) { + return (k == null) + ? DocPath.empty + : DocPath.create(k.name.toString()); + } + + @Override + public DocPath visitPackageElement(PackageElementKey k, Void _p) { + return getPackageDir(k).resolve("package-summary.html"); + } + + private DocPath getPackageDir(PackageElementKey k) { + return (k == null) + ? DocPath.empty + : getModuleDir((ModuleElementKey) k.moduleKey) + .resolve(k.name.toString().replace(".", File.separator)); + } + + @Override + public DocPath visitTypeElement(TypeElementKey k, Void _p) { + StringBuilder fn = new StringBuilder(k.name + ".html"); + while (k.enclosingKey instanceof TypeElementKey) { + k = (TypeElementKey) k.enclosingKey; + fn.insert(0,k.name + "."); + } + return getPackageDir((PackageElementKey) k.enclosingKey).resolve(fn.toString()); + } + + @Override + public DocPath visitExecutableElement(ExecutableElementKey k, Void _p) { + return k.typeKey.accept(this, null); + } + + @Override + public DocPath visitVariableElement(VariableElementKey k, Void _p) { + return k.typeKey.accept(this, null); + } + + @Override + public DocPath visitTypeParameterElement(TypeParameterElementKey k, Void _p) { + return null; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/HtmlDiffBuilder.java b/src/share/classes/jdk/codetools/apidiff/report/html/HtmlDiffBuilder.java new file mode 100644 index 0000000..58866f0 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/HtmlDiffBuilder.java @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.io.IOException; +import java.io.PrintStream; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.Stack; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Messages; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlAttr; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.RawHtml; +import jdk.codetools.apidiff.html.TagName; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.report.html.ResultTable.CountKind; + +import org.htmlcleaner.BaseToken; +import org.htmlcleaner.CommentNode; +import org.htmlcleaner.ContentNode; +import org.htmlcleaner.HtmlCleaner; +import org.htmlcleaner.SpecialEntities; +import org.htmlcleaner.SpecialEntity; +import org.htmlcleaner.TagNode; + +import org.outerj.daisy.diff.html.HTMLDiffer; +import org.outerj.daisy.diff.html.HtmlSaxDiffOutput; +import org.outerj.daisy.diff.html.TextNodeComparator; +import org.outerj.daisy.diff.html.dom.DomTree; +import org.outerj.daisy.diff.html.dom.DomTreeBuilder; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * A class to build HTML to display the differences between strings containing HTML, + * such as may be found in the documentation for corresponding elements in different + * instances of an API. + */ +public class HtmlDiffBuilder extends PairwiseDiffBuilder { + + /** + * Creates an instance of a {@code HtmlDiffBuilder}. + * + * @param pageReporter the reporter for the parent page + */ + public HtmlDiffBuilder(PageReporter pageReporter) { + super(pageReporter.parent.apis, pageReporter.log, pageReporter.msgs); + } + + /** + * Creates an instance of a {@code HtmlDiffBuilder}. + * + * @param log the log to which to report any problems while using this builder + */ + public HtmlDiffBuilder(Set apis, Log log, Messages msgs) { + super(apis, log, msgs); + } + + @Override + protected Content build(List refAPIs, String refItem, + List focusAPIs, String focusItem, + Consumer counter) { + String refNames = getNameList(refAPIs); + String focusNames = getNameList(focusAPIs); + if (refItem != null && focusItem != null) { + // item in both groups: display the comparison + Content title = Text.of(msgs.getString("htmldiffs.comparing", refNames, focusNames)); + return build(title, refItem, focusItem, counter); + } else if (refItem == null) { + Content title = Text.of(msgs.getString("htmldiffs.not-in-only-in", refNames, focusNames)); + counter.accept(CountKind.DESCRIPTION_ADDED); + return build(title, focusItem); + } else { + Content title = Text.of(msgs.getString("htmldiffs.only-in-not-in", refNames, focusNames)); + counter.accept(CountKind.DESCRIPTION_REMOVED); + return build(title, refItem); + } + } + + @Override + protected String getKeyString(String item) { + return item; + } + + public Content build(Content title, String refItem, String modItem, + Consumer counter) { + try { + Reader oldStream = new StringReader(refItem); + Reader newStream = new StringReader(modItem); + Handler handler = new Handler(log); + diffHtml(oldStream, newStream, handler); + + // TODO: consider styling the titles (will need custom formatter) + // msgs.getString("htmldiffs.comparing", refTitle, modTitle) + HtmlTree title2 = HtmlTree.DIV(title) + .setClass("hdiffs-title"); + HtmlTree doc = handler.doc; + count(doc, counter); + return HtmlTree.DIV(title2, doc).setClass("hdiffs"); + } catch (IOException | SAXException e) { + log.error("htmldiffs.err.exception-in-diff", e); + return Content.empty; + } + + } + + private Content build(Content title, String item) { + HtmlTree title2 = HtmlTree.DIV(title) + .setClass("hdiffs-title"); + + Content html = new RawHtml(item); + List contents = List.of(title2, html); + return new HtmlTree(TagName.DIV, contents).setClass("hdiffs"); + } + + // This method is a minimally edited extract from the DaisyDiff main program, + // lines 120-157, the body of "if (htmlDiff) { ... }". + // It runs the HtmlCleaner on the input text prior to calling HtmlDiffer, + // and uses the provided content handler to process the result. + void diffHtml(Reader oldStream, Reader newStream, ContentHandler postProcess) + throws IOException, SAXException { + + Locale locale = Locale.getDefault(); + String prefix = "diff"; + +// HtmlCleaner cleaner = new HtmlCleaner(); + +// InputSource oldSource = new InputSource(oldStream); +// InputSource newSource = new InputSource(newStream); + + DomTreeBuilder oldHandler = new DomTreeBuilder(); +// cleaner.cleanAndParse(oldSource, oldHandler); + cleanAndParse(oldStream, oldHandler); +// System.out.print("."); + TextNodeComparator leftComparator = new TextNodeComparator( + oldHandler, locale); + + DomTreeBuilder newHandler = new DomTreeBuilder(); +// cleaner.cleanAndParse(newSource, newHandler); + cleanAndParse(newStream, newHandler); +// System.out.print("."); + TextNodeComparator rightComparator = new TextNodeComparator( + newHandler, locale); + + postProcess.startDocument(); + postProcess.startElement("", "diffreport", "diffreport", + new AttributesImpl()); +// doCSS(css, postProcess); + postProcess.startElement("", "diff", "diff", + new AttributesImpl()); + HtmlSaxDiffOutput output = new HtmlSaxDiffOutput(postProcess, + prefix); + + HTMLDiffer differ = new HTMLDiffer(output); + differ.diff(leftComparator, rightComparator); +// System.out.print("."); + postProcess.endElement("", "diff", "diff"); + postProcess.endElement("", "diffreport", "diffreport"); + postProcess.endDocument(); + } + + private void cleanAndParse(Reader in, DomTreeBuilder builder) throws IOException, SAXException { + HtmlCleaner cleaner = new HtmlCleaner(); + builder.startDocument(); + convert(cleaner.clean(in), builder); + builder.endDocument(); + } + + private void convert(BaseToken node, DomTreeBuilder builder) throws SAXException { + if (node instanceof TagNode t) { + String name = t.getName(); + AttributesImpl attrs = new AttributesImpl(); + t.getAttributes().forEach((k, v) -> attrs.addAttribute("", k, k, "CDATA", v)); + builder.startElement("", name, name, attrs); + for (var c : t.getAllChildren()) { + convert(c, builder); + } + builder.endElement("", name, name); + } else if (node instanceof ContentNode c) { + var s = handleEntities(c.getContent()); + var chars = new char[s.length()]; + s.getChars(0, s.length(), chars, 0); + builder.characters(chars, 0, chars.length); + } else if (node instanceof CommentNode c) { + // ignore, at least for now: it's just a comment + } else { + throw new IllegalArgumentException(node.getClass().toString()); + } + } + + private static final Pattern entity = Pattern.compile("(?i)&(?:(?[a-z][a-z0-9]*)|#(?[0-9]+)|#x(?[0-9a-f]+))(;)?"); + private String handleEntities(String s) { + StringBuilder sb = null; + + var m = entity.matcher(s); + while (m.find()) { + if (sb == null) { + sb = new StringBuilder(); + } + String g; + if ((g = m.group("name")) != null) { + SpecialEntity e = SpecialEntities.INSTANCE.getSpecialEntity(g); + if (e != null) { + m.appendReplacement(sb, String.valueOf(e.charValue())); + } else { + m.appendReplacement(sb, m.group(0)); + } + } else if ((g = m.group("dec")) != null) { + m.appendReplacement(sb, escapeReplacementCharacter((char) Integer.parseInt(g))); + } else if ((g = m.group("hex")) != null) { + m.appendReplacement(sb, escapeReplacementCharacter((char) Integer.parseInt(g, 16))); + } else { + // should not happen, but if it does ... + m.appendReplacement(sb, m.group(0)); + } + } + + if (sb != null) { + m.appendTail(sb); + return sb.toString(); + } else { + return s; + } + } + + private String escapeReplacementCharacter(char ch) { + return switch (ch) { + case '\\' -> "\\\\"; + case '$' -> "\\$"; + default -> String.valueOf(ch); + }; + } + + private void show(DomTree tree, PrintStream out) { + show(tree.getBodyNode(), out,0); + } + + private void show(org.outerj.daisy.diff.html.dom.Node node, PrintStream out, int depth) { + var indent = " ".repeat(depth); + if (node instanceof org.outerj.daisy.diff.html.dom.TagNode tagNode) { + out.println(indent + tagNode.getOpeningTag()); + for (var c : tagNode) { + show(c, out, depth + 1); + } + } else if (node instanceof org.outerj.daisy.diff.html.dom.TextNode textNode) { + out.println(indent + textNode.getText()); + } else { + out.println(indent + node.getClass().getSimpleName()); + } + } + + private void count(HtmlTree tree, Consumer counter) { + for (int i = 0; i < tree.contents().size(); i++) { + HtmlTree t = getChildAsTree(tree.contents(), i); + if (t == null) { + continue; + } + if (isDiffSpan(t)) { + Set set = new HashSet<>(); + while (t != null && isDiffSpan(t)) { + set.add(t.get(HtmlAttr.CLASS)); + t = getChildAsTree(tree.contents(), ++i); + } + if (set.contains("diff-html-changed") + || (set.contains("diff-html-added") + && set.contains("diff-html-removed"))) { + counter.accept(CountKind.DESCRIPTION_CHANGED); + } else if (set.contains("diff-html-added")) { + counter.accept(CountKind.DESCRIPTION_ADDED); + } else { + counter.accept(CountKind.DESCRIPTION_REMOVED); + } + } else { + count(t, counter); + } + } + } + + private HtmlTree getChildAsTree(List contents, int i) { + if (i < contents.size()) { + Content c = contents.get(i); + if (c instanceof HtmlTree) { + return (HtmlTree) c; + } + } + return null; + } + + private boolean isDiffSpan(HtmlTree tree) { + if (tree.hasTag(TagName.SPAN)) { + String classAttr = tree.get(HtmlAttr.CLASS); + if (classAttr != null) { + switch (classAttr) { + case "diff-html-added": + case "diff-html-changed": + case "diff-html-removed": + return true; + } + } + } + return false; + } + + static class Handler implements ContentHandler { + /** The log, for reporting any errors. */ + private final Log log; + /** The stack of {@code HtmlTree} nodes being constructed. */ + private final Stack stack; + /** A buffer for sequences of characters. */ + private final StringBuilder text; + + HtmlTree doc; + + Handler(Log log) { + this.log = log; + stack = new Stack<>(); + text = new StringBuilder(); + } + + /** + * Returns the generated tree, after {@code endDocument} has been called. + * + * @return the generated tree. + */ + public HtmlTree getDoc() { + return doc; + } + + @Override + public void setDocumentLocator(Locator locator) { + // should not happen + } + + @Override + public void startDocument() { + stack.push(new HtmlTree(TagName.DIV)); + } + + @Override + public void endDocument() { + doc = stack.pop(); + } + + @Override + public void startPrefixMapping(String prefix, String uri) { + // should not happen + } + + @Override + public void endPrefixMapping(String prefix) { + // should not happen + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) { + flushText(); + + switch (localName) { + // ignore possibility of , , etc for now + case "diffreport": + case "diff": + return; + } + + HtmlTree tree; + try { + tree = new HtmlTree(TagName.of(localName)); + } catch (IllegalArgumentException e) { + log.warning("htmldiffs.warn.unknown-tag-name", localName); + tree = new HtmlTree(localName); + } + + for (int i = 0; i < atts.getLength(); i++) { + String name = atts.getLocalName(i); + String value = atts.getValue(i); + if (tree.hasTag(TagName.SPAN)) { + switch (name) { + case "changes": + // The value is an HTML fragment that describes the change. + // It may contain simple phrasing elements, as well as simple or nested lists. + // Convert it to a child node to display as a rich-text tooltip with CSS. + tree.add(getChangeTooltip(value)); + continue; + + case "changeId": + case "next": + case "previous": + continue; + + case "id": + // if changeId is present, skip id to avoid duplicates; + // an alternative would be to make the names unique + if (atts.getIndex("changeId") != -1) { + continue; + } + } + } + + try { + HtmlAttr a = HtmlAttr.of(name); + tree.set(a, value); + } catch (IllegalArgumentException e) { + log.warning("htmldiffs.warn.unknown-attribute-name", localName, name); + tree.set(name, value); + } + } + + stack.push(tree); + } + + @Override + public void endElement(String uri, String localName, String qName) { + flushText(); + switch (localName) { + // ignore possibility of , , etc for now + case "diffreport": + case "diff": + return; + } + HtmlTree tree = stack.pop(); + if (!tree.hasTag(localName)) { + log.err.println("popping unbalanced tree node: expect: " + localName + ", found " + tree.getTagString()); + } + + // DaisyDiff may generate empty and + // nodes, which get flagged by "tidy", so remove them + if (isEmptyDiff(tree)) { + return; + } + + HtmlTree top = stack.peek(); + // DaisyDiff may generate adjacent nodes that can be merged + if (canMergeWithPrevious(top, tree)) { + getPrevious(top).contents().addAll(tree.contents()); + } else { + top.add(tree); + } + } + + private boolean isEmptyDiff(HtmlTree tree) { + return isDiffAddedRemoved(tree) && tree.contents().isEmpty(); + } + + private boolean canMergeWithPrevious(HtmlTree container, HtmlTree tree) { + if (!container.contents().isEmpty() && isDiffAddedRemoved(tree)) { + Content prev = getLast(container.contents()); + if (prev instanceof HtmlTree prevTree) { + if (isDiffAddedRemoved(prevTree)) { + return tree.get(HtmlAttr.CLASS).equals(prevTree.get(HtmlAttr.CLASS)); + } + } + } + return false; + } + + private HtmlTree getPrevious(HtmlTree container) { + return (HtmlTree) getLast(container.contents()); + } + + private T getLast(List list) { + return list.get(list.size() - 1); + } + + private boolean isDiffAddedRemoved(HtmlTree tree) { + if (tree.hasTag(TagName.SPAN)) { + String classAttr = tree.get(HtmlAttr.CLASS); + if (classAttr != null) { + switch (classAttr) { + case "diff-html-added": + case "diff-html-removed": + return true; + } + } + } + return false; + } + + @Override + public void characters(char[] ch, int start, int length) { + text.append(ch, start, length); + } + + @Override + public void ignorableWhitespace(char[] ch, int start, int length) { + text.append(ch, start, length); + } + + @Override + public void processingInstruction(String target, String data) { + // should not happen + } + + @Override + public void skippedEntity(String name) { + // should not happen + // Note: + // known entities are translated into the equivalent character; e.g. < to < + // unknown entities are handled as literal strings; e.g. &foo; remains as &foo; + } + + private HtmlTree getChangeTooltip(String html) { + // We must return a span (or equivalent phrasing content), but the argument may contain a + // small number of block items, such as
                and
              • . Therefore we "cheat" and convert them + // to elements with an appropriate class, and fix the display in the CSS. + List contents = new ArrayList<>(); + Pattern p = Pattern.compile("(?i)<(/?)([a-z][a-z0-9]*)([^>]*)>"); + Matcher m = p.matcher(html); + int start = 0; + while (m.find(start)) { + if (m.start() > start) { + contents.add(new Text(html.substring(start, m.start()))); + } + if (m.group(1).isEmpty()) { + switch (m.group(2)) { + case "ul", "li" -> { + // ignore any existing attributes in group 3 -- e.g. class + contents.add(new RawHtml("")); + } + case "br" -> { + // ignore
                scattered in the text + } + default -> contents.add(new RawHtml(m.group())); + } + } else { + switch (m.group(2)) { + case "ul", "li" -> contents.add(new RawHtml("
                ")); + default -> contents.add(new RawHtml(m.group())); + } + } + start = m.end(); + } + if (start < html.length()) { + contents.add(new Text(html.substring(start))); + } + return HtmlTree.SPAN(contents).setClass("hdiffs-tooltip"); + } + + private void flushText() { + if (text.length() > 0) { + stack.peek().add(text); + text.setLength(0); + } + } + } + + +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/HtmlReporter.java b/src/share/classes/jdk/codetools/apidiff/report/html/HtmlReporter.java new file mode 100644 index 0000000..fe94a14 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/HtmlReporter.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.Abort; +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Messages; +import jdk.codetools.apidiff.Notes; +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.DocFile; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; +import jdk.codetools.apidiff.model.Position; +import jdk.codetools.apidiff.model.Position.ElementPosition; +import jdk.codetools.apidiff.model.Position.RelativePosition; +import jdk.codetools.apidiff.report.Reporter; + +/** + * The main class to generate the pages for an HTML report. + * The reporting methods are dispatched to individual reporters + * that handle the different types of pages. + */ +public class HtmlReporter implements Reporter { + final Set apis; + final API latestAPI; + final API previousAPI; + final Options options; + final Path outDir; + final Notes notes; + final Log log; + final Messages msgs; + + /** + * Creates a reporter that will generate an HTML report. + * + * @param apis the APIs being compared + * @param options the command-line options + * @param notes the notes to be associated with elements + * @param log the log to which to write any diagnostic messages + */ + public HtmlReporter(Set apis, Options options, Notes notes, Log log) { + if (apis.size() < 2) { + throw new IllegalArgumentException("too few APIs: " + apis.size()); + } + this.apis = apis; // TODO: change to List? + this.options = options; + this.outDir = options.getOutDir(); + this.notes = notes; + this.log = log; + this.msgs = Messages.instance("jdk.codetools.apidiff.report.html.resources.report"); + + List apiList = new ArrayList<>(apis); + latestAPI = apiList.get(apiList.size() - 1); + previousAPI = apiList.get(apiList.size() - 2); + + indexPageReporter = new IndexPageReporter(this); + + writeStylesheets(); + writeResourceFiles(); + } + + // + @Override + public void comparing(Position ePos, APIMap apiMap) { + getPageReporter(ePos).comparing(ePos, apiMap); + } + + @Override + public void completed(Position ePos, boolean equal) { + getPageReporter(ePos).completed(ePos, equal); + // TODO: if ePos is the pageKey for the pageReporter, can clean out pageReporter from pageVisitor map + } + + @Override + public void completed(boolean equal) { + indexPageReporter.writeFile(); + + // Debug reporting of to-do info + if (!toDoCounts.isEmpty()) { + log.err.println("ToDo info:"); + toDoCounts.forEach((k, v) -> log.err.println(" " + k + ": " + v)); + Map> inverseCounts = new TreeMap<>(Comparator.reverseOrder()); + toDoCounts.forEach((k, v) -> inverseCounts.computeIfAbsent(v, s -> new TreeSet<>()).add(k)); + inverseCounts.forEach((k, v) -> log.err.println(String.format("%6d: %s", k, v))); + } + } + + @Override + public void reportMissing(Position ePos, Set apis) { + getPageReporter(ePos).reportMissing(ePos, apis); + } + + @Override + public void reportDifferentAnnotations(Position amPos, APIMap amMap) { + getPageReporter(amPos).reportDifferentAnnotations(amPos, amMap); + } + + @Override + public void reportDifferentAnnotationValues(Position avPos, APIMap avMap) { + getPageReporter(avPos).reportDifferentAnnotationValues(avPos, avMap); + } + + @Override + public void reportDifferentDirectives(Position dPos, APIMap dMap) { + getPageReporter(dPos).reportDifferentDirectives(dPos, dMap); + } + + @Override + public void reportDifferentModifiers(Position ePos, APIMap eMap) { + getPageReporter(ePos).reportDifferentModifiers(ePos, eMap); + } + + @Override + public void reportDifferentKinds(Position ePos, APIMap eMap) { + getPageReporter(ePos).reportDifferentKinds(ePos, eMap); + } + + @Override + public void reportDifferentNames(Position ePos, APIMap eMap) { + getPageReporter(ePos).reportDifferentNames(ePos, eMap); + } + + @Override + public void reportDifferentTypeParameters(Position ePos, APIMap eMap) { + getPageReporter(ePos).reportDifferentTypeParameters(ePos, eMap); + } + + @Override + public void reportDifferentTypes(Position tPos, APIMap tMap) { + getPageReporter(tPos).reportDifferentTypes(tPos, tMap); + } + + @Override + public void reportDifferentThrownTypes(Position tPos, APIMap> tMap) { + getPageReporter(tPos).reportDifferentThrownTypes(tPos, tMap); + } + + @Override + public void reportDifferentSuperinterfaces(Position tPos, APIMap> tMap) { + getPageReporter(tPos).reportDifferentSuperinterfaces(tPos, tMap); + } + + @Override + public void reportDifferentPermittedSubclasses(Position tPos, APIMap> tMap) { + getPageReporter(tPos).reportDifferentPermittedSubclasses(tPos, tMap); + } + + @Override + public void reportDifferentValues(Position vPos, APIMap vMap) { + getPageReporter(vPos).reportDifferentValues(vPos, vMap); + } + + @Override + public void reportDifferentRawDocComments(Position tPos, APIMap cMap) { + getPageReporter(tPos).reportDifferentRawDocComments(tPos, cMap); + } + + @Override + public void reportDifferentApiDescriptions(Position tPos, APIMap dMap) { + getPageReporter(tPos).reportDifferentApiDescriptions(tPos, dMap); + } + + @Override + public void reportDifferentDocFiles(Position fPos, APIMap fMap) { + getPageReporter(fPos).reportDifferentDocFiles(fPos, fMap); + } + // + + Map toDoCounts = new TreeMap<>(); + void countToDo(String name) { + toDoCounts.put(name, toDoCounts.getOrDefault(name, 0) + 1); + } + + PageReporter getPageReporter(Position pos) { + return pos.accept(pageVisitor, null); + } + + PageReporter getPageReporter(ElementKey eKey) { + return (eKey == null) ? indexPageReporter : eKey.accept(pageVisitor, null); + } + + final IndexPageReporter indexPageReporter; + private final PageVisitor pageVisitor = new PageVisitor(this); + + /** The default stylesheet for all generated HTML files. */ + public static final String DEFAULT_STYLESHEET = "apidiff.css"; + + List getStylesheets() { + DocPath resourceDir = new DocPath("resources"); + List list = new ArrayList<>(); + if (options.getMainStylesheet() != null) { + list.add(resourceDir.resolve(options.getMainStylesheet().getFileName().toString())); + } else { + list.add(resourceDir.resolve(DEFAULT_STYLESHEET)); + } + for (Path extraStylesheet : options.getExtraStylesheets()) { + list.add(resourceDir.resolve(extraStylesheet.getFileName().toString())); + } + return list; + } + + private void writeStylesheets() throws Abort { + Path outResourceDir = outDir.resolve("resources"); + try { + Files.createDirectories(outResourceDir); + } catch (IOException e) { + log.error("report.err.cant-create-directory", outResourceDir, e); + throw new Abort(); + } + + if (options.getMainStylesheet() != null) { + copyFile(options.getMainStylesheet(), outResourceDir); + } else { + copyResource(DEFAULT_STYLESHEET, outResourceDir); + } + for (Path extraStylesheet : options.getExtraStylesheets()) { + copyFile(extraStylesheet, outResourceDir); + } + } + + private void writeResourceFiles() throws Abort { + // in the following map, the key for each entry is the relative path + // of a resource file to be written in the output directory, and the + // associated value gives the location from which it should be copied. + var map = new LinkedHashMap(); + + for (var apiOpts : options.getAllAPIOptions().values()) { + var apiDir = apiOpts.apiDir; + if (apiDir != null) { + listFiles(apiDir, this::isResourceFile) + .forEach(file -> map.put(apiDir.relativize(file), file)); + listFiles(apiDir.resolve("resource-files")) + .forEach(file -> map.put(apiDir.relativize(file), file)); + var absApiDir = apiDir.toAbsolutePath().normalize(); + for (Path resFile : options.getResourceFiles()) { + // resFile may explicitly begin with a specific API directory, + // or will be considered as relative to each of the API directories + Path absResFile = resFile.toAbsolutePath().normalize(); + Path pathFromApiDir; + if (absResFile.startsWith(absApiDir)) { + pathFromApiDir = absApiDir.relativize(absResFile); + } else if (!resFile.isAbsolute()) { + pathFromApiDir = resFile; + } else { + // TODO: check during Options.validate + continue; + } + Path pathInApiDir = apiDir.resolve(pathFromApiDir); + if (Files.isDirectory(pathInApiDir)) { + listFiles(pathInApiDir) + .forEach(file -> map.put(apiDir.relativize(file), file)); + } else if (Files.isRegularFile(pathInApiDir)) { + map.put(pathFromApiDir, pathInApiDir); + } + } + } + } + + map.forEach((to, from) -> copyFile(from, outDir.resolve(to))); + } + + private List listFiles(Path dir) { + return listFiles(dir, p -> true); + } + + private List listFiles(Path dir, DirectoryStream.Filter filter) { + if (Files.isDirectory(dir)) { + var list = new ArrayList(); + try (var ds = Files.newDirectoryStream(dir, filter)) { + for (Path p : ds) { + if (Files.isRegularFile(p)) { + list.add(p); + } + } + return list; + } catch (IOException e) { + log.error("report.err.error-finding-resource-files", dir, e); + throw new Abort(); + } + } else { + return List.of(); + } + } + + private boolean isResourceFile(Path file) { + return file.getFileName().toString().endsWith(".svg"); + + } + + // in this context: `resource` refers to a resource item in tool's jar file + private void copyResource(String name, Path dir) throws Abort { + Path toFile = dir.resolve(name); + try (InputStream in = getClass().getResourceAsStream("resources/" + name)) { + Files.copy(in, toFile, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + log.error("report.err.error-writing-file", toFile, e); + throw new Abort(); + } + } + + private void copyFile(Path fromFile, Path to) { + Path toFile = Files.isDirectory(to) ? to.resolve(fromFile.getFileName()) : to; + try { + Files.createDirectories(toFile.getParent()); + Files.copy(fromFile, toFile, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + log.error("report.err.error-writing-file", toFile, e); + throw new Abort(); + } + } + + /** + * A visitor to determine the page reporter for any given element or position. + */ + private static class PageVisitor + implements Position.Visitor, Void>, + ElementKey.Visitor, Void> { + HtmlReporter parent; + + PageVisitor(HtmlReporter parent) { + this.parent = parent; + } + + // TODO: should remove entries from the map when no longer required. + // when page has been completed, use pageReporters.values().removeIf(p -> p == pr) + Map> pageReporters = new ConcurrentHashMap<>(); + + @Override + public PageReporter visitModuleElement(ModuleElementKey mek, Void _p) { + return pageReporters.computeIfAbsent(mek, k -> new ModulePageReporter(parent, k)); + } + + @Override + public PageReporter visitPackageElement(PackageElementKey pek, Void _p) { + return pageReporters.computeIfAbsent(pek, k -> new PackagePageReporter(parent, k)); + } + + @Override + public PageReporter visitTypeElement(TypeElementKey tek, Void _p) { + return pageReporters.computeIfAbsent(tek, k -> new TypePageReporter(parent, k)); + } + + @Override + public PageReporter visitExecutableElement(ExecutableElementKey k, Void _p) { + return k.typeKey.accept(this, _p); + } + + @Override + public PageReporter visitVariableElement(VariableElementKey k, Void _p) { + return k.typeKey.accept(this, _p); + } + + @Override + public PageReporter visitTypeParameterElement(ElementKey.TypeParameterElementKey k, Void _p) { + return k.typeKey.accept(this, _p); + } + + @Override + public PageReporter visitElementPosition(ElementPosition kp, Void _p) { + return kp.key.accept(this, _p); + } + + @Override + public PageReporter visitRelativePosition(RelativePosition ip, Void _p) { + return ip.parent.accept(this, _p); + } + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/IndexPageReporter.java b/src/share/classes/jdk/codetools/apidiff/report/html/IndexPageReporter.java new file mode 100644 index 0000000..922edb2 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/IndexPageReporter.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.RawHtml; +import jdk.codetools.apidiff.html.TagName; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.ElementKey; + +/** + * A page reporter to create a top-level index page. + */ +public class IndexPageReporter extends PageReporter { + final NotesTable notesTable; + + IndexPageReporter(HtmlReporter parent) { + super(parent); + notesTable = new NotesTable(links); + } + + /** + * Returns the name to be used to link to this page in the nav bar. + * @return the name + */ + public String getName() { + return msgs.getString("overview.name"); + } + + @Override + protected HtmlTree buildPageContent() { + return new HtmlTree(TagName.HTML, buildHead(), buildBody()); + } + + @Override + public String getTitle() { + String t = getOptions().getTitle(); + return t != null ? t : msgs.getString("overview.title"); + } + + @Override + protected Content buildSignature() { + throw new Error(); + } + + @Override + protected HtmlTree buildBody() { + HtmlTree body = HtmlTree.BODY().setClass("index"); + body.add(buildHeader()); + HtmlTree main = HtmlTree.MAIN(); + main.add(buildPageHeading()); + main.add(buildSummary()); + main.add(buildEnclosedElements()); + main.add(buildNotes()); + main.add(buildResultTable()); + body.add(main); + body.add(buildFooter()); + if (getOptions().getHiddenOption("show-debug-summary") != null) { + body.add(new DebugSummary().build()); + } + return body; + } + + @Override + protected Content buildPageHeading() { + return HtmlTree.H1(Text.of(getTitle())); + } + + private List buildSummary() { + List summary = new ArrayList<>(); + String d = getOptions().getDescription(); + if (d != null) { + summary.add(new RawHtml(d)); + } + summary.add(new HtmlTree(TagName.H2, Text.of(msgs.getString("overview.heading.apis")))); + summary.add(HtmlTree.UL(getOptions().getAllAPIOptions().values().stream() + .map(a -> Text.of(a.label == null ? a.name : a.name + ": " + a.label)) + .map(HtmlTree::LI) + .collect(Collectors.toList()))); + return summary; + } + + @Override + protected List buildEnclosedElements() { + List list = new ArrayList<>(); + addEnclosedElements(list, "heading.modules", ek -> ek.kind == ElementKey.Kind.MODULE); + // This case should only occur when modules are not being compared + addEnclosedElements(list, "heading.packages", ek -> ek.kind == ElementKey.Kind.PACKAGE); + // This case should only occur when modules are not being compared, + // and some types are in the unnamed package. + // TODO: should we handle the unnamed package better? + addEnclosedElements(list, "heading.types", ek -> ek.kind == ElementKey.Kind.TYPE); + return list; + } + + /** + * Builds the list of notes (if any). + * + * @return the list of notes. + */ + protected Content buildNotes() { + if (notesTable.isEmpty()) { + return Content.empty; + } + + HtmlTree section = HtmlTree.SECTION(HtmlTree.H2(Text.of(msgs.getString("notes.heading")))) + .setClass("notes"); + section.add(notesTable.toContent()); + return section; + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/Keywords.java b/src/share/classes/jdk/codetools/apidiff/report/html/Keywords.java new file mode 100644 index 0000000..4a89993 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/Keywords.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.PrimitiveType; + +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.Text; + +/** + * Constants for language keywords and reserved identifiers. + */ +public class Keywords { + private Keywords() { } + + public static final Content ABSTRACT = keyword("abstract"); + public static final Content AT_INTERFACE = keyword("@interface"); + public static final Content BOOLEAN = keyword("boolean"); + public static final Content BYTE = keyword("byte"); + public static final Content CHAR = keyword("char"); + public static final Content CLASS = keyword("class"); + public static final Content DEFAULT = keyword("default"); + public static final Content DOUBLE = keyword("double"); + public static final Content EXPORTS = keyword("exports"); + public static final Content ENUM = keyword("enum"); + public static final Content EXTENDS = keyword("extends"); + public static final Content FALSE = keyword("false"); + public static final Content FINAL = keyword("final"); + public static final Content FLOAT = keyword("float"); + public static final Content IMPLEMENTS = keyword("implements"); + public static final Content INT = keyword("int"); + public static final Content INTERFACE = keyword("interface"); + public static final Content LONG = keyword("long"); + public static final Content NATIVE = keyword("native"); + public static final Content NON_SEALED = keyword("non-sealed"); + public static final Content MODULE = keyword("module"); + public static final Content OPEN = keyword("open"); + public static final Content OPENS = keyword("opens"); + public static final Content PACKAGE = keyword("package"); + public static final Content PERMITS = keyword("permits"); + public static final Content PRIVATE = keyword("private"); + public static final Content PROTECTED = keyword("protected"); + public static final Content PROVIDES = keyword("provides"); + public static final Content PUBLIC = keyword("public"); + public static final Content RECORD = keyword("record"); + public static final Content REQUIRES = keyword("requires"); + public static final Content SEALED = keyword("sealed"); + public static final Content SHORT = keyword("short"); + public static final Content STATIC = keyword("static"); + public static final Content STRICTFP = keyword("strictfp"); + public static final Content SUPER = keyword("super"); + public static final Content SYNCHRONIZED = keyword("synchronized"); + public static final Content THROWS = keyword("throws"); + public static final Content TO = keyword("to"); + public static final Content TRANSIENT = keyword("transient"); + public static final Content TRANSITIVE = keyword("transitive"); + public static final Content TRUE = keyword("true"); + public static final Content USES = keyword("uses"); + public static final Content VOID = keyword("void"); + public static final Content VOLATILE = keyword("volatile"); + public static final Content WITH = keyword("with"); + + /** + * Returns the keyword for a boolean value. + * + * @param b the value + * @return the keyword + */ + public static Content of(boolean b) { + return b ? TRUE : FALSE; + } + + /** + * Returns the keyword for a modifier. + * + * @param m the modifier + * @return the keyword + */ + public static Content of(Modifier m) { + return switch (m) { + case ABSTRACT -> ABSTRACT; + case DEFAULT -> DEFAULT; + case FINAL -> FINAL; + case NATIVE -> NATIVE; + case NON_SEALED -> NON_SEALED; + case PRIVATE -> PRIVATE; + case PROTECTED -> PROTECTED; + case PUBLIC -> PUBLIC; + case SEALED -> SEALED; + case STATIC -> STATIC; + case STRICTFP -> STRICTFP; + case SYNCHRONIZED -> SYNCHRONIZED; + case TRANSIENT -> TRANSIENT; + case VOLATILE -> VOLATILE; + }; + } + + /** + * Returns the keyword for a primitive type. + * + * @param t the type + * @return the keyword + */ + public static Content of(PrimitiveType t) { + return switch (t.getKind()) { + case BOOLEAN -> BOOLEAN; + case BYTE -> BYTE; + case CHAR -> CHAR; + case DOUBLE -> DOUBLE; + case FLOAT -> FLOAT; + case INT -> INT; + case LONG -> LONG; + case SHORT -> SHORT; + default -> throw new IllegalArgumentException((t.toString())); + }; + } + + /** + * Returns the keyword for the kind of a type element. + * + * @param k the kind + * @return the keyword + */ + public static Content of(ElementKind k) { + switch (k) { + case ANNOTATION_TYPE: + return AT_INTERFACE; + case CLASS: + return CLASS; + case ENUM: + return ENUM; + case INTERFACE: + return INTERFACE; + default: + if (k.name().equals("RECORD")) { + return RECORD; + } + throw new IllegalArgumentException((k.toString())); + } + } + + private static Content keyword(String name) { + return HtmlTree.SPAN(Text.of(name)).setClass("keyword"); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/Links.java b/src/share/classes/jdk/codetools/apidiff/report/html/Links.java new file mode 100644 index 0000000..0ca2e1a --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/Links.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.Locale; +import java.util.stream.Collectors; + +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeParameterElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; +import jdk.codetools.apidiff.model.Position; +import jdk.codetools.apidiff.model.Position.RelativePosition; +import jdk.codetools.apidiff.model.TypeMirrorKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.ArrayTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.DeclaredTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.PrimitiveTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.TypeVariableKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.WildcardTypeKey; + +/** + * Factory for links within the generated report. + */ +public class Links { + private GetFileVisitor getFile = new GetFileVisitor(); + private final DocPath file; + private final DocPath pathToRoot; + + Links(DocPath file) { + this.file = file; + pathToRoot = file.parent().invert(); + } + + DocPath getPath(String path) { + return pathToRoot.resolve(path); + } + + DocPath getPath(DocPath path) { + return pathToRoot.resolve(path); + } + + HtmlTree createLink(ElementKey key) { + return createLink(key, getName(key)); + } + + HtmlTree createLink(ElementKey key, CharSequence name) { + DocPath keyPath = getFile.getFile(key); + String id = idVisitor.getId(key); + DocLink keyLink = new DocLink(pathToRoot.resolve(keyPath), null, id); + + return HtmlTree.A(keyLink.toString(), Text.of(name)); + } + + private CharSequence getName(ElementKey eKey) { + return switch (eKey.kind) { + case MODULE -> ((ModuleElementKey) eKey).name; + case PACKAGE -> ((PackageElementKey) eKey).name; + case TYPE -> ((TypeElementKey) eKey).name; + default -> throw new IllegalArgumentException(eKey.toString()); + }; + } + + String getId(Position pos) { + if (pos.isElement()) { + return getId(pos.asElementKey()); + } else if (pos.isRelative()) { + RelativePosition rPos = (RelativePosition) pos; + switch (rPos.kind) { + case SERIALIZED_FIELD: + return "serial-field-" + rPos.index; + case SERIALIZATION_METHOD: + return "serial-method-" + rPos.index; + } + } + throw new IllegalArgumentException(pos.toString()); + } + + String getId(ElementKey eKey) { + return idVisitor.getId(eKey); + } + + private final IdVisitor idVisitor = new IdVisitor(); + + private class IdVisitor + implements ElementKey.Visitor, + TypeMirrorKey.Visitor { + + String getId(ElementKey eKey) { + CharSequence cs = eKey.accept(this, null); + return (cs == null) ? null : cs.toString(); + } + + @Override + public CharSequence visitModuleElement(ModuleElementKey mKey, Void aVoid) { + return null; + } + + @Override + public CharSequence visitPackageElement(PackageElementKey pKey, Void aVoid) { + return null; + } + + @Override + public CharSequence visitTypeElement(TypeElementKey tKey, Void aVoid) { + return null; + } + + @Override + public CharSequence visitExecutableElement(ExecutableElementKey k, Void aVoid) { + return k.name + k.params.stream() + .map(this::toString) + .collect(Collectors.joining(",", "(", ")")); + } + + @Override + public CharSequence visitVariableElement(VariableElementKey k, Void aVoid) { + return k.name; + } + + @Override + public CharSequence visitTypeParameterElement(TypeParameterElementKey k, Void aVoid) { + throw new UnsupportedOperationException(); + } + + String toString(TypeMirrorKey eKey) { + return eKey.accept(this, null).toString(); + } + + @Override + public CharSequence visitArrayType(ArrayTypeKey k, Void aVoid) { + return toString(k.componentKey) + "[]"; + } + + @Override + public CharSequence visitDeclaredType(DeclaredTypeKey k, Void aVoid) { + ElementKey eKey = k.elementKey; + return switch (eKey.kind) { + case TYPE -> ((TypeElementKey) eKey).name; + case TYPE_PARAMETER -> ((TypeParameterElementKey) eKey).name; + default -> throw new UnsupportedOperationException(); + }; + } + + @Override + public CharSequence visitPrimitiveType(PrimitiveTypeKey k, Void aVoid) { + return k.kind.name().toLowerCase(Locale.ROOT); + } + + @Override + public CharSequence visitTypeVariable(TypeVariableKey k, Void aVoid) { + return k.name; + } + + @Override + public CharSequence visitWildcardType(WildcardTypeKey k, Void aVoid) { + throw new UnsupportedOperationException(); + } + + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/ModulePageReporter.java b/src/share/classes/jdk/codetools/apidiff/report/html/ModulePageReporter.java new file mode 100644 index 0000000..5fb0426 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/ModulePageReporter.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.ModuleElement.ExportsDirective; +import javax.lang.model.element.ModuleElement.OpensDirective; +import javax.lang.model.element.ModuleElement.ProvidesDirective; +import javax.lang.model.element.ModuleElement.RequiresDirective; +import javax.lang.model.element.ModuleElement.UsesDirective; +import javax.lang.model.element.PackageElement; + +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.Entity; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.Position; +import jdk.codetools.apidiff.model.Position.RelativePosition; + +/** + * A reporter that generates an HTML page for the differences in + * a module declaration. + */ +class ModulePageReporter extends PageReporter { + APIMap mMap = null; + private final boolean allDirectiveDetails; + + ModulePageReporter(HtmlReporter parent, ElementKey mKey) { + super(parent, (ModuleElementKey) mKey); + + allDirectiveDetails = parent.options.getAccessKind().allModuleDetails(); + } + + @Override + protected String getTitle() { + return "module " + pageKey.name; + } + + @Override + protected Content buildSignature() { + List contents = new ArrayList<>(); + contents.addAll(buildAnnotations(Position.of(pageKey))); + contents.add(buildModifiers()); + contents.add(Text.SPACE); + contents.add(Keywords.MODULE); + contents.add(Text.SPACE); + contents.add(Text.of(pageKey.name)); + return HtmlTree.DIV(contents).setClass("signature"); + } + + @Override + @SuppressWarnings("unchecked") + public void comparing(Position pos, APIMap apiMap) { + super.comparing(pos, apiMap); + if (pos.isElement()) { + this.mMap = (APIMap) apiMap; + } + } + +// TODO: check if required +// /** +// * Writes a page containing details for a single module element in a given API. +// * +// * @param api the API containing the element +// * @param mdle the module element +// */ +// void writeFile(API api, ModuleElement mdle) { +// Position pagePos = Position.of(pageKey); +// APIMap apiMap = APIMap.of(api, mdle); +// comparing(pagePos, apiMap); +// parent.apis.stream() +// .filter(a -> a != api) +// .forEach(a -> reportMissing(pagePos, a)); +// completed(pagePos, true); +// } + + @Override + protected void writeFile() { + // If this is the only copy of the module in the APIs being compared, + // its enclosed elements will not have been compared or reported, and + // so will not be written out as a side effect of reporting the + // comparison. So, write the files for the enclosed packages now. + if (mMap.size() == 1) { + Map.Entry e = mMap.entrySet().iterator().next(); + API api = e.getKey(); + ModuleElement mdle = (ModuleElement) e.getValue(); + for (PackageElement pkg : api.getPackageElements(mdle)) { + PackagePageReporter r = (PackagePageReporter) parent.getPageReporter(ElementKey.of(pkg)); + r.writeFile(api, pkg); + } + } + + super.writeFile(); + } + + private Content buildModifiers() { + Position pos = Position.of(pageKey); + if (differentModifiers.containsKey(pos)) { + return new DiffBuilder().build(mMap, + me -> ((ModuleElement) me).isOpen() ? Keywords.OPEN : Entity.NBSP); + } else { + ModuleElement me = (ModuleElement) mMap.values().iterator().next(); + return me.isOpen() ? Keywords.OPEN : Content.empty; + } + } + + @Override + protected List buildEnclosedElements() { + List list = new ArrayList<>(); + addDirectives(list, "heading.exports", rp -> rp.kind == RelativePosition.Kind.MODULE_EXPORTS, this::buildExports); + addDirectives(list, "heading.opens", rp -> rp.kind == RelativePosition.Kind.MODULE_OPENS, this::buildOpens); + addDirectives(list, "heading.requires", rp -> rp.kind == RelativePosition.Kind.MODULE_REQUIRES, this::buildRequires); + addDirectives(list, "heading.provides", rp -> rp.kind == RelativePosition.Kind.MODULE_PROVIDES, this::buildProvides); + addDirectives(list, "heading.uses", rp -> rp.kind == RelativePosition.Kind.MODULE_USES, this::buildUses); + addEnclosedElements(list, "heading.packages", ek -> ek.kind == ElementKey.Kind.PACKAGE); + addDocFiles(list); + return list; + } + + private void addDirectives(List contents, + String headingKey, + Predicate> filter, + BiFunction, APIMap, Content> f) { + Map, APIMap> dMaps = new TreeMap<>(RelativePosition.elementKeyIndexComparator); + // apiMaps will only contain maps for directives which should be compared and displayed; + // i.e. they have already been filtered according to accessKind.allDirectiveDetails + for (Map.Entry> e : apiMaps.entrySet()) { + Position p = e.getKey(); + if (p instanceof RelativePosition) { + RelativePosition rp = (RelativePosition) p; + if (filter.test((RelativePosition) p)) { + @SuppressWarnings("unchecked") + APIMap dMap = (APIMap) e.getValue(); + dMaps.put(rp, dMap); + } + } + } + + if (!dMaps.isEmpty()) { + HtmlTree section = HtmlTree.SECTION().setClass("enclosed"); + section.add(HtmlTree.H2(Text.of(msgs.getString(headingKey)))); + HtmlTree ul = HtmlTree.UL(); + dMaps.forEach((rp, apiMap) -> ul.add(HtmlTree.LI(f.apply(rp, apiMap)))); + section.add(ul); + contents.add(section); + } + } + + private Content buildExports(RelativePosition rPos, APIMap apiMap) { + return buildExportsOpensProvides(rPos, apiMap, Keywords.EXPORTS, Keywords.TO, + ExportsDirective::getPackage, ExportsDirective::getTargetModules); + } + + private Content buildOpens(RelativePosition rPos, APIMap apiMap) { + return buildExportsOpensProvides(rPos, apiMap, Keywords.OPENS, Keywords.TO, + OpensDirective::getPackage, OpensDirective::getTargetModules); + } + + private Content buildProvides(RelativePosition rPos, APIMap apiMap) { + // ProvidesDirective is unusual in that part of it (i.e. the implementations) + // is not part of the public API, and should only be displayed if allDirectiveDetails + // is true. + return buildExportsOpensProvides(rPos, apiMap, Keywords.PROVIDES, Keywords.WITH, + ProvidesDirective::getService, + pd -> allDirectiveDetails ? pd.getImplementations() : Collections.emptyList()); + } + + private Content buildUses(RelativePosition rPos, APIMap apiMap) { + return buildExportsOpensProvides(rPos, apiMap, Keywords.USES, Content.empty, + UsesDirective::getService, d -> Collections.emptyList()); + } + + private + Content buildExportsOpensProvides(RelativePosition rPos, APIMap apiMap, + Content directiveKeyword, Content sublistKeyword, + Function getPrimaryElement, + Function> getSecondaryElements) { + List contents = new ArrayList<>(); + + contents.add(directiveKeyword); + contents.add(Text.SPACE); + + T archetype = apiMap.values().stream().filter(Objects::nonNull).findFirst().orElseThrow(); + contents.add(getName(ElementKey.of(getPrimaryElement.apply(archetype)))); + + APIMap> targetKeys = APIMap.of(); + Set allTargetKeys = new TreeSet<>(); + apiMap.forEach((api, d) -> { + List targets = getSecondaryElements.apply(d); + if (targets != null) { + Set s = targets.stream() + .map(ElementKey::of) + .collect(Collectors.toCollection(TreeSet::new)); + targetKeys.put(api, s); + allTargetKeys.addAll(s); + } + }); + + if (!allTargetKeys.isEmpty()) { + contents.add(Text.SPACE); + contents.add(sublistKeyword); + contents.add(Text.SPACE); + boolean first = true; + for (ElementKey ek : allTargetKeys) { + if (first) { + first = false; + } else { + contents.add(Text.of(", ")); + } + boolean allMatch = targetKeys.values().stream().allMatch(s -> s.contains(ek)); + if (allMatch) { + contents.add(getName(ek)); + } else { + APIMap map = APIMap.of(); + targetKeys.forEach((api, set) -> map.put(api, set.contains(ek) ? getName(ek) : Entity.NBSP)); + contents.add(new DiffBuilder().build(map)); + } + } + } + + + // TODO: for now, this is stylistically similar to buildEnclosedElement, + // but arguably a better way would be to move code for the check or cross into + // the enclosing loop that builds the list. + return HtmlTree.SPAN(getResultGlyph(rPos), Text.SPACE) + .add(HtmlTree.SPAN(contents).setClass("signature")); + + } + + private Content getName(ElementKey ek) { + return Text.of(getQualifiedName(ek)); + } + + private CharSequence getQualifiedName(ElementKey ek) { + switch (ek.kind) { + case MODULE: + return ((ModuleElementKey) ek).name; + case PACKAGE: + return ((PackageElementKey) ek).name; + case TYPE: + TypeElementKey tek = (TypeElementKey) ek; + return tek.enclosingKey == null ? tek.name : getQualifiedName(tek.enclosingKey) + "." + tek.name; + default: + throw new IllegalArgumentException((ek.toString())); + } + } + + private Content buildRequires(RelativePosition rPos, APIMap apiMap) { + List contents = new ArrayList<>(); + + contents.add(Keywords.REQUIRES); + contents.add(Text.SPACE); + + // TODO: would this be a useful method on APIMap? + RequiresDirective archetype = apiMap.values().stream() + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(); + + boolean allStaticEqual = apiMap.values().stream() + .allMatch(rd -> rd != null && rd.isStatic() == archetype.isStatic()); + if (allStaticEqual) { + if (archetype.isStatic()) { + contents.add(Keywords.STATIC); + contents.add(Text.SPACE); + } + } else { + APIMap alternatives = APIMap.of(); + apiMap.forEach((api, rd) -> { + Content kw = rd != null && rd.isStatic() ? Keywords.STATIC : Entity.NBSP; + alternatives.put(api, kw); + }); + contents.add(new DiffBuilder().build(alternatives)); + contents.add(Text.SPACE); + } + + boolean allTransitiveEqual = apiMap.values().stream() + .allMatch(rd -> rd != null && rd.isTransitive() == archetype.isTransitive()); + if (allTransitiveEqual) { + if (archetype.isTransitive()) { + contents.add(Keywords.TRANSITIVE); + contents.add(Text.SPACE); + } + } else { + APIMap alternatives = APIMap.of(); + apiMap.forEach((api, rd) -> { + Content kw = rd != null && rd.isTransitive() ? Keywords.TRANSITIVE : Entity.NBSP; + alternatives.put(api, kw); + }); + contents.add(new DiffBuilder().build(alternatives)); + contents.add(Text.SPACE); + } + + // TODO: would be nice to link to the module page if it can be determined to be available. + contents.add(Text.of(archetype.getDependency().getQualifiedName())); + + + // TODO: for now, this is stylistically similar to buildEnclosedElement, + // but arguably a better way would be to move code for the check or cross into + // the enclosing loop that builds the list. + return HtmlTree.SPAN(getResultGlyph(rPos), Text.SPACE) + .add(HtmlTree.SPAN(contents).setClass("signature")); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/NotesTable.java b/src/share/classes/jdk/codetools/apidiff/report/html/NotesTable.java new file mode 100644 index 0000000..9627b40 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/NotesTable.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import jdk.codetools.apidiff.Notes; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.ElementKey; + +/** + * A table recording the elements associated with each note. + */ +public class NotesTable { + private final Map> table; + private final Links links; + + /** + * Creates a table to record the elements associated with each note. + * + * @param links a factory to generate links to the associated elements + */ + NotesTable(Links links) { + table = new TreeMap<>(Comparator.comparing((Notes.Entry e) -> e.description)); + this.links = links; + } + + /** + * Returns whether the table is empty. + * + * @return {@code true} if and only if the table is empty + */ + boolean isEmpty() { + return table.isEmpty(); + } + + /** + * Add an association for an element key with a note. + * + * @param e the note + * @param eKey the element key + */ + void add(Notes.Entry e, ElementKey eKey) { + table.computeIfAbsent(e, e_ -> new ArrayList<>()).add(eKey); + } + + /** + * Generates HTML content representing the contents of the table. + * + * @return the content + */ + Content toContent() { + if (table.isEmpty()) { + return Content.empty; + } + + HtmlTree dl = HtmlTree.DL(); + table.forEach((e, list) -> add(dl, e, list)); + + return dl; + } + + private void add(HtmlTree dl, Notes.Entry e, List list) { + HtmlTree eLink = HtmlTree.A(e.uri, Text.of(e.description)); + dl.add(HtmlTree.DT(eLink)); + + // TODO: sort list? + + HtmlTree dd = HtmlTree.DD(); + boolean first = true; + for (ElementKey eKey : list) { + if (first) { + first = false; + } else { + dd.add(Text.of(", ")); + } + dd.add(toContent(eKey)); + } + dl.add(dd); + } + + private Content toContent(ElementKey eKey) { + return links.createLink(eKey, Notes.getName(eKey).replace("#", ".")); + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/PackagePageReporter.java b/src/share/classes/jdk/codetools/apidiff/report/html/PackagePageReporter.java new file mode 100644 index 0000000..8e4c4bc --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/PackagePageReporter.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.ElementFilter; + +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.AccessKind; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.Position; + +/** + * A reporter that generates an HTML page for the differences in + * a package declaration. + */ +class PackagePageReporter extends PageReporter { + + PackagePageReporter(HtmlReporter parent, ElementKey pKey) { + super(parent, (PackageElementKey) pKey); + } + + /** + * Writes a page containing details for a single package element in a given API. + * + * @param api the API containing the element + * @param pkg the package element + */ + void writeFile(API api, PackageElement pkg) { + Position pagePos = Position.of(pageKey); + APIMap apiMap = APIMap.of(api, pkg); + comparing(pagePos, apiMap); + Set missing = parent.apis.stream() + .filter(a -> a != api) + .collect(Collectors.toSet()); + if (!missing.isEmpty()) { + reportMissing(pagePos, missing); + } + completed(pagePos, true); + } + + @Override + protected void writeFile() { + // If this is the only copy of the package in the APIs being compared, + // its enclosed elements will not have been compared or reported, and + // so will not be written out as a side effect of reporting the + // comparison. So, write the files for the enclosed classes and + // interfaces now. + APIMap pMap = getElementMap(pageKey); + if (pMap.size() == 1) { + AccessKind accessKind = parent.options.getAccessKind(); + Map.Entry e = pMap.entrySet().iterator().next(); + API api = e.getKey(); + PackageElement pkg = (PackageElement) e.getValue(); + for (TypeElement te : ElementFilter.typesIn(pkg.getEnclosedElements())) { + if (!accessKind.accepts(te)) { + continue; + } + TypePageReporter r = (TypePageReporter) parent.getPageReporter(ElementKey.of(te)); + r.writeFile(api, te); + } + } + + super.writeFile(); + } + + @Override + protected String getTitle() { + StringBuilder sb = new StringBuilder(); + sb.append("package "); + if (pageKey.moduleKey != null) { + ModuleElementKey mKey = (ModuleElementKey) pageKey.moduleKey; + sb.append(mKey.name).append("/"); + } + sb.append(pageKey.name); + return sb.toString(); + } + + @Override + protected Content buildSignature() { + List contents = new ArrayList<>(); + contents.addAll(buildAnnotations(Position.of(pageKey))); + contents.add(Keywords.PACKAGE); + contents.add(Text.SPACE); + contents.add(Text.of(pageKey.name)); + return HtmlTree.DIV(contents).setClass("signature"); + } + + @Override + protected List buildEnclosedElements() { + List list = new ArrayList<>(); + addEnclosedElements(list, "heading.types", ek -> ek.kind == ElementKey.Kind.TYPE); + addDocFiles(list); + return list; + } + +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/PageReporter.java b/src/share/classes/jdk/codetools/apidiff/report/html/PageReporter.java new file mode 100644 index 0000000..35ae8e4 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/PageReporter.java @@ -0,0 +1,1553 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.SimpleElementVisitor14; +import javax.lang.model.util.SimpleTypeVisitor14; +import javax.tools.JavaFileObject; + +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTree; +import jdk.codetools.apidiff.Abort; +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Messages; +import jdk.codetools.apidiff.Notes; +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.Options.InfoTextKind; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.Entity; +import jdk.codetools.apidiff.html.HtmlAttr; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.RawHtml; +import jdk.codetools.apidiff.html.TagName; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.API.LocationKind; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.DocFile; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.MemberElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeParameterElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; +import jdk.codetools.apidiff.model.Position; +import jdk.codetools.apidiff.model.Position.ElementPosition; +import jdk.codetools.apidiff.model.Position.RelativePosition; +import jdk.codetools.apidiff.model.SerializedForm; +import jdk.codetools.apidiff.model.SerializedFormDocs; +import jdk.codetools.apidiff.report.Reporter; +import jdk.codetools.apidiff.report.SignatureVisitor; +import jdk.codetools.apidiff.report.html.ResultTable.CountKind; + +/** + * Base class for reporters handling the different pages of an HTML report. + */ +abstract class PageReporter implements Reporter { + + /** The enclosing HTML reporter. */ + protected final HtmlReporter parent; + + /** The log to which to report diagnostics. */ + protected final Log log; + + /** The element key for the page. */ + protected final K pageKey; + + /** The file for the page. */ + protected final DocPath file; + + /** A utility class to generate links to other pages. */ + protected final Links links; + + protected final Messages msgs; + + // The following collections accumulate the results reported with _report..._ methods. + // TODO: the methods that put items into these maps should check they are not + // overwriting any existing information + + protected Map> missing; + protected Map> differentAnnotations; + protected Map> differentAnnotationValues; + protected Map> differentDirectives; + protected Map> differentModifiers; + protected Map> differentKinds; + protected Map> differentTypeParameters; + protected Map> differentTypes; + protected Map>> differentThrownTypes; + protected Map>> differentSuperinterfaces; + protected Map>> differentPermittedSubclasses; + protected Map> differentValues; + protected Map> differentRawDocComments; + protected Map> differentApiDescriptions; + protected Map> differentDocFiles; + + /** The API maps for the items on this page. */ + protected final Map> apiMaps; + + /** The comparison results for the items on this page. */ + protected final Map results; + + protected final ResultTable resultTable; + + protected PageReporter(HtmlReporter parent) { + this(parent, null, new DocPath("index.html")); + } + + protected PageReporter(HtmlReporter parent, K eKey) { + this(parent, eKey, new GetFileVisitor().getFile(eKey)); + } + + private PageReporter(HtmlReporter parent, K eKey, DocPath file) { + this.parent = parent; + this.pageKey = eKey; + this.file = file; + + log = Objects.requireNonNull(parent.log); + msgs = Objects.requireNonNull(parent.msgs); + links = new Links(file); + resultTable = new ResultTable(msgs, links); + + missing = Collections.emptyMap(); + differentAnnotations = Collections.emptyMap(); + differentAnnotationValues = Collections.emptyMap(); + differentDirectives = Collections.emptyMap(); + differentModifiers = Collections.emptyMap(); + differentKinds = Collections.emptyMap(); + differentTypes = Collections.emptyMap(); + differentTypeParameters = Collections.emptyMap(); + differentThrownTypes = Collections.emptyMap(); + differentSuperinterfaces = Collections.emptyMap(); + differentPermittedSubclasses = Collections.emptyMap(); + differentValues = Collections.emptyMap(); + differentRawDocComments = Collections.emptyMap(); + differentApiDescriptions = Collections.emptyMap(); + differentDocFiles = Collections.emptyMap(); + + apiMaps = new HashMap<>(); + results = new HashMap<>(); + } + + // + + @Override + public void comparing(Position pos, APIMap apiMap) { + apiMaps.put(pos, apiMap); + } + + @Override + public void completed(Position ePos, boolean equal) { + results.put(ePos, equal); + if (ePos.isElement()) { + ElementKey eKey = ePos.asElementKey(); + if (eKey == pageKey) { + writeFile(); + ElementKey enclKey = eKey.getEnclosingKey(); + PageReporter enclPage = parent.getPageReporter(enclKey); + enclPage.completed(ePos, equal); + enclPage.resultTable.addAll(pageKey, resultTable.getTotals()); + } + } + } + + @Override + public void reportMissing(Position ePos, Set apis) { + if (missing.isEmpty()) { + missing = new HashMap<>(); + } + missing.put(ePos, apis); + if (apis.contains(parent.latestAPI)) { + resultTable.inc(ePos.getElementKey(), CountKind.ELEMENT_REMOVED); + } else { + resultTable.inc(ePos.getElementKey(), CountKind.ELEMENT_ADDED); + } + } + + @Override + public void reportDifferentAnnotations(Position amPos, APIMap amMap) { + if (differentAnnotations.isEmpty()) { + differentAnnotations = new HashMap<>(); + } + differentAnnotations.put(amPos, amMap); + if (amMap.containsKey(parent.latestAPI) && amMap.containsKey(parent.previousAPI)) { + resultTable.inc(amPos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentAnnotationValues(Position avPos, APIMap avMap) { + if (differentAnnotationValues.isEmpty()) { + differentAnnotationValues = new HashMap<>(); + } + differentAnnotationValues.put(avPos, avMap); + if (avMap.containsKey(parent.latestAPI) && avMap.containsKey(parent.previousAPI)) { + resultTable.inc(avPos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentDirectives(Position dPos, APIMap dMap) { + if (differentDirectives.isEmpty()) { + differentDirectives = new HashMap<>(); + } + differentDirectives.put(dPos, dMap); + if (dMap.containsKey(parent.latestAPI) && dMap.containsKey(parent.previousAPI)) { + resultTable.inc(dPos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentModifiers(Position ePos, APIMap eMap) { + if (differentModifiers.isEmpty()) { + differentModifiers = new HashMap<>(); + } + differentModifiers.put(ePos, eMap); + if (eMap.containsKey(parent.latestAPI) && eMap.containsKey(parent.previousAPI)) { + resultTable.inc(ePos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentKinds(Position ePos, APIMap eMap) { + if (differentKinds.isEmpty()) { + differentKinds = new HashMap<>(); + } + differentKinds.put(ePos, eMap); + if (eMap.containsKey(parent.latestAPI) && eMap.containsKey(parent.previousAPI)) { + resultTable.inc(ePos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentNames(Position ePos, APIMap eMap) { + // ignore, for now; compared locally for record components + } + + @Override + public void reportDifferentValues(Position ePos, APIMap vMap) { + if (differentValues.isEmpty()) { + differentValues = new HashMap<>(); + } + differentValues.put(ePos, vMap); + if (vMap.containsKey(parent.latestAPI) && vMap.containsKey(parent.previousAPI)) { + resultTable.inc(ePos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentTypeParameters(Position ePos, APIMap eMap) { + if (differentTypeParameters.isEmpty()) { + differentTypeParameters = new HashMap<>(); + } + differentTypeParameters.put(ePos, eMap); + if (eMap.containsKey(parent.latestAPI) && eMap.containsKey(parent.previousAPI)) { + resultTable.inc(ePos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentTypes(Position tPos, APIMap tMap) { + if (differentTypes.isEmpty()) { + differentTypes = new HashMap<>(); + } + differentTypes.put(tPos, tMap); + if (tMap.containsKey(parent.latestAPI) && tMap.containsKey(parent.previousAPI)) { + resultTable.inc(tPos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentThrownTypes(Position tPos, APIMap> tMap) { + if (differentThrownTypes.isEmpty()) { + differentThrownTypes = new HashMap<>(); + } + differentThrownTypes.put(tPos, tMap); + if (tMap.containsKey(parent.latestAPI) && tMap.containsKey(parent.previousAPI)) { + resultTable.inc(tPos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentSuperinterfaces(Position tPos, APIMap> tMap) { + if (differentSuperinterfaces.isEmpty()) { + differentSuperinterfaces = new HashMap<>(); + } + differentSuperinterfaces.put(tPos, tMap); + if (tMap.containsKey(parent.latestAPI) && tMap.containsKey(parent.previousAPI)) { + resultTable.inc(tPos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentPermittedSubclasses(Position tPos, APIMap> tMap) { + if (differentPermittedSubclasses.isEmpty()) { + differentPermittedSubclasses = new HashMap<>(); + } + differentPermittedSubclasses.put(tPos, tMap); + if (tMap.containsKey(parent.latestAPI) && tMap.containsKey(parent.previousAPI)) { + resultTable.inc(tPos.getElementKey(), CountKind.ELEMENT_CHANGED); + } + } + + @Override + public void reportDifferentRawDocComments(Position tPos, APIMap cMap) { + if (differentRawDocComments.isEmpty()) { + differentRawDocComments = new HashMap<>(); + } + differentRawDocComments.put(tPos, cMap); + // count changes in TextDiffBuilder + } + + @Override + public void reportDifferentApiDescriptions(Position tPos, APIMap dMap) { + if (differentApiDescriptions.isEmpty()) { + differentApiDescriptions = new HashMap<>(); + } + differentApiDescriptions.put(tPos, dMap); + // count changes in HtmlDiffBuilder + } + + @Override + public void reportDifferentDocFiles(Position fPos, APIMap fMap) { + if (differentDocFiles.isEmpty()) { + differentDocFiles = new HashMap<>(); + } + differentDocFiles.put(fPos, fMap); + // count changes in HtmlDiffBuilder + } + // + + protected void writeFile() throws Abort { + writeFile(file, buildPageContent()); + } + + protected void writeFile(DocPath file, HtmlTree content) { + Path path = file.resolveAgainst(parent.outDir); + + Path dir = path.getParent(); + try { + Files.createDirectories(dir); + } catch (IOException e) { + log.error("report.err.cant-create-directory", dir, e); + throw new Abort(); + } + + try (BufferedWriter out = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { + content.write(out); + } catch (IOException e) { + log.error("report.err.error-writing-file", path, e); + throw new Abort(); + } + } + + protected HtmlTree buildPageContent() { + return new HtmlTree(TagName.HTML, buildHead(), buildBody()); + } + + /** + * Returns the {@code } element for the page. + * + * The element is mostly standard, with customizations for the + * {@code } and possibly the set of stylesheets imported + * from the spec file, if the tool is comparing generated specs. + * + * @return the {@code <head>} element for the page. + */ + protected HtmlTree buildHead() { + String title = getTitle(); + if (parent.options.getTitle() != null) { + title = String.format("%s: %s", parent.options.getTitle(), title); + } + return HtmlTree.HEAD("UTF-8", title) + .add(HtmlTree.META("generator", "apidiff")) + .add(parent.getStylesheets().stream() + .map(links::getPath) + .map(l -> HtmlTree.LINK("stylesheet", l.getPath()))); + } + + protected abstract String getTitle(); + + /** + * Returns the {@code <body>} element for the page. + * + * TODO: the {@code <body>} element should follow a standard pattern: + * <ul> + * <li>A header, including navigation and a page heading + * <li>A report on the full "signature" of this element + * <li>A report on the doc comments for this element + * <li>A report on any enclosed elements: this will be + * a summary table with links for MODULE and PACKAGE, + * and inline details for members of TYPE. + * <li>A footer, including possible legal/copyright text + * </ul> + * + * @return the {@code <body>} element for the page. + */ + protected HtmlTree buildBody() { + Position pagePos = Position.of(pageKey); + String pageClass = pageKey.kind.toString().toLowerCase(Locale.ROOT); + HtmlTree body = HtmlTree.BODY().setClass(pageClass); + body.add(buildHeader()); + HtmlTree main = HtmlTree.MAIN(); + main.add(buildPageHeading()); + main.add(buildPageElement()); + main.add(buildDocComments(pagePos)); + main.add(buildAPIDescriptions(pagePos)); + main.add(buildEnclosedElements()); + main.add(buildResultTable()); + body.add(main); + body.add(buildFooter()); + if (parent.options.getHiddenOption("show-debug-summary") != null) { + body.add(new DebugSummary().build()); + } + return body; + } + + /** + * Builds the header for the page. + * + * @return the header + */ + protected Content buildHeader() { + List<Content> contents = new ArrayList<>(); + String topText = parent.options.getInfoText(InfoTextKind.TOP); + if (topText != null) { + contents.add(HtmlTree.DIV(new RawHtml(topText)).setClass("info")); + } + contents.add(buildNav(InfoTextKind.HEADER)); + return new HtmlTree(TagName.HEADER, contents); + } + + /** + * Builds the footer for the page. + * + * @return the footer + */ + protected Content buildFooter() { + List<Content> contents = new ArrayList<>(); + contents.add(buildNav(InfoTextKind.FOOTER)); + String bottomText = parent.options.getInfoText(InfoTextKind.BOTTOM); + if (bottomText != null) { + contents.add(HtmlTree.DIV(new RawHtml(bottomText)).setClass("info")); + } + return new HtmlTree(TagName.FOOTER, contents); + } + + /** + * Builds the main navigation bar for the page. + * + * @param kind the kind of info-text to be included in the bar + * + * @return the navigation bar + */ + protected Content buildNav(InfoTextKind kind) { + List<Content> contents = new ArrayList<>(); + String infoText = parent.options.getInfoText(kind); + if (infoText == null) { + infoText = String.join(" : ", parent.options.getAllAPIOptions().keySet()); + } + contents.add(HtmlTree.DIV(new RawHtml(infoText)).setClass("info")); + Text index = Text.of(parent.indexPageReporter.getName()); + HtmlTree ul = HtmlTree.UL(); + ul.add(HtmlTree.LI((pageKey == null) ? index : HtmlTree.A(links.getPath("index.html").getPath(), index))); + contents.add(HtmlTree.NAV(ul)); + return new HtmlTree(TagName.DIV, contents).setClass("bar"); + } + + /** + * Builds the page heading, based on the pageKey. + * + * @return the page heading + */ + protected Content buildPageHeading() { + return new PageHeading(Position.of(pageKey)).toContent(); + } + + /** + * Builds the "element" details for the pageKey, containing the check or cross, + * details about whether the element is missing in some instances of the API, + * any notes, and the signature of the element. + * + * @return the "element" details + */ + protected Content buildPageElement() { + Position pagePos = Position.of(pageKey); + List<Content> prelude = List.of(getResultGlyph(pagePos), buildMissingInfo(pagePos), buildNotes(pageKey)); + Content signature = buildSignature(); + return HtmlTree.DIV().setClass("element").add(prelude).add(signature); + } + + protected Content buildNotes(Position pos) { + return pos.isElement() ? buildNotes(((ElementPosition) pos).key) : Content.empty; + } + + /** + * Builds the list of notes (if any) associated with a given element key. + * + * @param eKey the element key + * + * @return the list of notes. + */ + protected Content buildNotes(ElementKey eKey) { + if (parent.notes == null) { + return Content.empty; + } + + Map<Notes.Entry, Boolean> entries = parent.notes.getEntries(eKey); + if (entries.isEmpty()) { + return Content.empty; + } + + var comp = Comparator.comparing((Notes.Entry e) -> e.description).thenComparing(e -> e.uri); + Map<Notes.Entry, Boolean> sorted = new TreeMap<>(comp); + sorted.putAll(entries); + + NotesTable notesTable = parent.indexPageReporter.notesTable; + sorted.forEach((e, isParent) -> { + if (!isParent) { + notesTable.add(e, eKey); + } + }); + + List<Content> contents = new ArrayList<>(); + contents.add(Text.of(msgs.getString("notes.prefix"))); + contents.add(Text.SPACE); + + boolean first = true; + for (Notes.Entry e : sorted.keySet()) { + if (first) { + first = false; + } else { + contents.add(Text.of(", ")); + } + contents.add(HtmlTree.A(e.uri, Text.of(e.description))); + } + contents.add(Text.of(".")); + + return HtmlTree.SPAN(contents).setClass("notes"); + } + + /** + * Builds the signature for the element key for the page. + * + * @return the signature + */ + protected abstract Content buildSignature(); + + /** + * Builds the information about the documentation comments for a position in the API. + * + * @param pos the position + * + * @return the information about the documentation comments + */ + protected Content buildDocComments(Position pos) { + var options = getOptions(); + if (options.compareDocComments()) { + APIMap<String> docComments = differentRawDocComments.get(pos); + if (docComments == null) { + docComments = getDocCommentMap(pos); + } + if (docComments != null && !docComments.isEmpty()) { + TextDiffBuilder b = new TextDiffBuilder(this); + List<Content> contents = b.build(docComments, ck -> resultTable.inc(pos.getElementKey(), ck)); + return new HtmlTree(TagName.DIV, contents).setClass("rawDocComments"); + } + } + + return Content.empty; + } + + private APIMap<String> getDocCommentMap(Position pos) { + if (pos.is(RelativePosition.Kind.SERIALIZED_FIELD)) { + // get the APIMap<SerializedForm> and build the field map from that + @SuppressWarnings("unchecked") + RelativePosition<String> sfPos = (RelativePosition<String>) pos; + APIMap<SerializedForm> sfMap = getSerializedFormMap(pos); + return sfMap == null ? null + : sfMap.map((api, sf) -> { + SerializedForm.Field f = sf.getField(sfPos.index); + List<? extends DocTree> tree = f == null ? null : f.getDocComment(); + return tree == null ? null : tree.toString(); // TODO check .toString() + }); + } else if (pos.is(RelativePosition.Kind.DOC_FILE)) { + @SuppressWarnings("unchecked") + APIMap<DocFile> fmap = (APIMap<DocFile>) apiMaps.get(pos); + return fmap.map((api, df) -> { + JavaFileObject fo = df.files.get(LocationKind.SOURCE); + DocCommentTree tree = fo == null ? null : api.getDocComment(fo); + return tree == null ? null : tree.toString(); + }); + } else { + APIMap<? extends Element> eMap = getElementMap(pos); + return eMap.map((api, e) -> { + DocCommentTree dct = api.getDocComment(e); + return dct == null ? null : dct.toString(); + }); + } + } + + /** + * Builds the information about the API descriptions for a position in the API. + * + * @param pos the position + * + * @return the information about the documentation comments + */ + protected Content buildAPIDescriptions(Position pos) { + var options = getOptions(); + if (options.compareApiDescriptions()) { + APIMap<String> apiDescriptions = differentApiDescriptions.get(pos); + if (apiDescriptions == null) { + apiDescriptions = getAPIDescriptionMap(pos); + } + if (apiDescriptions != null && !apiDescriptions.isEmpty()) { + var b = options.compareApiDescriptionsAsText() + ? new TextDiffBuilder(this) + : new HtmlDiffBuilder(this); + var contents = b.build(apiDescriptions, ck -> resultTable.inc(pos.getElementKey(), ck)); + return new HtmlTree(TagName.DIV, contents).setClass("apiDescriptions"); + } + } + + return Content.empty; + } + + private APIMap<String> getAPIDescriptionMap(Position pos) { + // TODO: reorder with using element map first + if (pos.is(RelativePosition.Kind.SERIALIZATION_OVERVIEW)) { + // get the APIMap<SerializedForm> and build the overview map from that + APIMap<SerializedForm> sfMap = getSerializedFormMap(pos); + return sfMap == null ? null + : sfMap.map((api, sf) -> { + SerializedFormDocs sfDocs = sf.getDocs(); + return sfDocs == null ? null : sfDocs.getOverview(); + }); + } else if (pos.is(RelativePosition.Kind.SERIALIZED_FIELD)) { + // get the APIMap<SerializedForm> and build the field map from that + @SuppressWarnings("unchecked") + RelativePosition<String> sfPos = (RelativePosition<String>) pos; + APIMap<SerializedForm> sfMap = getSerializedFormMap(pos); + return sfMap == null ? null + : sfMap.map((api, sf) -> { + SerializedFormDocs sfDocs = sf.getDocs(); + return sfDocs == null ? null : sfDocs.getFieldDescription(sfPos.index); + }); + } else if (pos.is(RelativePosition.Kind.DOC_FILE)) { + @SuppressWarnings("unchecked") + APIMap<DocFile> fmap = (APIMap<DocFile>) apiMaps.get(pos); + return fmap.map((api, df) -> { + JavaFileObject fo = df.files.get(LocationKind.SOURCE); + return fo == null ? null : api.getApiDescription(fo); + }); + } else { + APIMap<? extends Element> eMap = getElementMap(pos); + return eMap.map(API::getApiDescription); + } + } + + private APIMap<SerializedForm> getSerializedFormMap(Position pos) { + Position sfPos = pos.serializedForm(); + @SuppressWarnings("unchecked") + APIMap<SerializedForm> sfMap = (APIMap<SerializedForm>) apiMaps.get(sfPos); + return sfMap; + } + + /** + * Builds the details for the annotations at the position of an annotated construct in the API. + * + * @param acPos the position + * + * @return the details about the annotations + */ + protected List<Content> buildAnnotations(Position acPos) { + return new AnnotationBuilder().buildAnnotations(acPos); + } + + /** + * Builds the details about the enclosed elements of the element of the page. + * + * @return the details about the enclosed elements + */ + protected abstract List<Content> buildEnclosedElements(); + + /** + * Adds the details for selected enclosed elements. + * + * @param list the list to which the details should be added + * @param titleKey the resource key for a title (heading) for the list of enclosed elements + * @param filter a filter to select the enclosed elements to be added + */ + protected void addEnclosedElements(List<Content> list, String titleKey, Predicate<ElementKey> filter) { + Set<? extends ElementKey> enclosed = results.keySet().stream() + .filter(Position::isElement) + .map(Position::asElementKey) + .filter(ek -> ek != pageKey) + .filter(filter) + .collect(Collectors.toCollection(TreeSet::new)); + + if (!enclosed.isEmpty()) { + HtmlTree section = HtmlTree.SECTION().setClass("enclosed"); + section.add(HtmlTree.H2(Text.of(msgs.getString(titleKey)))); + HtmlTree ul = HtmlTree.UL(); + for (ElementKey eKey : enclosed) { + HtmlTree li = HtmlTree.LI(buildEnclosedElement(eKey)); + ul.add(li); + } + section.add(ul); + list.add(section); + } + } + + /** + * Builds the content for an enclosed element. + * + * <p>The default is to just generate the check/cross and a link to the enclosed element. + * + * @param eKey the key for the enclosed element + * + * @return the content + */ + protected Content buildEnclosedElement(ElementKey eKey) { + // The enclosed element may be on a different page, so use the appropriate page reporter + PageReporter<?> r = parent.getPageReporter(eKey); + return HtmlTree.SPAN(r.getResultGlyph(eKey), + Text.SPACE, + links.createLink(eKey)); + } + + protected void addDocFiles(List<Content> list) { + Set<? extends RelativePosition<String>> docFiles = results.keySet().stream() + .filter(p -> p.is(RelativePosition.Kind.DOC_FILE)) + .map(p -> p.as(RelativePosition.Kind.DOC_FILE, String.class)) + .collect(Collectors.toCollection(() -> new TreeSet<>(RelativePosition.stringIndexComparator))); + + if (!docFiles.isEmpty()) { + HtmlTree section = HtmlTree.SECTION().setClass("doc-files"); + section.add(HtmlTree.H2(Text.of(msgs.getString("heading.files")))); + HtmlTree ul = HtmlTree.UL(); + for (RelativePosition<String> p : docFiles) { + DocFilesBuilder b = new DocFilesBuilder(p); + HtmlTree li = HtmlTree.LI(getResultGlyph(p), buildMissingInfo(p)); + String name = p.index; + if (name.endsWith(".html")) { + b.buildFile(); + li.add(HtmlTree.A("doc-files/" + name, Text.of(name))); + } else { + li.add(Text.of(name)); + for (LocationKind lk : LocationKind.values()) { + li.add(b.buildTable(lk)); + } + } + ul.add(li); + } + section.add(ul); + list.add(section); + } + } + + private String getChecksum(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(bytes); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return ""; + } + } + + + /** + * Builds the details about any missing items at a position in the API. + * + * @param pos the position + * + * @return the details + */ + protected Content buildMissingInfo(Position pos) { + if (missing.containsKey(pos)) { + // TODO: use an L10N-friendly builder, or use an API list builder, building Content? + String onlyIn = apiMaps.get(pos).keySet().stream() + .map(a -> a.name) + .collect(Collectors.joining(", ")); + // The "missing" sets are not guaranteed ordered, so build the list according to + // the overall order of the APIs. + Set<API> missingAtPos = missing.get(pos); + String missingIn = parent.apis.stream() + .filter(missingAtPos::contains) + .map(a -> a.name) + .collect(Collectors.joining(", ")); + String info = msgs.getString("element.onlyInMissingIn", onlyIn, missingIn); + return HtmlTree.SPAN(Text.of(info)).setClass("missing"); + } else { + return Content.empty; + } + } + + protected Content buildResultTable() { + HtmlTree section = HtmlTree.SECTION(HtmlTree.H2(Text.of(msgs.getString("summary.heading")))) + .setClass("summary"); + if (resultTable.isEmpty()) { + section.add(Text.of(msgs.getString("summary.no-differences"))); + } else { + section.add(resultTable.toContent()); + } + return section; + } + + private static final Content CHECK = HtmlTree.SPAN(Entity.CHECK).setClass("same"); + private static final Content CROSS = HtmlTree.SPAN(Entity.CROSS).setClass("diff"); + private static final Content SINGLE = HtmlTree.SPAN(Entity.CIRCLED_DIGIT_ONE).setClass("single"); + + + protected Content getResultGlyph(ElementKey eKey) { + Position pos = Position.of(eKey); + return getResultGlyph(pos, apiMaps.get(pos)); + } + + protected Content getResultGlyph(Position pos) { + return getResultGlyph(pos, apiMaps.get(pos)); + } + + protected Content getResultGlyph(Position pos, APIMap<?> map) { + if (map == null) { + // TODO... + return Text.of("?"); + } + if (map.size() == 1) { + return SINGLE; + } + Boolean eq = results.get(pos); + return (eq == null) ? SINGLE : eq ? CHECK : CROSS; + } + + // TODO: improve abstraction; these args are typically reversed + protected Content getResultGlyph(APIMap<?> map, Position pos) { + if (map.size() == 1) { + return SINGLE; + } + Boolean eq = results.get(pos); + return (eq == null) ? SINGLE : eq ? CHECK : CROSS; + } + + protected APIMap<? extends Element> getElementMap(ElementKey eKey) { + Position ePos = Position.of(eKey); + @SuppressWarnings("unchecked") + APIMap<? extends Element> apiMap = (APIMap<? extends Element>) apiMaps.get(ePos); + return apiMap; + } + + protected APIMap<? extends Element> getElementMap(Position pos) { + if (!(pos.isElement() || pos.is(RelativePosition.Kind.SERIALIZATION_METHOD))) { + throw new IllegalArgumentException(pos.toString()); + } + @SuppressWarnings("unchecked") + APIMap<? extends Element> apiMap = (APIMap<? extends Element>) apiMaps.get(pos); + return apiMap; + } + + protected boolean hasMissing(APIMap<?> apiMap) { + return apiMap.size() < parent.apis.size(); + } + + protected boolean getResult(Position pos) { + Boolean b = results.get(pos); + if (b == null) { + throw new IllegalStateException(); // TODO: should this be an assertion + } + return b; + } + + protected boolean getResult(ElementKey eKey) { + return getResult(Position.of(eKey)); + } + + protected Content todo(String name) { + parent.countToDo(name); + return HtmlTree.SPAN(Text.of(name)).setClass("todo"); + } + + protected Options getOptions() { + return parent.options; + } + + + /** + * A utility class to generate the page heading for each page. + */ + protected class PageHeading implements ElementKey.Visitor<List<Content>, Void> { + Position pos; + + PageHeading(Position pos) { + this.pos = pos; + } + + Content toContent() { + List<Content> contents; + if (pos.isElement()) { + contents = pos.asElementKey().accept(this, null); + } else if (pos.is(RelativePosition.Kind.DOC_FILE)) { + @SuppressWarnings("unchecked") + RelativePosition<String> fPos = (RelativePosition<String>) pos; + contents = new ArrayList<>(); + ElementKey eKey = fPos.getElementKey(); + switch (eKey.kind) { + case MODULE -> { + ModuleElementKey mKey = (ModuleElementKey) eKey; + contents.add(minor("heading.module", links.createLink(mKey, mKey.name))); + } + case PACKAGE -> { + PackageElementKey pKey = (PackageElementKey) eKey; + ModuleElementKey mKey = (ModuleElementKey) pKey.moduleKey; + if (mKey != null) { + contents.add(minor("heading.module", links.createLink(mKey, mKey.name))); + } + contents.add(minor("heading.package", links.createLink(pKey, pKey.name))); + } + } + contents.add(major("heading.file", fPos.index)); + } else { + throw new IllegalStateException(pos.toString()); + } + + return new HtmlTree(TagName.DIV, contents).setClass("pageHeading"); + } + + @Override + public List<Content> visitModuleElement(ModuleElementKey mKey, Void _p) { + return List.of(major("heading.module", mKey.name)); + } + + @Override + public List<Content> visitPackageElement(PackageElementKey pKey, Void _p) { + List<Content> contents = new ArrayList<>(); + ModuleElementKey mKey = (ModuleElementKey) pKey.moduleKey; + if (mKey != null) { + contents.add(minor("heading.module", links.createLink(mKey, mKey.name))); + } + contents.add(major("heading.package", pKey.name)); + return contents; + } + + @Override + public List<Content> visitTypeElement(TypeElementKey tKey, Void _p) { + List<Content> contents = new ArrayList<>(); + + String tKind; + APIMap<? extends Element> tMap = getElementMap(tKey); + Set<ElementKind> eKinds = (tMap == null) ? Collections.emptySet() + : tMap.values().stream().map(Element::getKind).collect(Collectors.toSet()); + switch (eKinds.size()) { + case 0 -> + tKind = "heading.unknown"; + + case 1 -> { + ElementKind eKind = eKinds.iterator().next(); + tKind = switch (eKind) { + case ANNOTATION_TYPE -> "heading.annotation-type"; + case CLASS -> "heading.class"; + case ENUM -> "heading.enum"; + case INTERFACE -> "heading.interface"; + case RECORD -> "heading.record"; + default -> throw new IllegalStateException(eKind.toString()); + }; + } + + default -> + tKind = "heading.mixed"; + } + + StringBuilder tName = new StringBuilder(tKey.name); + while (tKey.enclosingKey instanceof TypeElementKey) { + tKey = (TypeElementKey) tKey.enclosingKey; + tName.insert(0, tKey.name + "."); + } + + PackageElementKey pKey = (PackageElementKey) tKey.enclosingKey; + if (pKey != null) { + ModuleElementKey mKey = (ModuleElementKey) pKey.moduleKey; + if (mKey != null) { + contents.add(minor("heading.module", links.createLink(mKey, mKey.name))); + } + contents.add(minor("heading.package", links.createLink(pKey, pKey.name))); + } + + contents.add(major(tKind, tName)); + return contents; + } + + @Override + public List<Content> visitExecutableElement(ExecutableElementKey k, Void aVoid) { + return null; + } + + @Override + public List<Content> visitVariableElement(VariableElementKey k, Void aVoid) { + return null; + } + + @Override + public List<Content> visitTypeParameterElement(TypeParameterElementKey k, Void aVoid) { + return null; + } + + private Content minor(String key, HtmlTree link) { + return HtmlTree.DIV( + HtmlTree.SPAN(Text.of(msgs.getString(key))).setClass("label"), + Entity.NBSP, + link + ); + } + + private Content major(String key, CharSequence name) { + return HtmlTree.H1(Text.of(msgs.getString(key)), Entity.NBSP, Text.of(name)); + } + } + + /** + * A builder for different instances of an annotation in different instances of an API. + */ + protected class AnnotationBuilder { + private AnnotationValueBuilder annoValueBuilder = new AnnotationValueBuilder(); + + public List<Content> buildAnnotations(Position acPos) { + Set<RelativePosition<?>> annos = getAnnotationsAt(acPos); + if (annos.isEmpty()) { + return List.of(); + } + + Content terminator = acPos.isElement() ? new HtmlTree(TagName.BR) : Text.SPACE; + + List<Content> contents = new ArrayList<>(); + for (RelativePosition<?> anno : annos) { + contents.add(build(anno)); + contents.add(terminator); + } + + return contents; + } + + private Content build(RelativePosition<?> aPos) { + // get the apiMap and result; + // if the anno is equal, get/print the first from the map; + // otherwise use DiffBuilder to show the differences + APIMap<? extends AnnotationMirror> aMap = getAnnotationsMap(aPos); + boolean equal = getResult(aPos); + AnnotationMirror archetype = aMap.values().iterator().next(); + Element annoType = archetype.getAnnotationType().asElement(); + List<Content> contents = new ArrayList<>(); + contents.add(new Text("@")); + contents.add(new Text(annoType.getSimpleName().toString())); + if (equal) { + Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = archetype.getElementValues(); + if (!elementValues.isEmpty()) { + contents.add(new Text("(")); + for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e : elementValues.entrySet()) { + ExecutableElement ee = e.getKey(); + AnnotationValue av = e.getValue(); + Name name = ee.getSimpleName(); + if (elementValues.size() > 1 || !name.contentEquals("value")) { + contents.add(new Text(name)); + contents.add(new Text("=")); + } + contents.add(annoValueBuilder.build(av)); + } + contents.add(new Text(")")); + } + } else { + // get the position for all the annotation values, sorted by the corresponding executable element + Set<RelativePosition<?>> values = new TreeSet<>(Position.elementKeyIndexComparator); + for (Position pos : results.keySet()) { + if (pos instanceof RelativePosition) { + RelativePosition<?> rp = (RelativePosition<?>) pos; + if (rp.kind == RelativePosition.Kind.ANNOTATION_VALUE && rp.parent.equals(aPos)) { + values.add(rp); + } + } + } + contents.add(new Text("(")); + contents.addAll(buildAnnoValues(values)); + contents.add(new Text(")")); + } + return HtmlTree.SPAN(contents).setClass("annotation"); + } + + Set<RelativePosition<?>> getAnnotationsAt(Position acPos) { + Set<RelativePosition<?>> annos = new TreeSet<>(Position.elementKeyIndexComparator); + for (Position pos : results.keySet()) { + if (pos instanceof RelativePosition) { + RelativePosition<?> rp = (RelativePosition<?>) pos; + if (rp.kind == RelativePosition.Kind.ANNOTATION && rp.parent.equals(acPos)) { + annos.add(rp); + } + } + } + return annos; + } + + @SuppressWarnings("unchecked") + APIMap<? extends AnnotationMirror> getAnnotationsMap(RelativePosition<?> aPos) { + return switch (aPos.kind) { + case ANNOTATION -> (APIMap<? extends AnnotationMirror>) apiMaps.get(aPos); + default -> throw new IllegalArgumentException(aPos.toString()); + }; + } + + private List<Content> buildAnnoValues(Set<RelativePosition<?>> values) { + List<Content> contents = new ArrayList<>(); + boolean first = true; + for (RelativePosition<?> value : values) { + if (first) { + first = false; + } else { + contents.add(new Text(", ")); + } + // TODO: make this an operation on Position; check RP.kind == ANNOTATION_VALUE + MemberElementKey meKey = (MemberElementKey) value.index; + assert meKey != null; + String name = meKey.name.toString(); + contents.add(new Text(name)); + contents.add(new Text(" = ")); + contents.addAll(annoValueBuilder.buildAnnoValue(value)); + } + return contents; + } + } + + /** + * A builder for different instances of an annotation value in different instances of an API. + */ + protected class AnnotationValueBuilder { + + List<Content> buildAnnoValue(RelativePosition<?> avPos) { + APIMap<? extends AnnotationValue> avMap = getAnnotationValuesMap(avPos); + boolean equal = getResult(avPos); + List<Content> contents = new ArrayList<>(); + if (equal) { + AnnotationValue archetype = avMap.values().iterator().next(); + contents.add(build(archetype)); + } else { + APIMap<Content> diffs = APIMap.of(); + for (Map.Entry<API, ? extends AnnotationValue> e : avMap.entrySet()) { + API api = e.getKey(); + AnnotationValue av = e.getValue(); + diffs.put(api, build(av)); + } + contents.add(new DiffBuilder().build(diffs)); + } + return contents; + } + + private Content build(AnnotationValue av) { + // for now, rely on javax.lang.model .toString() method, which is defined + // to return a source-friendly string. + // Note that this implies we can't show nested differences within composite values. + return new Text(av.toString()); + } + + @SuppressWarnings("unchecked") + APIMap<? extends AnnotationValue> getAnnotationValuesMap(RelativePosition<?> aPos) { + return switch (aPos.kind) { + case ANNOTATION_VALUE, DEFAULT_VALUE -> + (APIMap<? extends AnnotationValue>) apiMaps.get(aPos); + + default -> + throw new IllegalArgumentException(aPos.toString()); + }; + } + + } + + /** + * A builder for different instances of a type in different instances of an API. + */ + protected class TypeBuilder extends SimpleTypeVisitor14<Content, Void> { + + Content build(TypeMirror tm) { + // TODO: maybe handle annotations here + return tm.accept(this, null); + } + + @Override + protected Content defaultAction(TypeMirror tm, Void _p) { + return todo(tm.getKind() + " " + tm); + } + + @Override + public Content visitArray(ArrayType t, Void _p) { + List<Content> contents = new ArrayList<>(); + List<? extends AnnotationMirror> annos = t.getAnnotationMirrors(); + if (!annos.isEmpty()) { + contents.add(todo("type annotations " + annos)); + contents.add(Text.SPACE); + } + contents.add(visit(t.getComponentType(), _p)); + contents.add(Text.of("[]")); + return HtmlTree.SPAN(contents); + } + + @Override + public Content visitDeclared(DeclaredType t, Void _p) { + List<Content> contents = new ArrayList<>(); + List<? extends AnnotationMirror> annos = t.getAnnotationMirrors(); + if (!annos.isEmpty()) { + contents.add(todo("type annotations " + annos)); + contents.add(Text.SPACE); + } + contents.add(Text.of(elementNameVisitor.visit(t.asElement(), null))); + List<? extends TypeMirror> typeArgs = t.getTypeArguments(); + if (!typeArgs.isEmpty()) { + contents.add(Text.of("<")); + boolean needComma = false; + for (TypeMirror ta : typeArgs) { + if (needComma) { + contents.add(Text.of(", ")); + } else { + needComma = true; + } + contents.add(visit(ta, null)); + } + contents.add(Text.of(">")); + } + return (contents.size() == 1) ? contents.get(0) : HtmlTree.SPAN(contents); + } + + @Override + public Content visitNoType(NoType t, Void _p) { + return switch (t.getKind()) { + // NONE is most likely to be printed in a "diff" context, so generate a space + case NONE -> Entity.NBSP; + case VOID -> Keywords.VOID; + default -> throw new IllegalArgumentException(t.getKind().toString()); + }; + } + + @Override + public Content visitPrimitive(PrimitiveType t, Void _p) { + // TODO: annotations + return Keywords.of(t); + } + + @Override + public Content visitTypeVariable(TypeVariable t, Void _p) { + List<Content> contents = new ArrayList<>(); + List<? extends AnnotationMirror> annos = t.getAnnotationMirrors(); + if (!annos.isEmpty()) { + contents.add(todo("type annotations " + annos)); + contents.add(Text.SPACE); + } + contents.add(Text.of(t.asElement().getSimpleName())); // TODO: link? link to declaring element (type or executable?) + return (contents.size() == 1) ? contents.get(0) : HtmlTree.SPAN(contents); + } + + @Override + public Content visitWildcard(WildcardType t, Void _p) { + List<Content> contents = new ArrayList<>(); + List<? extends AnnotationMirror> annos = t.getAnnotationMirrors(); + if (!annos.isEmpty()) { + contents.add(todo("type " + + "annotations " + annos)); + contents.add(Text.SPACE); + } + contents.add(Text.of("?")); + addBound(contents, Keywords.EXTENDS, t.getExtendsBound()); + addBound(contents, Keywords.SUPER, t.getSuperBound()); + return (contents.size() == 1) ? contents.get(0) : HtmlTree.SPAN(contents); + } + + private void addBound(List<Content> contents, Content kw, TypeMirror b) { + if (b == null) { + return; + } + contents.add(Text.SPACE); + contents.add(kw); + contents.add(Text.SPACE); + contents.add(build(b)); + } + + private final ElementVisitor<Name,Void> elementNameVisitor = new SimpleElementVisitor14<>() { + @Override + public Name visitType(TypeElement te, Void p) { + return te.getQualifiedName(); + } + @Override + public Name visitTypeParameter(TypeParameterElement tpe, Void p) { + return tpe.getSimpleName(); + } + }; + } + + /** + * A builder for different instances of doc files across instances of an API. + * + * Two kinds of output are supported: either a short summary table giving the + * size and a checksum for the different instances, suitable for any type of file, + * or a separate file, displaying the differences for HTML files, compared as + * either doc comments (for instances in the source tree) or as API descriptions + * (for instances found in the API directory.) + */ + class DocFilesBuilder { + private final RelativePosition<String> fPos; + private final APIMap<DocFile> fMap; + private final DocPath file; + private final Links links; + + /** + * Creates a builder for the doc files at a given position. + * + * @param pos the position + */ + DocFilesBuilder(RelativePosition<String> pos) { + this.fPos = pos; + @SuppressWarnings("unchecked") + APIMap<DocFile> fMap = (APIMap<DocFile>) apiMaps.get(fPos); + this.fMap = fMap; + file = PageReporter.this.file.parent().resolve("doc-files").resolve(pos.index); + links = new Links(file); + } + + /** + * Returns a table displaying the size and a checksum for each of the + * instances of the doc file. The checksum is just intended to help + * visualize which files may be equal and which are different. + * The checksum is a short but sufficient substring of the SHA_256 digest. + * + * @param lk the kind of location for the files to be included in the table + * + * @return the table + */ + Content buildTable(LocationKind lk) { + @SuppressWarnings("unchecked") + APIMap<DocFile> fMap = (APIMap<DocFile>) apiMaps.get(fPos); + if (fMap.values().stream().allMatch(Objects::isNull)) { + return Content.empty; + } + + String captionKey = switch (lk) { + case API -> "docfile.details.caption.api"; + case SOURCE -> "docfile.details.caption.source"; + }; + + HtmlTree caption = HtmlTree.CAPTION(Text.of(msgs.getString(captionKey, fPos.index))); + HtmlTree tHead = HtmlTree.THEAD( + HtmlTree.TR( + HtmlTree.TH(Text.of(msgs.getString("docfile.details.th.api"))), + HtmlTree.TH(Text.of(msgs.getString("docfile.details.th.size"))), + HtmlTree.TH(Text.of(msgs.getString("docfile.details.th.checksum"))) + ) + ); + HtmlTree tBody = HtmlTree.TBODY(); + fMap.forEach((api, df) -> { + JavaFileObject fo = df.files.get(lk); + if (fo != null) { + byte[] bytes = api.getAllBytes(fo); + int size = bytes.length; + String cs = getChecksum(bytes); + tBody.add(HtmlTree.TR( + HtmlTree.TH(Text.of(api.name)).set(HtmlAttr.SCOPE, "row"), + HtmlTree.TD(Text.of(Integer.toString(size))), + HtmlTree.TD(Text.of(cs)) + )); + } + }); + + return HtmlTree.TABLE(caption, tHead, tBody).setClass("details"); + } + + /** + * Builds a file containing the comparison for instances of an HTML file. + */ + void buildFile() { + HtmlTree page = new HtmlTree(TagName.HTML, buildHead(), buildBody()); + writeFile(page); + } + + private void writeFile(HtmlTree content) { + PageReporter.this.writeFile(file, content); + } + + private Content buildHead() { + String title = getTitle(); + if (parent.options.getTitle() != null) { + title = String.format("%s: %s", parent.options.getTitle(), title); + } + return HtmlTree.HEAD("UTF-8", title) + .add(HtmlTree.META("generator", "apidiff")) + .add(parent.getStylesheets().stream() + .map(links::getPath) + .map(l -> HtmlTree.LINK("stylesheet", l.getPath()))); + } + + private String getTitle() { + return file.getPath(); + } + + private Content buildBody() { + HtmlTree body = HtmlTree.BODY().setClass("doc-files"); + body.add(buildHeader()); + HtmlTree main = HtmlTree.MAIN(); + main.add(buildPageHeading()); + main.add(HtmlTree.SPAN(getResultGlyph(fPos), buildMissingInfo(fPos)).setClass("doc-files")); + main.add(buildDocComments(fPos)); + main.add(buildAPIDescriptions(fPos)); +// main.add(buildEnclosedElements()); +// main.add(buildResultTable()); // TODO: info not broken out; could simulate a ResultTable? + body.add(main); + body.add(buildFooter()); + if (parent.options.getHiddenOption("show-debug-summary") != null) { + body.add(new DebugSummary().build()); // TODO will show package page info + } + return body; + } + + Content buildPageHeading() { + return new PageHeading(fPos).toContent(); + } + } + + /** + * A builder for a summary of the differences reported for different instances of an API. + */ + protected class DebugSummary { + protected Content build() { + List<Content> summary = new ArrayList<>(); + + if (!results.isEmpty()) { + long enclosedDiffs = results.values().stream() + .filter(b -> !b) + .count(); + if (enclosedDiffs > 0) { + summary.add(Text.of(String.format("%d different enclosed elements", enclosedDiffs))); + } + } + + List<Content> list = Stream.of( + build("missing items", missing), + build("different annotations", differentAnnotations), + build("different annotation values", differentAnnotationValues), + build("different directives", differentDirectives), + build("different kinds", differentKinds), + build("different type parameters", differentTypeParameters), + build("different modifiers", differentModifiers), + build("different types", differentTypes), + build("different thrown types", differentThrownTypes), + build("different superinterfaces", differentSuperinterfaces), + build("different raw doc comments", differentRawDocComments)) + .filter(c -> c != Content.empty) + .collect(Collectors.toList()); + if (!list.isEmpty()) { + summary.add(HtmlTree.UL(list)); + } + + if (summary.isEmpty()) { + summary.add(Text.of("no differences found")); + } + + return new HtmlTree(TagName.SECTION, summary).setClass("debug"); + } + + private Content build(String name, Map<Position, ?> map) { + if (map.isEmpty()) { + return Content.empty; + } + + SignatureVisitor sv = new SignatureVisitor(apiMaps); + + HtmlTree ul = HtmlTree.UL(); + for (Map.Entry<Position, ?> entry : map.entrySet()) { + Position pos = entry.getKey(); + Object details = entry.getValue(); + HtmlTree detailsTree; + if (details instanceof Set) { + @SuppressWarnings("unchecked") + Set<API> missing = (Set<API>) details; + HtmlTree list = HtmlTree.UL(); + missing.stream() + .map(a -> HtmlTree.LI(Text.of(a.name))) + .forEach(list::add); + detailsTree = list; + } else if (details instanceof APIMap<?> apiMap) { + HtmlTree table = new HtmlTree(TagName.TABLE); + apiMap.forEach((api, value) -> table.add( + HtmlTree.TR( + HtmlTree.TH(Text.of(api.name)), + HtmlTree.TD(Text.of(Objects.toString(value)))))); + detailsTree = table; + } else { + detailsTree = HtmlTree.DIV(Text.of(Objects.toString(details))); + } + ul.add(HtmlTree.LI(Text.of(sv.getSignature(pos)), detailsTree)); + } + + return new HtmlTree(TagName.SECTION, HtmlTree.H2(Text.of(name)), ul); + } + + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/PairwiseDiffBuilder.java b/src/share/classes/jdk/codetools/apidiff/report/html/PairwiseDiffBuilder.java new file mode 100644 index 0000000..ac34762 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/PairwiseDiffBuilder.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Messages; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; + +/** + * A class to build HTML to display pairwise differences between groups of API-specific items. + * + * @param <T> the type of the items. + */ +public abstract class PairwiseDiffBuilder<T> { + private final Set<API> apis; + protected final Log log; + protected final Messages msgs; + + /** + * Creates an instance of a {@code PairwiseDiffBuilder}. + * + * @param log the log to which to report any problems while using this builder + */ + public PairwiseDiffBuilder(Set<API> apis, Log log, Messages msgs) { + this.apis = apis; + this.log = log; + this.msgs = msgs; + } + + /** + * Builds HTML that displays the differences between API-specific items, + * by doing pair-wise comparisons between a reference API and the focus API. + * + * @param map the map of API-specific items + * @return the HTML nodes + */ + public List<Content> build(APIMap<T> map, Consumer<ResultTable.CountKind> counter) { + List<API> apiList = new ArrayList<>(apis); + API focusAPI = apiList.get(apiList.size() - 1); + + // first, determine the equivalence groups, + Map<String, List<API>> groups = new LinkedHashMap<>(); + for (API api : apis) { + groups.computeIfAbsent(getKeyString(map.get(api)), k -> new ArrayList<>()).add(api); + } + + List<API> focusGroup = groups.values().stream() + .filter(l -> l.contains(focusAPI)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("ref group not found")); + String focusNames = getNameList(focusGroup); + T focusItem = map.get(focusAPI); + + List<Content> contents = new ArrayList<>(); + // Now, compare each group against the group containing the reference API. + // Since there is only one group for all items with no content, there are + // effectively only 3 possibilities: + // 1. Different text in each group, display as a pair + // 2. Text only in one of the groups, but not the reference group + // 3. Text only in the reference group, not in the other one of the pair + for (Map.Entry<String, List<API>> entry : groups.entrySet()) { + String key = entry.getKey(); + List<API> apis = entry.getValue(); + if (apis == focusGroup) { + // if all the entries are same, there will only be one group, + // which will contain the focus, so no output will be generated; + // if the entries are not all the same, skip pairwise comparison + // of the focusGroup with itself! + continue; + } + + T other = map.get(apis.get(0)); + + /* + String otherNames = getNameList(apis); + if (focusItem != null && key != null) { + // item in both groups: display the comparison + Content title = Text.of(String.format("Comparing %s with %s", otherNames, focusNames)); // TODO: improve + contents.add(build(title, other, focusItem, counter)); + } else if (key == null) { + Content title = Text.of(String.format("Not in %s; only in %s", otherNames, focusNames)); // TODO: improve + contents.add(build(title, focusItem)); + } else if (focusItem == null) { + Content title = Text.of(String.format("Only in %s; not in %s", otherNames, focusNames)); // TODO: improve + contents.add(build(title, other)); + } + */ + + contents.add(build(apis, other, focusGroup, focusItem, counter)); + } + + return contents; + } + + protected abstract Content build(List<API> refAPIs, T refItem, + List<API> focusAPIs, T focusItem, + Consumer<ResultTable.CountKind> counter); + + protected abstract String getKeyString(T t); + +// protected abstract Content build(Content title, T refItem, T modItem, +// Consumer<ResultTable.CountKind> counter); +// +// protected abstract Content build(Content title, T item); + + protected String getNameList(List<API> apis) { + return apis.stream() + .map(a -> a.name) + .collect(Collectors.joining(", ")); + } +} + diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/ResultTable.java b/src/share/classes/jdk/codetools/apidiff/report/html/ResultTable.java new file mode 100644 index 0000000..e6141ae --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/ResultTable.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.EnumMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import jdk.codetools.apidiff.Messages; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlAttr; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.ModuleElementKey; +import jdk.codetools.apidiff.model.ElementKey.PackageElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeParameterElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; +import jdk.codetools.apidiff.model.TypeMirrorKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.ArrayTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.DeclaredTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.PrimitiveTypeKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.TypeVariableKey; +import jdk.codetools.apidiff.model.TypeMirrorKey.WildcardTypeKey; + +/** + * A class to accumulate statistics about the numbers of different kinds of changes. + */ +public class ResultTable { + /** + * The different kinds of count recorded in a result table. + * + * The members are ordered according to column order in the display table. + */ + public enum CountKind { + ELEMENT_ADDED, + ELEMENT_CHANGED, + ELEMENT_REMOVED, + COMMENT_ADDED, + COMMENT_CHANGED, + COMMENT_REMOVED, + DESCRIPTION_ADDED, + DESCRIPTION_CHANGED, + DESCRIPTION_REMOVED + } + + private final Map<ElementKey, Map<CountKind, Integer>> entries; + private final Messages msgs; + private final Links links; + + ResultTable(Messages msgs, Links links) { + this.msgs = msgs; + this.links = links; + entries = new TreeMap<>(); + } + + boolean isEmpty() { + return entries.isEmpty(); + } + + void inc(ElementKey eKey, CountKind ck) { + add(getEntry(eKey), ck, 1); + } + + void addAll(ElementKey eKey, Map<CountKind, Integer> counts) { + if (counts.values().stream().anyMatch(Objects::nonNull)) { + var e = getEntry(eKey); + counts.forEach((ck, i) -> add(e, ck, i)); + } + } + + Map<CountKind, Integer> getTotals() { + Map<CountKind, Integer> totals = new EnumMap<>(CountKind.class); + entries.values().forEach(e -> e.forEach((ck, i) -> add(totals, ck, i))); + return totals; + } + + private Map<CountKind, Integer> getEntry(ElementKey eKey) { + return entries.computeIfAbsent(eKey, e_ -> new EnumMap<>(CountKind.class)); + } + + private void add(Map<CountKind, Integer> counts, CountKind ck, int i) { + counts.put(ck, counts.computeIfAbsent(ck, ck_ -> 0) + i); + } + + Content toContent() { + Map<CountKind, Integer> totals = getTotals(); + + HtmlTree caption = HtmlTree.CAPTION(Text.of(msgs.getString("summary.caption"))); + + HtmlTree hRow1 = HtmlTree.TR(); + HtmlTree hRow2 = HtmlTree.TR(); + hRow1.add(HtmlTree.TD().set(HtmlAttr.ROWSPAN, "2")); + // The following assumes the CountKind values are in column order + Iterator<CountKind> iter = List.of(CountKind.values()).iterator(); + for (String k : List.of("summary.elements", "summary.comments", "summary.descriptions")) { + boolean noneAdded = totals.get(iter.next()) == null; + boolean noneChanged = totals.get(iter.next()) == null; + boolean noneRemoved = totals.get(iter.next()) == null; + hRow1.add(getHead(k, noneAdded && noneChanged && noneRemoved).set(HtmlAttr.COLSPAN, "3")); + hRow2.add(getHead("summary.added", noneAdded)); + hRow2.add(getHead("summary.changed", noneChanged)); + hRow2.add(getHead("summary.removed", noneRemoved)); + } + hRow1.add(getHead("summary.total", false).set(HtmlAttr.ROWSPAN, "2")); + HtmlTree head = HtmlTree.THEAD(hRow1, hRow2); + + HtmlTree body = HtmlTree.TBODY(); + entries.forEach((eKey, counts) -> { + HtmlTree bRow = HtmlTree.TR(); + bRow.add(HtmlTree.TH(links.createLink(eKey, toString(eKey))).set(HtmlAttr.SCOPE, "row")); + for (CountKind ck : CountKind.values()) { + HtmlTree cell = HtmlTree.TD(); + Integer c = counts.get(ck); + if (c != null) { + cell.add(Text.of(String.valueOf(c))); + } + bRow.add(cell); + } + int total = counts.values().stream().mapToInt(Integer::intValue).sum(); + bRow.add(HtmlTree.TD(Text.of(String.valueOf(total)))); + body.add(bRow); + }); + + HtmlTree fRow = HtmlTree.TR(); + fRow.add(HtmlTree.TH(Text.of(msgs.getString("summary.total"))).set(HtmlAttr.SCOPE, "row")); + HtmlTree foot = HtmlTree.TFOOT(fRow); + for (CountKind ck : CountKind.values()) { + HtmlTree cell = HtmlTree.TD(); + Integer c = totals.get(ck); + if (c != null) { + cell.add(Text.of(String.valueOf(c))); + } + fRow.add(cell); + } + int total = totals.values().stream().mapToInt(Integer::intValue).sum(); + fRow.add(HtmlTree.TD(Text.of(String.valueOf(total)))); + + return HtmlTree.TABLE(caption, head, body, foot).setClass("summary"); + } + + private HtmlTree getHead(String key, boolean allZero) { + HtmlTree th = HtmlTree.TH(Text.of(msgs.getString(key))); + if (allZero) { + th.setClass("allZero"); + } + return th; + } + + private String toString(ElementKey eKey) { + return toStringVisitor.toString(eKey); + } + + private final ToStringVisitor toStringVisitor = new ToStringVisitor(); + + private class ToStringVisitor + implements ElementKey.Visitor<CharSequence, Void>, + TypeMirrorKey.Visitor<CharSequence, Void> { + String toString(ElementKey eKey) { + return eKey.accept(this, null).toString(); + } + + @Override + public CharSequence visitModuleElement(ModuleElementKey mKey, Void aVoid) { + return mKey.name; + } + + @Override + public CharSequence visitPackageElement(PackageElementKey pKey, Void aVoid) { + return pKey.name; + } + + @Override + public CharSequence visitTypeElement(TypeElementKey tKey, Void aVoid) { + return tKey.enclosingKey instanceof TypeElementKey + ? toString(tKey.enclosingKey) + "." + tKey.name + : tKey.name; + } + + @Override + public CharSequence visitExecutableElement(ExecutableElementKey k, Void aVoid) { + return k.name + k.params.stream() + .map(this::toString) + .collect(Collectors.joining(",", "(", ")")); + } + + @Override + public CharSequence visitVariableElement(VariableElementKey k, Void aVoid) { + return k.name; + } + + @Override + public CharSequence visitTypeParameterElement(TypeParameterElementKey k, Void aVoid) { + throw new UnsupportedOperationException(); + } + + String toString(TypeMirrorKey eKey) { + return eKey.accept(this, null).toString(); + } + + @Override + public CharSequence visitArrayType(ArrayTypeKey k, Void aVoid) { + return toString(k.componentKey) + "[]"; + } + + @Override + public CharSequence visitDeclaredType(DeclaredTypeKey k, Void aVoid) { + return toString(k.elementKey); + } + + @Override + public CharSequence visitPrimitiveType(PrimitiveTypeKey k, Void aVoid) { + return k.kind.name().toLowerCase(Locale.ROOT); + } + + @Override + public CharSequence visitTypeVariable(TypeVariableKey k, Void aVoid) { + return k.name; + } + + @Override + public CharSequence visitWildcardType(WildcardTypeKey k, Void aVoid) { + StringBuilder sb = new StringBuilder("?"); + if (k.extendsBoundKey != null) { + sb.append("extends ").append(toString(k.extendsBoundKey)); + } + if (k.superBoundKey != null) { + sb.append("super ").append(toString(k.superBoundKey)); + } + return sb.toString(); + } + + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/TextDiffBuilder.java b/src/share/classes/jdk/codetools/apidiff/report/html/TextDiffBuilder.java new file mode 100644 index 0000000..128e0f1 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/TextDiffBuilder.java @@ -0,0 +1,606 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Chunk; +import com.github.difflib.patch.Patch; +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.TagName; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.report.html.ResultTable.CountKind; + +/** + * A class to build HTML to display the differences between plain-text strings, + * such as the documentation comments for corresponding elements in different + * instances of an API. + */ +public class TextDiffBuilder extends PairwiseDiffBuilder<String> { + + /** + * Creates an instance of a {@code TextDiffBuilder}. + * + * @param pageReporter the reporter for the parent page + */ + public TextDiffBuilder(PageReporter<?> pageReporter) { + super(pageReporter.parent.apis, pageReporter.log, pageReporter.msgs); + } + + @Override + protected Content build(List<API> refAPIs, String refItem, + List<API> focusAPIs, String focusItem, + Consumer<ResultTable.CountKind> counter) { + String refNames = getNameList(refAPIs); + String focusNames = getNameList(focusAPIs); + if (refItem != null && focusItem != null) { + // item in both groups: display the comparison + return build(refNames, getLines(refItem), focusNames, getLines(focusItem), counter); + } else if (refItem == null) { + Content title = Text.of(String.format("Not in %s; only in %s", refNames, focusNames)); // TODO: improve + counter.accept(CountKind.COMMENT_ADDED); + return build(title, focusItem); + } else { + Content title = Text.of(String.format("Only in %s; not in %s", refNames, focusNames)); // TODO: improve + counter.accept(CountKind.COMMENT_REMOVED); + return build(title, refItem); + } + } + + @Override + protected String getKeyString(String item) { + return item; + } + + /** + * Builds HTML that displays the differences between two sets of lines. + * + * @param refTitle a title for the "reference" set of lines + * @param refLines the "reference" set of lines + * @param modTitle a title for the "modified" set of lines + * @param modLines the "modified" set of lines + * @param counter a counter for the instances of the differences found in the strings + * + * @return the HTML nodes + */ + private Content build(String refTitle, List<String> refLines, + String modTitle, List<String> modLines, + Consumer<CountKind> counter) { + Patch<String> patch = DiffUtils.diff(refLines, modLines); + count(patch, counter); + return build(refTitle, refLines, modTitle, modLines, patch); + } + + private Content build(Content title, String item) { + HtmlTree title2 = HtmlTree.DIV(title) + .setClass("xdiffs-title"); + + HtmlTree pre = HtmlTree.PRE(Text.of(item)); + List<Content> contents = List.of(title2, pre); + return new HtmlTree(TagName.DIV, contents).setClass("xdiffs"); + } + + private void count(Patch<String> patch, Consumer<CountKind> counter) { + for (AbstractDelta<String> delta : patch.getDeltas()) { + Chunk<String> ref = delta.getSource(); + Chunk<String> mod = delta.getTarget(); + CountKind ck = isInsert(ref, mod) ? CountKind.COMMENT_ADDED + : isInsert(mod, ref) ? CountKind.COMMENT_REMOVED + : CountKind.COMMENT_CHANGED; + counter.accept(ck); + } + } + + /** + * Returns {@code true} if the chunk on the right is as if some text has been + * inserted into the chunk on the left. + * + * @param left the left chunk + * @param right the right chunk + * + * @return {@code true} if the chunk on the right is as if some text has been + * inserted into the chunk on the left + */ + private boolean isInsert(Chunk<String> left, Chunk<String> right) { + if (left.size() == 0) { + return true; + } + + if (left.size() == 1 && right.size() > 0) { + String leftLine = left.getLines().get(0); + + List<String> rightLines = right.getLines(); + String firstRightLine = rightLines.get(0); + String lastRightLine = rightLines.get(rightLines.size() - 1); + int l = Math.min(leftLine.length(), firstRightLine.length()); + for (int i = 0; i < l && leftLine.charAt(i) == firstRightLine.charAt(i); i++) { + if (lastRightLine.endsWith(leftLine.substring(i))) { + return true; + } + } + } + + return false; + } + + /** + * Build HTML representing the content of a patch. + * + * @param refTitle the title for the "reference" side of the comparison + * @param refLines the lines for the "reference" side of the comparison + * @param modTitle the title for the "modified" side of the comparison + * @param modLines the lines for the "modified" side of the comparison + * @param patch the patch containing the differences + * @return HTML displaying the differences + */ + // TODO: we could support alternate presentations, perhaps + // selecting one-of-n views using some input control + // (e.g. radio buttons or a choice item) and JavaScript. + Content build(String refTitle, List<String> refLines, + String modTitle, List<String> modLines, + Patch<String> patch) { + return new SDiffs() + .setReference(refTitle, refLines) + .setModified(modTitle, modLines) + .build(patch); + } + + private List<String> getLines(String text) { + return List.of(text.split("\\R")); + } + + /** + * A builder for side-by-side text diffs. + * + * <p>The structure is as follows: + * <pre>{@code + * <div class="sdiffs"> + * <div class="sdiffs-ref"> + * reference-title + * <pre> + * ... + * reference-diffs + * ... + * </pre> + * </div> + * <div class="sdiffs-mod"> + * modified-title + * <pre> + * ... + * modified-diffs + * ... + * </pre> + * </div> + * </div> + * }</pre> + */ + public static class SDiffs { + private String refTitle; + private List<String> refLines; + private String modTitle; + private List<String> modLines; + + private int contextSize = 5; + private boolean showLineNumbers = true; + + /** + * Sets the title and lines for the "reference" side of the comparison. + * + * @param title the title + * @param lines the lines + * @return this object + */ + public SDiffs setReference(String title, List<String> lines) { + this.refTitle = title; + this.refLines = lines; + return this; + } + + /** + * Sets the title and lines for the "modified" side of the comparison. + * + * @param title the title + * @param lines the lines + * @return this object + */ + public SDiffs setModified(String title, List<String> lines) { + this.modTitle = title; + this.modLines = lines; + return this; + } + + /** + * Sets the amount of context to show before and after each difference. + * + * @param size the number of lines to show + * @return this object + */ + public SDiffs setContextSize(int size) { + contextSize = size; + return this; + } + + /** + * Sets whether to show line numbers in the output. + * + * @param showLineNumbers whether to show line numbers + * @return this object + */ + public SDiffs setShowLineNumbers(boolean showLineNumbers) { + this.showLineNumbers = showLineNumbers; + return this; + } + + /** + * Build HTML to display the differences between the two sets of input. + * If an exception occurs while computing the differences, a message will + * be written to the log, and {@link Content#empty} returned. + * + * @param log the log + * @return the HTML node, or {@link Content#empty} + */ + public Content build(Log log) { + Patch<String> patch = DiffUtils.diff(refLines, modLines); // just differences; do not include EQUAL chunks + return build(patch); + } + + /** + * Build HTML to display the differences contained in a patch. + * + * @param patch the patch + * @return the HTML nodes + */ + public Content build(Patch<String> patch) { + if (patch.getDeltas().isEmpty()) { + return Content.empty; + } + + List<Content> refDiffs = new ArrayList<>(); + List<Content> modDiffs = new ArrayList<>(); + + int refIndex = 0; + int modIndex = 0; + for (AbstractDelta<String> delta : patch.getDeltas()) { + Chunk<String> refChunk = delta.getSource(); + Chunk<String> modChunk = delta.getTarget(); + + addContext(refDiffs, refLines, refIndex, refChunk.getPosition()); + addContext(modDiffs, modLines, modIndex, modChunk.getPosition()); + +// int maxSize = Math.max(refChunk.size(), modChunk.size()); +// addDiffLines(refDiffs, refChunk, maxSize); +// addDiffLines(modDiffs, modChunk, maxSize); + + addDiffLines(refDiffs, modDiffs, delta); + + refIndex = refChunk.last() + 1; + modIndex = modChunk.last() + 1; + } + + addContext(refDiffs, refLines, refIndex, Math.min(refIndex + contextSize, refLines.size())); + addContext(modDiffs, modLines, modIndex, Math.min(modIndex + contextSize, modLines.size())); + + Content refDiv = HtmlTree.DIV() + .setClass("sdiffs-ref") + .add(HtmlTree.DIV(new Text(refTitle)).setClass("sdiffs-title")) + .add(refDiffs); + Content modDiv = HtmlTree.DIV() + .setClass("sdiffs-mod") + .add(HtmlTree.DIV(new Text(modTitle)).setClass("sdiffs-title")) + .add(modDiffs); + return HtmlTree.DIV(refDiv, modDiv).setClass("sdiffs"); + } + + private void addContext(List<Content> contents, List<String> lines, int from, int to) { + if (to > from + 2 * contextSize) { + addLines(contents, lines, from, from + contextSize); + contents.add(new HtmlTree(TagName.HR)); + addLines(contents, lines, to - contextSize, to); + } else { + addLines(contents, lines, from, to); + } + } + + void addLines(List<Content> contents, List<String> lines, int from, int to) { + StringBuilder sb = new StringBuilder(); + for (int i = from; i < to; i++) { + if (showLineNumbers) { + sb.append(formatLineNumber(i + 1)); + } + sb.append(lines.get(i)).append("\n"); + } + HtmlTree pre = ensurePre(contents); + pre.add(new Text(sb.toString())); + } + + /** + * Show the content of a chunk from a line-oriented diff. + * The content is shown in a single block (including line numbers) + * with CSS class {@code sdiffs-changed}. + * The content is padding with blank lines, if necessary, + * up to a given number of lines. + * + * @param contents the contents to which to add the details + * @param chunk the chunk + * @param maxSize the number of lines to be displayed + */ + // An alternate presentation would be to just style the content + // of each line (but not the line number) but that would not + // highlight blank lines. + // Another alternate presentation would be to use background color + // for the CSS style. + void addDiffLines(List<Content> contents, Chunk<String> chunk, int maxSize) { + StringBuilder sb = new StringBuilder(); + List<String> lines = chunk.getLines(); + for (int i = 0; i < maxSize; i++) { + if (i < lines.size()) { + if (showLineNumbers) { + sb.append(formatLineNumber(chunk.getPosition() + i + 1)); + } + sb.append(lines.get(i)); + } else { + sb.append(" "); + } + sb.append("\n"); + } + HtmlTree pre = ensurePre(contents); + pre.add(HtmlTree.SPAN(new Text(sb.toString())).setClass("sdiffs-changed")); + } + + /** + * Show the differences in a delta from a line-oriented diff. + * The chunks are tokenized and diffed, in order to show intra-delta diffs. + * + * @param refDiffs the content for the diffs for the reference text + * @param modDiffs the content for the diffs for the modified text + * @param deltaLines the delta between the two sides + */ + void addDiffLines(List<Content> refDiffs, List<Content> modDiffs, AbstractDelta<String> deltaLines) { + var refChunk = deltaLines.getSource(); + var refDefaultCSSClass = switch (deltaLines.getType()) { + case CHANGE -> "sdiffs-lines-changed"; + case DELETE -> "sdiffs-lines-deleted"; + default -> null; + }; + var refTDiffs = new TokenDiffs(refDiffs, refChunk, refDefaultCSSClass); + List<String> refTokens = tokens(refChunk.getLines()); + + var modChunk = deltaLines.getTarget(); + var modDefaultCSSClass = switch (deltaLines.getType()) { + case CHANGE -> "sdiffs-lines-changed"; + case INSERT -> "sdiffs-lines-inserted"; + default -> null; + }; + var modTDiffs = new TokenDiffs(modDiffs, modChunk, modDefaultCSSClass); + List<String> modTokens = tokens(modChunk.getLines()); + + Patch<String> patch = DiffUtils.diff(refTokens, modTokens, true); // include EQUAL chunks + + for (var delta : patch.getDeltas()) { + var deltaType = delta.getType(); + String cssClass = switch (deltaType) { + case EQUAL -> null; + case DELETE -> "sdiffs-chars-deleted"; + case CHANGE -> "sdiffs-chars-changed"; + case INSERT -> "sdiffs-chars-inserted"; + }; + delta.getSource().getLines().forEach(t -> refTDiffs.add(t, cssClass)); + delta.getTarget().getLines().forEach(t -> modTDiffs.add(t, cssClass)); + } + + int refChunkSize = refChunk.size(); + int modChunkSize = modChunk.size(); + int maxChunkSize = Math.max(refChunkSize, modChunkSize); + refTDiffs.padNewlines(maxChunkSize - refChunkSize); + modTDiffs.padNewlines(maxChunkSize - modChunkSize); + } + + /** + * A builder for the diffs in a sequence of tokens, that can be used to show + * the diffs within a chunk in an {@link SDiffs} comparison. + */ + class TokenDiffs { + private final HtmlTree pre; + private StringBuilder pendingText; + private String pendingCSSClass; + private int pendingLineNumber; + private int displayedLineNumber; + private final String defaultCSSClass; + + /** + * Creates a builder to display one side of the differences in a {@code SDiffs} chunk. + * + * @param diffs the content of line-diffs to which the token-diffs will be added + * @param lineChunk the chunk containing the differences, used to get the initial line number + * @param defaultCSSClass the default CSS class for the diffs, such as a background color + */ + TokenDiffs(List<Content> diffs, Chunk<String> lineChunk, String defaultCSSClass) { + pre = ensurePre(diffs); + pendingText = new StringBuilder(); + pendingCSSClass = null; + pendingLineNumber = lineChunk.getPosition() + 1; + displayedLineNumber = -1; + this.defaultCSSClass = defaultCSSClass; + } + + /** + * Adds a token to the display. + * Newlines should be presented as a single-character string, and not within a longer string. + * + * @param token the token + * @param cssClass the class for the token, or {@code null} if none required + */ + void add(String token, String cssClass) { + if (token.equals("\n")) { + flush(); + pre.add("\n"); + pendingLineNumber++; + return; + } + + if (Objects.equals(cssClass, pendingCSSClass)) { + pendingText.append(token); + } else { + flush(); + pendingText.append(token); + pendingCSSClass = cssClass; + } + } + + /** + * Flush any pending text. + */ + void flush() { + if (pendingLineNumber > displayedLineNumber) { + pre.add(new Text(formatLineNumber(pendingLineNumber))); + displayedLineNumber = pendingLineNumber; + } + if (!pendingText.isEmpty()) { + var text = new Text(pendingText.toString()); + pre.add(pendingCSSClass != null ? HtmlTree.SPAN(text).setClass(pendingCSSClass) + : defaultCSSClass != null ? HtmlTree.SPAN(text).setClass(defaultCSSClass) + : text); + } + pendingText.setLength(0); + } + + /** + * Pad the output with newlines. + * + * @param n the number of newlines required + */ + void padNewlines(int n) { + if (n > 0) { + pre.add(new Text("\n".repeat(n))); + } + } + } + + /** + * Break a series of lines into a series of smaller tokens. + * In this implementation, the tokens are: + * + * <ul> + * <li>identifiers + * <li>decimal integers + * <li>runs of horizontal whitespace + * <li>other individual characters + * </ul> + * + * Newline characters should not be found in the input lines. + * A string containing a newline character will be added to the list of + * tokens after each line has been processed. + * + * Other implementations are possible, including all characters as individual tokens. + * The tradeoff is the desired granularity and resolution of the resulting tokens. + * + * @param lines the input lines + * @return the tokens + */ + List<String> tokens(List<String> lines) { + var result = new ArrayList<String>(); + for (var line : lines) { + int i = 0; + while (i < line.length()) { + char ch = line.charAt(i); + if (Character.isUnicodeIdentifierStart(ch)) { + int p = i++; + while (i < line.length()) { + ch = line.charAt(i); + if (!Character.isUnicodeIdentifierPart(ch)) { + break; + } + i++; + } + result.add(line.substring(p, i)); + } else if (Character.isDigit(ch)) { + int p = i++; + while (i < line.length()) { + ch = line.charAt(i); + if (!Character.isDigit(ch)) { + break; + } + i++; + } + result.add(line.substring(p, i)); + } else if (Character.isWhitespace(ch)) { + int p = i++; + while (i < line.length()) { + ch = line.charAt(i); + if (!Character.isWhitespace(ch)) { + break; + } + i++; + } + result.add(line.substring(p, i)); + } else { + result.add(String.valueOf(ch)); + i++; + } + } + result.add("\n"); + } + return result; + } + + /** + * Ensures that a list of contents has a {@code <pre>} element as the last + * element, and return that element. A new element will be created and + * added to the list if the last element is not a {@code <pre>} element. + * + * @param contents the list of contents + * @return the {@code <pre>} element at the end of the list + */ + HtmlTree ensurePre(List<Content> contents) { + if (contents.size() > 0) { + Content last = contents.get(contents.size() - 1); + if (last instanceof HtmlTree t && t.hasTag(TagName.PRE)) { + return t; + } + } + HtmlTree t = HtmlTree.PRE(); + contents.add(t); + return t; + } + + private String formatLineNumber(int n) { + return String.format("%4d ", n); + } + +// private void showChunk(String name, Chunk<String> c) { +// System.err.println("CHUNK: " + name + " " + (c == null ? "null" : c.toString())); +// } + + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/TypePageReporter.java b/src/share/classes/jdk/codetools/apidiff/report/html/TypePageReporter.java new file mode 100644 index 0000000..334f922 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/TypePageReporter.java @@ -0,0 +1,1124 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.apidiff.report.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.Parameterizable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.Entity; +import jdk.codetools.apidiff.html.HtmlAttr; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.model.AccessKind; +import jdk.codetools.apidiff.model.ElementExtras; +import jdk.codetools.apidiff.model.ElementKey; +import jdk.codetools.apidiff.model.ElementKey.ExecutableElementKey; +import jdk.codetools.apidiff.model.ElementKey.TypeElementKey; +import jdk.codetools.apidiff.model.ElementKey.VariableElementKey; +import jdk.codetools.apidiff.model.IntTable; +import jdk.codetools.apidiff.model.Position; +import jdk.codetools.apidiff.model.Position.RelativePosition; +import jdk.codetools.apidiff.model.SerializedForm; +import jdk.codetools.apidiff.model.TypeMirrorKey; +import jdk.codetools.apidiff.report.SignatureVisitor; + +/** + * A reporter that generates an HTML page for the differences in + * a type declaration. + */ +class TypePageReporter extends PageReporter<TypeElementKey> { + private APIMap<? extends TypeElement> tMap = null; + + private final ElementExtras elementExtras = ElementExtras.instance(); + + TypePageReporter(HtmlReporter parent, ElementKey tKey) { + super(parent, (TypeElementKey) tKey); + } + + /** + * Writes a page containing details for a single type element in a given API. + * + * @param api the API containing the element + * @param te the type element + */ + void writeFile(API api, TypeElement te) { + Position pagePos = Position.of(pageKey); + APIMap<TypeElement> apiMap = APIMap.of(api, te); + comparing(pagePos, apiMap); + Set<API> missing = parent.apis.stream() + .filter(a -> a != api) + .collect(Collectors.toSet()); + if (!missing.isEmpty()) { + reportMissing(pagePos, missing); + } + completed(pagePos, true); + } + + @Override + protected void writeFile() { + // If this is the only copy of the type in the APIs being compared, + // its enclosed elements will not have been compared or reported, and + // so will not be written out as a side effect of reporting the + // comparison. So, simulate the comparison of the enclosed members now, + // to initialize various data structures, and write out files for + // any nested classes and interfaces. + APIMap<? extends Element> tMap = getElementMap(pageKey); + if (tMap.size() == 1) { + AccessKind accessKind = parent.options.getAccessKind(); + Map.Entry<API, ? extends Element> entry = tMap.entrySet().iterator().next(); + API api = entry.getKey(); + TypeElement te = (TypeElement) entry.getValue(); + for (Element e : te.getEnclosedElements()) { + if (!accessKind.accepts(e)) { + continue; + } + if (e.getKind().isClass() || e.getKind().isInterface()) { + TypePageReporter r = (TypePageReporter) parent.getPageReporter(ElementKey.of(e)); + r.writeFile(api, (TypeElement) e); + } + ElementKey eKey = ElementKey.of(e); + Position ePos = Position.of(eKey); + comparing(ePos, APIMap.of(api, e)); + completed(ePos, true); + } + } + + super.writeFile(); + } + + @Override + protected String getTitle() { + return new SignatureVisitor(apiMaps).getSignature(pageKey); + } + + @Override + @SuppressWarnings("unchecked") + public void comparing(Position pos, APIMap<?> apiMap) { + super.comparing(pos, apiMap); + if (pos.isElement() && pos.asElementKey() == pageKey) { + this.tMap = (APIMap<? extends TypeElement>) apiMap; + } + } + + @Override + protected Content buildSignature() { + Position pagePos = Position.of(pageKey); + List<Content> contents = new ArrayList<>(); + contents.addAll(buildAnnotations(pagePos)); + contents.add(new ModifiersBuilder().build(pagePos, tMap)); + contents.add(Text.SPACE); + contents.add(buildKind()); + contents.add(Text.SPACE); + contents.add(Text.of(pageKey.name)); + + Content typarams = new TypeParameterBuilder().build(pagePos, tMap); + if (typarams != Content.empty) { + contents.add(typarams); + } + + Content components = new RecordComponentBuilder().build(pagePos, tMap); + if (components != Content.empty) { + contents.add(components); + } + + contents.add(Text.SPACE); + + Content superclass = buildSuperclass(); + if (superclass != Content.empty) { + contents.add(superclass); + } + + Content superinterfaces = buildSuperinterfaces(); + if (superinterfaces != Content.empty) { + contents.add(superinterfaces); + } + + Content permittedSubclasses = buildPermittedSubclasses(); + if (permittedSubclasses != Content.empty) { + contents.add(permittedSubclasses); + } + + return HtmlTree.DIV(contents).setClass("signature"); + } + + Content buildKind() { + Position pos = Position.of(pageKey); + if (differentKinds.containsKey(pos)) { + return new DiffBuilder().build(tMap, te -> Keywords.of(te.getKind())); + } else { + TypeElement te = (TypeElement) tMap.values().iterator().next(); + return Keywords.of(te.getKind()); + } + } + + private Content buildSuperclass() { + boolean noSuperclasses = tMap.values().stream() + .map(e -> ((TypeElement) e).getSuperclass()) + .allMatch(e -> e.getKind() == TypeKind.NONE); + if (noSuperclasses) { + return Content.empty; + } + + Content superclass = buildType(Position.of(pageKey).superclass(), + () -> ((TypeElement) tMap.values().iterator().next()).getSuperclass()); + return HtmlTree.DIV(Keywords.EXTENDS, Text.SPACE, superclass).setClass("superclass"); + } + + private Content buildSuperinterfaces() { + boolean noSuperinterfaces = tMap.values().stream() + .map(e -> ((TypeElement) e).getInterfaces()) + .allMatch(List::isEmpty); + if (noSuperinterfaces) { + return Content.empty; + } + + List<Content> contents = new ArrayList<>(); + Set<Content> keywords = tMap.values().stream() + .map(e -> { + return switch (e.getKind()) { + case CLASS, ENUM -> Keywords.IMPLEMENTS; + case ANNOTATION_TYPE, INTERFACE -> Keywords.EXTENDS; + default -> throw new IllegalStateException((e.getKind().toString())); + }; + }) + .collect(Collectors.toCollection(LinkedHashSet::new)); // preserve order of discovery + if (keywords.size() == 1) { + contents.add(keywords.iterator().next()); + } else { + contents.add(new DiffBuilder().build(new ArrayList<>(keywords))); + } + contents.add(Text.SPACE); + + // build the map of all superinterfaces for the type + Map<ElementKey, APIMap<TypeMirror>> allSuperinterfaces = new TreeMap<>(); + for (Map.Entry<API, ? extends Element> entry : tMap.entrySet()) { + API api = entry.getKey(); + TypeElement te = (TypeElement) entry.getValue(); + for (TypeMirror tm : te.getInterfaces()) { + Element i = api.getTypes().asElement(tm); + allSuperinterfaces.computeIfAbsent(ElementKey.of(i), _t -> APIMap.of()).put(api, tm); + } + } + + // TODO: should this be a list? the mildly tricky part is adding the comma separator + Set<API> apis = tMap.keySet(); + boolean needComma = false; + for (Map.Entry<ElementKey, APIMap<TypeMirror>> entry : allSuperinterfaces.entrySet()) { + ElementKey ek = entry.getKey(); + APIMap<TypeMirror> tMap = entry.getValue(); + if (needComma) { + contents.add(Text.of(", ")); + } else { + needComma = true; + } + // TODO: use factory method + Position pos = Position.of(pageKey).superinterface(ek); + contents.add(buildType(pos, apis, () -> tMap.values().iterator().next())); + } + + return HtmlTree.DIV(contents).setClass("superinterfaces"); + } + + private Content buildPermittedSubclasses() { + boolean noPermittedSubclasses = tMap.values().stream() + .map(e -> elementExtras.getPermittedSubclasses((TypeElement) e)) + .allMatch(List::isEmpty); + if (noPermittedSubclasses) { + return Content.empty; + } + + List<Content> contents = new ArrayList<>(); + contents.add(Keywords.PERMITS); + contents.add(Text.SPACE); + + // build the map of all permitted subclasses for the type + Map<ElementKey, APIMap<TypeMirror>> allPermittedSubclasses = new TreeMap<>(); + for (Map.Entry<API, ? extends Element> entry : tMap.entrySet()) { + API api = entry.getKey(); + TypeElement te = (TypeElement) entry.getValue(); + for (TypeMirror tm : elementExtras.getPermittedSubclasses(te)) { + Element i = api.getTypes().asElement(tm); + allPermittedSubclasses.computeIfAbsent(ElementKey.of(i), _t -> APIMap.of()).put(api, tm); + } + } + + // TODO: should this be a list? the mildly tricky part is adding the comma separator + Set<API> apis = tMap.keySet(); + boolean needComma = false; + for (Map.Entry<ElementKey, APIMap<TypeMirror>> entry : allPermittedSubclasses.entrySet()) { + ElementKey ek = entry.getKey(); + APIMap<TypeMirror> scMap = entry.getValue(); + if (needComma) { + contents.add(Text.of(", ")); + } else { + needComma = true; + } + // TODO: use factory method + Position pos = Position.of(pageKey).permittedSubclass(ek); + contents.add(buildType(pos, apis, scMap)); + } + + return HtmlTree.DIV(contents).setClass("permitted-subclasses"); + } + + private Content buildType(Position tPos, Supplier<TypeMirror> archetype) { + APIMap<? extends TypeMirror> types; + if ((types = differentTypes.get(tPos)) != null) { + return new DiffBuilder().build(types, this::buildType); + } else { + TypeMirror tm = archetype.get(); + if (tm == null) { + return Content.empty; + } + return buildType(tm); + } + } + + private Content buildType(Position tPos, Set<API> apis, Supplier<TypeMirror> archetype) { + APIMap<? extends TypeMirror> types; + if ((types = differentTypes.get(tPos)) != null) { + return new DiffBuilder().build(apis, types, this::buildType); + } else { + TypeMirror tm = archetype.get(); + if (tm == null) { + return Content.empty; + } + return buildType(tm); + } + } + + private Content buildType(Position tPos, Set<API> apis, APIMap<TypeMirror> tMap) { + if (differentTypes.get(tPos) != null || tMap.size() < apis.size()) { + return new DiffBuilder().build(apis, tMap, this::buildType); + } else { + return buildType(tMap.values().iterator().next()); + } + } + + // TODO: maybe use shared TypeBuilder and handle null there + private Content buildType(TypeMirror t) { + return (t == null) ? Entity.NBSP : new TypeBuilder().build(t); + } + + @Override + protected List<Content> buildEnclosedElements() { + List<Content> list = new ArrayList<>(); + addEnclosedElements(list, "heading.nested-types", ek -> ek.is(ElementKey.Kind.TYPE)); + addEnclosedElements(list, "heading.enum-constants", ek -> ek.is(ElementKind.ENUM_CONSTANT)); + addEnclosedElements(list, "heading.fields", ek -> ek.is(ElementKind.FIELD)); + addEnclosedElements(list, "heading.constructors", ek -> ek.is(ElementKind.CONSTRUCTOR)); + boolean isAnnoType = tMap.values().stream() + .map(Element::getKind) + .allMatch(k -> k == ElementKind.ANNOTATION_TYPE); + String mTitle = isAnnoType ? "heading.elements" : "heading.methods"; + addEnclosedElements(list, mTitle, ek -> ek.is(ElementKind.METHOD)); + + list.add(new SerializedFormBuilder().build()); + + return list; + } + + /** + * Build content for an enclosed element. + * + * <p>If the element is an executable element or variable element, the content is generated + * inline; if the element is a nested type, a link to the page for the type is generated. + * + * @param eKey the key for the enclosed element + * + * @return the content + */ + @Override + protected Content buildEnclosedElement(ElementKey eKey) { + return switch (eKey.kind) { + case TYPE -> super.buildEnclosedElement(eKey); + case EXECUTABLE -> new ExecutableBuilder((ExecutableElementKey) eKey).build(); + case VARIABLE -> new VariableBuilder((VariableElementKey) eKey).build(); + default -> throw new IllegalArgumentException((eKey.toString())); + }; + } + + /** + * A builder for different instances of a set of modifiers in different instances of an API. + */ + // TODO: display implicit modifiers in gray + // ... e.g. 'abstract' on interface, 'final' on enum declaration, etc. + protected class ModifiersBuilder { + private final Set<Modifier> allAccessMods = EnumSet.of(Modifier.PUBLIC, Modifier.PROTECTED, Modifier.PRIVATE); + + public Content build(ElementKey eKey, APIMap<? extends Element> eMap) { + return build(Position.of(eKey), eMap); + } + + public Content build(Position ePos, APIMap<? extends Element> eMap) { + if (differentModifiers.containsKey(ePos)) { + // full build, with differences + return build(eMap); + } else { + // fast-track build, since all are the same + List<Content> contents = new ArrayList<>(); + Element e = eMap.values().iterator().next(); + simpleAddModifiers(contents, e.getModifiers()); + return wrap(contents); + } + } + + public Content build(APIMap<? extends Element> eMap) { + List<Content> contents = new ArrayList<>(); + + if (eMap.size() == 1) { + simpleAddModifiers(contents, eMap.values().iterator().next().getModifiers()); + } else { + addAccessModifiers(contents, eMap); + + for (Modifier m : Modifier.values()) { + switch (m) { + case PUBLIC: + case PROTECTED: + case PRIVATE: + case NATIVE: + case SYNCHRONIZED: + break; + + default: + addModifier(contents, m, eMap); + } + } + } + + return wrap(contents); + } + + void simpleAddModifiers(List<Content> content, Set<Modifier> mods) { + for (Modifier m : mods) { + switch (m) { + case NATIVE: + case SYNCHRONIZED: + continue; + } + + if (!content.isEmpty()) { + content.add(Text.SPACE); + } + content.add(Keywords.of(m)); + } + } + + + void addAccessModifiers(List<Content> content, APIMap<? extends Element> eMap) { + // deal with the access modifiers as a group of which at most one may be set + boolean allEqual = true; + Set<Modifier> accessMods = null; + for (Element e : eMap.values()) { + Set<Modifier> mods; + if (e.getModifiers().isEmpty()) { + mods = Collections.emptySet(); + } else { + mods = EnumSet.copyOf(e.getModifiers()); + mods.retainAll(allAccessMods); + } + if (accessMods == null) { + accessMods = mods; + } else { + if (!mods.equals(accessMods)) { + allEqual = false; + break; + } + } + } + assert accessMods != null; + + if (allEqual) { + for (Modifier m : allAccessMods) { + if (accessMods.contains(m)) { + content.add(Keywords.of(m)); + break; + } + } + } else { + APIMap<Content> diffs = APIMap.of(); + for (Map.Entry<API, ? extends Element> entry : eMap.entrySet()) { + API api = entry.getKey(); + Element e = entry.getValue(); + Set<Modifier> eMods = e.getModifiers(); + Modifier m = null; + for (Modifier am : allAccessMods) { + if (eMods.contains(am)) { + m = am; + break; + } + } + diffs.put(api, (m != null) ? Keywords.of(m) : Entity.NBSP); + } + content.add(new DiffBuilder().build(diffs)); + } + } + + void addModifier(List<Content> content, Modifier m, APIMap<? extends Element> eMap) { + boolean allEqual = true; + Boolean present = null; + for (Element e : eMap.values()) { + boolean b = e.getModifiers().contains(m); + if (present == null) { + present = b; + } else { + if (b != present) { + allEqual = false; + break; + } + } + } + assert present != null; + + if (allEqual) { + if (present) { + if (!content.isEmpty()) { + content.add(Text.SPACE); + } + content.add(Keywords.of(m)); + } + } else { + if (!content.isEmpty()) { + content.add(Text.SPACE); + } + content.add(new DiffBuilder().build(eMap, e -> + (e.getModifiers().contains(m)) ? Keywords.of(m) : Entity.NBSP)); + } + } + + private Content wrap(List<Content> content) { + return HtmlTree.SPAN(content).setClass("modifiers"); + } + + } + + private class ExecutableBuilder { + private final Position ePos; + + ExecutableBuilder(ExecutableElementKey eKey) { + this.ePos = Position.of(eKey); + } + + ExecutableBuilder(Position ePos) { + this.ePos = ePos; + } + + Content build() { + Content eq = getResultGlyph(ePos); + + // TODO: could move to final field + APIMap<? extends Element> eMap = getElementMap(ePos); + // by design, they should all have the same ElementKind, + // so pick the first + Element e = eMap.values().iterator().next(); + ElementKind eKind = e.getKind(); + + List<Content> contents = new ArrayList<>(); + contents.addAll(buildAnnotations(ePos)); + contents.add(new ModifiersBuilder().build(ePos, eMap)); + contents.add(Text.SPACE); + + Content typarams = new TypeParameterBuilder().build(ePos, eMap); + if (typarams != Content.empty) { + contents.add(typarams); + contents.add(Text.SPACE); + } + + switch (eKind) { + case CONSTRUCTOR -> + // no return type + contents.add(Text.of(pageKey.name.toString())); + + case METHOD -> { + contents.add(buildType(ePos, + ((ExecutableElement) e)::getReturnType)); + contents.add(Text.SPACE); + contents.add(Text.of(e.getSimpleName())); + } + + default -> throw new IllegalStateException(eKind.toString()); + } + + contents.add(Text.of("(")); + Content parameters = buildParameters(ePos); + if (parameters != Content.empty) { + contents.add(parameters); + } + contents.add(Text.of(")")); + + Content defaultValue = buildDefaultValue(ePos); + if (defaultValue != Content.empty) { + contents.add(Text.SPACE); + contents.add(Keywords.DEFAULT); + contents.add(Text.SPACE); + contents.add(defaultValue); + } + + Content throwsTypes = buildThrows(ePos); + if (throwsTypes != Content.empty) { + contents.add(Text.SPACE); + contents.add(throwsTypes); + } + + HtmlTree signature = HtmlTree.DIV(contents).setClass("signature"); + Content docComments = buildDocComments(ePos); + Content apiDescriptions = buildAPIDescriptions(ePos); + + return HtmlTree.DIV(eq, buildMissingInfo(ePos), buildNotes(ePos), + signature, docComments, apiDescriptions) + .setClass("element") + .set(HtmlAttr.ID, links.getId(ePos)); + } + + private Content buildParameters(Position ePos) { + APIMap<? extends Element> eMap = getElementMap(ePos); + + boolean noReceiverAnnos = eMap.entrySet().stream() + .map(e -> { + API api = e.getKey(); + ExecutableElement ee = (ExecutableElement) e.getValue(); + TypeMirror tm = ee.getReceiverType(); + // The following conditional expression is a workaround; + // The spec says that NoType should be returned instead of null. + return tm == null ? api.getTypes().getNoType(TypeKind.NONE) : tm; + }) + .map(TypeMirror::getAnnotationMirrors) + .allMatch(List::isEmpty); + + int parameterCount = eMap.values().stream() + .mapToInt(e -> ((ExecutableElement) e).getParameters().size()) + .max() + .orElse(0); + + if (noReceiverAnnos && parameterCount == 0) { + return Content.empty; + } + + List<Content> contents = new ArrayList<>(); + boolean needComma = false; + if (!noReceiverAnnos) { + contents.add(todo("receiver")); + needComma = true; + } + + if (parameterCount > 0) { + IntTable<VariableElement> pTable = new IntTable<>(); + eMap.forEach((api, e) -> pTable.put(api, ((ExecutableElement) e).getParameters())); + for (int i = 0; i < parameterCount; i++) { + Position pPos = ePos.parameter(i); + Content parameter = buildParameter(pPos, pTable.entries(i)); + if (needComma) { + contents.add(Text.of(", ")); + } + contents.add(parameter); + needComma = true; + } + } + return HtmlTree.SPAN(contents).setClass("parameters"); + } + + Content buildParameter(Position vPos, APIMap<VariableElement> vMap) { + List<Content> contents = new ArrayList<>(); + contents.addAll(buildAnnotations(vPos)); + + VariableElement ve = vMap.values().iterator().next(); + // no modifiers for parameters + contents.add(buildType(vPos, ve::asType)); + contents.add(Text.SPACE); + + Name vName = ve.getSimpleName(); + boolean sameName = vMap.values().stream() + .map(Element::getSimpleName) + .allMatch(n -> n.contentEquals(vName)); + if (sameName) { + contents.add(Text.of(vName)); + } else { + contents.add(new DiffBuilder().build(vMap, e -> Text.of(e.getSimpleName()))); + } + + return HtmlTree.SPAN(contents).setClass("parameter"); + } + + private Content buildThrows(Position ePos) { + APIMap<? extends Element> eMap = getElementMap(ePos); + boolean noThrows = eMap.values().stream() + .map(e -> ((ExecutableElement) e).getThrownTypes()) + .allMatch(List::isEmpty); + if (noThrows) { + return Content.empty; + } + + // TODO: use latest apiMaps + // build the map of all throws for the executable + Map<TypeMirrorKey, APIMap<TypeMirror>> allThrows = new TreeMap<>(); + for (Map.Entry<API, ? extends Element> entry : eMap.entrySet()) { + API api = entry.getKey(); + ExecutableElement ee = (ExecutableElement) entry.getValue(); + for (TypeMirror tm : ee.getThrownTypes()) { + allThrows.computeIfAbsent(TypeMirrorKey.of(tm), _t -> APIMap.of()).put(api, tm); + } + } + + // TODO: should this be a list? the mildly tricky part is adding the comma separator + Set<API> apis = eMap.keySet(); + List<Content> contents = new ArrayList<>(); + boolean needComma = false; + for (Map.Entry<TypeMirrorKey, APIMap<TypeMirror>> entry : allThrows.entrySet()) { + TypeMirrorKey tmk = entry.getKey(); + APIMap<TypeMirror> tMap = entry.getValue(); + if (needComma) { + contents.add(Text.of(", ")); + } else { + needComma = true; + } + // TODO: use factory method + Position pos = ePos.exception(tmk); + contents.add(buildType(pos, apis, () -> tMap.values().iterator().next())); + } + return HtmlTree.SPAN(Keywords.THROWS, Text.SPACE).setClass("throws").add(contents); + } + + private Content buildDefaultValue(Position ePos) { + APIMap<? extends Element> eMap = getElementMap(ePos); + boolean noDefaultValues = eMap.values().stream() + .map(e -> ((ExecutableElement) e).getDefaultValue()) + .allMatch(Objects::isNull); + if (noDefaultValues) { + return Content.empty; + } + List<Content> contents = new ArrayList<>(); + RelativePosition<?> dvPos = ePos.defaultValue(); + contents.addAll(new AnnotationValueBuilder().buildAnnoValue(dvPos)); + return HtmlTree.SPAN(contents).setClass("defaultValue"); + } + } + + private class VariableBuilder { + + private final VariableElementKey vKey; + private final Position vPos; + + VariableBuilder(VariableElementKey vKey) { + this.vKey = vKey; + vPos = Position.of(vKey); + } + + private Content build() { + Content eq = getResultGlyph(vPos); + + APIMap<? extends Element> vMap = getElementMap(vKey); + // by design, they should all have the same ElementKind, + // so pick the first + VariableElement ve = (VariableElement) vMap.values().iterator().next(); + ElementKind eKind = ve.getKind(); + + List<Content> contents = new ArrayList<>(); + contents.addAll(buildAnnotations(vPos)); + + switch (eKind) { + case ENUM_CONSTANT -> + // no modifiers type needed + contents.add(Text.of(ve.getSimpleName())); + + case FIELD -> { + contents.add(new ModifiersBuilder().build(vKey, vMap)); + contents.add(Text.SPACE); + contents.add(buildType(vPos, ve::asType)); + contents.add(Text.SPACE); + contents.add(Text.of(ve.getSimpleName())); + Content value = buildValue(vMap); + if (value != Content.empty) { + contents.add(Text.of(" = ")); + contents.add(value); + } + } + } + + HtmlTree signature = HtmlTree.DIV(contents).setClass("signature"); + Content docComments = buildDocComments(vPos); + Content apiDescriptions = buildAPIDescriptions(vPos); + + return HtmlTree.DIV(eq, buildMissingInfo(vPos), buildNotes(vKey), + signature, docComments, apiDescriptions) + .setClass("element") + .set(HtmlAttr.ID, links.getId(vKey)); + } + + private Content buildValue(APIMap<? extends Element> vMap) { + Object v = ((VariableElement) vMap.values().iterator().next()).getConstantValue(); + boolean allEqual = vMap.values().stream() + .map(e -> ((VariableElement) e).getConstantValue()) + .allMatch(v1 -> Objects.equals(v1, v)); + if (allEqual) { + return (v == null) ? Content.empty : buildValue(v); + } else { + APIMap<Content> values = APIMap.of(); + vMap.forEach((api, ve) -> values.put(api, buildValue(((VariableElement) ve).getConstantValue()))); + return new DiffBuilder().build(values); + } + } + + private Content buildValue(Object o) { + if (o == null) { + return Entity.NBSP; + } else if (o instanceof Number) { + return Text.of(String.valueOf(o)); + } else if (o instanceof Boolean) { + return Keywords.of((Boolean) o); + } else if (o instanceof Character) { + return Text.of("'" + escape(String.valueOf(o)) + "'"); + } else if (o instanceof String) { + return Text.of("\"" + escape(String.valueOf(o)) + "\""); + } else { + throw new IllegalArgumentException(o.getClass() + "(" + o + ")"); + } + } + + private String escape(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isSurrogate(c)) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + switch (c) { + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '\\': + sb.append("\\\\"); + break; + case '\'': + sb.append("\\'"); + break; + case '\"': + sb.append("\\\""); + break; + default: + if (Character.isISOControl(c)) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + } + return sb.toString(); + } + } + + + /** + * A builder for different instances of a type parameter in different instances of an API. + */ + protected class TypeParameterBuilder { + protected Content build(Position ePos, APIMap<? extends Element> eMap) { + int maxTypeParameters = eMap.values().stream() + .mapToInt(e -> ((Parameterizable) e).getTypeParameters().size()) + .max().orElse(0); + if (maxTypeParameters == 0) { + return Content.empty; + } + + // use elements from this list when no differences given + List<? extends TypeParameterElement> archetype = + ((Parameterizable) eMap.values().iterator().next()).getTypeParameters(); + + List<Content> contents = new ArrayList<>(); + contents.add(Text.of("<")); + boolean needComma = false; + for (int i = 0; i < maxTypeParameters; i++) { + if (needComma) { + contents.add(Text.of(", ")); + } else { + needComma = true; + } + + Position tpePos = ePos.typeParameter(i); + APIMap<? extends TypeParameterElement> typarams; + if ((typarams = differentTypeParameters.get(tpePos)) != null) { + contents.add(new DiffBuilder().build(eMap.keySet(), typarams, this::buildTypeParameter)); + } else { + contents.add(buildTypeParameter(archetype.get(i))); + } + } + contents.add(Text.of(">")); + return HtmlTree.SPAN(contents).setClass("typarams"); + } + + private Content buildTypeParameter(TypeParameterElement tpe) { + List<Content> contents = new ArrayList<>(); + // TODO: annotations + contents.add(Text.of(tpe.getSimpleName())); + // TODO: bounds + return HtmlTree.SPAN(contents); + } + } + + /** + * A builder for different instances of the record components in different instances of an API. + */ + protected class RecordComponentBuilder { + protected Content build(Position ePos, APIMap<? extends TypeElement> eMap) { + int maxComponents = eMap.values().stream() + .mapToInt(e -> getComponents(e).size()) + .max().orElse(0); + if (maxComponents == 0) { + return Content.empty; + } + + IntTable<Element> rcTable = new IntTable<>(); + eMap.forEach((api, e) -> rcTable.put(api, getComponents(e))); + List<Content> contents = new ArrayList<>(); + contents.add(Text.of("(")); + boolean needComma = false; + for (int i = 0; i < maxComponents; i++) { + Position rcPos = ePos.recordComponent(i); + Content component = buildComponent(rcPos, eMap.keySet(), rcTable.entries(i)); + if (needComma) { + contents.add(Text.of(", ")); + } + contents.add(component); + needComma = true; + } + contents.add(Text.of(")")); + return HtmlTree.SPAN(contents).setClass("components"); + } + + private Content buildComponent(Position rcPos, Set<API> apis, APIMap</*RecordComponent*/Element> rcMap) { + List<Content> contents = new ArrayList<>(); + contents.addAll(buildAnnotations(rcPos)); + + // no modifiers for record components + APIMap<TypeMirror> tMap = rcMap.map(Element::asType); + contents.add(buildType(rcPos, apis, tMap)); + contents.add(Text.SPACE); + + Name rcn = rcMap.values().iterator().next().getSimpleName(); + boolean sameName = rcMap.size() == apis.size() && rcMap.values().stream() + .map(Element::getSimpleName) + .allMatch(n -> n.contentEquals(rcn)); + if (sameName) { + contents.add(Text.of(rcn)); + } else { + contents.add(new DiffBuilder().build(apis, rcMap, e -> Text.of(e.getSimpleName()))); + } + + return HtmlTree.SPAN(contents).setClass("parameter"); + } + + private List<? extends Element> getComponents(TypeElement e) { + return elementExtras.getRecordComponents(e); + } + } + + /** + * A builder for different instances of the serialized form of a type element. + */ + private class SerializedFormBuilder { + + Content build() { + Position sfPos = Position.of(pageKey).serializedForm(); + Boolean equal = results.get(sfPos); + if (equal == null) { + return Content.empty; + } + + HtmlTree section = HtmlTree.SECTION(HtmlTree.H2(Text.of(msgs.getString("serial.serialized-form")))).setClass("serial-form"); + section.add(getResultGlyph(sfPos)).add(buildMissingInfo(sfPos)); + addSerialVersionUID(section); + addSerializedFields(section); + addSerializationMethods(section); + return section; + } + + private void addSerialVersionUID(HtmlTree section) { + Position uPos = Position.of(pageKey).serialVersionUID(); + APIMap<?> values = apiMaps.get(uPos); + + // TODO: weave in the text from serialized-form.html + + section.add(HtmlTree.H3(Text.of("serialVersionUID"))); + section.add(getResultGlyph(uPos)); + if (differentValues.containsKey(uPos)) { + APIMap<Content> alternatives = APIMap.of(); + values.forEach((api, v) -> alternatives.put(api, Text.of(String.valueOf(v)))); + section.add(new DiffBuilder().build(alternatives)); + } else { + section.add(Text.of(String.valueOf(values.values().iterator().next()))); + } + +// TODO: The serialVersion info appears in two places, which should provide the same value. +// It should appear in the source or class file, and it should appear in the serialized form, +// available via the SerializedFormDocs object + +// Content c = buildAPIDescriptions(uPos); +// if (c != Content.empty) { +// // TODO: show the check/cross/etc glyph if any description present +// // implies need for reporter.comparing(oPos, map) with a map of descriptions +// section.add(c); +// } + } + + private void addSerializedFields(HtmlTree tree) { + RelativePosition<?> oPos = Position.of(pageKey).serializationOverview(); + Content c = buildAPIDescriptions(oPos); + if (c != Content.empty) { + // TODO: show the check/cross/etc glyph if any overview present + // implies need for reporter.comparing(oPos, map) with a map of descriptions + HtmlTree section = HtmlTree.SECTION().setClass("enclosed"); + section.add(HtmlTree.H3(Text.of(msgs.getString("serial.serialization-overview")))); + section.add(c); + tree.add(section); + } + + Set<RelativePosition<?>> fields = results.keySet().stream() + .filter(Position::isRelative) + .map(p -> (RelativePosition<?>) p) + .filter(p -> p.kind == RelativePosition.Kind.SERIALIZED_FIELD) + .collect(Collectors.toCollection(() -> new TreeSet<>(Position.stringIndexComparator))); + + if (!fields.isEmpty()) { + HtmlTree section = HtmlTree.SECTION().setClass("enclosed"); + section.add(HtmlTree.H3(Text.of(msgs.getString("serial.serialized-fields")))); + HtmlTree ul = HtmlTree.UL(); + for (RelativePosition<?> pos : fields) { + @SuppressWarnings("unchecked") + RelativePosition<String> fPos = (RelativePosition<String>) pos; + HtmlTree li = HtmlTree.LI(buildSerializedField(fPos)); + ul.add(li); + } + section.add(ul); + tree.add(section); + } + } + + private Content buildSerializedField(RelativePosition<String> fPos) { + @SuppressWarnings("unchecked") + APIMap<SerializedForm.Field> fMap = (APIMap<SerializedForm.Field>) apiMaps.get(fPos); + Content glyph = getResultGlyph(fPos); + + Content type; + APIMap<? extends TypeMirror> types; + // differentTypes for a serializedField position is somewhat different than usual + // in that a type of NONE represents an unresolved type + // (Ideally, the representation would use ERROR instead of NONE, but that is not possible.) + if ((types = differentTypes.get(fPos)) != null) { + if (types.values().stream().allMatch(t -> t.getKind() != TypeKind.NONE)) { + // no unresolved types, use standard type builder + type = buildType(fPos, () -> types.values().iterator().next()); + } else { + // some unresolved types, use custom DiffBuilder + type = new DiffBuilder().build(fMap, this::buildFieldType); + } + } else { + // types all equal: build the archetype + SerializedForm.Field archetype = fMap.values().iterator().next(); + type = buildFieldType(archetype); + } + + Content signature = HtmlTree.DIV(type, Entity.NBSP, Text.of(fPos.index)).setClass("signature"); + Content docComments = buildDocComments(fPos); + Content descriptions = buildAPIDescriptions(fPos); + + return HtmlTree.DIV(glyph, buildMissingInfo(fPos), + signature, docComments, descriptions) + .setClass("element"); + } + + private Content buildFieldType(SerializedForm.Field f) { + TypeMirror t = f.getType(); + if (t.getKind() == TypeKind.NONE) { + return HtmlTree.SPAN(Text.of(f.getSignature())) + .setClass("unresolved") + .setTitle("name could not be resolved"); // TODO: L10N + } else { + return TypePageReporter.this.buildType(t); + } + } + + private void addSerializationMethods(HtmlTree tree) { + Set<RelativePosition<?>> methods = results.keySet().stream() + .filter(p -> p instanceof RelativePosition) + .map(p -> (RelativePosition<?>) p) + .filter(p -> p.kind == RelativePosition.Kind.SERIALIZATION_METHOD) + .collect(Collectors.toCollection(() -> new TreeSet<>(Position.stringIndexComparator))); + + if (!methods.isEmpty()) { + HtmlTree section = HtmlTree.SECTION().setClass("enclosed"); + section.add(HtmlTree.H3(Text.of(msgs.getString("serial.serialization-methods")))); + HtmlTree ul = HtmlTree.UL(); + for (Position pos : methods) { + HtmlTree li = HtmlTree.LI(buildSerializedMethod(pos)); + ul.add(li); + } + section.add(ul); + tree.add(section); + } + } + + private Content buildSerializedMethod(Position mPos) { + return new ExecutableBuilder(mPos).build(); + } + + } +} diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/package-info.java b/src/share/classes/jdk/codetools/apidiff/report/html/package-info.java new file mode 100644 index 0000000..2325454 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Classes used to generate an HTML report of the differences between + * a set of APIs. + */ + +/** + * Classes used to generate HTML reports of the differences + * in a series of API. + */ +package jdk.codetools.apidiff.report.html; diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/resources/apidiff.css b/src/share/classes/jdk/codetools/apidiff/report/html/resources/apidiff.css new file mode 100644 index 0000000..a15c748 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/resources/apidiff.css @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* Global/default settings. */ + +body { + font-family:'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size:14px; + margin: 0; + padding: 0; +} + +/* Headings. */ + +h1 { + font-size:20px; +} +h2 { + font-size:18px; +} +h3 { + font-size:16px; + font-style:italic; +} +h4 { + font-size:13px; +} +h5 { + font-size:12px; +} +h6 { + font-size:11px; +} + +/* Links. */ + +a:link, a:visited { + text-decoration:none; + color:#4A6782; +} +a[href]:hover, a[href]:focus { + text-decoration:none; + color:#bb7a2a; +} + +/* Header, footer and navigation. */ + +header > div, +footer > div { + padding: 1em; +} + +header > div.info, +footer > div.info { + font-size: smaller; +} + +.bar { + background-color:#4D7A97; + color:#FFFFFF; +} + +.bar nav ul { + display: inline; + margin: 0; + padding: 0; + list-style-type: none; +} + +.bar nav li { + display: inline; + margin: 0 1em 0 0; + text-transform: uppercase; +} + +.bar nav a, +.bar nav a:link, +.bar nav a:visited, +.bar nav a:active { + color:#FFFFFF; + text-decoration:none; +} + +.bar nav a:hover, +.bar nav a:focus { + color:#bb7a2a; +} + +.bar div.info { + float: right; +} + +/* Main. */ + +main { + margin: 1em; +} + + +/* Page heading: the <h1> and preceding text. */ + +div.pageHeading { + border-bottom: 1px solid black; + margin-bottom: 2em; +} + +.pageHeading .label { + font-weight: bold; +} + +.pageHeading h1 { + color:#2c4557; +} + +/* The signature block. */ + +div.signature { + margin-top: 5px; + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; +} + +.signature .keyword { + font-weight: bold; +} + +.signature div.superclass, +.signature div.superinterfaces, +.signature div.permitted-subclasses { + margin-left:2em; +} + +.signature span.diffs, .serial-form span.diffs { + display: inline-block; + background-color: lightgrey; + border: 1px solid grey; + border-radius: 5px; + margin: 2px; + padding: 4px 1px; + list-style: none; +} + +.signature span.diffs > span, .serial-form span.diffs > span { + display: inline; + background-color: white; + border: 1px solid grey; + border-radius: 5px; + margin: 0 1px; + padding: 2px; +} + +.signature span.unresolved { + color: gray; +} + +/* Elements. */ + +.doc-files span.diff, .element span.diff, .enclosed span.diff, .serial-form span.diff, +.doc-files span.same, .element span.same, .enclosed span.same, .serial-form span.same, +.doc-files span.single, .element span.single, .enclosed span.single, .serial-form span.single { + display: inline-block; + width: 2em; + margin-right: 0.5ex; + text-align: center; + font-weight: bold; +} + +.doc-files span.diff, .element span.diff, .enclosed span.diff, .serial-form span.diff { + background: #fdd; + color: #800; +} + +.doc-files span.same, .element span.same, .enclosed span.same, .serial-form span.same { + background: #dfd; + color: #080; +} + +.doc-files span.single, .element span.single, .enclosed span.single, .serial-form span.single { + background: #ddf; + color: #008; +} + +.doc-files span.missing, .element span.missing, .enclosed span.missing, .serial-form span.missing, +.element span.notes { + margin-right: 10px; + font-style: italic; +} + +.doc-files table.details { + margin: 10px 0; +} + +.doc-files table.details, +.doc-files table.details thead, +.doc-files table.details tbody, +.doc-files table.details tfoot { + border: 1px solid black; + border-collapse: collapse; +} + +.doc-files table.details caption { + padding-bottom: 2px; +} + +.doc-files table.details thead, +.doc-files table.details tfoot { + background-color: #ddd +} + +.doc-files table.details tbody tr:nth-child(even) { + background-color: #eee +} + +.doc-files table.details th, +.doc-files table.details td { + padding: 2px 5px; +} + +.doc-files table.details thead th, +.doc-files table.details tfoot th { + border: 1px solid black; +} + +.doc-files table.details td { + border-left: 1px solid black; + text-align: center; +} + +.doc-files table.details tbody th { + font-weight: normal; +} + +.doc-files table.details tbody th, +.doc-files table.details tfoot th { + text-align:left; +} + +/* Side-by-side diffs. */ + +div.sdiffs { + display: grid; + grid-template-columns: auto auto; + grid-column-gap: 10px; + margin: 2px 10px; + padding: 2px 2px; + border: 1px solid grey; +} + +.sdiffs div.sdiffs-ref { + grid-column: 1; + overflow-x: auto; +} + +.sdiffs div.sdiffs-mod { + grid-column: 2; + overflow-x: auto; +} + +.sdiffs div.sdiffs-title { + padding-left: 2em; + text-weight: bold; + background-color: #eee; + border-bottom: 1px solid grey; + margin-bottom: 5px; +} + +.sdiffs span.sdiffs-changed { + color: blue; +} + +span.sdiffs-lines-inserted, span.sdiffs-lines-changed, span.sdiffs-lines-deleted { + background-color: hsl(200,100%,95%); +} + +span.sdiffs-chars-inserted, span.sdiffs-chars-changed, span.sdiffs-chars-deleted { + background-color: hsl(200,100%,85%); +} + +/* HTML diffs */ + +div.hdiffs { + margin: 2px 10px; + padding: 2px 2px; + border: 1px solid grey; +} + +div.hdiffs-title { + padding-left: 2em; + text-weight: bold; + background-color: #eee; + border-bottom: 1px solid grey; + margin-bottom: 5px; +} + +.hdiffs span.diff-html-added { background-color: #bfb } +.hdiffs span.diff-html-changed { background-color: #ffb; } +.hdiffs span.diff-html-removed { background-color: #fbb; } + +/* No diffs */ + +div.xdiffs { + margin: 2px 10px; + padding: 2px 2px; + border: 1px solid grey; +} + +div.xdiffs-title { + padding-left: 2em; + text-weight: bold; + background-color: #eee; + border-bottom: 1px solid grey; + margin-bottom: 5px; +} + +/* Tooltip container */ +.hdiffs .diff-html-changed { + position: relative; +} + +/* Tooltip text */ +.hdiffs .diff-html-changed .hdiffs-tooltip { + display:block; + visibility: hidden; + width: 300px; + background-color: #fafad2; + padding: 5px 10px; + border: 1px solid black; + text-align: left; + border-radius: 6px; + + color: black; + font-size: small; + font-style-normal; + font-weight: normal; + white-space: normal; + + /* Position the tooltip text */ + position: absolute; + z-index: 1; + top: 125%; + left: 50%; + margin-left: -150px; + + /* Fade in tooltip */ + opacity: 0; + transition: opacity 0.3s; +} + +/* Tooltip arrow */ +.hdiffs .diff-html-changed .hdiffs-tooltip::after { + content: " "; + position: absolute; + bottom: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent black transparent; +} + +/* Show the tooltip text when you mouse over the tooltip container */ +.hdiffs .diff-html-changed:hover .hdiffs-tooltip { + visibility: visible; + opacity: 1; +} + +.hdiffs span.hdiffs-tip-ul { display:block; list-style: outside } +.hdiffs span.hdiffs-tip-li { display:list-item; margin-left: 1em; padding-left: 0.1em; } + +/* The summary table */ + +section.summary { + border-top: 1px solid black; + margin-top: 2em; +} + +table.summary, +table.summary thead, +table.summary tbody, +table.summary tfoot { + border: 1px solid black; + border-collapse: collapse; +} + +table.summary caption { + padding-bottom: 2px; +} + +table.summary thead, +table.summary tfoot { + background-color: #ddd +} + +table.summary tbody tr:nth-child(even) { + background-color: #eee +} + +table.summary th, +table.summary td { + padding: 2px 5px; +} + +table.summary thead th, +table.summary tfoot th { + border: 1px solid black; +} + +table.summary thead th.allZero { + color: gray; +} + +table.summary td { + border-left: 1px solid black; + text-align: center; +} + +table.summary tbody th { + font-weight: normal; +} + +table.summary tbody th, +table.summary tfoot th { + text-align:left; +} + +/* The debug section. */ + +.todo { + display: inline-block; + border: 1px solid blue; + border-radius: 5px; + padding: 1px 2px; +} + + section.debug { + border: 1px solid grey; + border-radius: 5px; + border-color: blue; + margin: 2em 1em; + padding: 2px 5px; + } + + .debug table { + border: 1px solid grey; + border-collapse: collapse; + } + + .debug td, .debug th { + padding 2px 5px; + border-left: 1px solid grey; + border-top: 1px solid lightgrey; + } + + diff --git a/src/share/classes/jdk/codetools/apidiff/report/html/resources/report.properties b/src/share/classes/jdk/codetools/apidiff/report/html/resources/report.properties new file mode 100644 index 0000000..cf2aba0 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/html/resources/report.properties @@ -0,0 +1,185 @@ +# Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. + +# Content to be put in the generated report + +docfile.details.caption.api=\ + API File Details for {0} + +docfile.details.caption.source=\ + Source File Details for {0} + +docfile.details.th.api=\ + API + +docfile.details.th.size=\ + File Size + +docfile.details.th.checksum=\ + Checksum + +element.onlyInMissingIn=\ + Only in: {0}; not in: {1}. + +heading.annotation-type=\ + Annotation Type + +heading.class=\ + Class + +heading.constructors=\ + Constructors + +heading.elements=\ + Elements + +heading.enum=\ + Enum + +heading.enum-constants=\ + Enum Constants + +heading.exports=\ + Exports + +heading.fields=\ + Fields + +heading.file=\ + File + +heading.files=\ + Additional Files + +heading.interface=\ + Interface + +heading.methods=\ + Methods + +heading.mixed=\ + (Various) + +heading.module=\ + Module + +heading.modules=\ + Modules + +heading.nested-types=\ + Nested Types + +heading.opens=\ + Opens + +heading.package=\ + Package + +heading.packages=\ + Packages + +heading.provides=\ + Provides + +heading.record=\ + Record + +heading.requires=\ + Requires + +heading.types=\ + Types + +heading.unknown=\ + Unknown + +heading.uses=\ + Uses + +htmldiffs.comparing=\ + Comparing {0} and {1} + +htmldiffs.not-in-only-in=\ + Not in {0}; only in {1} + +htmldiffs.only-in-not-in=\ + Only in {0}; not i {1} + +notes.heading=\ + Notes + +notes.prefix=\ + Notes: + +overview.heading.apis=\ + APIs + +overview.name=\ + Overview + +overview.title=\ + Overview + +serial.serialization-methods=\ + Serialization Methods + +serial.serialization-overview=\ + Serialization Overview + +serial.serialized-fields=\ + Serialized Fields + +serial.serialized-form=\ + Serialized Form + +summary.heading=\ + Summary + +summary.caption=\ + Differences + +summary.elements=\ + Elements + +summary.comments=\ + Comments + +summary.descriptions=\ + Descriptions + +summary.changed=\ + Changed + +summary.added=\ + Added + +summary.no-differences=\ + No differences. + +summary.removed=\ + Removed + +summary.total=\ + Total + + diff --git a/src/share/classes/jdk/codetools/apidiff/report/package-info.java b/src/share/classes/jdk/codetools/apidiff/report/package-info.java new file mode 100644 index 0000000..eeb254d --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/report/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018,2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Classes for reporting the results of comparing elements in two or more APIs. + */ +package jdk.codetools.apidiff.report; diff --git a/src/share/classes/jdk/codetools/apidiff/resources/help.properties b/src/share/classes/jdk/codetools/apidiff/resources/help.properties new file mode 100644 index 0000000..98de44c --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/resources/help.properties @@ -0,0 +1,153 @@ +# Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. + +# Resources for command-line help + +opt.arg.access=public|protected|package|private +opt.arg.api=<name> +opt.arg.api-directory=<api-directory> +opt.arg.boolean=true|yes|on|false|no|off +opt.arg.class-path=<class-path> +opt.arg.source-path=<source-path> +opt.arg.directory=<directory> +opt.arg.file=<file> +opt.arg.file-or-directory=<file-or-directory> +opt.arg.html-text=<html> +opt.arg.info-text=<place>=<html> +opt.arg.jdk-build=<jdk-build> +opt.arg.jdk-docs=<name> +opt.arg.jdk-version=<jdk-version> +opt.arg.jdk-home=<jdk-install-dir> +opt.arg.module-path=<module-path> +opt.arg.module-source-path=<module-source-path> +opt.arg.patch-module=<module-name>=<path> +opt.arg.pattern=<pattern> +opt.arg.plain-text=<text> +opt.arg.verbose=<flag>[,<flag>]* + +opt.desc.access=\ + Specifies the access for elements to be compared. + +opt.desc.api=\ + Provides a name for an API to be compared and set the current API\n\ + for API-specific options that immediately follow on the command line. + +opt.desc.api-directory=\ + Specifies the location of the documentation generated by javadoc for an API. + +opt.desc.class-path=\ + Specifies the class path for an API. + +opt.desc.compare-api-descriptions=\ + Compares API descriptions (in files generated by javadoc) for elements found in each API. + +opt.desc.compare-api-descriptions-as-text=\ + Compares the HTML for the API descriptions (in files generated by javadoc) as plain text\n\ + for elements found in each API. + +opt.desc.compare-doc-comments=\ + Compares documentation comments (in source files) for elements found in each API. + +opt.desc.description=\ + Provides a short description to be included in the report. + +opt.desc.enable-preview=\ + Enables preview features for an API. + +opt.desc.exclude=\ + Specifies a pattern to match elements to be excluded from the comparison. + +opt.desc.extra-stylesheet=\ + Specifies an additional stylesheet to use in the generated report. + +opt.desc.help=\ + Shows this information. + +opt.desc.include=\ + Specifies a pattern to match elements to be included in the comparison. + +opt.desc.info-text=\ + Specifies text to be included at different positions in the report.\n\ + Positions include: 'top', 'header', 'footer', 'bottom'. + +opt.desc.jdk-build=\ + Specifies the location of a JDK build from which to infer detailed\n\ + options for an API. The directory should contain a "configuration" as\n\ + understood by the ''configure'' and ''make'' commands used to build JDK. + +opt.desc.jdk-docs=\ + Specifies the name of the docs bundle to use for an API,\n\ + in conjunction with the --jdk-build option. + +opt.desc.label=\ + Specifies a short string to identify an API in reports. + +opt.desc.main-stylesheet=\ + Specifies a alternative primary stylesheet to use in the generated report,\n\ + instead of the system default + +opt.desc.module-path=\ + Specifies the module path for an API. + +opt.desc.module-source-path=\ + Specifies the module source path for an API. + +opt.desc.notes=\ + Specifies a file providing notes to be added into the report. + +opt.desc.output-directory=\ + Specifies the directory in which to write the report. + +opt.desc.patch-module=\ + Specifies the path to patch a module for an API. + +opt.desc.release=\ + Specifies the Java SE release to be used for the platform classes for an API. + +opt.desc.resource-files=\ + Specifies resource files to be copied from an API directory + +opt.desc.source=\ + Specifies the version of the platform for an API. + +opt.desc.source-path=\ + Specifies the source path for an API. + +opt.desc.system=\ + Specifies the system classes for an API. + +opt.desc.title=\ + Specifies a title for the report. + +opt.desc.verbose=\ + Specifies level of verbose output.\n\ + Supported flags are all, none, or one of the following,\n\ + optionally preceded by -: {0} + +opt.desc.version=\ + Shows the version of the tool. + +options.usage.header=\ + Usage:\n\ + \ apidiff <options>\n\ + where <options> include: diff --git a/src/share/classes/jdk/codetools/apidiff/resources/log.properties b/src/share/classes/jdk/codetools/apidiff/resources/log.properties new file mode 100644 index 0000000..7912fc1 --- /dev/null +++ b/src/share/classes/jdk/codetools/apidiff/resources/log.properties @@ -0,0 +1,242 @@ +# Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. + +htmldiffs.err.exception-in-diff=\ + internal exception occurred while determining differences: {0} + +htmldiffs.warn.unknown-attribute-name=\ + unknown attribute ''{1}'' in ''{0}'' element + +htmldiffs.warn.unknown-tag-name=\ + unknown tag ''{0}'' + +jdkbuild.ioerror-finding-docs=\ + Error while looking for docs: {0} + +jdkbuild.err.no-docs=\ + No directories matching ''*docs*'' found in {0} + +jdkbuild.err.multiple-docs=\ + Multiple docs directories found in {0}: {1}\n\ + Use --jdk-docs NAME to specify the one to use + +jdkbuild.err.cannot-find-docs=\ + Cannot find specified docs ({0}) in {1} + +jdkbuild.err.error-reading-release-file=\ + Error reading JDK release file {0}: {1} + +jdkbuild.err.error-reading-src.zip=\ + Error reading src.zip file {0}: {1} + +log.errors = {0,choice,1<{0} error|{0} errors} +log.warnings = {0,choice,1<{0} warning|{0} warnings} + +log.err-prefix = Error: +log.note-prefix = Note: +log.warn-prefix = Warning: + +logReport.comparing=\ + Comparing: {0} + +logReport.completed=\ + Completed: {0}: {1,choice,0#different|0<equal} + +logReport.different-annotations=\ + Different annotations for {0} + +logReport.different-annotation-values=\ + Different annotation values for {0} + +logReport.different-api-descriptions=\ + Different API descriptions for {0} + +logReport.different-directives=\ + Different directives for {0} + +logReport.different-doc-files=\ + Different files for {0} + +logReport.different-kinds=\ + Different kinds for {0} + +logReport.different-names=\ + Different names for {0} + +logReport.different-modifiers=\ + Different modifiers for {0} + +logReport.different-raw-doc-comments=\ + Different raw doc comments for {0} + +logReport.different-superinterfaces=\ + Different superinterfaces for {0} + +logReport.different-permitted-subclasses=\ + Different permitted subclasses for {0} + +logReport.different-thrown-types=\ + Different thrown types for {0} + +logReport.different-types=\ + Different types for {0} + +logReport.different-type-parameters=\ + Different type parameters for {0} + +logReport.different-values=\ + Different values for {0} + +logReport.finished=\ + Completed: {0,choice,0#different|0<equal} + +logReport.item-not-found=\ + Item not found in API ''{0}'': {1} + +main.elapsed = Time: {0,choice,0#|0<{0,number}h }{1,choice,0#|0<{1,number}m }{2,number}s + +main.err.bad-@file=\ + Error reading @-files: {0} + +main.err.cant-create-output-directory=\ + Cannot create output directory: {0} + +main.err.cant-read-notes=\ + Cannot read notes file {0}: {1} + +notes.err.bad-line=\ + bad line: {0} + +notes.err.bad-signature=\ + bad signature: {0} + +notes.err.bad-uri=\ + bad uri: {0} + +notes.err.no-current-uri=\ + no current URI and description + +options.did-you-mean=\ + Did you mean: {0} + +options.did-you-mean-one-of=\ + Did you mean one of: {0} + +options.for-more-details-see-usage=\ + For more details on available options, use --help + +options.err.bad-access=\ + bad value for option: {0}; should be one of public, protected, package or private + +options.err.bad.argument=\ + bad argument: ''{0)'' + +options.err.bad-file=\ + bad filename: {0} + +options.err.bad-jdk-build-dir=\ + bad value for JDK build directory for API ''{0}'': {1}\n\ + (expected to find marker file spec.gmk) + +options.err.bad-module-name=\ + bad module name in pattern: {0} + +options.err.bad-package-name=\ + bad package name in pattern: {0} + +options.err.bad-source=\ + bad value for source version: {0} + +options.err.compare-api-but-missing-dir=\ + --compare-api-descriptions specified, but the API directory is not specified for some APIs + +options.err.empty-module-name=\ + empty module name in pattern: {0} + +options.err.empty-package-name=\ + empty package name in pattern: {0} + +options.err.file-not-found=\ + file not found: {0} + +options.err.invalid-arg-for-verbose=\ + invalid argument for --verbose: ''{0}'' + +options.err.invalid-boolean=\ + invalid value; must be one of: true, yes, on, false, no, off + +options.err.invalid.info.text=\ + invalid argument for --info-text + +options.err.invalid-info-text-kind=\ + invalid kind of --info-text: ''{0}'' + +options.err.missing-module-name=\ + no module name in pattern: {0} + +options.err.missing-value-for-option=\ + no value given for option {0} + +options.err.no-api-for-option=\ + current API not set for API-specific option: {0} + +options.err.no-images-in-jdk-build-dir=\ + no images directory in JDK build directory for API ''{0}'': {1} + +options.err.no-include-options=\ + no modules or packages specified with -include + +options.err.resource-file-not-found=\ + resource file not found: {0} + +options.err.resource-file-not-found-in-api-dirs=\ + resource file not found in any api directory: {0} + +options.err.unexpected-module-name=\ + unexpected module name in pattern: {0} + +options.err.unexpected-value-for-option=\ + unexpected value for option {0}: ''{1}'' + +options.err.unknown-option=\ + unknown option: ''{0}'' + +report.err.cant-create-directory=\ + cannot create directory {0}: {1} + +report.err.error-finding-resource-files=\ + error finding resource files in {0}: {1} + +report.err.error-writing-file=\ + error writing file {0}: {1} + +version.msg.info=\ + {0}, version {1} {2} {3}\n\ + Installed in {4}\n\ + Running on platform version {5} from {6}.\n\ + Built with {7} on {8}. + +version.msg.unknown=\ + unknown + + diff --git a/src/share/doc/README b/src/share/doc/README new file mode 100644 index 0000000..16203f5 --- /dev/null +++ b/src/share/doc/README @@ -0,0 +1,34 @@ +API Comparison Utility: apidiff + + +Introduction + +... + +Release Notes. + +... + + +System Requirements + +The following sections provide the recommended system requirements for running +apidiff. + +- Java platform + A platform equivalent to JDK 17 or later is required. + + + +Files and Directories + +Name Description +README This file +COPYRIGHT Copyright information +LICENSE License file +doc/ Documentation files +legal/ Copyright and license files +lib/ Directory containing the JAR files needed to run apidiff +bin/ Miscellaneous utility script for use on Linux, macOS, Solaris + and for Cygwin on Microsoft Windows platforms + diff --git a/src/share/doc/apidiff.css b/src/share/doc/apidiff.css new file mode 100644 index 0000000..ebd7ebf --- /dev/null +++ b/src/share/doc/apidiff.css @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +body { + margin: 2em 2em; + font-family: DejaVu Sans, Bitstream Vera Sans, Luxi Sans, Verdana, Arial, Helvetica; + font-size: 10pt; + line-height: 1.4; +} + +pre, code, tt { + font-family: DejaVu Sans Mono, Bitstream Vera Sans Mono, Luxi Mono, + Courier New, monospace; +} + +blockquote { + margin: 1.5ex 0em 1.5ex 2em; +} + +p { + padding: 0pt; + margin: 1ex 0em; +} + +p:first-child, pre:first-child { margin-top: 0pt; } + +h1 { + font-weight: bold; + padding: 0pt; + margin: 2ex .5ex 1ex 0pt; +} + +h1:first-child, h2:first-child { + margin-top: 0ex; +} + +h2 { + font-weight: bold; + padding: 0pt; + margin: 2ex 0pt 1ex 0pt; +} + +h3 { + font-weight: bold; + padding: 0pt; + margin: 1.5ex 0pt 1ex 0pt; +} + +h4 { + font-weight: bold; + padding: 0pt; + margin: 1.5ex 0pt 1ex 0pt; +} + +a:link { + color: #4A6782; +} + +a:visited { + color: #666666; +} + +a[href]:hover { + color: #e76f00; +} + +a img { + border-width: 0px; +} + +img { + background: white; +} + +table { + border-collapse: collapse; + margin-left: 15px; + margin-right: 15px; +} + +th, td { + padding: 3px; + vertical-align: top; +} + +table, th, td { + border: 1px solid black; +} + +caption { + text-align: left; + font-style: italic; + text-indent: 15px; + margin-bottom:10px; +} + +tr:nth-child(even), tr:nth-child(even) th[scope=row] { + background: #E3E3E3; +} + +tr:nth-child(odd), tr:nth-child(odd) th[scope=row] { + background: #FFF; +} + +th { + background: #DDF; +} + +table.centered { + margin-left: auto; + margin-right: auto; +} +table.centered td { + text-align: left; +} +.centered { + text-align: center; +} + +li > p + ul { + margin-top: -1ex; + margin-bottom: 1ex; +} \ No newline at end of file diff --git a/src/share/doc/apidiff.md b/src/share/doc/apidiff.md new file mode 100644 index 0000000..c675498 --- /dev/null +++ b/src/share/doc/apidiff.md @@ -0,0 +1,481 @@ +--- +# +# Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +title: 'APIDIFF(1) 0.1 | CodeTools' +date: 2021 +lang: en +--- + +<b style="color:red">WORK IN PROGRESS</b> + +## Name + +apidiff - compare different versions of an API + +## Synopsis + + +`apidiff` \[*options*\] + +*options* +: Command-line options + +## Description + +The `apidiff` command reads source, class and HTML files that provide different versions +of an API, compares corresponding files in the different versions and writes out +an HTML report. The comparison includes: + +* structural changes, such as whether an element was added, changed or removed, + using information in source or class files +* documentation contained in documentation comments, + using information in source files +* generated API documentation, + using information found in documentation generated by the `javadoc` tool + +## Options + +`apidiff` provides different groups of options, to specify +the [APIs](#the-apis) and +the [elements in those APIs](#the-elements-to-be-compared) to be compared, +[output options](#output-options) for the report to be generated, and +[other options](#other-options). + +### The APIs + +To specify each of the APIs to be compared, use the `--api` _name_ option +followed by a series of options that apply to that API. Most of the options are the +same as the corresponding _javac_ option; see the _javac_ documentation for more details +about those options. + +<a id="option-api">`--api` _name_</a> +: Specifies a name for the API and sets the [current API](#the-current-api) for + use by the API-specific options that immediately follow this option. + The option, and the API-specific options that follow, should be provided for each API + to be compared. + The order in which the `--api` options first appear on the command line determines + the order in which any differences are reported. It is recommended to specify the + options in chronological order, from the oldest version of the API to the most recent + version of the API. + +<a id="option-api-directory">`--api-directory` *directory*</a> +: Specifies where to find the generated API documentation for the [current API](#the-current-api). + If given, it should be the top-level directory of the documentation generated by + the `javadoc` command for the API. + +<a id="option-class-path">`--class-path` *path*, `-classpath` *path*, or `-cp` *path*</a> +: Specifies where to find files for the class path for the [current API](#the-current-api). + +<a id="option-enable-preview">`--enable-preview`</a> +: Enables preview language features for the [current API](#the-current-api). + +<a id="option-jdk-build">`--jdk-build` *directory*</a> +: Specifies a directory containing a JDK build from which to infer values for + various options. For example, *repository*`/build/`*configuration-name* + +<a id="option-label">`--label` *text*</a> +: Specifies a short plain-text label for the API, to be included in the generated + reports. For example, the full version string for the instance of the API being compared. + +<a id="option-module-path">`--module-path`</a> *path* or `-p` *path*</a> +: Specifies where to find application modules for the [current API](#the-current-api). + +<a id="option-module-source-path">`--module-source-path` *module-source-path*</a> +: Specifies where to find source files for the [current API](#the-current-api) when comparing code + in multiple modules. + +<a id="option-patch-module">`--patch-module` *module*`=`*path*</a> +: Overrides or augments a module in the [current API](#the-current-api) with classes and resources + in JAR files or directories. + +<a id="option-release">`--release` *release*</a> +: Specifies the release version for the [current API](#the-current-api) for any source and class + files that may be read. + +<a id="option-source">`--source` *release* or `-source` *release*</a> +: Specifies the source version for the [current API](#the-current-api) for any source files that + may be read. + +<a id="option-source-path">`--source-path` *path* or `-sourcepath` *path*</a> +: Specifies where to find source files for the [current API](#the-current-api). + Note that, unlike `javac`, you cannot use this option to specify where to find + the source code for a single module. Use the `--module-source-path` option instead, + possibly using the _module-specific form_ of that option. + +<a id="option-system">`--system` *jdk* \| `none`</a> +: Overrides the location of system modules for the [current API](#the-current-api). + +By convention, the APIs should be defined on the command in chronological order: oldest first, +newest last. When comparing documentation comments or API descriptions, the APIs will be compared +pairwise, with each of the older instances being compared against the newest instance. + +### The Elements to be Compared + +<a id="option-access">`--access` `public|protected|package|private`</a> +: Specifies the access of the declarations to be compared. The default is `protected`. + + * `public`: public declarations only + * `protected`: public and protected declarations + * `package`: public, protected and package-private declarations + * `private`: all declarations + + Note: this option does not apply to the parts of the serialized form + of a serializable class, even if those parts are provided by private + methods and fields. + +<a id="option-exclude">`--exclude` *pattern*</a> +: Specifies the patterns for modules or packages to be excluded from the + comparison. + +<a id="option-include">`--include` *pattern*</a> +: Specifies the patterns for modules or packages to be included from the + comparison. + +<a id="option-compare-api-descriptions">`--compare-api-descriptions` *boolean*</a> +: Specifies that the API descriptions (as generated by javadoc) should be compared + for each element being compared. + The option defaults to `true` if documentation comments are not to be compared + and if an API directory is given for each instance of the API to be + compared. (See `--compare-doc-comments`). + When the option is enabled, either explicitly or by default, + the API directory must be specified for each instance of the API + to be compared, and set to the location of the files generated by `javadoc` + that corresponds to the source and class files being compared. + The API directory for an API can be specified explicitly, with the `--api-directory` + option, or indirectly, with the `--jdk-build` and `--jdk-docs` options. + +<a id="option-compare-api-descriptions-as-text">`--compare-api-descriptions-as-text` *boolean*</a> +: Specifies that the HTML for the API descriptions (as generated by javadoc) + should be compared as plain text for each element being compared. If the argument is `true`, + this option also implies `--compare-api-descriptions true`. + +<a id="option-compare-doc-comments">`--compare-doc-comments` *boolean*</a> +: Specifies that the documentation comments should be compared for each element + being compared. This implies that the source files should be available for each + instance of the API being compared; if they are not available, the documentation + comments will not be compared. + The option defaults to `false` if the API descriptions are to be compared, + and `true` otherwise. (See `--compare-api-descriptions`). + +<a id="option-jdk-docs">`--jdk-docs` *name*</a> +: In conjunction with the `--jdk-build` option, specifies the name of the + documentation bundle to use when more than one is available. + For example, `docs`, `javase-docs`, `reference-docs`. + +### Output Options + +<a id="option-output-directory">`--output-directory` _directory_ or `-d` _directory_</a> +: Specifies the directory in which to write the report about the comparison. + The directory will be created if it does not already exist. + +<a id="option-title">`--title` _text_</a> +: Specifies a title for the report, to be used in each generated page. + +<a id="option-description">`--description` _html_</a> +: Specifies a short description for the report, to be used on the top level summary page. + +<a id="option-info-text">`--info-text` _name_`=`_html_</a> +: Specifies information to be included in different positions on each page. + _name_ can be one of: + + * `top`: at the top of each page; this may be used to indicate the status + of the pages. + * `header`: in the header bar on each page; if not specified, a default value + is generated, based on the names of the APIs being compared. + * `footer`: in the footer bar on each page; if not specified, a default value + is generated, based on the names of the APIs being compared. + * `bottom`: at the bottom of each page; this may be used to indicate + copyright and license information, + +<a id="option-notes">`--notes` _file_</a> +: Specifies a file containing [notes] to be added for various elements. + +<a id="option-main-stylesheet">`--main-stylesheet` _file_</a> +: Specifies an alternate stylesheet to use in the generated report + instead of the system default. + +<a id="option-extra-stylesheet">`--extra-stylesheet` _file_</a> +: Specifies an additional stylesheet to use in the generated report. + <p class="note">This option may be useful when comparing HTML documentation that + contains references to custom styles.</p> + +<a id="option-resource-files">`--resource-files` _file-or-directory_</a> +: Specifies resource files to be copied from one or more API directories. + <p class="note">This option may be useful when comparing HTML documentation that + depend on some non-HTML resource files.</p> + +### Other Options + +<a id="option-help">`--help`, `-help`, `-h`, `-?`</a> +: Displays command-line help. + +<a id="option-version">`--version`, `-v`</a> +: Displays the version of the tool. + +<a id="option-verbose">`--verbose` _flag_[`,`_flag_]*</a> +: Specifies the kinds of verbose output. _flag_ may be one of + `all`, `none`, or one of the following, optionally preceded by `-`: + `module`, `package`, `type`, `time`. + +<a id="option-at">`@`*filename*</a> +: Reads options from a file. To shorten or simplify the `apidiff` command, you can specify + one or more files that contain arguments for the `apidiff` command. This lets you to create + `apidiff` commands of any length on any operating system. + + +## The Current API + +The current API is the API specified by the most recent `--api` option on the command line, +and is the API for which any API-specific options will apply. + +The "current API" is cancelled when any option is given that is not specific to +any one API. Additional options for an API can be given by repeating the `--api` +option to set the API as the current API again. + + +## Patterns + +Patterns provide a way to specify groups of similarly-named modules, +packages and types to be included or excluded from the comparison. + +A pattern consists of a _module-part_ and/or a _type_part_. + +_module-part_: +: + | _qualified-identifier_ `/` + | _qualified-identifier_.* `/` + +_type-part_: +: + | `**` + | _qualified-identifier_ + | _qualified-identifier_`.*` + | _qualified-identifier_`.**` + +A _module-part_ that is just a qualified identifier matches the +named module. +A _module-part_ that ends in a wildcard matches all module names that begin +with the given qualified identifier. + +A _type-part_ that is just a qualified identifier matches the named type. +A _type-part_ that ends in a single `*` matches all types in the package +with the given qualified identifier. +A _type-part_ that ends in `**` matches all types in all packages that begin +with the given qualified identifier. +A _type-part_ of `**` can only be used in conjunction with a non-empty +module part. + +`apidiff` cannot compare a combination of types in named modules and types +in the unnamed module, and so either all patterns must include a module part, +or none must. + +## Notes + +A "notes" file is used to specify links to be injected into the generated report +for some elements. + +The file is a plain text file. Blank lines and lines beginning with `#` are ignored. +The remaining lines are interpreted as a series of blocks, each of which must start +with a line containing a URL and a short plain-text description, followed by +a series of lines, each containing a signature describing an element or set of elements. +For each block, a link based on the URL and description, will be added to any +element appearing in the generated report that matches any of the corresponding +signatures. + +The following signatures are supported: + +* _module_ +* _module_ `/*` +* [ _module_ `/` ] _package_ +* [ _module_ `/` ] _package_ `.*` +* [ _module_ `/` ] _package_ `.` _type_ +* [ _module_ `/` ] _package_ `.` _type_ `.*` +* [ _module_ `/` ] _package_ `.` _type_ `#` _field_ +* [ _module_ `/` ] _package_ `.` _type_ `#` `<init>` `(` _parameters_ `)` +* [ _module_ `/` ] _package_ `.` _type_ `#` _method_ `(` _parameters_ `)` + +Signatures should not contain any white-space characters. +Signatures ending in `/*` or `.*` match the specified item, and any enclosed elements down +to the level of a type element. +For methods and constructors, _parameters_ is a comma-separated list of parameter types, +omitting any type parameters, and using just the simple name of any declared types. + +For example, + +* `java.base/*` matches the `java.base` module, all packages in `java.base`, + and all types in those packages. +* `java.base/java.lang.*` matches the `java.lang` package in the `java.base` module, + and all types in that package. + Note that it does not include subpackages, such as `java.lang.reflect`, + or any types in those subpackages. +* `java.base/java.lang.String` just matches the type `java.lang.String`. +* `java.base/java.lang.String#equals(Object)` just matches the `equals(Object)`. + +## Configuring the APIs to be compared + +At a minimum, the source or class files must be provided for each instance +of the API to be compared. +If the API does not provide or is not part of a module, use the `--source-path` +and `--class-path` options. +If the API provides or is part of one or more modules, use the +`--source-path`, `--module-source-path`, `--module-path` and related options. + +If you want to compare the content of documentation comments, you must provide +source files for all the elements to be compared; any dependencies of those source +files can be specified as either source or class files. + +If you want to compare the API descriptions, you must provide the locations +of the directories generated by the `javadoc` tool for each of the instances +of the API being compared. This is often a directory whose path ends in `api`, +although that is not a requirement. + +### Configuring instances of JDK to be compared + +`apidiff` can be used to compare different instances of JDK, but that can be +tricky to set up, depending on the kind of comparison that is required. +That being said, there is a "convenience" option to specify a JDK build, +as generated by the standard JDK makefiles. + +* If you want to compare the files in two separate JDK builds, for each instance + use the following: + + * the `--api` option to introduce the set of API-specific options that follow + * the `--jdk-build` option to specify the location of the build containing + the files to be compared, such as `build/macosx-x64`, + `build/linux-x86_64-server-release`, or the path for any directory that is + used for a custom configuration. + + The directory specified with `--jdk-build` should contain the following files: + + * the "marker" file `spec.gmk`, + * the JDK image, in `images/jdk`, + * one or more documentation bundles, in `images/*docs*`, if API descriptions + should be compared, and + * the source code, in `support/src.zip`, if documentation comments should + be compared + + In conjunction with this option, use the `jdk-docs` option to specify the name + of the docs bundle to be compared, when there is more than one. Note this option + applies to all instances to be compared, and is not specific to the + [current API](#the-current-api). Use the `--compare-doc-comments` and + `--compare-api-descriptions` as needed to indicate the comparisons to be included + in the report. If neither are specified, `--compare-api-descriptions` is + the default. + +* If you just want to compare some or all of the declarations in different + instances of JDK, without comparing the documentation comments or API documentation, + for each instance use the following: + + * the `--api` option to introduce the set of API-specific options that follow + * the `--system` option to point to the instance of JDK + +* If you want to compare the declarations and the corresponding API descriptions + generated by `javadoc`, for each instance of JDK use the following: + + * the `--api` option to introduce the set of API-specific options that follow + * the `--system` option to point to the instance of JDK + * the `--api-directory` option to point to the corresponding documentation + generated by the `javadoc` tool + +* If you want to compare the declarations and the corresponding documentation + comments, it is recommended to use the following: + + * the `--api` option to introduce the set of API-specific options that follow + * the `--system` option to point to the instance of JDK + * a series of `--patch-module` options to specify the location of the + source for each of the modules to be compared + +When using `--patch-module`, you do not need to specify all the source directories +for the module: you just need to specify the source directories containing +the classes whose documentation comments are to be compared. Any supporting +declarations will be found in the JDK specified by the `--system` option. + +If the list of `--patch-module` options is large, it may be convenient to +place them in a file and use the `@`_file_ option to specify the location +of the file. You might also choose to put all the options for an API in +an API-specific `@`_file_. + +If you want to compare the API descriptions as well as the documentation comments, +you can combine the recommended options for the two modes, specifying both +`--patch-module` options for the source files and `--api-directory` for the +generated documentation. + +### Comparing different releases of JDK + +When comparing any generated API documentation, the comparison is sensitive to any +variations caused by the version of `javadoc` used to generate the documentation. +Therefore, it is highly recommended to use the same version of `javadoc` to generate +all the versions of the documentation to be compared. When building JDK, although +the standard `docs` and `docs-jdk` targets will use the version of `javadoc` in the +same repository, you can specify the version of JDK to be used for targets like +`docs-reference-api` with the `--with-docs-reference-jdk` option to `configure`, +allowing you to specify the same version of JDK and hence the same version of +`javadoc` to be used to generate the API documentation for each build to be compared. +Generally, the JDK version used to generate the documentation should be at least as +recent as the latest version to be compared. + +When comparing recent API changes in JDK, such as when creating a report for a +CSR request, when there is no change in `javadoc` in the versions being compared, +it is reasonable to use the standard `docs` or `docs-jdk` targets to generate the API +documentation to be compared. + +### Comparing documentation comments or API descriptions + +As well as comparing the declarations found in source files or compiled class files, +`apidiff` can compare documentation comments and/or the API documentation generated +by `javadoc` and the Standard Doclet. + +* Comparing documentation comments is easy, and does not require API documentation + to have been generated. However, it is just a simple text comparison of the text of + the documentation comments, and so does not take into account any of the analysis + and processing that is done by the Standard Doclet. Most notably, it does not + take `{@inheritDoc}` into account, and so might miss some differences in any + inherited documentation. + +* Comparing API descriptions is better for comparing the documentation as generated + by the Standard Doclet, and as seen by the end user reading the API specification. + However, it does require that API documentation needs to be generated beforehand, + and generally using the same version of `javadoc` for all the versions of the API + that are being compared. + +## Operation + +The tool operates by creating an instance of the Java compiler front-end +(as found in the [`jdk.compiler`][jdk.compiler] module) +from which it can obtain the selected elements to be compared, +using the [Java Language Model API] and [Compiler Tree API]. + +_Note: Because the compiler is reading the source and class files for each instance of +the API being compared, the release of the JDK platform used to run `apidiff` must be +at least as recent as each of the releases used to compile the instances to be compared._ + +When comparing the API descriptions for each selected element, the tool attempts to find +the relevant content in the API documentation that is provided using the +<a href="#option-api-directory">`--api-directory`</a> or +<a href="#option-jdk-build">`--jdk-build`</a> options. +The tool does _not_ attempt to run `javadoc` locally to generate the page on the fly. + +[jdk.compiler]: https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/module-summary.html +[Java Language Model API]: https://docs.oracle.com/en/java/javase/17/docs/api/java.compiler/javax/lang/model/package-summary.html +[Compiler Tree API]: https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/source/doctree/package-summary.html diff --git a/src/share/doc/jdk17.api/element-list b/src/share/doc/jdk17.api/element-list new file mode 100644 index 0000000..34e3d59 --- /dev/null +++ b/src/share/doc/jdk17.api/element-list @@ -0,0 +1,284 @@ +module:java.base +java.io +java.lang +java.lang.annotation +java.lang.constant +java.lang.invoke +java.lang.module +java.lang.ref +java.lang.reflect +java.lang.runtime +java.math +java.net +java.net.spi +java.nio +java.nio.channels +java.nio.channels.spi +java.nio.charset +java.nio.charset.spi +java.nio.file +java.nio.file.attribute +java.nio.file.spi +java.security +java.security.cert +java.security.interfaces +java.security.spec +java.text +java.text.spi +java.time +java.time.chrono +java.time.format +java.time.temporal +java.time.zone +java.util +java.util.concurrent +java.util.concurrent.atomic +java.util.concurrent.locks +java.util.function +java.util.jar +java.util.random +java.util.regex +java.util.spi +java.util.stream +java.util.zip +javax.crypto +javax.crypto.interfaces +javax.crypto.spec +javax.net +javax.net.ssl +javax.security.auth +javax.security.auth.callback +javax.security.auth.login +javax.security.auth.spi +javax.security.auth.x500 +javax.security.cert +module:java.compiler +javax.annotation.processing +javax.lang.model +javax.lang.model.element +javax.lang.model.type +javax.lang.model.util +javax.tools +module:java.datatransfer +java.awt.datatransfer +module:java.desktop +java.applet +java.awt +java.awt.color +java.awt.desktop +java.awt.dnd +java.awt.event +java.awt.font +java.awt.geom +java.awt.im +java.awt.im.spi +java.awt.image +java.awt.image.renderable +java.awt.print +java.beans +java.beans.beancontext +javax.accessibility +javax.imageio +javax.imageio.event +javax.imageio.metadata +javax.imageio.plugins.bmp +javax.imageio.plugins.jpeg +javax.imageio.plugins.tiff +javax.imageio.spi +javax.imageio.stream +javax.print +javax.print.attribute +javax.print.attribute.standard +javax.print.event +javax.sound.midi +javax.sound.midi.spi +javax.sound.sampled +javax.sound.sampled.spi +javax.swing +javax.swing.border +javax.swing.colorchooser +javax.swing.event +javax.swing.filechooser +javax.swing.plaf +javax.swing.plaf.basic +javax.swing.plaf.metal +javax.swing.plaf.multi +javax.swing.plaf.nimbus +javax.swing.plaf.synth +javax.swing.table +javax.swing.text +javax.swing.text.html +javax.swing.text.html.parser +javax.swing.text.rtf +javax.swing.tree +javax.swing.undo +module:java.instrument +java.lang.instrument +module:java.logging +java.util.logging +module:java.management +java.lang.management +javax.management +javax.management.loading +javax.management.modelmbean +javax.management.monitor +javax.management.openmbean +javax.management.relation +javax.management.remote +javax.management.timer +module:java.management.rmi +javax.management.remote.rmi +module:java.naming +javax.naming +javax.naming.directory +javax.naming.event +javax.naming.ldap +javax.naming.ldap.spi +javax.naming.spi +module:java.net.http +java.net.http +module:java.prefs +java.util.prefs +module:java.rmi +java.rmi +java.rmi.dgc +java.rmi.registry +java.rmi.server +javax.rmi.ssl +module:java.scripting +javax.script +module:java.se +module:java.security.jgss +javax.security.auth.kerberos +org.ietf.jgss +module:java.security.sasl +javax.security.sasl +module:java.smartcardio +javax.smartcardio +module:java.sql +java.sql +javax.sql +module:java.sql.rowset +javax.sql.rowset +javax.sql.rowset.serial +javax.sql.rowset.spi +module:java.transaction.xa +javax.transaction.xa +module:java.xml +javax.xml +javax.xml.catalog +javax.xml.datatype +javax.xml.namespace +javax.xml.parsers +javax.xml.stream +javax.xml.stream.events +javax.xml.stream.util +javax.xml.transform +javax.xml.transform.dom +javax.xml.transform.sax +javax.xml.transform.stax +javax.xml.transform.stream +javax.xml.validation +javax.xml.xpath +org.w3c.dom +org.w3c.dom.bootstrap +org.w3c.dom.events +org.w3c.dom.ls +org.w3c.dom.ranges +org.w3c.dom.traversal +org.w3c.dom.views +org.xml.sax +org.xml.sax.ext +org.xml.sax.helpers +module:java.xml.crypto +javax.xml.crypto +javax.xml.crypto.dom +javax.xml.crypto.dsig +javax.xml.crypto.dsig.dom +javax.xml.crypto.dsig.keyinfo +javax.xml.crypto.dsig.spec +module:jdk.accessibility +com.sun.java.accessibility.util +module:jdk.attach +com.sun.tools.attach +com.sun.tools.attach.spi +module:jdk.charsets +module:jdk.compiler +com.sun.source.doctree +com.sun.source.tree +com.sun.source.util +com.sun.tools.javac +module:jdk.crypto.cryptoki +module:jdk.crypto.ec +module:jdk.dynalink +jdk.dynalink +jdk.dynalink.beans +jdk.dynalink.linker +jdk.dynalink.linker.support +jdk.dynalink.support +module:jdk.editpad +module:jdk.hotspot.agent +module:jdk.httpserver +com.sun.net.httpserver +com.sun.net.httpserver.spi +module:jdk.incubator.foreign +jdk.incubator.foreign +module:jdk.incubator.vector +jdk.incubator.vector +module:jdk.jartool +com.sun.jarsigner +jdk.security.jarsigner +module:jdk.javadoc +jdk.javadoc.doclet +module:jdk.jcmd +module:jdk.jconsole +com.sun.tools.jconsole +module:jdk.jdeps +module:jdk.jdi +com.sun.jdi +com.sun.jdi.connect +com.sun.jdi.connect.spi +com.sun.jdi.event +com.sun.jdi.request +module:jdk.jdwp.agent +module:jdk.jfr +jdk.jfr +jdk.jfr.consumer +module:jdk.jlink +module:jdk.jpackage +module:jdk.jshell +jdk.jshell +jdk.jshell.execution +jdk.jshell.spi +jdk.jshell.tool +module:jdk.jsobject +netscape.javascript +module:jdk.jstatd +module:jdk.localedata +module:jdk.management +com.sun.management +module:jdk.management.agent +module:jdk.management.jfr +jdk.management.jfr +module:jdk.naming.dns +module:jdk.naming.rmi +module:jdk.net +jdk.net +jdk.nio +module:jdk.nio.mapmode +jdk.nio.mapmode +module:jdk.sctp +com.sun.nio.sctp +module:jdk.security.auth +com.sun.security.auth +com.sun.security.auth.callback +com.sun.security.auth.login +com.sun.security.auth.module +module:jdk.security.jgss +com.sun.security.jgss +module:jdk.xml.dom +org.w3c.dom.css +org.w3c.dom.html +org.w3c.dom.stylesheets +org.w3c.dom.xpath +module:jdk.zipfs \ No newline at end of file diff --git a/src/share/doc/overview.html b/src/share/doc/overview.html new file mode 100644 index 0000000..884a72c --- /dev/null +++ b/src/share/doc/overview.html @@ -0,0 +1,68 @@ +<!doctype html> +<html> +<head> +<title>APIDiff: Overview + + + +

                APIDiff: Overview

                + +

                +APIDiff is an API-comparison utility, comparing elements in +different versions of an API, as represented by some combination of source files, class files, +and generated documentation. + +

                +The tool operates by creating an instance of the Java compiler front-end +(as found in the +jdk.compiler module) +from which it can obtain the selected elements to be compared, +using the +Java Language Model API +and +Compiler Tree API. + +

                +
                {@link jdk.codetools.apidiff.model Comparing declarations} +

                +Elements, type mirrors and names obtained from different instances of the +compiler front-end cannot be compared referentially or by using {@link java.lang.Object#equals(Object) equals}. +Instead, API-specific items can be represented in an API-independent manner +using keys for {@link jdk.codetools.apidiff.model.ElementKey elements} and +{@link jdk.codetools.apidiff.model.TypeMirrorKey types}; +they can be identified within an API by means of {@link jdk.codetools.apidiff.model.Position positions}; +and corresponding items in different instances of an API can be associated together using +{@link jdk.codetools.apidiff.model.APIMap API maps}, and which can be compared structurally, +using a series of custom "comparator" classes. + +

                +When comparing the API descriptions for each selected element, the tool attempts to find +the relevant content in the API documentation, provided by the +--api-directory option. +The content is extracted from the generated pages using one of two classes: +{@link jdk.codetools.apidiff.model.APIReader APIReader} and +{@link jdk.codetools.apidiff.model.SerializedFormReader SerializedFormReader}, +for reading general declaration pages and the "Serialized Form" pages respectively. + +

                {@link jdk.codetools.apidiff.report Reporting differences} +

                +Differences between corresponding elements in different versions of the API +are reported using the {@link jdk.codetools.apidiff.report.Reporter Reporter} +interface. This interface is the primary means of communication between the +front-end, comparing declarations, and the back-end, generating reports. + +

                {@link jdk.codetools.apidiff.report Generating reports} +

                +The primary reporter is {@link jdk.codetools.apidiff.report.html.HtmlReporter HtmlReporter}. +This reporter dispatches the reports of any differences to handlers that collect +the information for the individual pages that will be generated once all the +information for each page has been collected. + +

                + + + diff --git a/test/daisydiff/src/DaisyDiffViewer.java b/test/daisydiff/src/DaisyDiffViewer.java new file mode 100644 index 0000000..e554a27 --- /dev/null +++ b/test/daisydiff/src/DaisyDiffViewer.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Toolkit; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Stack; +import java.util.TreeMap; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.text.JTextComponent; + +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlAttr; +import jdk.codetools.apidiff.html.HtmlTag; +import jdk.codetools.apidiff.html.HtmlTree; +import org.outerj.daisy.diff.HtmlCleaner; +import org.outerj.daisy.diff.html.HTMLDiffer; +import org.outerj.daisy.diff.html.HtmlSaxDiffOutput; +import org.outerj.daisy.diff.html.TextNodeComparator; +import org.outerj.daisy.diff.html.dom.DomTreeBuilder; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +public class DaisyDiffViewer { + public static void main(String... args) { + new DaisyDiffViewer().run(args); + } + + public void run(String... args) { + + JTextArea leftArea = new JTextArea(10, 40); + leftArea.setLineWrap(true); + + JTextArea rightArea = new JTextArea(10, 40); + rightArea.setLineWrap(true); + + JSplitPane leftRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, + new JScrollPane(leftArea, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), + new JScrollPane(rightArea, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)); + + JTextArea diffArea = new JTextArea(); + diffArea.setLineWrap(true); + + Toolkit tk = Toolkit.getDefaultToolkit(); + Dimension screenSize = tk.getScreenSize(); + int prefWidth = (int) (screenSize.width * .8); + int prefHeight = screenSize.height / 2; + diffArea.setPreferredSize(new Dimension(prefWidth, prefHeight)); + + JSplitPane topBottom = new JSplitPane(JSplitPane.VERTICAL_SPLIT, + leftRight, + new JScrollPane(diffArea, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER) + ); + + JPanel body = new JPanel(new BorderLayout()); + body.add(topBottom, BorderLayout.CENTER); + + JButton goBtn = new JButton("Go"); + goBtn.addActionListener(ev -> go(leftArea, rightArea, diffArea)); + JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 5)); + btnPanel.add(goBtn); + body.add(btnPanel, BorderLayout.SOUTH); + + JFrame frame = new JFrame(); + frame.getContentPane().add(body); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.pack(); + frame.setVisible(true); + } + + void go(JTextArea leftArea, JTextArea rightArea, JTextComponent result) { + System.err.println("go"); + String leftText = leftArea.getText(); + String rightText = rightArea.getText(); + try { + String diff = diffHtml(leftText, rightText); + result.setText(diff); + } catch (Exception e) { + result.setText(e.toString()); + } + } + + + String diffHtml(String oldText, String newText) { + try { + Reader oldStream = new StringReader(oldText); + Reader newStream = new StringReader(newText); + Handler handler = new Handler(); + diffHtml(oldStream, newStream, handler); + StringWriter sw = new StringWriter(); + handler.doc.write(sw); + return sw.toString(); + } catch (IOException | SAXException e) { + return "Exception: " + e; + } + } + + // This method is an extract from the DaisyDiff main program, lines 120-157, + // the body of "if (htmlDiff) { ... }". It runs the HtmlCleaner on the input + // text prior to calling HtmlDiffer. + void diffHtml(Reader oldStream, Reader newStream, ContentHandler postProcess) + throws IOException, SAXException { + + Locale locale = Locale.getDefault(); + String prefix = "diff"; + + HtmlCleaner cleaner = new HtmlCleaner(); + + InputSource oldSource = new InputSource(oldStream); + InputSource newSource = new InputSource(newStream); + + DomTreeBuilder oldHandler = new DomTreeBuilder(); + cleaner.cleanAndParse(oldSource, oldHandler); + System.out.print("."); + TextNodeComparator leftComparator = new TextNodeComparator( + oldHandler, locale); + + DomTreeBuilder newHandler = new DomTreeBuilder(); + cleaner.cleanAndParse(newSource, newHandler); + System.out.print("."); + TextNodeComparator rightComparator = new TextNodeComparator( + newHandler, locale); + + postProcess.startDocument(); + postProcess.startElement("", "diffreport", "diffreport", + new AttributesImpl()); +// doCSS(css, postProcess); + postProcess.startElement("", "diff", "diff", + new AttributesImpl()); + HtmlSaxDiffOutput output = new HtmlSaxDiffOutput(postProcess, + prefix); + + HTMLDiffer differ = new HTMLDiffer(output); + differ.diff(leftComparator, rightComparator); + System.out.print("."); + postProcess.endElement("", "diff", "diff"); + postProcess.endElement("", "diffreport", "diffreport"); + postProcess.endDocument(); + } + + class Handler implements ContentHandler { + Stack stack; + StringBuilder text; + HtmlTree doc; + + Handler() { + stack = new Stack<>(); + text = new StringBuilder(); + } + + Content toContent() { + return stack.peek(); + } + + + @Override + public void setDocumentLocator(Locator locator) { + // should not happen + } + + @Override + public void startDocument() { + stack.push(new HtmlTree(HtmlTag.DIV)); + } + + @Override + public void endDocument() { + doc = stack.pop(); + } + + @Override + public void startPrefixMapping(String prefix, String uri) { + // should not happen + } + + @Override + public void endPrefixMapping(String prefix) { + // should not happen + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) { + flushText(); + switch (localName) { + // ignore possibility of , , etc for now + case "diffreport": + case "diff": + return; + } + try { + HtmlTag tag = HtmlTag.valueOf(localName.toUpperCase(Locale.US)); + HtmlTree tree = new HtmlTree(tag); + for (int i = 0; i < atts.getLength(); i++) { + String name = atts.getLocalName(i); + String value = atts.getValue(i); + if (tag == HtmlTag.SPAN) { + // if changeId is found, we might want to ignore id as well, + // to avoid duplicate ids across different diff blocks + switch (name) { + case "changes": + case "changeId": + case "next": + case "previous": + System.err.println("!! " + name + " '" + value + "'"); + continue; + } + } + try { + HtmlAttr a = HtmlAttr.valueOf(name.toUpperCase(Locale.US)); + tree.set(a, value); + } catch (IllegalArgumentException e) { + System.err.println("unknown attribute name: " + localName + "; " + e); + } + } + stack.peek().add(tree); + stack.push(tree); + } catch (IllegalArgumentException e) { + System.err.println("unknown element name: " + localName + "; " + e); + } + } + + @Override + public void endElement(String uri, String localName, String qName) { + flushText(); + switch (localName) { + // ignore possibility of , , etc for now + case "diffreport": + case "diff": + return; + } + HtmlTree tree = stack.pop(); + if (!tree.tag.name().equals(localName.toUpperCase(Locale.US))) { + System.err.println("popping unbalanced tree node: expect: " + localName + ", found " + tree.tag); + } + } + + @Override + public void characters(char[] ch, int start, int length) { + text.append(ch, start, length); + } + + @Override + public void ignorableWhitespace(char[] ch, int start, int length) { + text.append(ch, start, length); + } + + @Override + public void processingInstruction(String target, String data) { + // should not happen + } + + @Override + public void skippedEntity(String name) { + // should not happen + // Note: + // known entities are translated into the equivalent character; e.g. < to < + // unknown entities are handled as literal strings; e.g. &foo; remains as &foo; + } + + private void flushText() { + if (text.length() > 0) { + stack.peek().add(text.toString()); + text.setLength(0); + } + } + } + + class Handler2 implements ContentHandler { + List info = new ArrayList<>(); + + Handler2() { } + + java.util.List toList() { + return info; + } + + + @Override + public void setDocumentLocator(Locator locator) { + info.add("setDocumentLocator: " + locator); + } + + @Override + public void startDocument() { + info.add("startDocument"); + } + + @Override + public void endDocument() { + info.add("startDocument"); + } + + @Override + public void startPrefixMapping(String prefix, String uri) { + info.add("startPrefixMapping " + prefix + " " + uri); + + } + + @Override + public void endPrefixMapping(String prefix) { + info.add("endPrefixMapping " + prefix); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) { + Map map = new TreeMap<>(); + for (int i = 0; i < atts.getLength(); i++) { + String name = atts.getLocalName(i); + String value = atts.getValue(i); + map.put(name, value); + } + info.add("startElement " + uri + " " + localName + " " + qName + " " + map); + } + + @Override + public void endElement(String uri, String localName, String qName) { + info.add("endElement " + uri + " " + localName + " " + qName); + } + + @Override + public void characters(char[] ch, int start, int length) { + String s = new String(ch, start, length); + if (s.length() > 30) { + s = s.substring(0, 10) + "..." + s.substring(s.length() - 10); + } + info.add("characters " + s); + } + + @Override + public void ignorableWhitespace(char[] ch, int start, int length) { + String s = new String(ch, start, length); + if (s.length() > 30) { + s = s.substring(0, 10) + "..." + s.substring(s.length() - 10); + } + info.add("ignorableWhitespace '" + s + "' (" + length + ")"); + } + + @Override + public void processingInstruction(String target, String data) { + info.add("processingInstruction " + target + " " + data); + } + + @Override + public void skippedEntity(String name) { + info.add("skippedEntity " + name); + } + } + +} diff --git a/test/junit/JUnitTests.gmk b/test/junit/JUnitTests.gmk new file mode 100644 index 0000000..04e02e6 --- /dev/null +++ b/test/junit/JUnitTests.gmk @@ -0,0 +1,68 @@ +# +# Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +JUnitTests.files := $(shell find $(TESTDIR)/junit -type f -name \*.java) + +JUnitTest.add-exports = \ + --add-modules jdk.jdeps \ + --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ + --add-exports jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED + +JUnitTest.classpath = \ + $(BUILDTESTDIR)/JUnitTests/classes:$(APIDIFF_IMAGEDIR)/lib/apidiff.jar:$(JAVADIFFUTILS_JAR):$(DAISYDIFF_JAR):$(HTMLCLEANER_JAR) + +$(BUILDTESTDIR)/JUnitTests.ok: \ + $(BUILDTESTDIR)/JUnitTests.classes.ok + $(RM) $(@:%.ok=%/work) $(@:%.ok=%/report) + $(MKDIR) -p $(@:%.ok=%/work) $(@:%.ok=%/report) + cd $(@:%.ok=%/work) ; \ + set -o pipefail ; \ + $(JAVA) \ + $(JUnitTest.add-exports) \ + -jar $(JUNIT_JAR) \ + -classpath $(JUnitTest.classpath) \ + --reports-dir=$(@:%.ok=%/report) \ + --select-package=apitest \ + 2>&1 | tee $(@:%.ok=%/log) + echo $@ passed at `date` > $@ + +$(BUILDTESTDIR)/JUnitTests.classes.ok: \ + $(JUnitTests.files) \ + $(APIDIFF_IMAGEDIR)/lib/apidiff.jar \ + $(JUNIT_JAR) + $(JAVAC) \ + -d $(BUILDTESTDIR)/JUnitTests/classes \ + --class-path $(JUNIT_JAR):$(JUnitTest.classpath) \ + $(JUnitTest.add-exports) \ + $(JUnitTests.files) + echo $@ compiled at `date` > $@ + +# ignore exit code from $(TIDY) until we resolve the duplicate-id problem +$(BUILDTESTDIR)/JUnitTests.tidy.ok: + -$(TIDY) -q -e --gnu-emacs true `$(FIND) $(@:%.tidy.ok=%/work) -name \*.html` + echo files checked by tidy at `date` > $@ + +TESTS += \ + $(BUILDTESTDIR)/JUnitTests.ok \ + $(BUILDTESTDIR)/JUnitTests.tidy.ok diff --git a/test/junit/apitest/APIReaderTest.java b/test/junit/apitest/APIReaderTest.java new file mode 100644 index 0000000..f91ec01 --- /dev/null +++ b/test/junit/apitest/APIReaderTest.java @@ -0,0 +1,448 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.model.APIDocs; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; +import toolbox.JavadocTask; +import toolbox.ModuleBuilder; +import toolbox.Task; + +/** + * A test for the ability to extract documentation from API documentation + * generated by javadoc. + */ +// TODO: either here or in a JUnit-free class, we could have a main program +// that reads a file, and prints out the resulting APIDocs in a stylized +// HTML format, that makes it easy to see the text that is extracted. +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class APIReaderTest extends APITester { + private Log log; + private Path api; + + /** + * Generates sample API documentation from sample API. + * + * @throws IOException if an IO exception occurs + */ + @BeforeAll + public void generateAPIDocs() throws IOException { + Path base = getScratchDir(); + super.log.println(base); + + Path src = base.resolve("src"); + generateSampleAPI(src); + + // Run javadoc on sample API + api = Files.createDirectories(base.resolve("api")); + Task.Result r = new JavadocTask(tb) + .sourcepath(src.resolve("mA")) + .outdir(api) + .options("-noindex", "-quiet", "--module", "mA") + .run(); + r.writeAll(); + + PrintWriter out = new PrintWriter(System.out) { + @Override + public void close() { + flush(); + } + }; + PrintWriter err = new PrintWriter(System.err, true){ + @Override + public void close() { + flush(); + } + }; + + log = new Log(out, err); + } + + /** + * Flushes any output that has been written to the log streams. + */ + @AfterEach + public void flushLog() { + log.out.flush(); + log.err.flush(); + } + + void generateSampleAPI(Path dir) throws IOException { + new ModuleBuilder(tb, "mA") + .comment("This is module mA. This is more text for mA.\n@see \"See Text\"") + .exports("p") + .classes("/** This is package p. This is more text for p.\n@see \"See Text\" */ package p;") + .classes(""" + package p; + /** + * This is anno-type A. This is more text for A. + * @see "See Text" + */ + public @interface A { + /** + * This is required element r1. This is more text for r1. + * @return dummy. + * @see "See Text" + */ + int r1(); + /** + * This is required element r2. This is more text for r2. + * @return dummy. + * @see "See Text" + */ + int r2(); + /** + * This is optional element o1. This is more text for o1. + * @return dummy. + * @see "See Text" + */ + int o1() default 0; + /** + * This is optional element o2. This is more text for o2. + * @return dummy. + * @see "See Text" + */ + int o2() default 0; + }""") + .classes(""" + package p; + /** + * This is enum E; This is more text for E. + * @see "See Text" + */ + public enum E { + /** + * This is enum constant E1. This is more text for E1. + * @see "See Text" + */ + E1, + /** + * This is enum constant E2. This is more text for E2. + * @see "See Text" + */ + E2; + /** + * This is field f1. This is more text for f1. + * @see "See Text" + */ + public int f1; + /** + * This is field f2. This is more text for f2. + * @see "See Text" + */ + public int f2; + /** + * This is method m1. This is more text for m1. + * @see "See Text" + */ + public void m1() { } + /** + * This is method m2. This is more text for m2. + * @see "See Text" + */ + public void m2() { } + /** + * This is nested class N. This is more test for N. + * @see "See Text" + */ + public class N { + /** + * This is field f1 in nested class N. + */ + public int f1; + } + }""") + .classes(""" + package p; + /** + * This is class C; This is more text for C. + * @see "See Text" + */ + public class C { + /** + * This is field f1. This is more text for f1. + * @see "See Text" + */ + public int f1; + /** + * This is field f2. This is more text for f2. + * @see "See Text" + */ + public int f2; + /** + * This is the no-args constructor. This is more text for the no-args constructor. + * @see "See Text" + */ + public C() { } + /** + * This is the 1-arg constructor. This is more text for the 1-arg constructor. + * @param i the arg + * @see "See Text" + */ + public C(int i) { } + /** + * This is method m1. This is more text for m1. + * @see "See Text" + */ + public void m1() { } + /** + * This is method m2. This is more text for m2. + * @see "See Text" + */ + public void m2() { } + /** + * This is nested class N. This is more test for N. + * @see "See Text" + */ + public class N { + /** + * This is field f1 in nested class N. + */ + public int f1; + } + }""") + .classes(""" + package p; + /** + * This is interface I; This is more text for I. + * @see "See Text" + */ + public interface I { + /** + * This is field f1. This is more text for f1. + * @see "See Text" + */ + static final int f1 = 0; + /** + * This is field f2. This is more text for f2. + * @see "See Text" + */ + static final int f2 = 0; + /** + * This is method m1. This is more text for m1. + * @see "See Text" + */ + void m1(); + /** + * This is method m2. This is more text for m2. + * @see "See Text" + */ + void m2(); + /** + * This is nested class N. This is more test for N. + * @see "See Text" + */ + public class N { + /** + * This is field f1 in nested class N. + */ + public int f1; + } + }""") + .write(dir); + } + + /** + * Find all the files that are in the api directory. + * + * @return the files + * @see #checkFile(Path) + */ + public Stream findFiles() { + List files = new ArrayList<>(); + try { + walkFileTree(api, log, (log, file) -> files.add(new Object[] { file })); + } catch (IOException e) { + Assertions.fail("problem finding files", e); + } + return files.stream().map(Arguments::of); + } + + private static void walkFileTree(Path dir, Log log, BiConsumer f) throws IOException { + Pattern p = Pattern.compile("(module-summary|package-summary|[A-Z].*)\\.html"); + Files.walkFileTree(dir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (p.matcher(file.getFileName().toString()).matches()) { + System.err.println("file: " + file); + f.accept(log, file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + System.err.println("dir: " + dir); + return switch (dir.getFileName().toString()) { + case "jquery", "resources" -> FileVisitResult.SKIP_SUBTREE; + default -> FileVisitResult.CONTINUE; + }; + } + }); + } + + /** + * Tests the content of a file. + * + * @param file the file + */ + @ParameterizedTest + @MethodSource("findFiles") + public void checkFile(Path file) { + // TestNG oddity: the test works as expecting when using the following call, + // but not if the next 5 source lines are removed, causing the body of the + // method to be executed directly; in that case, the test is only executed + // once, for the first file, with no indication of why other data values + // are not used. + checkFile(log, file); + } + + // TODO: add more checks for content of members + private void checkFile(Log log, Path file) { + APIDocs docs = APIDocs.read(log, file); + showDocs(log, file, docs); + + switch (file.getFileName().toString()) { + case "module-summary.html" -> { + checkDescription(docs.getDescription(), "module-description", null, "This is module m[A-Z]. This is more"); + checkMemberDescriptions(docs.getMemberDescriptions()); + } + + case "package-summary.html" -> { + checkDescription(docs.getDescription(), "package-description", null, "This is package p. This is more"); + checkMemberDescriptions(docs.getMemberDescriptions()); + } + + case "A.html" -> { + checkDescription(docs.getDescription(), null, null, "This is anno-type A. This is more"); + checkMemberDescriptions(docs.getMemberDescriptions(), "r1()", "r2()", "o1()", "o2()"); + } + + case "C.html" -> { + checkDescription(docs.getDescription(), null, null, "This is class C. This is more"); + checkMemberDescriptions(docs.getMemberDescriptions(), "()", "(int)", "f1", "f2", "m1()", "m2()"); + checkDescription(docs.getDescription("()"), + "()", "C", "This is the no-args constructor. This is more"); + } + + case "C.N.html", + "E.N.html", + "I.N.html" -> { + checkDescription(docs.getDescription(), null, null, "This is nested class N. This is more"); + checkMemberDescriptions(docs.getMemberDescriptions(), "()", "f1"); + } + + case "E.html" -> { + checkDescription(docs.getDescription(), null, null, "This is enum E. This is more"); + checkMemberDescriptions(docs.getMemberDescriptions(), "E1", "E2", "f1", "f2", "m1()", "m2()", "values()", "valueOf(java.lang.String)"); + } + + case "I.html" -> { + checkDescription(docs.getDescription(), null, null, "This is interface I. This is more"); + checkMemberDescriptions(docs.getMemberDescriptions(), "f1", "f2", "m1()", "m2()"); + } + + default -> Assertions.fail(file.toString()); + } + } + + private void checkDescription(String desc, String id, String heading, String body) { + if (id != null && !desc.contains("id=\"" + escape(id) + "\"")) { + Assertions.fail("expected id not found: " + id); + } + if (heading != null && !Pattern.compile("]*>\\Q" + heading + "\\E descriptions, String... ids) { + Assertions.assertEquals(Set.of(ids), descriptions.keySet()); + } + + void showFile(Log log, Path file) { + APIDocs docs = APIDocs.read(log, file); + log.out.println("File: " + file); + log.out.println("Description: "); + printDescription(log, docs.getDescription()); + new TreeMap<>(docs.getMemberDescriptions()).forEach((id, d) -> { + log.out.println("Member: " + id); + printDescription(log, docs.getDescription()); + + }); + } + + void showDocs(Log log, Path file, APIDocs docs) { + log.out.println("File: " + file); + log.out.println("Description: "); + printDescription(log, docs.getDescription()); + new TreeMap<>(docs.getMemberDescriptions()).forEach((id, d) -> { + log.out.println("Member: " + id); + printDescription(log, docs.getDescription(id)); + }); + } + + private void printDescription(Log log, String s) { + if (s == null) { + log.out.println(""); + } else { + s.lines().forEach(l -> log.out.println("| " + l)); + } + } + + private String escape(String s) { + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">"); + } +} diff --git a/test/junit/apitest/AddRemoveElementTest.java b/test/junit/apitest/AddRemoveElementTest.java new file mode 100644 index 0000000..204eb42 --- /dev/null +++ b/test/junit/apitest/AddRemoveElementTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; + +public class AddRemoveElementTest extends APITester { + + @Test + public void testAddDecls() throws IOException { + Path base = getScratchDir(); + log.println(base); + Path src = base.resolve("src"); + List options = new ArrayList<>(); + int apiCount = 3; + generateAPI(src, apiCount); + + for (int a = 0; a < apiCount; a++) { + options.addAll(List.of( + "--api", "api" + a, + "--module-source-path", src.resolve("api" + a).toString())); + } + for (int a = 0; a < apiCount; a++) { + String moduleName = "m" + a; + options.addAll(List.of("--include", moduleName + "/**")); + } + options.addAll(List.of("-d", base.resolve("out").toString())); + log.println("Options: " + options); + + Map outMap = run(options); + outMap.forEach((k, s) -> { + log.println("[" + k + "]"); + log.println(s); + }); + } + + void generateAPI(Path base, int count) throws IOException { + for (int a = 0; a < count ; a++) { + Path apiDir = Files.createDirectories(base.resolve("api" + a)); + for (int m = a; m < count; m++) { + String moduleName = "m" + m; + Path moduleDir = Files.createDirectories(apiDir.resolve(moduleName)); + StringBuilder moduleSrc = new StringBuilder(); + moduleSrc.append("module m").append(m).append(" {\n"); + for (int p = m; p < count; p++) { + moduleSrc.append(" exports p").append(p).append(";\n"); + Path packageDir = Files.createDirectories(moduleDir.resolve("p" + p)); + for (int t = p; t < count; t++) { + StringBuilder typeSrc = new StringBuilder(); + typeSrc.append("package p").append(p).append(";\n") + .append("public class C" + t + " {\n"); + for (int f = t; f < count; f++) { + typeSrc.append(" public int f" + f + ";\n"); + } + typeSrc.append("}\n"); + Files.writeString(packageDir.resolve("C" + t +".java"), typeSrc); + } + } + moduleSrc.append("}\n"); + Files.writeString(moduleDir.resolve("module-info.java"), moduleSrc); + } + } + } +} diff --git a/test/junit/apitest/AnnoTest.java b/test/junit/apitest/AnnoTest.java new file mode 100644 index 0000000..37ee16b --- /dev/null +++ b/test/junit/apitest/AnnoTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +/** + * Tests for the ability to compare annotations. + */ +public class AnnoTest extends APITester { + + /** + * Tests different default values. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDefaultValues() throws IOException { + Path base = getScratchDir(); + log.println(base); + testAnnos(base, i -> { + String dv = (i == 0) ? "" : " default 1"; + return "int v()" + dv + ";"; + }); + } + + private void testAnnos(Path base, Function f) throws IOException { + List options = new ArrayList<>(); + + for (int i = 0; i < 2; i++) { + String apiName = "api" + i; + Path apiDir = base.resolve(apiName).resolve("src"); + + String mods = (i == 0) ? "public" : "protected"; + Path p = new ModuleBuilder(tb, "mA") + .exports("p") + .classes("package p; public @interface Anno {\n" + f.apply(i) + "\n}\n") + .write(apiDir); + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + public static Stream provideOptions() { + return Stream.of( + Arguments.of(null, false), + Arguments.of("public", false), + Arguments.of("protected", false), + Arguments.of("package", true), + Arguments.of("private", true) + ); + } + + @ParameterizedTest + @MethodSource("provideOptions") + public void testDocumentedMix(String accessKind, boolean expectNotDoc) throws IOException { + String name = (accessKind == null) ? "none" : accessKind; + Path base = getScratchDir(name); + log.println(base); + + List options = new ArrayList<>(); + + for (int i = 0; i < 2; i++) { + String apiName = "api" + i; + Path apiDir = base.resolve(apiName).resolve("src"); + + String mods = (i == 0) ? "public" : "protected"; + Path p = new ModuleBuilder(tb, "mA") + .exports("p") + .classes("package p; import java.lang.annotation.*; public @Documented @interface Doc { }\n") + .classes("package p; public @interface NotDoc { }\n") + .classes("package p; public @Doc @NotDoc class C { }\n") + .write(apiDir); + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + + if (accessKind != null) { + options.addAll(List.of("--access", accessKind)); + } + + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + String c_html = Files.readString(base.resolve("out").resolve("mA").resolve("p").resolve("C.html")); + checkContains(c_html, "@Doc", true); + checkContains(c_html, "@NotDoc", expectNotDoc); + } + + private void checkContains(String full, String s, boolean expect) { + if (full.contains(s)) { + if (!expect) { + throw new AssertionError("string found unexpectedly"); + } + } else { + if (expect) { + throw new AssertionError("expected string not found"); + } + } + } +} diff --git a/test/junit/apitest/CheckManPage.java b/test/junit/apitest/CheckManPage.java new file mode 100644 index 0000000..5fde21d --- /dev/null +++ b/test/junit/apitest/CheckManPage.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.codetools.apidiff.Options.Option; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CheckManPage { + @Test + public void checkManPage() throws Exception { + Path base = findBaseDir(); + Path manPage = base.resolve("src/share/doc/apidiff.md".replace("/", File.separator)); + if (!Files.exists(manPage)) { + throw new Error("man page not found: " + manPage); + } + + Set documented = new TreeSet<>(); + Pattern p = Pattern.compile("<[a-z]+[^>]* id=\"option-([a-z0-9-]+)\""); + for (String line : Files.readAllLines(manPage)) { + Matcher m = p.matcher(line); + while (m.find()) { + documented.add(m.group(1)); + } + } + System.err.println("documented: " + documented); + + Set declared = Stream.of(Option.values()) + .map(o -> o.getNames().get(0).replaceFirst("^-+", "")) + .collect(Collectors.toCollection(TreeSet::new)); + declared.add("at"); // special case for @file + System.err.println("declared: " + declared); + + if (!documented.equals(declared)) { + Set s1 = new TreeSet<>(declared); + s1.removeAll(documented); + if (!s1.isEmpty()) { + System.err.println("declared but not documented:"); + s1.stream().forEach(s -> System.err.println(" " + s)); + } + Set s2 = new TreeSet<>(documented); + s2.removeAll(declared); + if (!s2.isEmpty()) { + System.err.println("documented but not declared:"); + s2.stream().forEach(s -> System.err.println(" " + s)); + } + Assertions.fail("discrepancies found"); + } + } + + private Path findBaseDir() { + Path d = Path.of(".").toAbsolutePath().normalize(); + while (d != null) { + if (Files.isDirectory(d.resolve("src"))) { + return d; + } + d = d.getParent(); + } + throw new Error("can't find base directory"); + } +} diff --git a/test/junit/apitest/CheckResourceKeys.java b/test/junit/apitest/CheckResourceKeys.java new file mode 100644 index 0000000..3c34068 --- /dev/null +++ b/test/junit/apitest/CheckResourceKeys.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.EnumSet; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeSet; + +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import com.sun.tools.classfile.ClassFile; +import com.sun.tools.classfile.ConstantPool; +import com.sun.tools.classfile.ConstantPoolException; + +import jdk.codetools.apidiff.Main; +import jdk.codetools.apidiff.Main.Result; +import jdk.codetools.apidiff.report.html.HtmlReporter; + +import org.junit.jupiter.api.Test; + +/** + * Compare string constants in apidiff classes against keys in apidiff resource bundles. + */ +public class CheckResourceKeys { + /** + * Main program. + * Options: + * --find-unused-keys + * look for keys in resource bundles that are no longer required + * --find-missing-keys + * look for keys in resource bundles that are missing + * + * @param args commnd-line arguments + * @throws Exception if invoked by jtreg and errors occur + */ + public static void main(String... args) throws Exception { + CheckResourceKeys c = new CheckResourceKeys(); + if (!c.run(args)) { + System.exit(1); + } + } + + private PrintStream log = System.out; + + /** + * Main entry point. + */ + boolean run(String... args) throws Exception { + boolean findUnusedKeys = false; + boolean findMissingKeys = false; + + if (args.length == 0) { + System.err.println("Usage: java CheckResourceKeys "); + System.err.println("where options include"); + System.err.println(" --find-unused-keys find keys in resource bundles which are no longer required"); + System.err.println(" --find-missing-keys find keys in resource bundles that are required but missing"); + return true; + } else { + for (String arg: args) { + if (arg.equalsIgnoreCase("--find-unused-keys")) + findUnusedKeys = true; + else if (arg.equalsIgnoreCase("--find-missing-keys")) + findMissingKeys = true; + else + error("bad option: " + arg); + } + } + + if (errors > 0) + return false; + + Set codeKeys = getCodeKeys(); + Set resourceKeys = getResourceKeys(); + + System.err.println("found " + codeKeys.size() + " keys in code"); + System.err.println("found " + resourceKeys.size() + " keys in resource bundles"); + + if (findUnusedKeys) + findUnusedKeys(codeKeys, resourceKeys); + + if (findMissingKeys) + findMissingKeys(codeKeys, resourceKeys); + + usageTests(); + + return (errors == 0); + } + + @Test + public void checkResourceKeys() throws Exception { + boolean ok = run("--find-unused-keys", "--find-missing-keys"); + if (!ok) { + throw new Exception("Check failed"); + } + } + + void usageTests() { + String[] argarray = { "--help" }; + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + if (new Main(pw, pw).run("--help") == Result.OK) { + pw.flush(); + String s = sw.toString(); + if (s.isEmpty()) { + error("no output from apidiff"); + return; + } + if (sw.toString().contains("WARNING: missing resource")) { + log.println(s); + error("missing resources in output ?"); + } + } else { + error("failed to execute apidiff"); + } + } + + /** + * Find keys in resource bundles which are probably no longer required. + * A key is required if there is a string in the code that is a resource key, + * or if the key is well-known according to various pragmatic rules. + */ + void findUnusedKeys(Set codeKeys, Set resourceKeys) { + for (String rk: resourceKeys) { + // ignore these synthesized keys, tested by usageTests + if (rk.startsWith("apidiff.usage.")) + continue; + // ignore these synthesized keys, tested by usageTests + if (rk.matches("opt\\.(arg|desc)\\.[-a-z]+")) + continue; + if (codeKeys.contains(rk)) + continue; + + error("Resource key not found in code: '" + rk + '"'); + } + } + + /** + * For all strings in the code that look like they might be + * a resource key, verify that a key exists. + */ + void findMissingKeys(Set codeKeys, Set resourceKeys) { + for (String ck: codeKeys) { + // ignore keys that are defined in a resource file + if (resourceKeys.contains(ck)) + continue; + error("No resource for \"" + ck + "\""); + } + } + + /** + * Get the set of strings from the apidiff classfiles. + */ + Set getCodeKeys() throws IOException { + Set results = new TreeSet<>(); + JavaCompiler c = ToolProvider.getSystemJavaCompiler(); + try (JavaFileManager fm = c.getStandardFileManager(null, null, null)) { + List pkgs = List.of("jdk.codetools.apidiff"); + for (String pkg: pkgs) { + for (JavaFileObject fo: fm.list(StandardLocation.CLASS_PATH, + pkg, EnumSet.of(JavaFileObject.Kind.CLASS), true)) { + String name = fo.getName(); + // ignore resource files + if (name.matches(".*resources.[A-Za-z_0-9]+\\.class.*")) + continue; + scan(fo, results); + } + } + } + + return results; + } + + // depending on how the test is run, javadoc may be on bootclasspath or classpath + JavaFileManager.Location findJavadocLocation(JavaFileManager fm) { + JavaFileManager.Location[] locns = + { StandardLocation.PLATFORM_CLASS_PATH, StandardLocation.CLASS_PATH }; + try { + for (JavaFileManager.Location l: locns) { + JavaFileObject fo = fm.getJavaFileForInput(l, + "jdk.javadoc.internal.tool.Main", JavaFileObject.Kind.CLASS); + if (fo != null) { + System.err.println("found javadoc in " + l); + return l; + } + } + } catch (IOException e) { + throw new Error(e); + } + throw new IllegalStateException("Cannot find javadoc"); + } + + /** + * Get the set of strings from a class file. + * Only strings that look like they might be a resource key are returned. + */ + void scan(JavaFileObject fo, Set results) throws IOException { + //System.err.println("scan " + fo.getName()); + try (InputStream in = fo.openInputStream()) { + ClassFile cf = ClassFile.read(in); + for (ConstantPool.CPInfo cpinfo : cf.constant_pool.entries()) { + if (cpinfo.getTag() == ConstantPool.CONSTANT_Utf8) { + String v = ((ConstantPool.CONSTANT_Utf8_info) cpinfo).value; + // ignore SourceFile attribute values + if (v.matches("[A-Za-z][A-Za-z0-9-]*\\.java")) { + continue; + } + // ignore system names + if (v.matches("(java|jdk)\\..*")) { + continue; + } + // ignore standard javadoc file names + if (v.matches("((module|package)-summary|serialized-form)\\.html")) { + continue; + } + // ignore standard javadoc CSS class names + if (v.matches("(module|package).description")) { + continue; + } + // ignore standard apidiff file names + if (v.equals("index.html") || v.equals(HtmlReporter.DEFAULT_STYLESHEET)) { + continue; + } + // ignore names used by --jdk-build + if (v.equals("apidiff.tmp") || v.equals("src.zip")|| v.equals("spec.gmk")) { + continue; + } + // ignore debug options + if (v.startsWith("debug.")) { + continue; + } + if (v.matches("[A-Za-z][A-Za-z0-9-]+\\.[A-Za-z0-9-_@.]+")) + results.add(v); + } + } + } catch (ConstantPoolException ignore) { + } + } + + /** + * Get the set of keys from the apidiff resource bundles. + */ + Set getResourceKeys() { + String[] names = { + "jdk.codetools.apidiff.resources.help", + "jdk.codetools.apidiff.resources.log", + "jdk.codetools.apidiff.report.html.resources.report" + }; + Set results = new TreeSet<>(); + for (String name : names) { + ResourceBundle b = ResourceBundle.getBundle(name); + results.addAll(b.keySet()); + } + return results; + } + + /** + * Report an error. + */ + void error(String msg) { + System.err.println("Error: " + msg); + errors++; + } + + int errors; +} diff --git a/test/junit/apitest/CompareOptionsTest.java b/test/junit/apitest/CompareOptionsTest.java new file mode 100644 index 0000000..8d9bf0a --- /dev/null +++ b/test/junit/apitest/CompareOptionsTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.JavadocTask; +import toolbox.Task; + +public class CompareOptionsTest extends APITester { + @Test + public void testCompareAPIDescriptions() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path outDir = run(base, "--compare-api-descriptions=true"); + checkOutput(outDir.resolve("p/C.html"), + "abc. BEFOREAFTER. def."); + } + + @Test + public void testCompareAPIDescriptionsAsText() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path outDir = run(base, "--compare-api-descriptions-as-text=true"); + checkOutput(outDir.resolve("p/C.html"), + """ + 7 abc. BEFORE. def. + """, + """ + 7 abc. AFTER. def. + """); + } + + @Test + public void testCompareDocComments() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path outDir = run(base, "--compare-doc-comments=true"); + checkOutput(outDir.resolve("p/C.html"), + """ + 2 abc. BEFORE. def. + """, + """ + 2 abc. AFTER. def. + """); + } + + Path run(Path base, String... compareOpts) throws IOException { + var options = new ArrayList(); + for (var phase : List.of("before", "after")) { + Path src = base.resolve(phase).resolve("src"); + tb.writeJavaFiles(src, + """ + package p; + /** + * First sentence. + * abc. #PHASE#. def. + * Tail. + */ + public class C { } + """.replace("#PHASE#", phase.toUpperCase(Locale.ROOT))); + + Path api = base.resolve(phase).resolve("api"); + Files.createDirectories(api); + Task.Result r = new JavadocTask(tb) + .sourcepath(src) + .outdir(api) + .options("p") + .run(); + r.writeAll(); + + options.addAll(List.of( + "--api", phase, + "--source-path", src.toString(), + "--api-directory", api.toString())); + } + + Path outDir = base.resolve("out"); + options.addAll(List.of("--output-directory", outDir.toString())); + options.addAll(List.of(compareOpts)); + options.addAll(List.of("--include", "p.*")); + + log.println(options); + + var outMap = run(options); + + return outDir; + + } +} diff --git a/test/junit/apitest/DocFilesTest.java b/test/junit/apitest/DocFilesTest.java new file mode 100644 index 0000000..9267eb6 --- /dev/null +++ b/test/junit/apitest/DocFilesTest.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import jdk.codetools.apidiff.Main.Result; +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.Options.Mode; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; +import toolbox.JavadocTask; +import toolbox.ModuleBuilder; +import toolbox.Task; + +public class DocFilesTest extends APITester { + interface DocFilesWriter { + void write(Path src, int i) throws IOException; + } + + public static Stream getModes() { + return Stream.of( + Arguments.of(Mode.PACKAGE, Mode.PACKAGE), + Arguments.of(Mode.MODULE, Mode.PACKAGE), + // module doc-files are not supported before JDK 13, and only partially supported in JDK 13. + // In JDK 13, non-HTML files are OK, HTML files cause a crash. + // The tests will automatically skip as needed. + Arguments.of(Mode.MODULE, Mode.MODULE) + ); + } + + private void requireVersion(int v, String msg) { + Assumptions.assumeTrue(Runtime.version().feature() >= v, msg); + } + + @ParameterizedTest + @MethodSource("getModes") + public void testAddHtml(Mode sourceMode, Mode docFileKind) throws IOException { + if (sourceMode == Mode.MODULE && docFileKind == Mode.MODULE) { + requireVersion(14, "HTML module doc files not supported in this version of JDK"); + } + + Path base = getScratchDir(sourceMode + "-" + docFileKind); + log.println(base); + + testDocFiles(base, sourceMode, docFileKind, (dir, i) -> { + if (i > 0) { + tb.writeFile(dir.resolve("added.html"), + """ + + + info + + First line.
                + Last line.
                + + + """); + } + }); + } + + @ParameterizedTest + @MethodSource("getModes") + public void testChangeHtml(Mode sourceMode, Mode docFileKind) throws IOException { + if (sourceMode == Mode.MODULE && docFileKind == Mode.MODULE) { + requireVersion(14, "HTML module doc files not supported in this version of JDK"); + } + Path base = getScratchDir(sourceMode + "-" + docFileKind); + log.println(base); + + testDocFiles(base, sourceMode, docFileKind, (dir, i) -> + tb.writeFile(dir.resolve("changed.html"), + "\n" + + "\n" + + "info\n" + + "\n" + + "First line.
                \n" + + "Before the change " + (i == 0 ? "old" : "new") + " after the change.
                \n" + + "Last line.
                \n" + + "\n" + + "\n") + ); + } + + @ParameterizedTest + @MethodSource("getModes") + public void testAddText(Mode sourceMode, Mode docFileKind) throws IOException { + if (sourceMode == Mode.MODULE && docFileKind == Mode.MODULE) { + requireVersion(13, "Module doc files not supported in this version of JDK"); + } + Path base = getScratchDir(sourceMode + "-" + docFileKind); + log.println(base); + + testDocFiles(base, sourceMode, docFileKind, (dir, i) -> { + if (i > 0) { + tb.writeFile(dir.resolve("added.txt"), + """ + First line. + Last line. + """); + } + }); + } + + @ParameterizedTest + @MethodSource("getModes") + public void testChangeText(Mode sourceMode, Mode docFileKind) throws IOException { + if (sourceMode == Mode.MODULE && docFileKind == Mode.MODULE) { + requireVersion(13, "Module doc files not supported in this version of JDK"); + } + Path base = getScratchDir(sourceMode + "-" + docFileKind); + log.println(base); + + testDocFiles(base, sourceMode, docFileKind, (dir, i) -> + tb.writeFile(dir.resolve("changed.txt"), + "First line.\n" + + "Before the change " + (i == 0 ? "old" : "new") + " after the change.\n" + + "Last line.\n") + ); + } + + @ParameterizedTest + @MethodSource("getModes") + public void testMulti(Mode sourceMode, Mode docFileKind) throws IOException { + if (sourceMode == Mode.MODULE && docFileKind == Mode.MODULE) { + requireVersion(14, "HTML module doc files not supported in this version of JDK"); + } + Path base = getScratchDir(sourceMode + "-" + docFileKind); + log.println(base); + + testDocFiles(base, sourceMode, docFileKind, (dir, i) -> { + tb.writeFile(dir.resolve("changed.html"), + "\n" + + "\n" + + "info\n" + + "\n" + + "First line.
                \n" + + "Before the change " + (i == 0 ? "old" : "new") + " after the change.
                \n" + + "Last line.
                \n" + + "\n" + + "\n"); + + tb.writeFile(dir.resolve("changed.txt"), + "First line.\n" + + "Before the change " + (i == 0 ? "old" : "new") + " after the change.\n" + + "Last line.\n"); + + tb.writeFile(dir.resolve("same.html"), + """ + + + info + + First line.
                + Last line.
                + + + """); + + tb.writeFile(dir.resolve("same.txt"), + """ + First line. + Last line. + """); + + if (i > 0) { + tb.writeFile(dir.resolve("added.html"), + """ + + + info + + First line.
                + Last line.
                + + + """); + + tb.writeFile(dir.resolve("added.txt"), + """ + First line. + Last line. + """); + } + }); + } + + void testDocFiles(Path base, Options.Mode sourceMode, Mode docFileKind, DocFilesWriter docFilesWriter) throws IOException { + List options = new ArrayList<>(); + + String apidiffIncludes; + String apidiffSourcePathOption; + switch (sourceMode) { + case MODULE -> { + apidiffIncludes = "m/**"; + apidiffSourcePathOption = "--module-source-path"; + } + + case PACKAGE -> { + apidiffIncludes = "p.**"; + apidiffSourcePathOption = "--source-path"; + } + + default -> throw new Error(); + } + + for (int i = 0; i < 2; i++) { + String apiName = "api" + i; + Path src = base.resolve(apiName).resolve("src"); + Path pkgSrc; + List javadocOptions = new ArrayList<>(); + javadocOptions.addAll(List.of("-noindex", "-quiet")); + switch (sourceMode) { + case MODULE -> { + new ModuleBuilder(tb, "m") + .exports("p") + .write(src); + pkgSrc = src.resolve("m"); + javadocOptions.addAll(List.of("--module", "m")); + } + + case PACKAGE -> { + pkgSrc = src; + javadocOptions.add("p"); + } + + default -> throw new Error(); + } + tb.writeJavaFiles(pkgSrc, "package p; public class C { }\n"); + Path dfDir = ((docFileKind == Mode.MODULE) ? pkgSrc : pkgSrc.resolve("p")).resolve("doc-files"); + docFilesWriter.write(dfDir, i); + + Path api = base.resolve(apiName).resolve("api"); + Files.createDirectories(api); + Task.Result r = new JavadocTask(tb) + .sourcepath(pkgSrc) + .outdir(api) + .options(javadocOptions) + .run(); + r.writeAll(); + + options.addAll(List.of( + "--api", apiName, + apidiffSourcePathOption, src.toString(), + "--api-directory", api.toString())); + } + + options.addAll(List.of( + "--include", apidiffIncludes, + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + Map outMap = run(options, EnumSet.of(Result.DIFFS)); + } +} diff --git a/test/junit/apitest/EntityTest.java b/test/junit/apitest/EntityTest.java new file mode 100644 index 0000000..f3f2139 --- /dev/null +++ b/test/junit/apitest/EntityTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.JavadocTask; +import toolbox.Task; + +public class EntityTest extends APITester { + @Test + public void testEntities() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path outDir = run(base); + checkOutput(outDir.resolve("p/C.html"), + "entities aacute '\u00e1' agrave '\u00e0' nbsp '\u00A0' @-dec '@' at-hex '@' Tail."); + } + + Path run(Path base, String... extraOpts) throws IOException { + var options = new ArrayList(); + for (var phase : List.of("before", "after")) { + Path src = base.resolve(phase).resolve("src"); + tb.writeJavaFiles(src, + """ + package p; + /** + * First sentence. + * abc. #PHASE#. def. + * entities aacute 'á' agrave 'à' nbsp ' ' @-dec '@' at-hex '@' + * Tail. + */ + public class C { } + """.replace("#PHASE#", phase.toUpperCase(Locale.ROOT))); + + Path api = base.resolve(phase).resolve("api"); + Files.createDirectories(api); + Task.Result r = new JavadocTask(tb) + .sourcepath(src) + .outdir(api) + .options("-quiet", "p") + .run(); + r.writeAll(); + + options.addAll(List.of( + "--api", phase, + "--source-path", src.toString(), + "--api-directory", api.toString())); + } + + Path outDir = base.resolve("out"); + options.addAll(List.of("--output-directory", outDir.toString())); + options.addAll(List.of(extraOpts)); + options.addAll(List.of("--include", "p.*")); + + log.println(options); + + var outMap = run(options); + + return outDir; + + } +} diff --git a/test/junit/apitest/EnumTest.java b/test/junit/apitest/EnumTest.java new file mode 100644 index 0000000..755ef16 --- /dev/null +++ b/test/junit/apitest/EnumTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +/** + * Tests for the ability to compare {@code enum}s. + */ +public class EnumTest extends APITester { + + /** + * Tests field modifiers. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testFieldMods() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> { + String mods = (i == 0) ? "public" : "protected"; + return mods + " int f;"; + }); + } + + /** + * Tests field types. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testFieldTypes() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> (i == 0) + ? "public int f;" + : "public float f;" + + ); + } + + private void testFields(Path base, Function f) throws IOException { + List options = new ArrayList<>(); + + for (int i = 0; i < 2; i++) { + String apiName = "api" + i; + Path apiDir = base.resolve(apiName).resolve("src"); + + String mods = (i == 0) ? "public" : "protected"; + Path p = new ModuleBuilder(tb, "mA") + .exports("p") + .classes("package p; public enum E {\n A, B, C;\n" + f.apply(i) + "\n}\n") + .write(apiDir); + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } +} diff --git a/test/junit/apitest/FieldTest.java b/test/junit/apitest/FieldTest.java new file mode 100644 index 0000000..21304ef --- /dev/null +++ b/test/junit/apitest/FieldTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +/** + * Tests for the ability to compare fields. + */ +public class FieldTest extends APITester { + + /** + * Tests equal fields. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testSame() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> "public int f;" ); + } + + /** + * Tests fields with different modifiers. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testModifiers() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> { + String mods = (i == 0) ? "public" : "protected"; + return mods + " int f;"; + }); + } + + /** + * Tests fields with different types. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testType() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> (i == 0) + ? "public int f;" + : "public float f;" + ); + } + + /** + * Tests fields with different constant values. + * + * @throws IOException if an IO exception occurs + */ + @Test + void testValues() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> + "public final int i = " + i + ";\n" + + "public final char c = '" + ((char)('0' + i)) + "';\n" + + "public final String s = \"" + i + "\";\n" + + "public final boolean b = " + (i == 0 ? "false" : "true") + ";"); + } + + /** + * Tests fields with annotations with different values. + * + * @throws IOException if an IO exception occurs + */ + @Test + void testSimpleAnnotations() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> + "@SuppressWarnings(\"none\")\n" + + "@Deprecated(since=\"" + i + "\")\n" + + "public final int i;\n"); + } + + /** + * Tests characters with "special" values. + * It is not so much the whether differences are detected as whether values are written correctly, + * and with no exception occurring on output. + * + * @throws IOException if an IO exception occurs + */ + @Test + void testSpecialCharValues() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> + "public static final char c0 = 0;" + + "public static final char cn = '\\n';" + + "public static final char c31 = (char) 0x1f;" + + "public static final char cs = ' ';" + + "public static final char cq = '\\'';" + + "public static final char cMinLS = Character.MIN_LOW_SURROGATE;" + + "public static final char cMaxLS = Character.MAX_LOW_SURROGATE;" + + "public static final char cMinHS = Character.MIN_HIGH_SURROGATE;" + + "public static final char cMaxHS = Character.MAX_HIGH_SURROGATE;"); + } + + /** + * Tests fields with different doc comments. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testRawDocComments() throws IOException { + Path base = getScratchDir(); + log.println(base); + testFields(base, i -> { + String cs = "/**\n * This is 'same'.\n * Unchanged.\n * More.\n **/\n"; + String ci = "/**\n * This is 'insert'.\n" + (i == 1 ? " * Inserted.\n" : "") + " * More.\n **/\n"; + String cr = "/**\n * This is 'remove'.\n" + (i == 0 ? " * Removed.\n" : "") + " * More.\n **/\n"; + String cc = "/**\n * This is 'change'.\n * API " + i + "\n * More.\n **/\n"; + return cs + "public int same;\n" + + ci + "public int insert;\n" + + cr + "public int remove;\n" + + cc + "public int change;\n"; + }); + + } + + private void testFields(Path base, Function f) throws IOException { + List options = new ArrayList<>(); + + for (int i = 0; i < 2; i++) { + String apiName = "api" + i; + Path apiDir = base.resolve(apiName).resolve("src"); + + String mods = (i == 0) ? "public" : "protected"; + Path p = new ModuleBuilder(tb, "mA") + .exports("p") + .classes("package p; public class C {\n" + f.apply(i) + "\n}\n") + .write(apiDir); + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } +} diff --git a/test/junit/apitest/GetSerialVersionUIDTest.java b/test/junit/apitest/GetSerialVersionUIDTest.java new file mode 100644 index 0000000..2abd47a --- /dev/null +++ b/test/junit/apitest/GetSerialVersionUIDTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.ObjectStreamClass; +import java.io.PrintWriter; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.spi.ToolProvider; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.lang.model.element.TypeElement; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; + +import com.sun.source.util.DocTrees; +import com.sun.source.util.JavacTask; + +import jdk.codetools.apidiff.model.SerializedForm; +import jdk.codetools.apidiff.model.SerializedFormFactory; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; + +public class GetSerialVersionUIDTest extends APITester { + + ToolProvider javac = ToolProvider.findFirst("javac").orElseThrow(() -> new Error("can't find javac")); + + public static Stream provideLocalClasses() { + return Stream.of( + Arguments.of("", ""), + Arguments.of("", "int i;"), + Arguments.of("", "Object o;"), + Arguments.of("", "byte[] ba;"), + Arguments.of("", "String[] sa;"), + Arguments.of("", "int i; Object o;"), + Arguments.of("", "int i; static Object o;"), + Arguments.of("", "int i; transient Object o;"), + Arguments.of("", "byte b; static int i;"), + Arguments.of("", "byte b; static int i0 = 42;"), + Arguments.of("", "byte b; static final int i1 = 42;"), + Arguments.of("", "byte b; static int i2 = Integer.parseInt(\"1\");"), + Arguments.of("", "byte b; static Object o1 = null;"), + Arguments.of("", "byte b; static final Object o2 = null;"), + Arguments.of("", "byte b1; static { System.out.println(\"HW\"); }"), + Arguments.of("", """ + int i; + /** @serialField i int an int field */ + private static final ObjectStreamField[] serialPersistentFields = { + new ObjectStreamField("i", int.class) + };"""), + Arguments.of("", "int i; static final long serialVersionUID = 123;"), + Arguments.of("Runnable", "String[] sa; public void run() { }"), + Arguments.of("Runnable, java.util.concurrent.Callable", + "String[] sa; public void run() { } public Object call() { return null; }") + ); + } + + @ParameterizedTest + @MethodSource("provideLocalClasses") + public void testLocal(String interfaces, String members) throws Exception { + log.printf("interfaces: %s; members: %s%n", interfaces, members); + Path base = getScratchDir().resolve(getDirectory(interfaces, members)); + log.println(base); + + Path src = Files.createDirectories(base.resolve("src")); + + StringBuilder sb = new StringBuilder(); + sb.append("package p;\n") + .append("import java.io.*;\n") + .append("public class C implements Serializable"); + if (!interfaces.isEmpty()) { + sb.append(", ").append(interfaces); + } + sb.append(" {\n"); + if (!members.isEmpty()) { + Stream.of(members.split(";\\s*")).forEach(s -> sb.append(" ").append(s).append(";\n")); + } + sb.append("}\n"); + tb.writeJavaFiles(src, sb.toString()); + + Path classes = base.resolve("classes"); + List options = new ArrayList<>(); + options.addAll(List.of("-d", classes.toString())); + Arrays.stream(tb.findJavaFiles(src)).map(Object::toString).forEach(options::add); + javac.run(log, log, options.toArray(new String[0])); + + long platformSerialVersionUID = getPlatformSerialVersionUID(classes, "p.C"); + log.println("platform " + platformSerialVersionUID); + + long sourceSerialVersionUID = getSourceSerialVersionUID(src, "p.C"); + log.println("source " + sourceSerialVersionUID); + + long classSerialVersionUID = getClassSerialVersionUID(classes, "p.C"); + log.println("class " + classSerialVersionUID); + + Assertions.assertEquals(platformSerialVersionUID, sourceSerialVersionUID, "serialVersionUID from source"); + Assertions.assertEquals(platformSerialVersionUID, classSerialVersionUID, "serialVersionUID from class"); + } + + public static Stream provideSystemClasses() { + return Stream.of( + Arguments.of("java.lang.Exception"), + Arguments.of("java.io.IOException"), + Arguments.of("java.awt.Component") + ); + } + + @ParameterizedTest + @MethodSource("provideSystemClasses") + public void testSystemClass(String name) throws Exception { + log.println(name); + + Path base = getScratchDir(); + Path classes = Files.createDirectories(base.resolve("classes")); + + long platformSerialVersionUID = getPlatformSerialVersionUID(classes, name); + log.println("platform " + platformSerialVersionUID); + + long classSerialVersionUID = getClassSerialVersionUID(classes, name); + log.println("class " + classSerialVersionUID); + + Assertions.assertEquals(platformSerialVersionUID, classSerialVersionUID, "serialVersionUID from class"); + } + + Path getDirectory(String interfaces, String members) { + String i = Stream.of(interfaces.split("\\s+")) + .map(s -> s.substring(s.lastIndexOf(".") + 1)) + .collect(Collectors.joining("-")); + String m = Stream.of(members.split(";\\s*")) + .map(s -> s.replaceAll("=.*", "")) + .map(s -> s.substring(s.lastIndexOf(" ") + 1)) + .collect(Collectors.joining("-")); + String sep = i.isEmpty() || m.isEmpty() ? "" : "-"; + return Path.of(i + sep + m); + } + + long getPlatformSerialVersionUID(Path classes, String name) throws Exception { + URLClassLoader cl = new URLClassLoader(new URL[] { classes.toUri().toURL()}); + Class c = cl.loadClass(name); + ObjectStreamClass osc = ObjectStreamClass.lookup(c); + return osc.getSerialVersionUID(); + } + + long getSourceSerialVersionUID(Path src, String name) throws Exception { + JavaCompiler javac = javax.tools.ToolProvider.getSystemJavaCompiler(); + List options = List.of("-proc:only", "--source-path", src.toString()); + List classes = List.of(name); + List files = List.of(); + PrintWriter out = new PrintWriter(log, true); + JavacTask task = (JavacTask) javac.getTask(out, null, null, options, classes, files); + task.analyze(); + SerializedFormFactory sff = getSerializedFormFactory(task); + TypeElement te = task.getElements().getTypeElement(name); + SerializedForm sf = sff.get(te); + return sf.getSerialVersionUID(); + } + + long getClassSerialVersionUID(Path classes, String name) throws Exception { + JavaCompiler javac = javax.tools.ToolProvider.getSystemJavaCompiler(); + List options = List.of("-proc:only", "--class-path", classes.toString()); + List classes2 = List.of(name); + List files = List.of(); + PrintWriter out = new PrintWriter(log, true); + JavacTask task = (JavacTask) javac.getTask(out, null, null, options, classes2, files); + task.analyze(); + SerializedFormFactory sff = getSerializedFormFactory(task); + TypeElement te = task.getElements().getTypeElement(name); + SerializedForm sf = sff.get(te); + return sf.getSerialVersionUID(); + } + + private SerializedFormFactory getSerializedFormFactory(JavacTask task) { + return new SerializedFormFactory(task.getElements(), task.getTypes(), DocTrees.instance(task)); + } +} diff --git a/test/junit/apitest/HtmlDiffBuilderTest.java b/test/junit/apitest/HtmlDiffBuilderTest.java new file mode 100644 index 0000000..d846ae0 --- /dev/null +++ b/test/junit/apitest/HtmlDiffBuilderTest.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Messages; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.TagName; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.APIMap; +import jdk.codetools.apidiff.report.html.HtmlDiffBuilder; +import jdk.codetools.apidiff.report.html.ResultTable.CountKind; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; + +/** + * Unit tests for the {@code TextDiffBuilder} class. + */ +public class HtmlDiffBuilderTest extends APITester { + /** + * Tests the behavior when the two sets of input are equal. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testEqual() throws IOException { + List list1 = lines(10); + List list2 = new ArrayList<>(list1); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when a line is inserted into the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testSimpleInsert() throws IOException { + List list1 = lines(10); + List list2 = new ArrayList<>(list1); + list2.add(5, "inserted line"); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when a line is removed from the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testSimpleDelete() throws IOException { + List list1 = lines(10); + List list2 = new ArrayList<>(list1); + list2.remove(5); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when a line is changed in the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testSimpleChange() throws IOException { + List list1 = lines(20); + List list2 = new ArrayList<>(list1); + list2.set(5, "changed line"); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when multiple changes are made in the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testMultiple() throws IOException { + List list1 = lines(20, 32); + List list2 = new ArrayList<>(list1); + list2.add(3, "inserted line"); + list2.set(10, "changed line"); + list2.remove(15); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when changes are made in different parts of the modified set, + * such that they are presented as disjoint differences. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDisjoint() throws IOException { + List list1 = IntStream.range(1, 50) + .mapToObj(i -> ("line:" + i)) + .collect(Collectors.toList()); + List list2 = new ArrayList<>(list1); + list2.add(10, "insert"); + list2.set(20, "change"); + list2.remove(30); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior if a style is added. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testStyleAdded() throws IOException { + List list1 = lines(20); + List list2 = new ArrayList<>(list1); + list2.set(5, list2.get(5).replaceAll("^(\\S+\\s+)(\\S+)(.*)", "$1$2$3")); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior if the change is just to the HTML style. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testStyleChange() throws IOException { + List list1 = lines(20); + List list2 = new ArrayList<>(list1); + list1.set(5, list1.get(5).replaceAll("^(\\S+\\s+)(\\S+)(.*)", "$1$2$3")); + list2.set(5, list2.get(5).replaceAll("^(\\S+\\s+)(\\S+)(.*)", "$1$2$3")); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior if a style is removed. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testStyleRemoved() throws IOException { + List list1 = lines(20); + List list2 = new ArrayList<>(list1); + list1.set(5, list1.get(5).replaceAll("^(\\S+\\s+)(\\S+)(.*)", "$1$2$3")); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior if a block element, like a heading, is changed. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testHeadingChanged() throws IOException { + List list1 = lines(20); + List list2 = new ArrayList<>(list1); + list1.set(3, "

                " + list1.get(3) + "

                "); + list2.set(3, "

                " + list2.get(3) + "

                "); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior if a link is changed. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testLinkChanged() throws IOException { + List list1 = lines(20); + List list2 = new ArrayList<>(list1); + list1.set(3, "" + list1.get(3) + ""); + list2.set(3, "" + list2.get(3) + ""); + test(getScratchDir(), list1, list2); + } + + void test(Path dir, List list1, List list2) throws IOException { + test(dir, String.join("
                \n", list1), String.join("
                \n", list2)); + } + + void test(Path dir, String html1, String html2) throws IOException { + try (PrintWriter out = wrap(System.out); PrintWriter err = wrap(System.err)) { + APIMap apiMap = APIMap.of(); + apiMap.put(new TestAPI("api1"), html1); + apiMap.put(new TestAPI("api2"), html1); + Log log = new Log(out, err); + Messages msgs = Messages.instance("jdk.codetools.apidiff.report.html.resources.report"); + HtmlDiffBuilder b = new HtmlDiffBuilder(apiMap.keySet(), log, msgs); + Map counts = new EnumMap<>(CountKind.class); + List c = b.build(apiMap, ck -> counts.put(ck, counts.computeIfAbsent(ck, ck_ -> 0) + 1)); + try (Writer w = Files.newBufferedWriter(dir.resolve("out.html"))) { + HtmlTree head = HtmlTree.HEAD("utf-8", "test") + .add(new HtmlTree(TagName.STYLE, new Text(style))); + HtmlTree body = HtmlTree.BODY(c); + HtmlTree html = new HtmlTree(TagName.HTML, head, body); + html.write(w); + } + log.out.println("Counts: " + counts); + } + } + + PrintWriter wrap(PrintStream out) { + return new PrintWriter(out) { + @Override + public void close() { + flush(); + } + }; + } + + List lines(int size) { + return lines(size, 64); + } + + List lines(int lineCount, int lineLength) { + List lines = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + Pattern ws = Pattern.compile("\\s"); + Matcher m = ws.matcher(lorem_ipsum); + int start = 0; + while (m.find()) { + // wrap long lines + if (sb.length() + (m.start() - start) > lineLength) { + lines.add(sb.toString()); + if (lines.size() > lineCount) { + return lines; + } + sb = new StringBuilder(); + } + + // append word + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(lorem_ipsum, start, m.start()); + + // handle explicit newline + if (m.group().equals("\n")) { + lines.add(sb.toString()); + if (lines.size() > lineCount) { + return lines; + } + sb = new StringBuilder(); + } + + start = m.end(); + } + sb.append(lorem_ipsum.substring(start)); + lines.add(sb.toString()); + System.err.println(lines); + return lines; + } + + // TODO: consider using apidiff.css either by linking to it + // or copying it inline. + private static final String style = """ + div.hdiffs { + margin: 2px 10px; + padding: 2px 2px; + border: 1px solid grey; + } + + div.hdiffs-title { + padding-left: 2em; + text-weight: bold; + background-color: #eee; + border-bottom: 1px solid grey; + margin-bottom: 5px;} + .hdiffs span.diff-html-added { background-color: #bfb } + .hdiffs span.diff-html-changed { background-color: #ffb } + .hdiffs span.diff-html-removed { background-color: #fbb; } + """; + + private static final String lorem_ipsum = LoremIpsum.text; +} diff --git a/test/junit/apitest/HtmlParserTest.java b/test/junit/apitest/HtmlParserTest.java new file mode 100644 index 0000000..2c88ae8 --- /dev/null +++ b/test/junit/apitest/HtmlParserTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import jdk.codetools.apidiff.model.HtmlParser; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; + +public class HtmlParserTest extends APITester { + @Test + public void testSimple() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, "content", + "Start: tag {} false", + "Content: content", + "End: tag"); + } + + @Test + public void testAttr() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, "", + "Start: tag {a1=value, a2=value, a3=value} false"); + } + + @Test + public void testMultilineContent() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, "line 1\nline 2", + "Start: tag {} false", + "Content: line 1\\n", + "Content: line 2", + "End: tag"); + } + + @Test + public void testComment() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, "beforeafter", + "Start: tag {} false", + "Content: before", + "Comment: comment", + "Content: after", + "End: tag"); + } + + @Test + public void testDocType() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base,"", + "DocType: doctype html", + "Start: head {} false"); + } + + @Test + public void testSample() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base,"This is the titleThis is contentlink", + "Start: html {} false", + "Start: head {} false", + "Start: title {} false", + "Content: This is the title", + "End: title", + "End: head", + "Start: body {} false", + "Content: This is content", + "Start: a {href=#foo} false", + "Content: link", + "End: a", + "End: body", + "End: html"); + } + + private void test(Path base, String html, String... expect) throws IOException { + // avoid using .html extension, to avoid 'tidy' errors + Path file = base.resolve("test.htmlx"); + Files.writeString(file, html); + List list = new ArrayList<>(); + HtmlParser p = new HtmlParser() { + @Override + public void startElement(String name, Map attrs, boolean selfClosing) { + record("Start: " + name + " " + attrs + " " + selfClosing); + } + + @Override + public void endElement(String name) { + record("End: " + name); + + } + + @Override + public void content(Supplier content) { + record("Content: " + content.get().replace("\n", "\\n")); + } + + @Override + public void comment(Supplier comment) { + record("Comment: " + comment.get().replace("\n", "\\n")); + } + + @Override + public void doctype(Supplier doctype) { + record("DocType: " + doctype.get().replace("\n", "\\n")); + } + + @Override + protected void error(Path file, int lineNumber, String message) { + System.err.println("Error: " + file + ":" + lineNumber + ":" + message); + } + + @Override + protected void error(Path file, int lineNumber, Throwable t) { + System.err.println("Error: " + file + ":" + lineNumber + ":" + t); + + } + + private void record(String msg) { + System.out.println(msg); + list.add(msg); + } + }; + + p.read(file); + Assertions.assertEquals(List.of(expect), list); + } +} diff --git a/test/junit/apitest/LoremIpsum.java b/test/junit/apitest/LoremIpsum.java new file mode 100644 index 0000000..22d33b3 --- /dev/null +++ b/test/junit/apitest/LoremIpsum.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +/** + * A class to provide a chunk of "lorem ipsum" text, + * and to encapsulate the spelling errors inherent therein! + */ +public class LoremIpsum { + static final String text; + + static { + text = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et \ + dolore magna aliqua. Dolor sed viverra ipsum nunc aliquet bibendum enim. In massa tempor nec feugiat. \ + Nunc aliquet bibendum enim facilisis gravida. Nisl nunc mi ipsum faucibus vitae aliquet nec ullamcorper. \ + Amet luctus venenatis lectus magna fringilla. Volutpat maecenas volutpat blandit aliquam etiam erat \ + velit scelerisque in. Egestas egestas fringilla phasellus faucibus scelerisque eleifend. Sagittis orci \ + a scelerisque purus semper eget duis. Nulla pharetra diam sit amet nisl suscipit. Sed adipiscing diam \ + donec adipiscing tristique risus nec feugiat in. Fusce ut placerat orci nulla. Pharetra vel turpis nunc \ + eget lorem dolor. Tristique senectus et netus et malesuada.\ + \ + Etiam tempor orci eu lobortis elementum nibh tellus molestie. Neque egestas congue quisque egestas. \ + Egestas integer eget aliquet nibh praesent tristique. Vulputate mi sit amet mauris. Sodales neque \ + sodales ut etiam sit. Dignissim suspendisse in est ante in. Volutpat commodo sed egestas egestas. \ + Felis donec et odio pellentesque diam. Pharetra vel turpis nunc eget lorem dolor sed viverra. \ + Porta nibh venenatis cras sed felis eget. Aliquam ultrices sagittis orci a. Dignissim diam quis enim \ + lobortis. Aliquet porttitor lacus luctus accumsan. Dignissim convallis aenean et tortor at risus \ + viverra adipiscing at."""; + } + + private LoremIpsum() { } +} diff --git a/test/junit/apitest/MainTest.java b/test/junit/apitest/MainTest.java new file mode 100644 index 0000000..77bf5c2 --- /dev/null +++ b/test/junit/apitest/MainTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import jdk.codetools.apidiff.Main; +import jdk.codetools.apidiff.Main.Result; +import jdk.codetools.apidiff.Messages; +import jdk.codetools.apidiff.Options; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Basic tests for the main program. + */ +public class MainTest extends APITester { + + /** + * Tests the {@code --help} option. + * + * The test verifies that all the necessary resources are defined + * and that the corresponding value appears in the output generated + * by the option. + */ + @Test + public void testHelp() { + for (String help : Options.Option.HELP.getNames()) { + Map outMap = run(List.of(help)); + String out = outMap.get(OutputKind.OUT); + for (Options.Option option : Options.Option.values()) { + option.getNames().forEach(name -> assertTrue(out.contains(name))); + } + assertFalse(out.contains("opt.desc.")); + assertFalse(out.contains("opt.arg.")); + Messages messages = Messages.instance("jdk.codetools.apidiff.resources.help"); + for (String key : messages.getKeys()) { + if (key.startsWith("opt.arg")) { + assertTrue(out.contains(messages.getString(key))); + } + if (key.startsWith("opt.desc")) { + messages.getString(key).lines().forEach(line -> + assertTrue(line.contains("{0}") || out.contains(line))); + } + } + } + } + + @Test + public void testBadOption() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "mA") + .exports("p") + .classes("package p; public class C { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-notAnOption", + "--help", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options, EnumSet.of(Main.Result.BAD_ARGS)); + + String err = outMap.get(OutputKind.ERR); + assertTrue(err.contains("unknown option")); + + // verify processing stopped after detecting a bad option + String out = outMap.get(OutputKind.OUT); + assertFalse(out.contains("help")); + assertFalse(out.contains("Completed comparison:")); + } + + @Test + public void testMakeOutputDirectory() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "mA") + .exports("p") + .classes("package p; public class C { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + assertTrue(Files.exists(base.resolve("out"))); + } + + @Test + public void testSyntaxError() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "mA") + .exports("p") + .classes("package p; public class C " + (a == 0 ? "{ }" : "") +"\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options, EnumSet.of(Result.FAULT)); + + String err = outMap.get(OutputKind.ERR); + assertTrue(err.contains("1 error")); + + String out = outMap.get(OutputKind.ERR); + assertFalse(out.contains("Completed comparison:")); + } +} diff --git a/test/junit/apitest/MethodTest.java b/test/junit/apitest/MethodTest.java new file mode 100644 index 0000000..abbe6d9 --- /dev/null +++ b/test/junit/apitest/MethodTest.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +/** + * Tests for the ability to compare methods. + */ +public class MethodTest extends APITester { + + /** + * Tests methods with different modifiers. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testModifiers() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> { + String mods = (i == 0) ? "public" : "protected"; + return mods + " void m() { }"; + }); + } + + /** + * Tests methods with different return types. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testReturnType() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> (i == 0) + ? "public void m() { }" + : "public int m() { return 0; }" + + ); + } + + /** + * Tests methods with different receiver types. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testReceiverType() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> (i == 0) + ? "public void m(int m) { }" + : "public void m(@Anno C this, int m) { }" + + ); + } + + /** + * Tests methods with different annotations on parameters. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testParameterAnnos() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> (i == 0) + ? "public void m(int m) { }" + : "public void m(@Anno int m) { }" + + ); + } + + /** + * Tests methods with different modifiers on annotations. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testParameterModifiers() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> (i == 0) + ? "public void m(int m) { }" + : "public void m(final int m) { }" + + ); + } + + /** + * Tests methods with different names of parameters. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testParameterNames() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> (i == 0) + ? "public void m(int m0) { }" + : "public void m(int m1) { }" + + ); + } + + /** + * Tests methods with different exceptions declared to be thrown. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testThrows() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> new StringBuilder() + .append("public void m1()") + .append(i == 0 ? "" : " throws Exception") + .append(" { }") + .append("public void m2() throws ") + .append(i == 0 ? "Exception" : "Error") + .append(" { }") + .toString()); + } + + /** + * Tests methods with different doc comments. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testRawDocComments() throws IOException { + Path base = getScratchDir(); + log.println(base); + testMethods(base, i -> { + String cs1 = "/**\n * This is 'same()'.\n * Unchanged.\n * More.\n **/\n"; + String ci1 = "/**\n * This is 'insert1()'.\n" + (i == 1 ? " * Inserted.\n" : "") + " * More.\n **/\n"; + String ci2 = "/**\n * This is 'insert2()'." + + (i == 1 ? " Inserted 1\n * Inserted 2\n * Inserted 3" : "") + " rest of line\n * More.\n **/\n"; + String ci3 = "/**\n * This is 'insert3()'." + + (i == 1 ? " Inserted." : "") + " rest of line\n * More.\n **/\n"; + String cr1 = "/**\n * This is 'remove1()'.\n" + (i == 0 ? " * Removed.\n" : "") + " * More.\n **/\n"; + String cr2 = "/**\n * This is 'remove2()'." + + (i == 0 ? " Removed 1\n * Removed 2\n * Removed 3" : "") + " rest of line\n * More.\n **/\n"; + String cr3 = "/**\n * This is 'remove3()'." + + (i == 0 ? " Removed." : "") + " rest of line\n * More.\n **/\n"; + String cc1 = "/**\n * This is 'change()'.\n * API " + i + "\n * More.\n **/\n"; + return cs1 + "public void same() { }\n" + + ci1 + "public void insert1() { }\n" + + ci2 + "public void insert2() { }\n" + + ci3 + "public void insert3() { }\n" + + cr1 + "public void remove1() { }\n" + + cr2 + "public void remove2() { }\n" + + cr3 + "public void remove3() { }\n" + + cc1 + "public void change() { }\n"; + }); + } + + private void testMethods(Path base, Function f) throws IOException { + List options = new ArrayList<>(); + + for (int i = 0; i < 2; i++) { + String apiName = "api" + i; + Path apiDir = base.resolve(apiName).resolve("src"); + + String mods = (i == 0) ? "public" : "protected"; + new ModuleBuilder(tb, "mA") + .exports("p") + .classes("package p; import java.lang.annotation.*; " + + "@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE}) " + + "@interface Anno { }") + .classes("package p; public class C {\n" + f.apply(i) + "\n}\n") + .write(apiDir); + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } +} diff --git a/test/junit/apitest/MissingTest.java b/test/junit/apitest/MissingTest.java new file mode 100644 index 0000000..146bd01 --- /dev/null +++ b/test/junit/apitest/MissingTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +/** + * Tests for the ability to detect missing items. + */ +public class MissingTest extends APITester { + @Test + public void testMissingModule() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + for (int a = 0; a < 3; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int i = 0; i < 3; i++) { + if (a != 1 || i != 1) { + new ModuleBuilder(tb, "m." + (char)('A' + i)) + .exports("p" + i) + .classes("package p" + i + "; public class C { }") + .write(apiDir); + } + } + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + + options.addAll(List.of( + "-XDshow-debug-summary", + "-XDtrace-reporter", + "--include", "m.*/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testMissingPackage() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + for (int a = 0; a < 3; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "m"); + for (int i = 0; i < 3; i++) { + if (a != 1 || i != 1) { + mb.exports("p" + i).classes("package p" + i + "; public class C { }"); + } + } + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testMissingType() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + for (int a = 0; a < 3; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "m"); + mb.exports("p"); + for (int i = 0; i < 3; i++) { + if (a != 1 || i != 1) { + mb.classes("package p; public class C" + i + " { }"); + } + } + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testMissingField() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + for (int a = 0; a < 3; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "m"); + mb.exports("p"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3; i++) { + if (a != 1 || i != 1) { + sb.append("public int f" + i + "; "); + } + } + mb.classes("package p; public class C { " + sb + "}"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testMissingMethod() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + for (int a = 0; a < 3; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "m"); + mb.exports("p"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3; i++) { + if (a != 1 || i != 1) { + sb.append("public void m" + i + "(); "); + } + } + mb.classes("package p; public class C { " + sb + "}"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } +} diff --git a/test/junit/apitest/ModuleTest.java b/test/junit/apitest/ModuleTest.java new file mode 100644 index 0000000..487ef00 --- /dev/null +++ b/test/junit/apitest/ModuleTest.java @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +/** + * Tests for the ability to compare modules. + */ +public class ModuleTest extends APITester { + /** + * Tests handling of missing modules. + * Three APIs are generated. + *
                  + *
                • all APIs contain equal definitions of module mA + *
                • two APIs contain equal definitions of mB + *
                • only one API contains a definition of mC + *
                + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testMissingModules() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 3; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int m = 0; m <= a; m++) { + new ModuleBuilder(tb, "m%m%".replace("%m%", String.valueOf((char) ('A' + m)))) + .exports("p%m%".replace("%m%", String.valueOf(m))) + .classes("package p%m%; public class C%m% { }\n".replace("%m%", String.valueOf(m))) + .write(apiDir); + } + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "--include", "mB/**", + "--include", "mC/**", + "-d", base.resolve("out").toString(), + "--verbose", "missing")); + + log.println("Options: " + options); + + Map outMap = run(options); + long notFound = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found")) + .count(); + Assertions.assertEquals(3, notFound); + + } + + /** + * Tests handling of modules with different modifiers. + * Two APIs are generated, containing four modules. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDifferentModifiers() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int m = 0; m < 4; m++) { + boolean open = (m & (1 << a)) != 0; + new ModuleBuilder(tb, open, "m%m%".replace("%m%", String.valueOf((char) ('A' + m)))) + .exports("p%m%".replace("%m%", String.valueOf(m))) + .classes("package p%m%; public class C%m% { }\n".replace("%m%", String.valueOf(m))) + .write(apiDir); + } + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "--include", "mB/**", + "--include", "mC/**", + "--include", "mD/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } + + /** + * Tests handling of modules with different doc comments. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testRawDocComments() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + String cs = "/**\n * This is 'm.same'.\n * Unchanged.\n * More.\n **/\n"; + String ci = "/**\n * This is 'm.insert'.\n" + (a == 1 ? " * Inserted.\n" : "") + " * More.\n **/\n"; + String cr = "/**\n * This is 'm.remove'.\n" + (a == 0 ? " * Removed.\n" : "") + " * More.\n **/\n"; + String cc = "/**\n * This is 'm.change'.\n * API " + a + "\n * More.\n **/\n"; + + new ModuleBuilder(tb, "m.same") + .comment(cs) + .classes("package p; public class C { }") + .write(apiDir); + + new ModuleBuilder(tb, "m.insert") + .comment(ci) + .classes("package p; public class C { }") + .write(apiDir); + + new ModuleBuilder(tb, "m.remove") + .comment(cr) + .classes("package p; public class C { }") + .write(apiDir); + + new ModuleBuilder(tb, "m.change") + .comment(cc) + .classes("package p; public class C { }") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "m.same/**", + "--include", "m.insert/**", + "--include", "m.remove/**", + "--include", "m.change/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } + + @Test + public void testDifferentRequiresModule() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "mA") + .requires(a == 0 ? "java.compiler" : "jdk.compiler") + .exports("p") + .classes("package p; public class C { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } + + @Test + public void testDifferentRequiresStatic() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + boolean isStatic = (a != 0); + new ModuleBuilder(tb, false, "mA") + .requires("java.compiler", isStatic, false) + .exports("p") + .classes("package p; public class C { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } + + @Test + public void testDifferentRequiresTransitive() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + boolean isTransitive = (a != 0); + new ModuleBuilder(tb, false, "mA") + .requires("java.compiler", false, isTransitive) + .exports("p") + .classes("package p; public class C { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testDifferentExportTargets() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "m") + .exports("p1") + .exportsTo("p2", + "mX", "m%a%".replace("%a%", String.valueOf((char) ('A' + a))), "mZ") + .classes("package p1; public class C { }\n") + .classes("package p2; public class C { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testDifferentOpenTargets() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "m") + .opens("p1") + .opensTo("p2", + "mX", "m%a%".replace("%a%", String.valueOf((char) ('A' + a))), "mZ") + .classes("package p1; public class C { }\n") + .classes("package p2; public class C { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testDifferentProvides() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "m") + .exports("p") + .provides("p.S", + "p.I0", "p.I%a%".replace("%a%", String.valueOf(a + 1)), "p.I3") + .classes("package p; public class S { }\n") + .classes("package p; public class I0 extends S { }\n") + .classes("package p; public class I1 extends S { }\n") + .classes("package p; public class I2 extends S { }\n") + .classes("package p; public class I3 extends S { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testDifferentUses() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + new ModuleBuilder(tb, false, "m") + .exports("p") + .uses("p.S0") + .uses("p.S%a%".replace("%a%", String.valueOf(a + 1))) + .uses("p.S3") + .classes("package p; public class S0 { }\n") + .classes("package p; public class S1 { }\n") + .classes("package p; public class S2 { }\n") + .classes("package p; public class S3 { }\n") + .write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "-XDshow-debug-summary", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } +} diff --git a/test/junit/apitest/NoArgConstructorTest.java b/test/junit/apitest/NoArgConstructorTest.java new file mode 100644 index 0000000..f70425f --- /dev/null +++ b/test/junit/apitest/NoArgConstructorTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; + +public class NoArgConstructorTest extends APITester { + @Test + public void addNoArgConstructor() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, + "package p;\n" + + "public class C { }"); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, + """ + package p; + public class C { + /** Explicit no-args constructor. */ + public C() { } + }"""); + + List options = List.of( + "--api", "A", + "--source-path", srcA.toString(), + "--api", "B", + "--source-path", srcB.toString(), + "--include", "p.*", + "-d", base.resolve("out").toString(), + "--verbose", "differences"); + + log.println("Options: " + options); + + Map outMap = run(options); + long differences = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Different raw doc comments for constructor p.C#C()")) + .count(); + Assertions.assertEquals(1, differences); + } + + @Test + public void addPrivateNoArgConstructor() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; + public class C { }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB,""" + package p; + public class C { + /** Private no-args constructor. */ + private C() { } + }"""); + + List options = List.of( + "--api", "A", + "--source-path", srcA.toString(), + "--api", "B", + "--source-path", srcB.toString(), + "--include", "p.*", + "-d", base.resolve("out").toString(), + "--verbose", "missing"); + + log.println("Options: " + options); + + Map outMap = run(options); + long notFound = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found in API 'B': constructor p.C#C()")) + .count(); + Assertions.assertEquals(1, notFound); + + } +} diff --git a/test/junit/apitest/NotesTest.java b/test/junit/apitest/NotesTest.java new file mode 100644 index 0000000..de223bc --- /dev/null +++ b/test/junit/apitest/NotesTest.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.Elements; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import com.sun.source.util.JavacTask; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.Notes; +import jdk.codetools.apidiff.Notes.Entry; +import jdk.codetools.apidiff.model.ElementKey; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +public class NotesTest extends APITester { + @Test + public void testGetEntries() throws IOException { + Path base = getScratchDir(); + log.println(base); + String text =""" + # this is a comment + + http://openjdk.org/jeps/0 JEP 0 + mA + mA/* + mA/p + mA/p.* + mA/p.C.* + mA/p.C + mA/p.C#f1 + mA/p.C#m1(int) + """; + readNotes(base, text); + + Path src = base.resolve("src"); + for (int i = 0; i < 2; i++){ + ModuleBuilder mb = new ModuleBuilder(tb, "m" + (char)('A' + i)); + mb.classes(""" + package p; public class C { + public static class Nested { } + public int f1; + public int f2; + public void m1(String s) { } + public void m1(int i) { } + public void m2(String s) { } + public void m2(int i) { } + }""", + "package p;") + .write(src); + } + + JavaCompiler c = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fm = c.getStandardFileManager(null, null, null); + fm.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, List.of(src)); + List options = List.of("-proc:only"); + Iterable files = fm.getJavaFileObjects(tb.findFiles(".java", src)); + JavacTask t = (JavacTask) c.getTask(new PrintWriter(log, true), fm, null, options, null, files); + t.analyze(); + + Elements elements = t.getElements(); + ModuleElement mA = elements.getModuleElement("mA"); + checkEntries(mA, + "Entry[name=mA,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=false]", + "Entry[name=mA,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]"); + + PackageElement p = elements.getPackageElement(mA, "p"); + checkEntries(p, + "Entry[name=mA,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]", + "Entry[name=mA/p,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=false]", + "Entry[name=mA/p,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]"); + + TypeElement pC = elements.getTypeElement(mA, "p.C"); + checkEntries(pC, + "Entry[name=mA,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]", + "Entry[name=mA/p,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]", + "Entry[name=mA/p.C,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]", + "Entry[name=mA/p.C,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=false]"); + + TypeElement pCN = elements.getTypeElement(mA, "p.C.Nested"); + checkEntries(pCN, + "Entry[name=mA,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]", + "Entry[name=mA/p,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]", + "Entry[name=mA/p.C,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=true]"); + + VariableElement f1 = getField(pC, "f1"); + checkEntries(f1, + "Entry[name=mA/p.C#f1,uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=false]"); + VariableElement f2 = getField(pC, "f2"); + checkEntries(f2); + + ExecutableElement m1_int = getMethod(pC, "m1", "int"); + checkEntries(m1_int, + "Entry[name=mA/p.C#m1(int),uri=http://openjdk.org/jeps/0,description=JEP 0,recursive=false]"); + + ExecutableElement m1_String = getMethod(pC, "m1", "java.lang.String"); + checkEntries(m1_String); + checkEntries(f2); + + ExecutableElement m2_int = getMethod(pC, "m2", "int"); + checkEntries(m2_int); + } + + @Test + public void testBadLine() throws IOException { + Path base = getScratchDir(); + log.println(base); + String text = """ + # this is a comment + + http:jeps/0 JEP 0 + m/p/C stuff + """; + readNotes(base, text); + checkError("notes.txt:4: bad line: m/p/C stuff"); + } + + @Test + public void testBadSignature() throws IOException { + Path base = getScratchDir(); + log.println(base); + String text = """ + # this is a comment + + http:jeps/0 JEP 0 + m/p/C + """; + readNotes(base, text); + checkError("notes.txt:4: bad signature: m/p/C"); + } + + @Test + public void testBadURI() throws IOException { + Path base = getScratchDir(); + log.println(base); + String text = """ + # this is a comment + + http: JEP 0 + m/p.C + """; + readNotes(base, text); + checkError("notes.txt:3: bad uri: http:"); + } + + @Test + public void testNoCurrentURI() throws IOException { + Path base = getScratchDir(); + log.println(base); + String text = """ + # this is a comment + + m/p.C + """; + readNotes(base, text); + checkError("notes.txt:3: no current URI and description"); + } + + @Test + public void testModule() throws IOException { + Path base = getScratchDir(); + log.println(base); + Path notes = base.resolve("notes.txt"); + Files.writeString(notes,""" + http://example.com/module example-module-A + mA + http://example.com/module example-module-B + mB + """); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int m = 0; m <= a; m++) { + new ModuleBuilder(tb, "m%m%".replace("%m%", String.valueOf((char) ('A' + m)))) + .exports("p%m%".replace("%m%", String.valueOf(m))) + .classes("package p%m%; public class C%m% { }\n".replace("%m%", String.valueOf(m))) + .write(apiDir); + } + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--notes", notes.toString(), + "--include", "mA/**", + "--include", "mB/**", + "--include", "mC/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testPackage() throws IOException { + Path base = getScratchDir(); + log.println(base); + Path notes = base.resolve("notes.txt"); + Files.writeString(notes, """ + http://example.com/module example-module-A + mA/* + http://example.com/module example-module-A-p0 + mA/p0 + http://example.com/module example-module-B + mB/* + """); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int m = 0; m <= a; m++) { + new ModuleBuilder(tb, "m%m%".replace("%m%", String.valueOf((char) ('A' + m)))) + .exports("p%m%".replace("%m%", String.valueOf(m))) + .classes("package p%m%; public class C%m% { }\n".replace("%m%", String.valueOf(m))) + .write(apiDir); + } + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--notes", notes.toString(), + "--include", "mA/**", + "--include", "mB/**", + "--include", "mC/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testMethod() throws IOException { + Path base = getScratchDir(); + log.println(base); + Path notes = base.resolve("notes.txt"); + Files.writeString(notes, """ + http://example.com/module example-module-A + mA/* + http://example.com/module example-module-A-p0 + mA/p0 + http://example.com/module example-module-A-p0-C0-m + mA/p0.C0#m() + http://example.com/module example-module-B + mB/* + """); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int m = 0; m <= a; m++) { + new ModuleBuilder(tb, "m%m%".replace("%m%", String.valueOf((char) ('A' + m)))) + .exports("p%m%".replace("%m%", String.valueOf(m))) + .classes("package p%m%; public class C%m% { public void m() { } }\n".replace("%m%", String.valueOf(m))) + .write(apiDir); + } + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--notes", notes.toString(), + "--include", "mA/**", + "--include", "mB/**", + "--include", "mC/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testConstructor() throws IOException { + Path base = getScratchDir(); + log.println(base); + Path notes = base.resolve("notes.txt"); + Files.writeString(notes, """ + http://example.com/module example-module-A + mA/* + http://example.com/module example-module-A-p0 + mA/p0 + http://example.com/module example-module-A-p0-C0-init + mA/p0.C0#() + http://example.com/module example-module-B + mB/* + """); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int m = 0; m <= a; m++) { + new ModuleBuilder(tb, "m%m%".replace("%m%", String.valueOf((char) ('A' + m)))) + .exports("p%m%".replace("%m%", String.valueOf(m))) + .classes("package p%m%; public class C%m% { public C%m%() { } }\n".replace("%m%", String.valueOf(m))) + .write(apiDir); + } + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--notes", notes.toString(), + "--include", "mA/**", + "--include", "mB/**", + "--include", "mC/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + private String notesErr; + private Notes notes; + + private void readNotes(Path base, String text) throws IOException { + Path file = writeNotes(base.resolve("notes.txt"), text); + StringWriter notesOutSW = new StringWriter(); + StringWriter notesErrSW = new StringWriter(); + Log log = new Log(new PrintWriter(notesOutSW), new PrintWriter(notesErrSW)); + try { + notes = Notes.read(file, log); + } finally { + String notesOut = notesOutSW.toString(); + if (!notesOut.isEmpty()) { + NotesTest.this.log.println("out:\n" + notesOut); + } + notesErr = notesErrSW.toString(); + if (!notesErr.isEmpty()) { + NotesTest.this.log.println("err:\n" + notesErr); + } + } + } + + + private void checkEntries(Element e, String... expect) { + log.println("Check entries for " + e); + Objects.requireNonNull(e); + ElementKey eKey = ElementKey.of(e); + Set entries = notes.getEntries(eKey).keySet(); + Set found = entries.stream().map(Notes.Entry::toString).collect(Collectors.toSet()); + Assertions.assertEquals(Set.of(expect), found); + } + + private void checkError(String msg) { + if (!notesErr.contains(msg)) { + Assertions.fail("expected message not found: " + msg); + } + } + + private Path writeNotes(Path file, String text) throws IOException { + Files.createDirectories((file.getParent())); + Files.writeString(file, text); + return file; + } + + VariableElement getField(TypeElement te, String name) { + return te.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.FIELD && e.getSimpleName().contentEquals(name)) + .map(e -> (VariableElement) e) + .findFirst() + .orElse(null); + } + + ExecutableElement getMethod(TypeElement te, String name, String... paramTypes) { + return te.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.METHOD + && e.getSimpleName().contentEquals(name)) + .map(e -> (ExecutableElement) e) + .filter(e -> hasParams(e, paramTypes)) + .findFirst() + .orElse(null); + } + + private boolean hasParams(ExecutableElement e, String... paramTypes) { + List et = e.getParameters().stream() + .map(p -> p.asType().toString()) + .collect(Collectors.toList()); + return Objects.equals(List.of(paramTypes), et); + } +} diff --git a/test/junit/apitest/PackageTest.java b/test/junit/apitest/PackageTest.java new file mode 100644 index 0000000..e76f49a --- /dev/null +++ b/test/junit/apitest/PackageTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.JavadocTask; +import toolbox.ModuleBuilder; +import toolbox.Task; + +/** + * Tests for the ability to compare packages. + */ +public class PackageTest extends APITester { + /** + * Tests handling of missing packages. + * Three APIs are generated. + *
                  + *
                • all APIs contain equal definitions of package p1 + *
                • two APIs contain equal definitions of package p2 + *
                • only one API contains a definition of package p3 + *
                + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testMissingPackages() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 3; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + for (int p = 0; p <= a; p++) { + tb.writeJavaFiles(apiDir, + "package p.p%p%; public class C%p% { }\n".replace("%p%", String.valueOf(p))); + } + + options.addAll(List.of( + "--api", apiName, + "--source-path", apiDir.toString())); + } + options.addAll(List.of( + "-XDshow-debug-summary", + "-XDtrace-reporter", + "--include", "p.**", + "-d", base.resolve("out").toString(), + "--verbose", "missing")); + + log.println("Options: " + options); + // TODO: Main needs resource for bad option + // TODO: Main needs to check for output dir + // TODO: handle compilation errors in source + Map outMap = run(options); + long notFound = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found")) + .count(); + Assertions.assertEquals(3, notFound); + } + + /** + * Tests handling of missing packages. + * Three APIs are generated, all containing module mA + *
                  + *
                • all APIs contain equal definitions of package p1 + *
                • two APIs contain equal definitions of package p2 + *
                • only one API contains a definition of package p3 + *
                + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testMissingPackagesInModules() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 3; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA"); + for (int p = 0; p <= a; p++) { + mb.classes("package p%p%; public class C%p% { }\n".replace("%p%", String.valueOf(p))); + } + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString(), + "--access", "private")); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString(), + "--verbose", "missing")); + + log.println("Options: " + options); + // TODO: Main needs resource for bad option + // TODO: Main needs to check for output dir + // TODO: handle compilation errors in source + Map outMap = run(options); + long notFound = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found")) + .count(); + Assertions.assertEquals(3, notFound); + } + + /** + * Tests handling of different doc comments. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testRawDocComments() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + String cs = "/**\n * This is 'p.same'.\n * Unchanged.\n * More.\n **/\n"; + String ci = "/**\n * This is 'p.insert'.\n" + (a == 1 ? " * Inserted.\n" : "") + " * More.\n **/\n"; + String cr = "/**\n * This is 'p.remove'.\n" + (a == 0 ? " * Removed.\n" : "") + " * More.\n **/\n"; + String cc = "/**\n * This is 'p.change'.\n * API " + a + "\n * More.\n **/\n"; + + ModuleBuilder mb = new ModuleBuilder(tb, "mA"); + mb.classes(cs + "package p.same;\n", + ci + "package p.insert;\n", + cr + "package p.remove;\n", + cc + "package p.change;\n"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } + + @Test + public void testPackageHtml() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + String cs = "This is 'p.same'.\nUnchanged.\nMore.\n"; + String ci = "This is 'p.insert'.\n" + (a == 1 ? "Inserted.\n" : "") + "More.\n"; + String cr = "This is 'p.remove'.\n" + (a == 0 ? " Removed.\n" : "") + "More.\n"; + String cc = "This is 'p.change'.\nAPI " + a + "\nMore.\n"; + + new ModuleBuilder(tb, "m") + .exports("p.same") + .exports("p.insert") + .exports("p.remove") + .exports("p.change") + .classes("package p.same; public class Same { }\n", + "package p.insert; public class Insert { }\n", + "package p.remove; public class Remove { }\n", + "package p.change; public class Change { }\n") + .write(apiDir); + writePackageHtml(apiDir, "m", "p.same", cs); + writePackageHtml(apiDir, "m", "p.insert", ci); + writePackageHtml(apiDir, "m", "p.remove", cr); + writePackageHtml(apiDir, "m", "p.change", cc); + + Path api = base.resolve(apiName).resolve("api"); + Files.createDirectories(api); + List javadocOptions = List.of( + "-noindex", "-quiet", + "--module", "m"); + Task.Result r = new JavadocTask(tb) + .sourcepath(apiDir.resolve("m")) + .outdir(api) + .options(javadocOptions) + .run(); + r.writeAll(); + + options.addAll(List.of( + "--api", apiName, + "--api-directory", api.toString(), + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "m/**", + "--compare-doc-comments", "yes", + "--compare-api-descriptions", "yes", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + private void writePackageHtml(Path dir, String mName, String pName, String body) + throws IOException { + tb.writeFile(dir.resolve(mName) + .resolve(pName.replace(".", File.separator)) + .resolve("package.html"), + "\n" + + "\n" + + "pName\n" + + "\n" + + body + + "\n\n" + + "\n"); + } +} diff --git a/test/junit/apitest/RecordTest.java b/test/junit/apitest/RecordTest.java new file mode 100644 index 0000000..f115570 --- /dev/null +++ b/test/junit/apitest/RecordTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; + +public class RecordTest extends APITester { + final Runtime.Version version; + final boolean enablePreview; + + RecordTest() { + version = Runtime.version(); + Assumptions.assumeTrue(version.feature() >= 14, "records not supported in JDK " + version); + + enablePreview = switch (version.feature()) { + case 14, 15 -> true; + default -> false; + }; + } + + @Test + public void changeKind() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; + public class C { + private final int c; + public C(int c) { this.c = c; } + public int c() { return c; } + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; + public record C(int c) { + }"""); + + Map outMap = run(base, srcA, srcB); + long differences = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Different")) + .count(); + Assertions.assertEquals(3, differences); + + } + + @Test + public void changeNames() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; + public record C(int c1) { + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; + public record C(int c2) { + }"""); + + Map outMap = run(base, srcA, srcB); + List found = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found") | l.contains("Different")) + .sorted() + .collect(Collectors.toList()); + + List expect = List.of( + "Different names for record p.C, record component 0", + "Item not found in API 'A': method p.C#c2()", + "Item not found in API 'B': method p.C#c1()" + ); + + tb.checkEqual(expect, found); + } + + @Test + public void changeType() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; + public record C(int c) { + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; + public record C(long c) { + }"""); + + Map outMap = run(base, srcA, srcB); + List found = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found") || l.contains("Different")) + .sorted() + .collect(Collectors.toList()); + + List expect = List.of( + "Different types for method p.C#c() return type", + "Different types for record p.C, record component 0", + "Item not found in API 'A': constructor p.C#C(long)", + "Item not found in API 'B': constructor p.C#C(int)" + ); + + tb.checkEqual(expect, found); + } + + @Test + public void addComponent() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; + public record C(int c1) { + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; + public record C(int c1, int c2) { + }"""); + + Map outMap = run(base, srcA, srcB); + List found = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found")) + .sorted() + .collect(Collectors.toList()); + + List expect = List.of( + "Item not found in API 'A': constructor p.C#C(int,int)", + "Item not found in API 'A': method p.C#c2()", + "Item not found in API 'A': record p.C, record component 1", + "Item not found in API 'B': constructor p.C#C(int)" + ); + + tb.checkEqual(expect, found); + } + + private Map run(Path base, Path srcA, Path srcB) { + + List options = new ArrayList<>(); + for (String api : List.of("A", "B")) { + options.addAll(List.of( + "--api", api, + "--source-path", (api.equals("A") ? srcA : srcB).toString())); + if (enablePreview) { + options.add("--enable-preview"); + options.addAll(List.of("--source", String.valueOf(version.feature()))); + } + } + + options.addAll(List.of( + "--include", "p.*", + "-d", base.resolve("out").toString(), + "--verbose", "differences,missing")); + + log.println("Options: " + options); + + return run(options); + } + +} diff --git a/test/junit/apitest/ResourceFileTest.java b/test/junit/apitest/ResourceFileTest.java new file mode 100644 index 0000000..d97d0b1 --- /dev/null +++ b/test/junit/apitest/ResourceFileTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.JavadocTask; + +public class ResourceFileTest extends APITester { + + @Test + public void testSingleFile() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = setup(base, "A", "", "test-resource-files/test.txt"); + Path srcB = setup(base, "B", "public void m() { }"); + + Path out = base.resolve("out"); + + Map outMap = run(base, + srcA, base.resolve("apiA"), + srcB, base.resolve("apiB"), + out, + "--resource-files", "test-resource-files/test.txt"); + + List found = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found") | l.contains("Different")) + .sorted().toList(); + + checkResourceFiles(out, "test-resource-files/test.txt"); + } + + @Test + public void testDirectory() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = setup(base, "A", "", + "test-resource-files/test1.txt", + "test-resource-files/test2.txt"); + Path srcB = setup(base, "B", "public void m() { }"); + + Path out = base.resolve("out"); + + Map outMap = run(base, + srcA, base.resolve("apiA"), + srcB, base.resolve("apiB"), + out, + "--resource-files", "test-resource-files"); + + List found = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found") | l.contains("Different")) + .sorted().toList(); + + checkResourceFiles(out, + "test-resource-files/test1.txt", + "test-resource-files/test2.txt"); + } + + @Test + public void testStandard() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = setup(base, "A", "", + "test.svg", + "testA.svg", + "resource-files/test.txt", + "resource-files/testA.txt"); + Path srcB = setup(base, "B", "public void m() { }", + "test.svg", + "testB.svg", + "resource-files/test.txt", + "resource-files/testB.txt"); + + Path out = base.resolve("out"); + + Map outMap = run(base, + srcA, base.resolve("apiA"), + srcB, base.resolve("apiB"), + out); + + List found = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found") | l.contains("Different")) + .sorted().toList(); + + String FS = File.separator; + Predicate apiA = s -> s.contains(FS + "apiA" + FS); + Predicate apiB = s -> s.contains(FS + "apiB" + FS); + + checkResourceFiles(out, + new RFInfo("test.svg", apiB), + new RFInfo("testA.svg", apiA), + new RFInfo("testB.svg", apiB), + new RFInfo("resource-files/test.txt", apiB), + new RFInfo("resource-files/testA.txt", apiA), + new RFInfo("resource-files/testB.txt", apiB)); + + } + + Path setup(Path base, String id, String s, String... resFiles) throws IOException { + Path src = base.resolve("src" + id); + tb.writeJavaFiles(src, + "package p;\n" + + "public class C{\n" + + s + "\n" + + "}"); + Path api = base.resolve("api" + id); + javadoc(src, api); + for (var resFile : resFiles) { + addResourceFile(api, Path.of(resFile)); + } + return src; + } + + private void javadoc(Path src, Path out) throws IOException { + Files.createDirectories(out); + JavadocTask t = new JavadocTask(tb); + t.sourcepath(src) + .outdir(out) + .options("-quiet", "p") + .run() + .writeAll(); + } + + private void addResourceFile(Path api, Path resFile) throws IOException { + var p = api.resolve(resFile); + Files.createDirectories(p.getParent()); + Files.writeString(p, "dummy resource: " + p); + } + + private void checkResourceFiles(Path out, String... files) { + for (String s : files) { + if (Files.exists(out.resolve(s))) { + log.println("found " + s); + } else { + Assertions.fail("resource file not found: " + s); + } + } + } + + private record RFInfo(String file, Predicate test) { } + private void checkResourceFiles(Path out, RFInfo... infos) { + for (var info : infos) { + if (Files.exists(out.resolve(info.file))) { + try { + String s = Files.readString(out.resolve(info.file)); + Assertions.assertTrue(info.test.test(s), "found " + info.file + " but failed check"); + } catch (IOException e) { + Assertions.fail("exception " + e); + } + } else { + Assertions.fail("resource file not found: " + info.file); + } + } + } + + private Map run(Path base, Path srcA, Path apiA, Path srcB, Path apiB, Path out, String...opts) { + + List options = new ArrayList<>(); + for (String api : List.of("A", "B")) { + options.addAll(List.of( + "--api", api, + "--source-path", (api.equals("A") ? srcA : srcB).toString(), + "--api-directory", (api.equals("A") ? apiA : apiB).toString())); + } + + options.addAll(List.of( + "--include", "p.*", + "-d", out.toString())); + options.addAll(List.of(opts)); + + log.println("Options: " + options); + + return run(options); + } +} diff --git a/test/junit/apitest/SealedTest.java b/test/junit/apitest/SealedTest.java new file mode 100644 index 0000000..343a06a --- /dev/null +++ b/test/junit/apitest/SealedTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; + +public class SealedTest extends APITester { + final Runtime.Version version; + final boolean enablePreview; + + SealedTest() { + version = Runtime.version(); + Assumptions.assumeTrue(version.feature() >= 15, "sealed classes not supported in JDK " + version); + + enablePreview = switch (version.feature()) { + case 15, 16 -> true; + default -> false; + }; + } + + @Test + public void addSealedModifier() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; public class C { + public class C1 { } + public final class C2 extends C1 { } + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; public class C { + public sealed class C1 { } + public final class C2 extends C1 { } + }"""); + + Map outMap = run(base, srcA, srcB, getClassMethodName()); + long differences = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Different")) + .count(); + Assertions.assertEquals(3, differences); + + } + + @Test + public void addNonSealedModifier() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; public class C { + public sealed class C1 { } + public final class C2 extends C1 { } + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; public class C { + public sealed class C1 { } + public non-sealed class C2 extends C1 { } + }"""); + + Map outMap = run(base, srcA, srcB, getClassMethodName()); + long differences = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Different")) + .count(); + Assertions.assertEquals(1, differences); + + } + + @Test + public void addPermits() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; public class C { + public sealed class C1 { } + public final class C2 extends C1 { } + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; public class C { + public sealed class C1 permits C2 { } + public final class C2 extends C1 { } + }"""); + + Map outMap = run(base, srcA, srcB, getClassMethodName()); + long differences = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Different")) + .count(); + Assertions.assertEquals(0, differences); + + } + + @Test + public void changePermits() throws IOException { + Path base = getScratchDir(); + log.println(base); + + Path srcA = Files.createDirectories(base.resolve("srcA")); + tb.writeJavaFiles(srcA, """ + package p; public class C { + public sealed class C1 permits C2 { } + public final class C2 extends C1 { } + public final class C3 { } + }"""); + + Path srcB = Files.createDirectories(base.resolve("srcB")); + tb.writeJavaFiles(srcB, """ + package p; public class C { + public sealed class C1 permits C3 { } + public final class C2 { } + public final class C3 extends C1 { } + }"""); + + Map outMap = run(base, srcA, srcB, getClassMethodName()); + long differences = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Different")) + .count(); + Assertions.assertEquals(5, differences); + + } + + private Map run(Path base, Path srcA, Path srcB, String description) { + + List options = new ArrayList<>(); + for (String api : List.of("A", "B")) { + options.addAll(List.of( + "--api", api, + "--source-path", (api.equals("A") ? srcA : srcB).toString())); + if (enablePreview) { + options.add("--enable-preview"); + options.addAll(List.of("--source", String.valueOf(version.feature()))); + } + } + + options.addAll(List.of( + "--description", description, + "--include", "p.*", + "-d", base.resolve("out").toString(), + "--verbose", "differences,missing")); + + log.println("Options: " + options); + + return run(options); + } + +} diff --git a/test/junit/apitest/SelectorTest.java b/test/junit/apitest/SelectorTest.java new file mode 100644 index 0000000..7918010 --- /dev/null +++ b/test/junit/apitest/SelectorTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import jdk.codetools.apidiff.model.Selector; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class SelectorTest { + + public static Stream provideCases() { + // The names here are just example strings; they are not interpreted as actual element names + return Stream.of( + Arguments.of( + // module, all packages and types in the module; no excludes + List.of("java.base/**"), + List.of(), + Map.of("java.base/java.lang", true, + "java.base/java.lang.String", true, + "java.se/java.io.IOException", false) + ), + Arguments.of( + // module, one package; no excludes + List.of("java.base/java.lang.*"), + List.of(), + Map.of("java.base/java.lang", true, + "java.base/java.lang.String", true, + "java.base/java.lang.reflect.Method", false, + "java.base/java.util.Map", false, + "java.se/java.io.IOException", false) + ), + Arguments.of( + // module, all packages and types in the module; subpackage excluded + List.of("java.base/**"), + List.of("java.base/java.lang.reflect.*"), + Map.of("java.base/java.lang.String", true, + "java.lang.String", false, + "java.base/java.lang.reflect.Method", false, + "java.se/java.io.IOException", false) + ), + Arguments.of( + List.of("java.lang.**"), + List.of("java.lang.reflect.*"), + Map.of("java.base/java.lang.String", false, + "java.lang.String", true, + "java.lang.reflect.Method", false, + "java.se.IOException", false) + ) + ); + } + + // optional module, package, optional capitalized type + Pattern pattern = Pattern.compile("((?[A-Za-z0-9.]+)/)?(?

                [a-z0-9.]+)(\\.(?[A-Z][A-Za-z0-9.]*))?"); + + @ParameterizedTest + @MethodSource("provideCases") + public void test(List includes, List excludes, Map cases) { + System.out.println("includes: " + includes); + System.out.println("excludes: " + excludes); + Selector s = new Selector(includes, excludes); + boolean ok = true; + for (var e : cases.entrySet()) { + String c = e.getKey(); + boolean expect = e.getValue(); + + Matcher m = pattern.matcher(c); + if (!m.matches()) { + throw new IllegalArgumentException(c); + } + + String mdl = m.group("m"); + String pkg = m.group("p"); + String typ = m.group("t"); + System.out.println(" m:" + mdl + " p:" + pkg + " t:" + typ); + + boolean found = typ == null + ? s.acceptsPackage(mdl, pkg) + : s.acceptsType(mdl, pkg, typ); + if (found == expect) { + System.out.println(" OK"); + } else { + System.out.println(" expect: " + expect + ", found: " + found); + ok = false; + } + } + Assertions.assertTrue(ok); + } +} diff --git a/test/junit/apitest/SerialCommentsTest.java b/test/junit/apitest/SerialCommentsTest.java new file mode 100644 index 0000000..cfc9fb9 --- /dev/null +++ b/test/junit/apitest/SerialCommentsTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.JavadocTask; +import toolbox.ModuleBuilder; +import toolbox.Task; + +public class SerialCommentsTest extends APITester { + @Test + public void testSerialVersionUID() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, i -> + "private static final long serialVersionUID = " + ((i == 0) ? "123L" : "456L") + ";\n"); + } + + @Test + public void testOverview() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, i -> + "/**\n" + + " * This is " + ((i == 0) ? "a" : "an updated") + " overview.\n" + + " */\n" + + "private static final ObjectStreamField[] serialPersistentFields = null;\n"); + } + + @Test + public void testDefaultField() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, i -> + "/**\n" + + " * This is " + ((i == 0) ? "a" : "an updated") + " field.\n" + + "*/\n" + + "private int i;\n"); + } + + @Test + public void testSerialPersistentField() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, i -> + "/**\n" + + " * @serialField i int This is " + ((i == 0) ? "a" : "an updated") + " field.\n" + + " */\n" + + "private static final ObjectStreamField[] serialPersistentFields = null;\n"); + } + + @Test + public void testMethod() throws IOException { + Path base = getScratchDir(); + log.println(base); + + test(base, i -> + "/**\n" + + " * This is " + ((i == 0) ? "a" : "an updated") + " method.\n" + + " * @param in the input stream" + + "*/\n" + + "private void readObject(ObjectInputStream in) { }\n"); + + } + + private void test(Path base, Function f) throws IOException { + List options = new ArrayList<>(); + + for (int i = 0; i < 2; i++) { + String apiName = "api" + i; + Path src = base.resolve(apiName).resolve("src"); + + String mods = (i == 0) ? "public" : "protected"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes("package p;\n" + + "import java.io.*;\n" + + "public class C implements Serializable {\n" + + f.apply(i) + + "}\n") + .write(src); + + Path api = base.resolve(apiName).resolve("api"); + Files.createDirectories(api); + Task.Result r = new JavadocTask(tb) + .sourcepath(src.resolve("m")) + .outdir(api) + .options("-noindex", "-quiet", "--module", "m") + .run(); + r.writeAll(); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString(), + "--api-directory", api.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } +} diff --git a/test/junit/apitest/SerialFieldsTest.java b/test/junit/apitest/SerialFieldsTest.java new file mode 100644 index 0000000..8e4eccc --- /dev/null +++ b/test/junit/apitest/SerialFieldsTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +public class SerialFieldsTest extends APITester { + + public static Stream provideSimpleFields() { + return Stream.of( + Arguments.of("none", "", ""), + Arguments.of("add", "", "int i;"), + Arguments.of("remove", "int i;", ""), + Arguments.of("changeName", "int i;", "int j;"), // effectively, delete and add + Arguments.of("changeType", "int i;", "long i;"), + Arguments.of("changeMods1", "private int i;", "public int i;"), + Arguments.of("changeMods2", "int i;", "static int i;") // effectively, delete + ); + } + + @ParameterizedTest + @MethodSource("provideSimpleFields") + public void testSimpleFields(String name, String api0, String api1) throws IOException { + log.printf("Test %s: %s | %s%n", name, api0, api1); + Path base = getScratchDir(name); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String s = "package p;\n" + + "import java.io.*;\n" + + "public class C implements Serializable {\n" + + " " + (api == 0 ? api0 : api1) + "\n" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(s) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + public static Stream provideDocComments() { + return Stream.of( + Arguments.of("changeMain", + "This is a comment", + "This is a different comment"), + Arguments.of("changeSerial", + "This is a comment.\n@serial This is a description", + "This is a comment.\n@serial This is a different description"), + Arguments.of("changeBoth", + "This is a comment.\n@serial This is a description", + "This is a different comment.\n@serial This is a different description") + ); + } + + @ParameterizedTest + @MethodSource("provideDocComments") + public void testDocComments(String name, String c0, String c1) throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String s = "package p;\n" + + "import java.io.*;\n" + + "public class C implements Serializable {\n" + + " " + (api == 0 ? toComment(c0) : toComment(c1)) + "\n" + + " int i;\n" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(s) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + String toComment(String s) { + return "/**\n *" + s.replace("\n", "\n *") + " */"; + } +} diff --git a/test/junit/apitest/SerialPersistentFieldsTest.java b/test/junit/apitest/SerialPersistentFieldsTest.java new file mode 100644 index 0000000..0757a8f --- /dev/null +++ b/test/junit/apitest/SerialPersistentFieldsTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +public class SerialPersistentFieldsTest extends APITester { + + public static Stream provideSingleFields() { + return Stream.of( + Arguments.of("boolean"), + Arguments.of("byte"), + Arguments.of("char"), + Arguments.of("double"), + Arguments.of("float"), + Arguments.of("int"), + Arguments.of("long"), + Arguments.of("short"), + Arguments.of("java.lang.Object"), + Arguments.of("java.util.Hashtable"), + Arguments.of("Object"), + Arguments.of("String"), + Arguments.of("Hashtable"), + Arguments.of("int[]"), + Arguments.of("String[]"), + Arguments.of("UNKNOWN") + ); + } + + @ParameterizedTest + @MethodSource("provideSingleFields") + public void testSingleFields(String type) throws IOException { + String name = type.toLowerCase().replaceAll("[^A-Za-z0-9]+", "_"); + log.printf("Test %s: %s%n", name, type); + Path base = getScratchDir(name); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String s = "package p;\n" + + "import java.io.*;\n" + + "import java.util.*;\n" + + "public class C implements Serializable {\n" + + " /**\n" + + " * A field.\n" + + " * @serialField f " + type + " a description\n" + + " */\n" + + " private static final ObjectStreamField[] serialPersistentFields = { };\n" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(s) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--info-text", "header=" + name, + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + + } +} diff --git a/test/junit/apitest/SerialVersionUIDTest.java b/test/junit/apitest/SerialVersionUIDTest.java new file mode 100644 index 0000000..0c573ce --- /dev/null +++ b/test/junit/apitest/SerialVersionUIDTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +public class SerialVersionUIDTest extends APITester { + enum Kind { + IMPLICIT(null), + DEFAULT(-6240593132596067277L), // from serialver + EXPLICIT(123L); + final Long value; + Kind(Long value) { + this.value = value; + } + } + + public static Stream provideSUIDs() { + List list = new ArrayList<>(); + for (Kind k1 : Kind.values()) { + for (Kind k2 : Kind.values()) { + list.add(Arguments.of(k1.name().toLowerCase() + "-" + k2.name().toLowerCase(), k1.value, k2.value)); + } + } + return list.stream(); + } + + @ParameterizedTest + @MethodSource("provideSUIDs") + public void testSUIDs(String name, Long api0, Long api1) throws IOException { + log.printf("Test %s: %s | %s%n", name, api0, api1); + Path base = getScratchDir(name); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + Long v = (api == 0) ? api0 : api1; + String f = v == null ? "" : " private static final long serialVersionUID = " + v + "L;\n"; + String c = "package p;\n" + + "import java.io.*;\n" + + "public class C implements Serializable {\n" + + f + + " int i;" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(c) + .write(src); + tb.writeJavaFiles(src, "module m { exports p; }", c); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + + } +} diff --git a/test/junit/apitest/SerializationMethodsTest.java b/test/junit/apitest/SerializationMethodsTest.java new file mode 100644 index 0000000..46e7682 --- /dev/null +++ b/test/junit/apitest/SerializationMethodsTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.codetools.apidiff.Main; + +import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +public class SerializationMethodsTest extends APITester { + + public static Stream getMethods() { + return Stream.of( + Arguments.of("none", "Serializable", List.of()), + Arguments.of("serSingle", "Serializable", + List.of("private void readObject(ObjectInputStream in) { }")), + Arguments.of("serMulti", "Serializable", + List.of("private void readObject(ObjectInputStream in) { }", + "private Object readResolve() { return null; }")), + Arguments.of("extMin", "Externalizable", + List.of("public void readExternal(ObjectInput in) { }", + "public void writeExternal(ObjectOutput out) { }")), + Arguments.of("extOpt", "Externalizable", + List.of("public void readExternal(ObjectInput in) { }", + "public void writeExternal(ObjectOutput out) { }", + "private Object readResolve() { return null; }")) + ); + } + + @ParameterizedTest + @MethodSource("getMethods") + public void testEqualMethods(String name, String intf, List methods) throws IOException { + log.printf("Test %s: %s %s%n", name, intf, methods); + Path base = getScratchDir(name); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String s = "package p;\n" + + "import java.io.*;\n" + + "public class C implements " + intf + " {\n" + + methods.stream().map(m -> " " + m + "\n").collect((Collectors.joining())) + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(s) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--info-text", "header=" + name, + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options, EnumSet.of(Main.Result.OK)); + + } + + @ParameterizedTest + @MethodSource("getMethods") + public void testAddMethod(String name, String intf, List methods) throws IOException { + log.printf("Test %s: %s %s%n", name, intf, methods); + Path base = getScratchDir(name); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String s = "package p;\n" + + "import java.io.*;\n" + + "public class C implements " + intf + " {\n" + + methods.stream().map(m -> " " + m + "\n").collect((Collectors.joining())) + + (api == 0 ? "" : " private Object writeReplace() { return this; }\n") + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(s) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--info-text", "header=" + name, + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options, EnumSet.of(Main.Result.DIFFS)); + + } + + @Test + public void testAddThrows() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + String m = "private void readObject(ObjectInputStream in) { };"; + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String s = "package p;\n" + + "import java.io.*;\n" + + "public class C implements Serializable {\n" + + " " + ((api == 0) ? m : m.replace(") {", ") throws IOException {")) + "\n" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(s) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--info-text", "header=testAddThrows", + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options, EnumSet.of(Main.Result.DIFFS)); + + } +} diff --git a/test/junit/apitest/SerializedFormReaderTest.java b/test/junit/apitest/SerializedFormReaderTest.java new file mode 100644 index 0000000..b8f157c --- /dev/null +++ b/test/junit/apitest/SerializedFormReaderTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.model.SerializedFormDocs; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import apitest.lib.APITester; +import toolbox.JavadocTask; +import toolbox.ModuleBuilder; +import toolbox.Task; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SerializedFormReaderTest extends APITester { + private Log log; + private Path api; + + /** + * Generates sample API documentation from sample API. + * + * @throws IOException if an IO exception occurs + */ + @BeforeAll + public void generateAPIDocs() throws IOException { + Path base = getScratchDir(); + super.log.println(base); + + Path src = base.resolve("src"); + generateSampleAPI(src); + + // Run javadoc on sample API + api = Files.createDirectories(base.resolve("api")); + Task.Result r = new JavadocTask(tb) + .sourcepath(src.resolve("m")) + .outdir(api) + .options("-noindex", "-quiet", "--module", "m") + .run(); + r.writeAll(); + + PrintWriter out = new PrintWriter(System.out) { + @Override + public void close() { + flush(); + } + }; + PrintWriter err = new PrintWriter(System.err, true){ + @Override + public void close() { + flush(); + } + }; + + log = new Log(out, err); + } + + void generateSampleAPI(Path dir) throws IOException { + new ModuleBuilder(tb, "m") + .exports("p") + .classes("package p; import java.io.*; public class NoFields implements Serializable { }") + .classes("package p; import java.io.*; public class OneDefaultFieldNoComments implements Serializable { int i; }") + .classes("package p; import java.io.*; public class TwoDefaultFieldsNoComments implements Serializable { int i; int j; }") + .classes(""" + package p; + import java.io.*; + public class TwoDefaultFieldsWithComments implements Serializable { + /** + * This is the main description for {@code i}. This is more description for {@code i}. + * @serial This is the serial description for {@code i}. + */ + int i; + /** + * This is the main description for {@code j}. This is more description for {@code j}. + * @serial This is the serial description for {@code j}. + */ + int j; + } + """) + .classes(""" + package p; + import java.io.*; + public class OnePersistentField implements Serializable { + /** + * @serialField i int This is {@code i}. + */ + private static final ObjectStreamField[] serialPersistentFields = null; + } + """) + .classes(""" + package p; + import java.io.*; + public class TwoPersistentFields implements Serializable { + /** + * @serialField i int This is {@code i}. + * @serialField j int This is {@code j}. + */ + private static final ObjectStreamField[] serialPersistentFields = null; + } + """) + .classes(""" + package p; + import java.io.*; + public class SVUID implements Serializable { + private static final long serialVersionUID = 123L; + } + """) + .classes(""" + package p; + import java.io.*; + public class Overview implements Serializable { + /** + * This is the serialization overview. + */ + private static final ObjectStreamField[] serialPersistentFields = null; + } + """) + .classes(""" + package p; + import java.io.*; + public class ReadObject implements Serializable { + /** + * This is {@code readObject}. This is more about {@code readObject}. + * @param in the input stream + */ + private void readObject(ObjectInputStream in) { } + } + """) + .classes(""" + package p; + import java.io.*; + public class WriteObject implements Serializable { + /** + * This is {@code writeObject}. This is more about {@code writeObject}. + * @param out the output stream + * @serialData This is the serial data description. + */ + private void writeObject(ObjectOutputStream out) { } + } + """) + .classes(""" + package p; + import java.io.*; + public class ReadWriteObject implements Serializable { + /** + * This is {@code readObject}. This is more about {@code readObject}. + * @param in the input stream + */ + private void readObject(ObjectInputStream in) { } + /** + * This is {@code writeObject}. This is more about {@code writeObject}. + * @param out the output stream + * @serialData This is the serial data description. + */ + private void writeObject(ObjectOutputStream out) { } + } + """) + .write(dir); + } + + @Test + public void checkAPI() { + Map serializedFormDocs = SerializedFormDocs.read(log, api.resolve("serialized-form.html")); + + serializedFormDocs.forEach((name, docs) -> { + log.flush(); + super.log.println("Type " + name); + if (docs.getSerialVersionUID() != null) { + super.log.println(" serialVersionUID: " + docs.getSerialVersionUID()); + } + if (docs.getOverview() != null) { + super.log.println(" overview: " + docs.getOverview()); + } + docs.getFieldDescriptions().forEach((f, d) -> { + super.log.println(" " + f + ": " + d); + }); + docs.getMethodDescriptions().forEach((m, d) -> { + super.log.println(" " + m + ": " + d); + }); + super.log.println(); + }); + } +} diff --git a/test/junit/apitest/SerializedFormTest.java b/test/junit/apitest/SerializedFormTest.java new file mode 100644 index 0000000..6237b59 --- /dev/null +++ b/test/junit/apitest/SerializedFormTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.ModuleBuilder; + +public class SerializedFormTest extends APITester { + + @Test() + public void testAddSuperclass() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String c = "package p;\n" + + "import java.io.*;\n" + + "public class C" + (api == 0 ? "" : " implements Serializable") + " {\n" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(c) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test() + public void testClassExclude() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String c = "package p;\n" + + "import java.io.*;\n" + + (api == 0 ? "" : "/** Sentence.\n * @serial exclude\n */\n") + + "public class C implements Serializable {\n" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(c) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test() + public void testPackageExclude() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String p = + (api == 0 ? "" : "/** Sentence.\n * @serial exclude\n */\n") + + "package p;"; + String c = """ + package p; + import java.io.*; + public class C implements Serializable { + } + """; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(p, c) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test() + public void testPackageExcludeClassInclude() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + for (int api = 0; api < 2; api++) { + String apiName = "api" + api; + Path src = Files.createDirectories(base.resolve(apiName) .resolve("src")); + String p = + (api == 0 ? "" : "/** Sentence.\n * @serial exclude\n */\n") + + "package p;"; + String c = "package p;\n" + + "import java.io.*;\n" + + (api == 0 ? "" : "/** Sentence.\n * @serial include\n */\n") + + "public class C implements Serializable {\n" + + "}\n"; + new ModuleBuilder(tb, "m") + .exports("p") + .classes(p, c) + .write(src); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", src.toString())); + } + + options.addAll(List.of( + "--include", "m/**", + "-d", base.resolve("out").toString())); + + options.add("-XDshow-debug-summary"); + + log.println("Options: " + options); + + Map outMap = run(options); + } +} diff --git a/test/junit/apitest/TestAPI.java b/test/junit/apitest/TestAPI.java new file mode 100644 index 0000000..13669a4 --- /dev/null +++ b/test/junit/apitest/TestAPI.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.util.DocTrees; +import jdk.codetools.apidiff.Options; +import jdk.codetools.apidiff.model.API; +import jdk.codetools.apidiff.model.SerializedForm; + +/** + * A dummy API for use in testing. It has a name. All methods return null. + * Create subclasses to provide additional, specific properties. + */ +public class TestAPI extends API { + TestAPI(String name) { + super(new Options.APIOptions(name), null, null, null); + } + + @Override + public Set getPackageElements() { + return null; + } + + @Override + public Set getModuleElements() { + return null; + } + + @Override + public Set getPackageElements(ModuleElement m) { + return null; + } + + @Override + public Set getExportedPackageElements(ModuleElement m) { + return null; + } + + @Override + public Set getTypeElements(PackageElement p) { + return null; + } + + @Override + public Map getAnnotationValuesWithDefaults(AnnotationMirror am) { + return null; + } + + @Override + public SerializedForm getSerializedForm(TypeElement e) { + return null; + } + + @Override + public DocCommentTree getDocComment(Element e) { + return null; + } + + @Override + public DocCommentTree getDocComment(JavaFileObject fo) { + return null; + } + + @Override + public String getApiDescription(Element e) { + return null; + } + + @Override + public String getApiDescription(JavaFileObject fo) { + return null; + } + + @Override + public byte[] getAllBytes(JavaFileObject fo) { + return null; + } + + @Override + public List listFiles(LocationKind kind, Element e, String subdirectory, Set kinds, boolean recurse) { + return null; + } + + @Override + public Elements getElements() { + return null; + } + + @Override + public Types getTypes() { + return null; + } + + @Override + public DocTrees getTrees() { + return null; + } +} diff --git a/test/junit/apitest/TextDiffBuilderTest.java b/test/junit/apitest/TextDiffBuilderTest.java new file mode 100644 index 0000000..28104ff --- /dev/null +++ b/test/junit/apitest/TextDiffBuilderTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.TagName; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.report.html.TextDiffBuilder; + +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; + +/** + * Unit tests for the {@code TextDiffBuilder} class. + */ +public class TextDiffBuilderTest extends APITester { + /** + * Tests the behavior when the two sets of input are equal. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testEqual() throws IOException { + List list1 = lines(10); + List list2 = new ArrayList<>(list1); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when a line is inserted into the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testSimpleInsert() throws IOException { + List list1 = lines(10); + List list2 = new ArrayList<>(list1); + list2.add(5, "inserted line"); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when a line is removed from the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testSimpleDelete() throws IOException { + List list1 = lines(10); + List list2 = new ArrayList<>(list1); + list2.remove(5); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when a line is changed in the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testSimpleChange() throws IOException { + List list1 = lines(20); + List list2 = new ArrayList<>(list1); + list2.set(5, "changed line"); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when multiple changes are made in the "modified" set. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testMultiple() throws IOException { + List list1 = lines(20, 32); + List list2 = new ArrayList<>(list1); + list2.add(3, "inserted line"); + list2.set(10, "changed line"); + list2.remove(15); + test(getScratchDir(), list1, list2); + } + + /** + * Tests the behavior when changes are made in different parts of the modified set, + * such that they are presented as disjoint differences. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDisjoint() throws IOException { + List list1 = IntStream.range(1, 50) + .mapToObj(i -> ("line:" + i)) + .collect(Collectors.toList()); + List list2 = new ArrayList<>(list1); + list2.add(10, "insert"); + list2.set(20, "change"); + list2.remove(30); + test(getScratchDir(), list1, list2); + } + + void test(Path dir, List list1, List list2) throws IOException { + try (PrintWriter out = wrap(System.out); PrintWriter err = wrap(System.err)) { + Log log = new Log(out, err); + TextDiffBuilder.SDiffs sd = new TextDiffBuilder.SDiffs(); + Content c = sd + .setReference("Reference Text", list1) + .setModified("Modified Text", list2) + .setContextSize(3) + .setShowLineNumbers(true) + .build(log); + + try (Writer w = Files.newBufferedWriter(dir.resolve("out.html"))) { + HtmlTree head = HtmlTree.HEAD("utf-8", "test") + .add(new HtmlTree(TagName.STYLE, new Text(style))); + HtmlTree body = HtmlTree.BODY(List.of(c)); + HtmlTree html = new HtmlTree(TagName.HTML, head, body); + html.write(w); + } + } + } + + PrintWriter wrap(PrintStream out) { + return new PrintWriter(out) { + @Override + public void close() { + flush(); + } + }; + } + + List lines(int size) { + return lines(size, 64); + } + + List lines(int lineCount, int lineLength) { + List lines = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + Pattern ws = Pattern.compile("\\s"); + Matcher m = ws.matcher(lorem_ipsum); + int start = 0; + while (m.find()) { + if (sb.length() + (m.start() - start) > lineLength || m.group().equals("\n")) { + lines.add(sb.toString()); + if (lines.size() > lineCount) { + return lines; + } + sb = new StringBuilder(); + } + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(lorem_ipsum, start, m.start()); + start = m.end(); + } + sb.append(lorem_ipsum.substring(start)); + lines.add(sb.toString()); + System.err.println(lines); + return lines; + } + + private static final String style = """ + div.sdiffs { + display: grid; + grid-template-columns: auto auto; + grid-column-gap: 10px; + margin: 2px 10px; + padding: 2px 2px; + border: 1px solid grey; + } + .sdiffs div.sdiffs-ref { grid-column: 1; overflow-x: auto } + .sdiffs div.sdiffs-mod { grid-column: 2; overflow-x: auto } + .sdiffs span.sdiffs-title { margin-left:2em; text-weight: bold } + .sdiffs span.sdiffs-changed { color: blue } + """; + + private static final String lorem_ipsum = LoremIpsum.text; +} diff --git a/test/junit/apitest/TypeTest.java b/test/junit/apitest/TypeTest.java new file mode 100644 index 0000000..60a280b --- /dev/null +++ b/test/junit/apitest/TypeTest.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import apitest.lib.APITester; +import toolbox.JavacTask; +import toolbox.JavadocTask; +import toolbox.ModuleBuilder; + +/** + * Tests the ability to compare types. + */ +public class TypeTest extends APITester { + /** + * Tests handling of missing types. + * Three APIs are generated, all containing module mA, package p + *

                  + *
                • all APIs contain equal definitions of class p.C1 + *
                • two APIs contain equal definitions of class p.C2 + *
                • only one API contains a definition of class p.C3 + *
                + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testMissingTypes() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 3; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + for (int c = 0; c <= a; c++) { + mb.classes("package p; public class C%c% { }\n".replace("%c%", String.valueOf(c))); + } + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString(), + "--verbose", "missing")); + + log.println("Options: " + options); + + Map outMap = run(options); + long notFound = outMap.get(OutputKind.ERR).lines() + .filter(l -> l.contains("Item not found")) + .count(); + Assertions.assertEquals(3, notFound); + + } + + /** + * Tests handling of different kinds of types. + * Four APIs are generated, all containing module mA, package p. + * Each API declares a class T of a different kind. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDifferentKinds() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 4; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + String t = switch (a) { + case 0 -> "@interface T { }"; + case 1 -> "class T { }"; + case 2 -> "enum T { V }"; + case 3 -> "interface T { }"; + default -> throw new IllegalStateException(String.valueOf(a)); + }; + mb.classes("package p; public " + t + "\n"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } + /** + * Tests handling of different kinds of types with supertypes. + * Two APIs are generated, both containing module mA, package p. + * One API contains class T which implements Runnable, + * the other contains interface T which extends Runnable. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDifferentKindsWithSupertype() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + String t = switch (a) { + case 0 -> "abstract class T implements Runnable { }"; + case 1 -> "interface T extends Runnable { }"; + default -> throw new IllegalStateException(String.valueOf(a)); + }; + mb.classes("package p; public " + t + "\n"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + + } + + /** + * Tests handling of different kinds of types. + * Two APIs are generated, all containing module mA, package p. + * Each API declares a class C with a different superclass, + * and a class D which does or does not have an explicit superclass. + * + * Note that only CLASS kinds can have an explicit superclass, and + * all classes except {@code java.lang.Object} will have a superclass. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDifferentSuperclasses() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + String scc = "extends " + ((char) ('A' + a)); + String scd = (a == 0) ? "" : "extends A"; + mb.classes( + "package p; public class A { }\n", + "package p; public class B { }\n", + "package p; public class C " + scc + " { }\n", + "package p; public class D " + scd + " { }\n"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + /** + * Tests handling of different kinds of types. + * Two APIs are generated, all containing module mA, package p. + * Each API declares a class C with a different superclass, + * and a class D which does or does not have an explicit superclass. + * + * Note that only CLASS kinds can have an explicit superclass, and + * all classes except {@code java.lang.Object} will have a superclass. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testDifferentSuperinterfaces() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + String scc = "implements " + ((char) ('A' + a)); + String scd = (a == 0) ? "" : "implements A"; + String sce = "implements java.util.List<" + ((char) ('A' + a)) + ">"; + mb.classes( + "package p; public interface A { }\n", + "package p; public interface B { }\n", + "package p; public class C " + scc + " { }\n", + "package p; public class D " + scd + " { }\n", + "package p; public class E " + sce + " { }\n"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + /** + * Tests handling of type parameters. + * + * @throws IOException if an IO exception occurs + */ + @Test + public void testTypeParameters() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + String tpeC = a == 0 ? "" : ""; + String tpeD = a == 0 ? "" : ""; + String tpeE = a == 0 ? "" : ""; + mb.classes( + "package p; public interface A { }\n", + "package p; public interface B { }\n", + "package p; public interface C" + tpeC + " { }\n", + "package p; public interface D" + tpeD + " { }\n", + "package p; public interface E" + tpeE+ " { }\n"); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testDifferentRawDocComments() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path apiDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + String cs = "/**\n * This is Same.\n * Unchanged.\n * More.\n **/"; + String ci = "/**\n * This is Insert.\n" + (a == 1 ? " * Inserted.\n" : "") + " * More.\n **/"; + String cr = "/**\n * This is Remove.\n" + (a == 0 ? " * Removed.\n" : "") + " * More.\n **/"; + String cc = "/**\n * This is Change.\n * API " + apiName + "\n * More.\n **/"; + mb.classes( + "package p; " + cs + "public class Same { }\n", + "package p; " + ci + "public class Insert { }\n", + "package p; " + cr + "public class Remove { }\n", + "package p; " + cc + "public class Change { }\n" + ); + mb.write(apiDir); + + options.addAll(List.of( + "--api", apiName, + "--module-source-path", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } + + @Test + public void testDifferentAPIDescriptions() throws IOException { + Path base = getScratchDir(); + log.println(base); + + List options = new ArrayList<>(); + + int APIS = 2; + for (int a = 0; a < APIS; a++) { + String apiName = "api" + a; + Path srcDir = base.resolve(apiName).resolve("src"); + + ModuleBuilder mb = new ModuleBuilder(tb, "mA").exports("p"); + String cs = "/**\n * This is Same.\n * Unchanged.\n * More.\n **/"; + String ci = "/**\n * This is Insert.\n" + (a == 1 ? " * Inserted.\n" : "") + " * More.\n **/"; + String cr = "/**\n * This is Remove.\n" + (a == 0 ? " * Removed.\n" : "") + " * More.\n **/"; + String cc = "/**\n * This is Change.\n * API " + apiName + "\n * More.\n **/"; + mb.classes( + "package p; " + cs + "public class Same { }\n", + "package p; " + ci + "public class Insert { }\n", + "package p; " + cr + "public class Remove { }\n", + "package p; " + cc + "public class Change { }\n" + ); + mb.write(srcDir); + + Path modulesDir = Files.createDirectories(base.resolve(apiName).resolve("modules")); + new JavacTask(tb) + .outdir(modulesDir) + .options("--module-source-path", srcDir.toString()) + .files(tb.findJavaFiles(srcDir)) + .run(); + + Path apiDir = Files.createDirectories(base.resolve(apiName).resolve("api")); + new JavadocTask(tb) + .outdir(apiDir) + .options("--module-source-path", srcDir.toString(), + "--module", "mA") + .run(); + + options.addAll(List.of( + "--api", apiName, + "--module-path", modulesDir.toString(), + "--api-directory", apiDir.toString())); + } + options.addAll(List.of( + "--include", "mA/**", + "-d", base.resolve("out").toString())); + + log.println("Options: " + options); + + Map outMap = run(options); + } +} diff --git a/test/junit/apitest/lib/APITester.java b/test/junit/apitest/lib/APITester.java new file mode 100644 index 0000000..9e698b2 --- /dev/null +++ b/test/junit/apitest/lib/APITester.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package apitest.lib; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.StackWalker.StackFrame; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jdk.codetools.apidiff.Main; +import jdk.codetools.apidiff.Main.Result; +import toolbox.ToolBox; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * A base class providing utility methods for test classes. + */ +public class APITester { + private static final String lineSeparator = System.lineSeparator(); + Path base = Paths.get("build/test"); // TODO: better if injected + + /** A toolbox. */ + protected final ToolBox tb = new ToolBox(); + + /** A stream for logging messages. */ + protected final PrintStream log = System.out; + + /** The kind of output. */ + public enum OutputKind { + /** Output written to the standard output stream. */ + OUT, + /** Output written to the standard error stream. */ + ERR + } + + public Map run(List options) throws Error { + return run(options, EnumSet.of(Result.OK, Result.DIFFS)); + } + + /** + * Executes a new instance of apidiff with a given set of options, + * returning the output that is written to the standard output and error + * stream. + * + * @param options the options + * @return the output + */ + public Map run(List options, Set expectResult) { + Main.Result r; + StringWriter outSW = new StringWriter(); + StringWriter errSW = new StringWriter(); + try (PrintWriter out = new PrintWriter(outSW); PrintWriter err = new PrintWriter(errSW)) { + Main m = new Main(out, err); + r = m.run(options); + } + Map map = new EnumMap<>(OutputKind.class); + + String out = outSW.toString(); + log.println("stdout:"); + log.println(out); + map.put(OutputKind.OUT, out); + + String err = errSW.toString(); + log.println("stderr:"); + log.println(err); + map.put(OutputKind.ERR, err); + + // defer this check until stdout and stderr have been written out + if (!expectResult.contains(r)) { + throw new AssertionError("unexpected result: " + r + "; expected: " + expectResult); + } + + return map; + } + + /** + * Returns an empty scratch directory based on the name of the class and method + * calling this method. + * + * @return the path of a clean scratch directory + * @throws IOException if there is a problem creating the directory + */ + protected Path getScratchDir() throws IOException { + StackFrame caller = StackWalker.getInstance() + .walk(s -> s.filter(f -> !f.getClassName().equals(APITester.class.getName())) + .findFirst() + .orElseThrow()); + + Path dir = Files.createDirectories(base + .resolve("work") + .resolve(caller.getClassName().replace(".", File.separator)) + .resolve(caller.getMethodName())); + tb.cleanDirectory(dir); + return dir; + } + + /** + * Returns an empty scratch directory based on the name of the class and method + * calling this method, and a given subdirectory name. + * + * @param subDir the name of the subdirectory + * + * @return the path of a clean scratch directory + * @throws IOException if there is a problem creating the directory + */ + protected Path getScratchDir(String subDir) throws IOException { + StackFrame caller = StackWalker.getInstance() + .walk(s -> s.filter(f -> !f.getClassName().equals(APITester.class.getName())) + .findFirst() + .orElseThrow()); + + Path dir = Files.createDirectories(base + .resolve("work") + .resolve(caller.getClassName().replace(".", File.separator)) + .resolve(caller.getMethodName()) + .resolve(subDir)); + tb.cleanDirectory(dir); + return dir; + } + + protected String getClassMethodName() { + StackFrame caller = StackWalker.getInstance() + .walk(s -> s.filter(f -> !f.getClassName().equals(APITester.class.getName())) + .findFirst() + .orElseThrow()); + return caller.getClassName().replaceAll("^.*\\.", "") + "." + caller.getMethodName(); + } + + public void checkOutput(Path p, String... expect) throws IOException { + String s = Files.readString(p); + for (String e : expect) { + assertTrue(s.contains(e), "expected content not found: " + e); + } + } +} diff --git a/test/junit/apitest/lib/package-info.java b/test/junit/apitest/lib/package-info.java new file mode 100644 index 0000000..c1da5dd --- /dev/null +++ b/test/junit/apitest/lib/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Library and support code for testing the "apidiff" tool. + */ +package apitest.lib; diff --git a/test/junit/apitest/package-info.java b/test/junit/apitest/package-info.java new file mode 100644 index 0000000..1a15460 --- /dev/null +++ b/test/junit/apitest/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Tests for the "apidiff" tool. + */ +package apitest; diff --git a/test/junit/toolbox/AbstractTask.java b/test/junit/toolbox/AbstractTask.java new file mode 100644 index 0000000..83aca07 --- /dev/null +++ b/test/junit/toolbox/AbstractTask.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Path; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import static toolbox.ToolBox.lineSeparator; + +/** + * A utility base class to simplify the implementation of tasks. + * Provides support for running the task in a process and for + * capturing output written by the task to stdout, stderr and + * other writers where applicable. + * @param the implementing subclass + */ +abstract class AbstractTask> implements Task { + protected final ToolBox toolBox; + protected final Mode mode; + private final Map redirects = new EnumMap<>(OutputKind.class); + private final Map envVars = new HashMap<>(); + private Expect expect = Expect.SUCCESS; + int expectedExitCode = 0; + + /** + * Create a task that will execute in the specified mode. + * @param mode the mode + */ + protected AbstractTask(ToolBox tb, Mode mode) { + toolBox = tb; + this.mode = mode; + } + + /** + * Sets the expected outcome of the task and calls {@code run()}. + * @param expect the expected outcome + * @return the result of calling {@code run()} + */ + public Result run(Expect expect) { + expect(expect, Integer.MIN_VALUE); + return run(); + } + + /** + * Sets the expected outcome of the task and calls {@code run()}. + * @param expect the expected outcome + * @param exitCode the expected exit code if the expected outcome + * is {@code FAIL} + * @return the result of calling {@code run()} + */ + public Result run(Expect expect, int exitCode) { + expect(expect, exitCode); + return run(); + } + + /** + * Sets the expected outcome and expected exit code of the task. + * The exit code will not be checked if the outcome is + * {@code Expect.SUCCESS} or if the exit code is set to + * {@code Integer.MIN_VALUE}. + * @param expect the expected outcome + * @param exitCode the expected exit code + */ + protected void expect(Expect expect, int exitCode) { + this.expect = expect; + this.expectedExitCode = exitCode; + } + + /** + * Checks the exit code contained in a {@code Result} against the + * expected outcome and exit value + * @param result the result object + * @return the result object + * @throws TaskError if the exit code stored in the result object + * does not match the expected outcome and exit code. + */ + protected Result checkExit(Result result) throws TaskError { + switch (expect) { + case SUCCESS: + if (result.exitCode != 0) { + result.writeAll(); + throw new TaskError("Task " + name() + " failed: rc=" + result.exitCode); + } + break; + + case FAIL: + if (result.exitCode == 0) { + result.writeAll(); + throw new TaskError("Task " + name() + " succeeded unexpectedly"); + } + + if (expectedExitCode != Integer.MIN_VALUE + && result.exitCode != expectedExitCode) { + result.writeAll(); + throw new TaskError("Task " + name() + "failed with unexpected exit code " + + result.exitCode + ", expected " + expectedExitCode); + } + break; + } + return result; + } + + /** + * Sets an environment variable to be used by this task. + * @param name the name of the environment variable + * @param value the value for the environment variable + * @return this task object + * @throws IllegalStateException if the task mode is not {@code EXEC} + */ + public T envVar(String name, String value) { + if (mode != Mode.EXEC) + throw new IllegalStateException(); + envVars.put(name, value); + return (T) this; + } + + /** + * Redirects output from an output stream to a file. + * @param outputKind the name of the stream to be redirected. + * @param path the file + * @return this task object + * @throws IllegalStateException if the task mode is not {@code EXEC} + */ + public T redirect(OutputKind outputKind, String path) { + if (mode != Mode.EXEC) + throw new IllegalStateException(); + redirects.put(outputKind, path); + return (T) this; + } + + /** + * Returns a {@code ProcessBuilder} initialized with any + * redirects and environment variables that have been set. + * @return a {@code ProcessBuilder} + */ + protected ProcessBuilder getProcessBuilder() { + if (mode != Mode.EXEC) + throw new IllegalStateException(); + ProcessBuilder pb = new ProcessBuilder(); + if (redirects.get(OutputKind.STDOUT) != null) + pb.redirectOutput(Path.of(redirects.get(OutputKind.STDOUT)).toFile()); + if (redirects.get(OutputKind.STDERR) != null) + pb.redirectError(Path.of(redirects.get(OutputKind.STDERR)).toFile()); + pb.environment().putAll(envVars); + return pb; + } + + /** + * Collects the output from a process and saves it in a {@code Result}. + * @param tb the {@code ToolBox} containing the task {@code t} + * @param t the task initiating the process + * @param p the process + * @return a Result object containing the output from the process and its + * exit value. + * @throws InterruptedException if the thread is interrupted + */ + protected Result runProcess(ToolBox tb, Task t, Process p) throws InterruptedException { + if (mode != Mode.EXEC) + throw new IllegalStateException(); + ProcessOutput sysOut = new ProcessOutput(p.getInputStream()).start(); + ProcessOutput sysErr = new ProcessOutput(p.getErrorStream()).start(); + sysOut.waitUntilDone(); + sysErr.waitUntilDone(); + int rc = p.waitFor(); + Map outputMap = new EnumMap<>(OutputKind.class); + outputMap.put(OutputKind.STDOUT, sysOut.getOutput()); + outputMap.put(OutputKind.STDERR, sysErr.getOutput()); + return checkExit(new Result(toolBox, t, rc, outputMap)); + } + + /** + * Thread-friendly class to read the output from a process until the stream + * is exhausted. + */ + static class ProcessOutput implements Runnable { + ProcessOutput(InputStream from) { + in = new BufferedReader(new InputStreamReader(from)); + out = new StringBuilder(); + } + + ProcessOutput start() { + new Thread(this).start(); + return this; + } + + @Override + public void run() { + try { + String line; + while ((line = in.readLine()) != null) { + out.append(line).append(lineSeparator); + } + } catch (IOException e) { + } + synchronized (this) { + done = true; + notifyAll(); + } + } + + synchronized void waitUntilDone() throws InterruptedException { + boolean interrupted = false; + + // poll interrupted flag, while waiting for copy to complete + while (!(interrupted = Thread.interrupted()) && !done) + wait(1000); + + if (interrupted) + throw new InterruptedException(); + } + + String getOutput() { + return out.toString(); + } + + private final BufferedReader in; + private final StringBuilder out; + private boolean done; + } + + /** + * Utility class to simplify the handling of temporarily setting a + * new stream for System.out or System.err. + */ + static class StreamOutput { + // Functional interface to set a stream. + // Expected use: System::setOut, System::setErr + interface Initializer { + void set(PrintStream s); + } + + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final PrintStream ps = new PrintStream(baos); + private final PrintStream prev; + private final Initializer init; + + StreamOutput(PrintStream s, Initializer init) { + prev = s; + init.set(ps); + this.init = init; + } + + /** + * Closes the stream and returns the contents that were written to it. + * @return the contents that were written to it. + */ + String close() { + init.set(prev); + ps.close(); + return baos.toString(); + } + } + + /** + * Utility class to simplify the handling of creating an in-memory PrintWriter. + */ + static class WriterOutput { + private final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + + /** + * Closes the stream and returns the contents that were written to it. + * @return the contents that were written to it. + */ + String close() { + pw.close(); + return sw.toString(); + } + } +} diff --git a/test/junit/toolbox/Assert.java b/test/junit/toolbox/Assert.java new file mode 100644 index 0000000..b046158 --- /dev/null +++ b/test/junit/toolbox/Assert.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.util.function.Supplier; + +/** + * Simple facility for unconditional assertions. + * The methods in this class are described in terms of equivalent assert + * statements, assuming that assertions have been enabled. + */ +public class Assert { + /** Equivalent to + * assert cond; + */ + public static void check(boolean cond) { + if (!cond) + error(); + } + + /** Equivalent to + * assert (o == null); + */ + public static void checkNull(Object o) { + if (o != null) + error(); + } + + /** Equivalent to + * assert (t != null); return t; + */ + public static T checkNonNull(T t) { + if (t == null) + error(); + return t; + } + + /** Equivalent to + * assert cond : value; + */ + public static void check(boolean cond, int value) { + if (!cond) + error(String.valueOf(value)); + } + + /** Equivalent to + * assert cond : value; + */ + public static void check(boolean cond, long value) { + if (!cond) + error(String.valueOf(value)); + } + + /** Equivalent to + * assert cond : value; + */ + public static void check(boolean cond, Object value) { + if (!cond) + error(String.valueOf(value)); + } + + /** Equivalent to + * assert cond : msg; + */ + public static void check(boolean cond, String msg) { + if (!cond) + error(msg); + } + + /** Equivalent to + * assert cond : msg.get(); + * Note: message string is computed lazily. + */ + public static void check(boolean cond, Supplier msg) { + if (!cond) + error(msg.get()); + } + + /** Equivalent to + * assert (o == null) : value; + */ + public static void checkNull(Object o, Object value) { + if (o != null) + error(String.valueOf(value)); + } + + /** Equivalent to + * assert (o == null) : msg; + */ + public static void checkNull(Object o, String msg) { + if (o != null) + error(msg); + } + + /** Equivalent to + * assert (o == null) : msg.get(); + * Note: message string is computed lazily. + */ + public static void checkNull(Object o, Supplier msg) { + if (o != null) + error(msg.get()); + } + + /** Equivalent to + * assert (o != null) : msg; + */ + public static T checkNonNull(T t, String msg) { + if (t == null) + error(msg); + return t; + } + + /** Equivalent to + * assert (o != null) : msg.get(); + * Note: message string is computed lazily. + */ + public static T checkNonNull(T t, Supplier msg) { + if (t == null) + error(msg.get()); + return t; + } + + /** Equivalent to + * assert false; + */ + public static void error() { + throw new AssertionError(); + } + + /** Equivalent to + * assert false : msg; + */ + public static void error(String msg) { + throw new AssertionError(msg); + } + + /** Prevent instantiation. */ + private Assert() { } +} diff --git a/test/junit/toolbox/ExecTask.java b/test/junit/toolbox/ExecTask.java new file mode 100644 index 0000000..18ae8d3 --- /dev/null +++ b/test/junit/toolbox/ExecTask.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A task to configure and run a general command. + */ +public class ExecTask extends AbstractTask { + private final String command; + private List args; + + /** + * Create a task to execute a given command, to be run using {@code EXEC} mode. + * @param toolBox the {@code ToolBox} to use + * @param command the command to be executed + */ + public ExecTask(ToolBox toolBox, String command) { + super(toolBox, Task.Mode.EXEC); + this.command = command; + } + + /** + * Create a task to execute a given command, to be run using {@code EXEC} mode. + * @param toolBox the {@code ToolBox} to use + * @param command the command to be executed + */ + public ExecTask(ToolBox toolBox, Path command) { + super(toolBox, Task.Mode.EXEC); + this.command = command.toString(); + } + + /** + * Sets the arguments for the command to be executed + * @param args the arguments + * @return this task object + */ + public ExecTask args(String... args) { + this.args = Arrays.asList(args); + return this; + } + + /** + * {@inheritDoc} + * @return the name "exec" + */ + @Override + public String name() { + return "exec"; + } + + /** + * Calls the command with the arguments as currently configured. + * @return a Result object indicating the outcome of the task + * and the content of any output written to stdout or stderr. + * @throws TaskError if the outcome of the task is not as expected. + */ + @Override + public Task.Result run() { + List cmdArgs = new ArrayList<>(); + cmdArgs.add(command); + if (args != null) + cmdArgs.addAll(args); + ProcessBuilder pb = getProcessBuilder(); + pb.command(cmdArgs); + try { + return runProcess(toolBox, this, pb.start()); + } catch (IOException | InterruptedException e) { + throw new Error(e); + } + } +} diff --git a/test/junit/toolbox/JarTask.java b/test/junit/toolbox/JarTask.java new file mode 100644 index 0000000..0d15d12 --- /dev/null +++ b/test/junit/toolbox/JarTask.java @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.tools.FileObject; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import static toolbox.ToolBox.currDir; + +/** + * A task to configure and run the jar file utility. + */ +public class JarTask extends AbstractTask { + private Path jar; + private Manifest manifest; + private String classpath; + private String mainClass; + private Path baseDir; + private List paths; + private Set fileObjects; + + /** + * Creates a task to write jar files, using API mode. + * @param toolBox the {@code ToolBox} to use + */ + public JarTask(ToolBox toolBox) { + super(toolBox, Task.Mode.API); + paths = Collections.emptyList(); + fileObjects = new LinkedHashSet<>(); + } + + /** + * Creates a JarTask for use with a given jar file. + * @param toolBox the {@code ToolBox} to use + * @param path the file + */ + public JarTask(ToolBox toolBox, String path) { + this(toolBox); + jar = Paths.get(path); + } + + /** + * Creates a JarTask for use with a given jar file. + * @param toolBox the {@code ToolBox} to use + * @param path the file + */ + public JarTask(ToolBox toolBox, Path path) { + this(toolBox); + jar = path; + } + + /** + * Sets a manifest for the jar file. + * @param manifest the manifest + * @return this task object + */ + public JarTask manifest(Manifest manifest) { + this.manifest = manifest; + return this; + } + + /** + * Sets a manifest for the jar file. + * @param manifest a string containing the contents of the manifest + * @return this task object + * @throws IOException if there is a problem creating the manifest + */ + public JarTask manifest(String manifest) throws IOException { + this.manifest = new Manifest(new ByteArrayInputStream(manifest.getBytes())); + return this; + } + + /** + * Sets the classpath to be written to the {@code Class-Path} + * entry in the manifest. + * @param classpath the classpath + * @return this task object + */ + public JarTask classpath(String classpath) { + this.classpath = classpath; + return this; + } + + /** + * Sets the class to be written to the {@code Main-Class} + * entry in the manifest.. + * @param mainClass the name of the main class + * @return this task object + */ + public JarTask mainClass(String mainClass) { + this.mainClass = mainClass; + return this; + } + + /** + * Sets the base directory for files to be written into the jar file. + * @param baseDir the base directory + * @return this task object + */ + public JarTask baseDir(String baseDir) { + this.baseDir = Paths.get(baseDir); + return this; + } + + /** + * Sets the base directory for files to be written into the jar file. + * @param baseDir the base directory + * @return this task object + */ + public JarTask baseDir(Path baseDir) { + this.baseDir = baseDir; + return this; + } + + /** + * Sets the files to be written into the jar file. + * @param files the files + * @return this task object + */ + public JarTask files(String... files) { + this.paths = Stream.of(files) + .map(file -> Paths.get(file)) + .collect(Collectors.toList()); + return this; + } + + /** + * Adds a set of file objects to be written into the jar file, by copying them + * from a Location in a JavaFileManager. + * The file objects to be written are specified by a series of paths; + * each path can be in one of the following forms: + *
                  + *
                • The name of a class. For example, java.lang.Object. + * In this case, the corresponding .class file will be written to the jar file. + *
                • the name of a package followed by {@code .*}. For example, {@code java.lang.*}. + * In this case, all the class files in the specified package will be written to + * the jar file. + *
                • the name of a package followed by {@code .**}. For example, {@code java.lang.**}. + * In this case, all the class files in the specified package, and any subpackages + * will be written to the jar file. + *
                + * + * @param fm the file manager in which to find the file objects + * @param l the location in which to find the file objects + * @param paths the paths specifying the file objects to be copied + * @return this task object + * @throws IOException if errors occur while determining the set of file objects + */ + public JarTask files(JavaFileManager fm, JavaFileManager.Location l, String... paths) + throws IOException { + for (String p : paths) { + if (p.endsWith(".**")) + addPackage(fm, l, p.substring(0, p.length() - 3), true); + else if (p.endsWith(".*")) + addPackage(fm, l, p.substring(0, p.length() - 2), false); + else + addFile(fm, l, p); + } + return this; + } + + private void addPackage(JavaFileManager fm, JavaFileManager.Location l, String pkg, boolean recurse) + throws IOException { + for (JavaFileObject fo : fm.list(l, pkg, EnumSet.allOf(JavaFileObject.Kind.class), recurse)) { + fileObjects.add(fo); + } + } + + private void addFile(JavaFileManager fm, JavaFileManager.Location l, String path) throws IOException { + JavaFileObject fo = fm.getJavaFileForInput(l, path, JavaFileObject.Kind.CLASS); + fileObjects.add(fo); + } + + /** + * Provides limited jar command-like functionality. + * The supported commands are: + *
                  + *
                • jar cf jarfile -C dir files... + *
                • jar cfm jarfile manifestfile -C dir files... + *
                + * Any values specified by other configuration methods will be ignored. + * @param args arguments in the style of those for the jar command + * @return a Result object containing the results of running the task + */ + public Task.Result run(String... args) { + if (args.length < 2) + throw new IllegalArgumentException(); + + ListIterator iter = Arrays.asList(args).listIterator(); + String first = iter.next(); + switch (first) { + case "cf": + jar = Paths.get(iter.next()); + break; + case "cfm": + jar = Paths.get(iter.next()); + try (InputStream in = Files.newInputStream(Paths.get(iter.next()))) { + manifest = new Manifest(in); + } catch (IOException e) { + throw new IOError(e); + } + break; + } + + if (iter.hasNext()) { + if (iter.next().equals("-C")) + baseDir = Paths.get(iter.next()); + else + iter.previous(); + } + + paths = new ArrayList<>(); + while (iter.hasNext()) + paths.add(Paths.get(iter.next())); + + return run(); + } + + /** + * {@inheritDoc} + * @return the name "jar" + */ + @Override + public String name() { + return "jar"; + } + + /** + * Creates a jar file with the arguments as currently configured. + * @return a Result object indicating the outcome of the compilation + * and the content of any output written to stdout, stderr, or the + * main stream by the compiler. + * @throws TaskError if the outcome of the task is not as expected. + */ + @Override + public Task.Result run() { + Manifest m = (manifest == null) ? new Manifest() : manifest; + Attributes mainAttrs = m.getMainAttributes(); + if (mainClass != null) + mainAttrs.put(Attributes.Name.MAIN_CLASS, mainClass); + if (classpath != null) + mainAttrs.put(Attributes.Name.CLASS_PATH, classpath); + + AbstractTask.StreamOutput sysOut = new AbstractTask.StreamOutput(System.out, System::setOut); + AbstractTask.StreamOutput sysErr = new AbstractTask.StreamOutput(System.err, System::setErr); + + Map outputMap = new HashMap<>(); + + try (OutputStream os = Files.newOutputStream(jar); + JarOutputStream jos = openJar(os, m)) { + writeFiles(jos); + writeFileObjects(jos); + } catch (IOException e) { + error("Exception while opening " + jar, e); + } finally { + outputMap.put(Task.OutputKind.STDOUT, sysOut.close()); + outputMap.put(Task.OutputKind.STDERR, sysErr.close()); + } + return checkExit(new Task.Result(toolBox, this, (errors == 0) ? 0 : 1, outputMap)); + } + + private JarOutputStream openJar(OutputStream os, Manifest m) throws IOException { + if (m == null || m.getMainAttributes().isEmpty() && m.getEntries().isEmpty()) { + return new JarOutputStream(os); + } else { + if (m.getMainAttributes().get(Attributes.Name.MANIFEST_VERSION) == null) + m.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + return new JarOutputStream(os, m); + } + } + + private void writeFiles(JarOutputStream jos) throws IOException { + Path base = (baseDir == null) ? currDir : baseDir; + for (Path path : paths) { + Files.walkFileTree(base.resolve(path), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + try { + String p = base.relativize(file) + .normalize() + .toString() + .replace(File.separatorChar, '/'); + JarEntry e = new JarEntry(p); + jos.putNextEntry(e); + try { + jos.write(Files.readAllBytes(file)); + } finally { + jos.closeEntry(); + } + return FileVisitResult.CONTINUE; + } catch (IOException e) { + error("Exception while adding " + file + " to jar file", e); + return FileVisitResult.TERMINATE; + } + } + }); + } + } + + private void writeFileObjects(JarOutputStream jos) throws IOException { + for (FileObject fo : fileObjects) { + String p = guessPath(fo); + JarEntry e = new JarEntry(p); + jos.putNextEntry(e); + try { + byte[] buf = new byte[1024]; + try (BufferedInputStream in = new BufferedInputStream(fo.openInputStream())) { + int n; + while ((n = in.read(buf)) > 0) + jos.write(buf, 0, n); + } catch (IOException ex) { + error("Exception while adding " + fo.getName() + " to jar file", ex); + } + } finally { + jos.closeEntry(); + } + } + } + + /* + * A jar: URL is of the form jar:URL!/ where URL is a URL for the .jar file itself. + * In Symbol files (i.e. ct.sym) the underlying entry is prefixed META-INF/sym/. + */ + private final Pattern jarEntry = Pattern.compile(".*!/(?:META-INF/sym/[^/]+/)?(.*)"); + + /* + * A jrt: URL is of the form jrt:/modules/// + */ + private final Pattern jrtEntry = Pattern.compile("/modules/([^/]+)/(.*)"); + + /* + * A file: URL is of the form file:/path/to/{modules,patches}/// + */ + private final Pattern fileEntry = Pattern.compile(".*/(?:modules|patches)/([^/]+)/(.*)"); + + private String guessPath(FileObject fo) { + URI u = fo.toUri(); + switch (u.getScheme()) { + case "jar": { + Matcher m = jarEntry.matcher(u.getSchemeSpecificPart()); + if (m.matches()) { + return m.group(1); + } + break; + } + case "jrt": { + Matcher m = jrtEntry.matcher(u.getSchemeSpecificPart()); + if (m.matches()) { + return m.group(2); + } + break; + } + case "file": { + Matcher m = fileEntry.matcher(u.getSchemeSpecificPart()); + if (m.matches()) { + return m.group(2); + } + break; + } + } + throw new IllegalArgumentException(fo.getName() + "--" + fo.toUri()); + } + + private void error(String message, Throwable t) { + toolBox.out.println("Error: " + message + ": " + t); + errors++; + } + + private int errors; +} diff --git a/test/junit/toolbox/JavaTask.java b/test/junit/toolbox/JavaTask.java new file mode 100644 index 0000000..ec1b07c --- /dev/null +++ b/test/junit/toolbox/JavaTask.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A task to configure and run the Java launcher. + */ +public class JavaTask extends AbstractTask { + boolean includeStandardOptions = true; + private String classpath; + private List vmOptions; + private String className; + private List classArgs; + + /** + * Create a task to run the Java launcher, using {@code EXEC} mode. + * @param toolBox the {@code ToolBox} to use + */ + public JavaTask(ToolBox toolBox) { + super(toolBox, Task.Mode.EXEC); + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavaTask classpath(String classpath) { + this.classpath = classpath; + return this; + } + + /** + * Sets the VM options. + * @param vmOptions the options + * @return this task object + */ + public JavaTask vmOptions(String... vmOptions) { + this.vmOptions = Arrays.asList(vmOptions); + return this; + } + + /** + * Sets the VM options. + * @param vmOptions the options + * @return this task object + */ + public JavaTask vmOptions(List vmOptions) { + this.vmOptions = vmOptions; + return this; + } + + /** + * Sets the name of the class to be executed. + * @param className the name of the class + * @return this task object + */ + public JavaTask className(String className) { + this.className = className; + return this; + } + + /** + * Sets the arguments for the class to be executed. + * @param classArgs the arguments + * @return this task object + */ + public JavaTask classArgs(String... classArgs) { + this.classArgs = Arrays.asList(classArgs); + return this; + } + + /** + * Sets the arguments for the class to be executed. + * @param classArgs the arguments + * @return this task object + */ + public JavaTask classArgs(List classArgs) { + this.classArgs = classArgs; + return this; + } + + /** + * Sets whether or not the standard VM and java options for the test should be passed + * to the new VM instance. If this method is not called, the default behavior is that + * the options will be passed to the new VM instance. + * + * @param includeStandardOptions whether or not the standard VM and java options for + * the test should be passed to the new VM instance. + * @return this task object + */ + public JavaTask includeStandardOptions(boolean includeStandardOptions) { + this.includeStandardOptions = includeStandardOptions; + return this; + } + + /** + * {@inheritDoc} + * @return the name "java" + */ + @Override + public String name() { + return "java"; + } + + /** + * Calls the Java launcher with the arguments as currently configured. + * @return a Result object indicating the outcome of the task + * and the content of any output written to stdout or stderr. + * @throws TaskError if the outcome of the task is not as expected. + */ + @Override + public Task.Result run() { + List args = new ArrayList<>(); + args.add(toolBox.getJDKTool("java").toString()); + if (includeStandardOptions) { + args.addAll(toolBox.split(System.getProperty("test.vm.opts"), " +")); + args.addAll(toolBox.split(System.getProperty("test.java.opts"), " +")); + } + if (classpath != null) { + args.add("-classpath"); + args.add(classpath); + } + if (vmOptions != null) + args.addAll(vmOptions); + if (className != null) + args.add(className); + if (classArgs != null) + args.addAll(classArgs); + ProcessBuilder pb = getProcessBuilder(); + pb.command(args); + try { + return runProcess(toolBox, this, pb.start()); + } catch (IOException | InterruptedException e) { + throw new Error(e); + } + } +} diff --git a/test/junit/toolbox/JavacTask.java b/test/junit/toolbox/JavacTask.java new file mode 100644 index 0000000..ead7262 --- /dev/null +++ b/test/junit/toolbox/JavacTask.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; + +import com.sun.tools.javac.api.JavacTaskImpl; +import com.sun.tools.javac.api.JavacTool; + +/** + * A task to configure and run the Java compiler, javac. + */ +public class JavacTask extends AbstractTask { + private boolean includeStandardOptions; + private List classpath; + private List sourcepath; + private Path outdir; + private List options; + private List classes; + private List files; + private List fileObjects; + private JavaFileManager fileManager; + private Consumer callback; + + private JavaCompiler compiler; + private StandardJavaFileManager internalFileManager; + + /** + * Creates a task to execute {@code javac} using API mode. + * @param toolBox the {@code ToolBox} to use + */ + public JavacTask(ToolBox toolBox) { + super(toolBox, Task.Mode.API); + } + + /** + * Creates a task to execute {@code javac} in a specified mode. + * @param toolBox the {@code ToolBox} to use + * @param mode the mode to be used + */ + public JavacTask(ToolBox toolBox, Task.Mode mode) { + super(toolBox, mode); + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavacTask classpath(String classpath) { + this.classpath = Stream.of(classpath.split(File.pathSeparator)) + .filter(s -> !s.isEmpty()) + .map(s -> Paths.get(s)) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavacTask classpath(Path... classpath) { + this.classpath = Arrays.asList(classpath); + return this; + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavacTask classpath(List classpath) { + this.classpath = classpath; + return this; + } + + /** + * Sets the sourcepath. + * @param sourcepath the sourcepath + * @return this task object + */ + public JavacTask sourcepath(String sourcepath) { + this.sourcepath = Stream.of(sourcepath.split(File.pathSeparator)) + .filter(s -> !s.isEmpty()) + .map(s -> Paths.get(s)) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the sourcepath. + * @param sourcepath the sourcepath + * @return this task object + */ + public JavacTask sourcepath(Path... sourcepath) { + this.sourcepath = Arrays.asList(sourcepath); + return this; + } + + /** + * Sets the sourcepath. + * @param sourcepath the sourcepath + * @return this task object + */ + public JavacTask sourcepath(List sourcepath) { + this.sourcepath = sourcepath; + return this; + } + + /** + * Sets the output directory. + * @param outdir the output directory + * @return this task object + */ + public JavacTask outdir(String outdir) { + this.outdir = Paths.get(outdir); + return this; + } + + /** + * Sets the output directory. + * @param outdir the output directory + * @return this task object + */ + public JavacTask outdir(Path outdir) { + this.outdir = outdir; + return this; + } + + /** + * Sets the options. + * @param options the options + * @return this task object + */ + public JavacTask options(String... options) { + this.options = Arrays.asList(options); + return this; + } + + /** + * Sets the options. + * @param spaceSeparatedOption the space separated options + * @return this task object + */ + public JavacTask spaceSeparatedOptions(String spaceSeparatedOption) { + this.options = Arrays.asList(spaceSeparatedOption.split("\\s+")); + return this; + } + + /** + * Sets the options. + * @param options the options + * @return this task object + */ + public JavacTask options(List options) { + this.options = options; + return this; + } + + /** + * Sets the classes to be analyzed. + * @param classes the classes + * @return this task object + */ + public JavacTask classes(String... classes) { + this.classes = Arrays.asList(classes); + return this; + } + + /** + * Sets the files to be compiled or analyzed. + * @param files the files + * @return this task object + */ + public JavacTask files(String... files) { + this.files = Arrays.asList(files); + return this; + } + + /** + * Sets the files to be compiled or analyzed. + * @param files the files + * @return this task object + */ + public JavacTask files(Path... files) { + this.files = Stream.of(files) + .map(Path::toString) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the files to be compiled or analyzed. + * @param files the files + * @return this task object + */ + public JavacTask files(List files) { + this.files = files.stream() + .map(Path::toString) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the sources to be compiled or analyzed. + * Each source string is converted into an in-memory object that + * can be passed directly to the compiler. + * @param sources the sources + * @return this task object + */ + public JavacTask sources(String... sources) { + fileObjects = Stream.of(sources) + .map(s -> new ToolBox.JavaSource(s)) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the file manager to be used by this task. + * @param fileManager the file manager + * @return this task object + */ + public JavacTask fileManager(JavaFileManager fileManager) { + this.fileManager = fileManager; + return this; + } + + /** + * Set a callback to be used by this task. + * @param callback the callback + * @return this task object + */ + public JavacTask callback(Consumer callback) { + this.callback = callback; + return this; + } + + /** + * {@inheritDoc} + * @return the name "javac" + */ + @Override + public String name() { + return "javac"; + } + + /** + * Calls the compiler with the arguments as currently configured. + * @return a Result object indicating the outcome of the compilation + * and the content of any output written to stdout, stderr, or the + * main stream by the compiler. + * @throws TaskError if the outcome of the task is not as expected. + */ + @Override + public Task.Result run() { + if (mode == Task.Mode.EXEC) + return runExec(); + + AbstractTask.WriterOutput direct = new AbstractTask.WriterOutput(); + // The following are to catch output to System.out and System.err, + // in case these are used instead of the primary (main) stream + AbstractTask.StreamOutput sysOut = new AbstractTask.StreamOutput(System.out, System::setOut); + AbstractTask.StreamOutput sysErr = new AbstractTask.StreamOutput(System.err, System::setErr); + int rc; + Map outputMap = new HashMap<>(); + try { + switch (mode == null ? Task.Mode.API : mode) { + case API: + rc = runAPI(direct.pw); + break; + case CMDLINE: + if (fileManager != null) { + throw new IllegalStateException("file manager set in CMDLINE mode"); + } + if (callback != null) { + throw new IllegalStateException("callback set in CMDLINE mode"); + } + rc = runCommand(direct.pw); + break; + default: + throw new IllegalStateException("unknown mode " + mode); + } + } catch (IOException e) { + toolBox.out.println("Exception occurred: " + e); + rc = 99; + } finally { + outputMap.put(Task.OutputKind.STDOUT, sysOut.close()); + outputMap.put(Task.OutputKind.STDERR, sysErr.close()); + outputMap.put(Task.OutputKind.DIRECT, direct.close()); + } + return checkExit(new Task.Result(toolBox, this, rc, outputMap)); + } + + private int runAPI(PrintWriter pw) throws IOException { + try { +// if (compiler == null) { + // TODO: allow this to be set externally +// compiler = ToolProvider.getSystemJavaCompiler(); + compiler = JavacTool.create(); +// } + + if (fileManager == null) + fileManager = internalFileManager = compiler.getStandardFileManager(null, null, null); + if (outdir != null) + setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singletonList(outdir)); + if (classpath != null) + setLocationFromPaths(StandardLocation.CLASS_PATH, classpath); + if (sourcepath != null) + setLocationFromPaths(StandardLocation.SOURCE_PATH, sourcepath); + List allOpts = new ArrayList<>(); + if (options != null) + allOpts.addAll(options); + + Iterable allFiles = joinFiles(files, fileObjects); + JavaCompiler.CompilationTask task = compiler.getTask(pw, + fileManager, + null, // diagnostic listener; should optionally collect diags + allOpts, + classes, + allFiles); + JavacTaskImpl taskImpl = (JavacTaskImpl) task; + if (callback != null) { + callback.accept(taskImpl); + } + return taskImpl.doCall().exitCode; + } finally { + if (internalFileManager != null) + internalFileManager.close(); + } + } + + private void setLocationFromPaths(StandardLocation location, List files) throws IOException { + if (!(fileManager instanceof StandardJavaFileManager)) + throw new IllegalStateException("not a StandardJavaFileManager"); + ((StandardJavaFileManager) fileManager).setLocationFromPaths(location, files); + } + + private int runCommand(PrintWriter pw) { + List args = getAllArgs(); + String[] argsArray = args.toArray(new String[args.size()]); + return com.sun.tools.javac.Main.compile(argsArray, pw); + } + + private Task.Result runExec() { + List args = new ArrayList<>(); + Path javac = toolBox.getJDKTool("javac"); + args.add(javac.toString()); + if (includeStandardOptions) { + args.addAll(toolBox.split(System.getProperty("test.tool.vm.opts"), " +")); + args.addAll(toolBox.split(System.getProperty("test.compiler.opts"), " +")); + } + args.addAll(getAllArgs()); + + String[] argsArray = args.toArray(new String[args.size()]); + ProcessBuilder pb = getProcessBuilder(); + pb.command(argsArray); + try { + return runProcess(toolBox, this, pb.start()); + } catch (IOException | InterruptedException e) { + throw new Error(e); + } + } + + private List getAllArgs() { + List args = new ArrayList<>(); + if (options != null) + args.addAll(options); + if (outdir != null) { + args.add("-d"); + args.add(outdir.toString()); + } + if (classpath != null) { + args.add("-classpath"); + args.add(toSearchPath(classpath)); + } + if (sourcepath != null) { + args.add("-sourcepath"); + args.add(toSearchPath(sourcepath)); + } + if (classes != null) + args.addAll(classes); + if (files != null) + args.addAll(files); + + return args; + } + + private String toSearchPath(List files) { + return files.stream() + .map(Path::toString) + .collect(Collectors.joining(File.pathSeparator)); + } + + private Iterable joinFiles( + List files, List fileObjects) { + if (files == null) + return fileObjects; + if (internalFileManager == null) + internalFileManager = compiler.getStandardFileManager(null, null, null); + Iterable filesAsFileObjects = + internalFileManager.getJavaFileObjectsFromStrings(files); + if (fileObjects == null) + return filesAsFileObjects; + List combinedList = new ArrayList<>(); + for (JavaFileObject o : filesAsFileObjects) + combinedList.add(o); + combinedList.addAll(fileObjects); + return combinedList; + } +} diff --git a/test/junit/toolbox/JavadocTask.java b/test/junit/toolbox/JavadocTask.java new file mode 100644 index 0000000..4bc03a6 --- /dev/null +++ b/test/junit/toolbox/JavadocTask.java @@ -0,0 +1,418 @@ +/* + * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.tools.DocumentationTool.DocumentationTask; +import javax.tools.DocumentationTool; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +/** + * A task to configure and run the documentation tool, javadoc. + */ +public class JavadocTask extends AbstractTask { + private boolean includeStandardOptions; + private List classpath; + private List sourcepath; + private Path outdir; + private List options; + private List classes; + private List files; + private List fileObjects; + private JavaFileManager fileManager; + + private DocumentationTool jdtool; + private StandardJavaFileManager internalFileManager; + private Class docletClass = null; // use the standard doclet by default + + /** + * Creates a task to execute {@code javadoc} using API mode. + * @param toolBox the {@code ToolBox} to use + */ + public JavadocTask(ToolBox toolBox) { + super(toolBox, Task.Mode.API); + } + + /** + * Creates a task to execute {@code javadoc} in a specified mode. + * @param toolBox the {@code ToolBox} to use + * @param mode the mode to be used + */ + public JavadocTask(ToolBox toolBox, Task.Mode mode) { + super(toolBox, mode); + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavadocTask classpath(String classpath) { + this.classpath = Stream.of(classpath.split(File.pathSeparator)) + .filter(s -> !s.isEmpty()) + .map(s -> Paths.get(s)) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavadocTask classpath(Path... classpath) { + this.classpath = Arrays.asList(classpath); + return this; + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavadocTask classpath(List classpath) { + this.classpath = classpath; + return this; + } + + /** + * Sets the sourcepath. + * @param sourcepath the sourcepath + * @return this task object + */ + public JavadocTask sourcepath(String sourcepath) { + this.sourcepath = Stream.of(sourcepath.split(File.pathSeparator)) + .filter(s -> !s.isEmpty()) + .map(s -> Paths.get(s)) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the sourcepath. + * @param sourcepath the sourcepath + * @return this task object + */ + public JavadocTask sourcepath(Path... sourcepath) { + this.sourcepath = Arrays.asList(sourcepath); + return this; + } + + /** + * Sets the sourcepath. + * @param sourcepath the sourcepath + * @return this task object + */ + public JavadocTask sourcepath(List sourcepath) { + this.sourcepath = sourcepath; + return this; + } + + /** + * Sets the output directory. + * @param outdir the output directory + * @return this task object + */ + public JavadocTask outdir(String outdir) { + this.outdir = Paths.get(outdir); + return this; + } + + /** + * Sets the output directory. + * @param outdir the output directory + * @return this task object + */ + public JavadocTask outdir(Path outdir) { + this.outdir = outdir; + return this; + } + + /** + * Sets the options. + * @param options the options + * @return this task object + */ + public JavadocTask options(String... options) { + this.options = Arrays.asList(options); + return this; + } + + /** + * Sets the options. + * @param options the options + * @return this task object + */ + public JavadocTask options(List options) { + this.options = options; + return this; + } + + /** + * Sets the files to be documented. + * @param files the files + * @return this task object + */ + public JavadocTask files(String... files) { + this.files = Arrays.asList(files); + return this; + } + + /** + * Sets the files to be documented. + * @param files the files + * @return this task object + */ + public JavadocTask files(Path... files) { + this.files = Stream.of(files) + .map(Path::toString) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the files to be documented. + * @param files the files + * @return this task object + */ + public JavadocTask files(List files) { + this.files = files.stream() + .map(Path::toString) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the sources to be documented. + * Each source string is converted into an in-memory object that + * can be passed directly to the tool. + * @param sources the sources + * @return this task object + */ + public JavadocTask sources(String... sources) { + fileObjects = Stream.of(sources) + .map(s -> new ToolBox.JavaSource(s)) + .collect(Collectors.toList()); + return this; + } + + /** + * Sets the file manager to be used by this task. + * @param fileManager the file manager + * @return this task object + */ + public JavadocTask fileManager(JavaFileManager fileManager) { + this.fileManager = fileManager; + return this; + } + + /** + * Sets the doclet class to be invoked by javadoc. + * Note: this is applicable only in API mode. + * @param docletClass the user specified doclet + * @return this task object + */ + public JavadocTask docletClass(Class docletClass) { + this.docletClass = docletClass; + return this; + } + + /** + * {@inheritDoc} + * @return the name "javadoc" + */ + @Override + public String name() { + return "javadoc"; + } + + /** + * Calls the javadoc tool with the arguments as currently configured. + * @return a Result object indicating the outcome of the execution + * and the content of any output written to stdout, stderr, or the + * main stream by the tool. + */ + @Override + public Task.Result run() { + if (mode == Task.Mode.EXEC) + return runExec(); + + AbstractTask.WriterOutput direct = new AbstractTask.WriterOutput(); + // The following are to catch output to System.out and System.err, + // in case these are used instead of the primary (main) stream + AbstractTask.StreamOutput sysOut = new AbstractTask.StreamOutput(System.out, System::setOut); + AbstractTask.StreamOutput sysErr = new AbstractTask.StreamOutput(System.err, System::setErr); + int rc; + Map outputMap = new HashMap<>(); + try { + switch (mode == null ? Task.Mode.API : mode) { + case API: + rc = runAPI(direct.pw); + break; + case CMDLINE: + rc = runCommand(direct.pw); + break; + default: + throw new IllegalStateException(); + } + } catch (IOException e) { + toolBox.out.println("Exception occurred: " + e); + rc = 99; + } finally { + outputMap.put(Task.OutputKind.STDOUT, sysOut.close()); + outputMap.put(Task.OutputKind.STDERR, sysErr.close()); + outputMap.put(Task.OutputKind.DIRECT, direct.close()); + } + return checkExit(new Task.Result(toolBox, this, rc, outputMap)); + } + + private int runAPI(PrintWriter pw) throws IOException { + try { + jdtool = ToolProvider.getSystemDocumentationTool(); + + if (fileManager == null) + fileManager = internalFileManager = jdtool.getStandardFileManager(null, null, null); + if (outdir != null) + setLocationFromPaths(DocumentationTool.Location.DOCUMENTATION_OUTPUT, + Collections.singletonList(outdir)); + if (classpath != null) + setLocationFromPaths(StandardLocation.CLASS_PATH, classpath); + if (sourcepath != null) + setLocationFromPaths(StandardLocation.SOURCE_PATH, sourcepath); + List allOpts = new ArrayList<>(); + if (options != null) + allOpts.addAll(options); + + Iterable allFiles = joinFiles(files, fileObjects); + DocumentationTask task = jdtool.getTask(pw, + fileManager, + null, // diagnostic listener; should optionally collect diags + docletClass, + allOpts, + allFiles); + return task.call() ? 0 : 1; + } finally { + if (internalFileManager != null) + internalFileManager.close(); + } + } + + private void setLocationFromPaths(Location location, List files) throws IOException { + if (!(fileManager instanceof StandardJavaFileManager)) + throw new IllegalStateException("not a StandardJavaFileManager"); + ((StandardJavaFileManager) fileManager).setLocationFromPaths(location, files); + } + + private int runCommand(PrintWriter pw) { + List args = getAllArgs(); + String[] argsArray = args.toArray(new String[args.size()]); + java.util.spi.ToolProvider jdTool = java.util.spi.ToolProvider.findFirst("javadoc") + .orElseThrow(() -> new Error("can't find javadoc")); + return jdTool.run(pw, pw, argsArray); + } + + private Task.Result runExec() { + List args = new ArrayList<>(); + Path javadoc = toolBox.getJDKTool("javadoc"); + args.add(javadoc.toString()); + if (includeStandardOptions) { + args.addAll(toolBox.split(System.getProperty("test.tool.vm.opts"), " +")); + } + args.addAll(getAllArgs()); + + String[] argsArray = args.toArray(new String[args.size()]); + ProcessBuilder pb = getProcessBuilder(); + pb.command(argsArray); + try { + return runProcess(toolBox, this, pb.start()); + } catch (IOException | InterruptedException e) { + throw new Error(e); + } + } + + private List getAllArgs() { + List args = new ArrayList<>(); + if (options != null) + args.addAll(options); + if (outdir != null) { + args.add("-d"); + args.add(outdir.toString()); + } + if (classpath != null) { + args.add("-classpath"); + args.add(toSearchPath(classpath)); + } + if (sourcepath != null) { + args.add("-sourcepath"); + args.add(toSearchPath(sourcepath)); + } + if (classes != null) + args.addAll(classes); + if (files != null) + args.addAll(files); + + return args; + } + + private String toSearchPath(List files) { + return files.stream() + .map(Path::toString) + .collect(Collectors.joining(File.pathSeparator)); + } + + private Iterable joinFiles( + List files, List fileObjects) { + if (files == null) + return fileObjects; + if (internalFileManager == null) + internalFileManager = jdtool.getStandardFileManager(null, null, null); + Iterable filesAsFileObjects = + internalFileManager.getJavaFileObjectsFromStrings(files); + if (fileObjects == null) + return filesAsFileObjects; + List combinedList = new ArrayList<>(); + for (JavaFileObject o : filesAsFileObjects) + combinedList.add(o); + combinedList.addAll(fileObjects); + return combinedList; + } +} diff --git a/test/junit/toolbox/JavapTask.java b/test/junit/toolbox/JavapTask.java new file mode 100644 index 0000000..01b684b --- /dev/null +++ b/test/junit/toolbox/JavapTask.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A task to configure and run the disassembler tool, javap. + */ +public class JavapTask extends AbstractTask { + private String classpath; + private List options; + private List classes; + + /** + * Create a task to execute {@code javap} using {@code CMDLINE} mode. + * @param toolBox the {@code ToolBox} to use + */ + public JavapTask(ToolBox toolBox) { + super(toolBox, Task.Mode.CMDLINE); + } + + /** + * Sets the classpath. + * @param classpath the classpath + * @return this task object + */ + public JavapTask classpath(String classpath) { + this.classpath = classpath; + return this; + } + + /** + * Sets the options. + * @param options the options + * @return this task object + */ + public JavapTask options(String... options) { + this.options = Arrays.asList(options); + return this; + } + + /** + * Sets the classes to be analyzed. + * @param classes the classes + * @return this task object + */ + public JavapTask classes(String... classes) { + this.classes = Arrays.asList(classes); + return this; + } + + /** + * {@inheritDoc} + * @return the name "javap" + */ + @Override + public String name() { + return "javap"; + } + + /** + * Calls the javap tool with the arguments as currently configured. + * @return a Result object indicating the outcome of the task + * and the content of any output written to stdout, stderr, or the + * main stream. + * @throws TaskError if the outcome of the task is not as expected. + */ + @Override + public Task.Result run() { + List args = new ArrayList<>(); + if (options != null) + args.addAll(options); + if (classpath != null) { + args.add("-classpath"); + args.add(classpath); + } + if (classes != null) + args.addAll(classes); + + AbstractTask.WriterOutput direct = new AbstractTask.WriterOutput(); + // These are to catch output to System.out and System.err, + // in case these are used instead of the primary streams + AbstractTask.StreamOutput sysOut = new AbstractTask.StreamOutput(System.out, System::setOut); + AbstractTask.StreamOutput sysErr = new AbstractTask.StreamOutput(System.err, System::setErr); + + int rc; + Map outputMap = new HashMap<>(); + try { + String[] argsArray = args.toArray(new String[args.size()]); + java.util.spi.ToolProvider javapTool = java.util.spi.ToolProvider.findFirst("javap") + .orElseThrow(() -> new Error("can't find javap")); + rc = javapTool.run(direct.pw, direct.pw, argsArray); + } finally { + outputMap.put(Task.OutputKind.STDOUT, sysOut.close()); + outputMap.put(Task.OutputKind.STDERR, sysErr.close()); + outputMap.put(Task.OutputKind.DIRECT, direct.close()); + } + return checkExit(new Task.Result(toolBox, this, rc, outputMap)); + } +} diff --git a/test/junit/toolbox/ModuleBuilder.java b/test/junit/toolbox/ModuleBuilder.java new file mode 100644 index 0000000..a894606 --- /dev/null +++ b/test/junit/toolbox/ModuleBuilder.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Builder for module declarations. + */ +public class ModuleBuilder { + + private final ToolBox tb; + private final String name; + private String comment = ""; + private boolean open; + private List requires = new ArrayList<>(); + private List exports = new ArrayList<>(); + private List opens = new ArrayList<>(); + private List uses = new ArrayList<>(); + private List provides = new ArrayList<>(); + private List content = new ArrayList<>(); + private Set modulePath = new LinkedHashSet<>(); + + /** + * Creates a builder for a module. + * @param tb a Toolbox that can be used to compile the module declaration + * @param name the name of the module to be built + */ + public ModuleBuilder(ToolBox tb, String name) { + this(tb, false, name); + } + + /** + * Creates a builder for a module. + * @param tb a Toolbox that can be used to compile the module declaration + * @param open whether or not this is an open module + * @param name the name of the module to be built + */ + public ModuleBuilder(ToolBox tb, boolean open, String name) { + this.tb = tb; + this.open = open; + this.name = name; + } + + /** + * Sets the doc comment for the declaration. + * @param comment the content of the comment, excluding the initial + * '/**', leading whitespace and asterisks, and the final trailing 'a;/'. + * @return this builder + */ + public ModuleBuilder comment(String comment) { + this.comment = comment; + return this; + } + + /** + * Adds a "requires" directive to the declaration. + * @param module the name of the module that is required + * @param modulePath a path in which to locate the modules + * if the declaration is compiled + * @return this builder + */ + public ModuleBuilder requires(String module, Path... modulePath) { + addDirective(requires, "requires " + module + ";"); + this.modulePath.addAll(Arrays.asList(modulePath)); + return this; + + } + + /** + * Adds a "requires" directive to the declaration. + * @param module the name of the module that is required + * @param isStatic indicates a static dependency + * @param isTransitive indicates a transitive dependency + * @param modulePath a path in which to locate the modules + * if the declaration is compiled + * @return this builder + */ + public ModuleBuilder requires(String module, boolean isStatic, boolean isTransitive, Path... modulePath) { + addDirective(requires, "requires " + + (isStatic ? "static " : "") + + (isTransitive ? "transitive " : "") + + module + ";"); + this.modulePath.addAll(Arrays.asList(modulePath)); + return this; + + } + + /** + * Adds a "requires static" directive to the declaration. + * @param module the name of the module that is required + * @param modulePath a path in which to locate the modules + * if the declaration is compiled + * @return this builder + */ + public ModuleBuilder requiresStatic(String module, Path... modulePath) { + addDirective(requires, "requires static " + module + ";"); + this.modulePath.addAll(Arrays.asList(modulePath)); + return this; + } + + /** + * Adds a "requires transitive" directive to the declaration. + * @param module the name of the module that is required + * @param modulePath a path in which to locate the modules + * if the declaration is compiled + * @return this builder + */ + public ModuleBuilder requiresTransitive(String module, Path... modulePath) { + addDirective(requires, "requires transitive " + module + ";"); + this.modulePath.addAll(Arrays.asList(modulePath)); + return this; + } + + /** + * Adds a "requires static transitive" directive to the declaration. + * @param module the name of the module that is required + * @param modulePath a path in which to locate the modules + * if the declaration is compiled + * @return this builder + */ + public ModuleBuilder requiresStaticTransitive(String module, Path... modulePath) { + addDirective(requires, "requires static transitive " + module + ";"); + this.modulePath.addAll(Arrays.asList(modulePath)); + return this; + } + + /** + * Adds an unqualified "exports" directive to the declaration. + * @param pkg the name of the package to be exported + * @return this builder + */ + public ModuleBuilder exports(String pkg) { + return addDirective(exports, "exports " + pkg + ";"); + } + + /** + * Adds a qualified "exports" directive to the declaration. + * @param pkg the name of the package to be exported + * @param modules the names of the modules to which it is to be exported + * @return this builder + */ + public ModuleBuilder exportsTo(String pkg, String... modules) { + return addDirective(exports, "exports " + pkg + + (modules.length == 0 ? "" : " to " + String.join(", ", modules) + ";")); + } + + /** + * Adds an unqualified "opens" directive to the declaration. + * @param pkg the name of the package to be opened + * @return this builder + */ + public ModuleBuilder opens(String pkg) { + return addDirective(opens, "opens " + pkg + ";"); + } + + /** + * Adds a qualified "opens" directive to the declaration. + * @param pkg the name of the package to be opened + * @param modules the names of the modules to which it is to be opened + * @return this builder + */ + public ModuleBuilder opensTo(String pkg, String... modules) { + return addDirective(opens, "opens " + pkg + + (modules.length == 0 ? "" : " to " + String.join(", ", modules) + ";")); + } + + /** + * Adds a "uses" directive to the declaration. + * @param service the name of the service type + * @return this builder + */ + public ModuleBuilder uses(String service) { + return addDirective(uses, "uses " + service + ";"); + } + + /** + * Adds a "provides" directive to the declaration. + * @param service the name of the service type + * @param implementations the names of the implementation types + * @return this builder + */ + public ModuleBuilder provides(String service, String... implementations) { + return addDirective(provides, "provides " + service + + (implementations.length == 0 ? "" : " with " + String.join(", ", implementations) + ";")); + } + + private ModuleBuilder addDirective(List directives, String directive) { + directives.add(directive); + return this; + } + + /** + * Adds type definitions to the module. + * @param content a series of strings, each representing the content of + * a compilation unit to be included with the module + * @return this builder + */ + public ModuleBuilder classes(String... content) { + this.content.addAll(Arrays.asList(content)); + return this; + } + + /** + * Writes the module declaration and associated additional compilation + * units to a module directory within a given directory. + * + * @param srcDir the directory in which a directory will be created + * to contain the source files for the module + * @return the directory containing the source files for the module + * @throws IOException if an IO exception occurs + */ + public Path write(Path srcDir) throws IOException { + Files.createDirectories(srcDir); + List sources = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + if (!comment.isEmpty()) { + if (comment.startsWith("/**")) { + sb.append(comment); + } else { + sb.append("/**\n * ") + .append(comment.replace("\n", "\n * ")) + .append("\n */\n"); + } + } + if (open) { + sb.append("open "); + } + sb.append("module ").append(name).append(" {\n"); + requires.forEach(r -> sb.append(" " + r + "\n")); + exports.forEach(e -> sb.append(" " + e + "\n")); + opens.forEach(o -> sb.append(" " + o + "\n")); + uses.forEach(u -> sb.append(" " + u + "\n")); + provides.forEach(p -> sb.append(" " + p + "\n")); + sb.append("}"); + sources.add(sb.toString()); + sources.addAll(content); + Path moduleSrc = srcDir.resolve(name); + tb.writeJavaFiles(moduleSrc, sources.toArray(new String[]{})); + return moduleSrc; + } + + /** + * Writes the source files for the module to an interim directory, + * and then compiles them to a given directory. + * @param modules the directory in which a directory will be created + * to contain the compiled class files for the module + * @throws IOException if an error occurs while compiling the files + */ + public void build(Path modules) throws IOException { + build(Paths.get(modules + "Src"), modules); + } + + /** + * Writes the source files for the module to a specified directory, + * and then compiles them to a given directory. + * @param src the directory in which a directory will be created + * to contain the source files for the module + * @param modules the directory in which a directory will be created + * to contain the compiled class files for the module + * @throws IOException if an error occurs while compiling the files + */ + public void build(Path src, Path modules) throws IOException { + Path moduleSrc = write(src); + String mp = modulePath.stream() + .map(Path::toString) + .collect(Collectors.joining(File.pathSeparator)); + new JavacTask(tb) + .outdir(Files.createDirectories(modules.resolve(name))) + .options("--module-path", mp) + .files(tb.findJavaFiles(moduleSrc)) + .run() + .writeAll(); + } +} diff --git a/test/junit/toolbox/Task.java b/test/junit/toolbox/Task.java new file mode 100644 index 0000000..d4e183c --- /dev/null +++ b/test/junit/toolbox/Task.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import static toolbox.ToolBox.lineSeparator; + +/** + * The supertype for tasks. + * Complex operations are modeled by building and running a "Task" object. + * Tasks are typically configured in a fluent series of calls. + */ +public interface Task { + /** + * Returns the name of the task. + * @return the name of the task + */ + String name(); + + /** + * Executes the task as currently configured. + * @return a Result object containing the results of running the task + * @throws TaskError if the outcome of the task was not as expected + */ + Result run() throws TaskError; + + /** + * Exception thrown by {@code Task.run} when the outcome is not as + * expected. + */ + public static class TaskError extends Error { + private static final long serialVersionUID = 0; + + /** + * Creates a TaskError object with the given message. + * @param message the message + */ + public TaskError(String message) { + super(message); + } + } + + /** + * An enum to indicate the mode a task should use it is when executed. + */ + public enum Mode { + /** + * The task should use the interface used by the command + * line launcher for the task. + * For example, for javac: com.sun.tools.javac.Main.compile + */ + CMDLINE, + /** + * The task should use a publicly defined API for the task. + * For example, for javac: javax.tools.JavaCompiler + */ + API, + /** + * The task should use the standard launcher for the task. + * For example, $JAVA_HOME/bin/javac + */ + EXEC + } + + /** + * An enum to indicate the expected success or failure of executing a task. + */ + public enum Expect { + /** It is expected that the task will complete successfully. */ + SUCCESS, + /** It is expected that the task will not complete successfully. */ + FAIL + } + + /** + * An enum to identify the streams that may be written by a {@code Task}. + */ + public enum OutputKind { + /** Identifies output written to {@code System.out} or {@code stdout}. */ + STDOUT, + /** Identifies output written to {@code System.err} or {@code stderr}. */ + STDERR, + /** Identifies output written to a stream provided directly to the task. */ + DIRECT + }; + + /** + * The results from running a {@link Task}. + * The results contain the exit code returned when the tool was invoked, + * and a map containing the output written to any streams during the + * execution of the tool. + * All tools support "stdout" and "stderr". + * Tools that take an explicit PrintWriter save output written to that + * stream as "main". + */ + public static class Result { + final ToolBox toolBox; + final Task task; + final int exitCode; + final Map outputMap; + + Result(ToolBox toolBox, Task task, int exitCode, Map outputMap) { + this.toolBox = toolBox; + this.task = task; + this.exitCode = exitCode; + this.outputMap = outputMap; + } + + /** + * Returns the content of a specified stream. + * @param outputKind the kind of the selected stream + * @return the content that was written to that stream when the tool + * was executed. + */ + public String getOutput(OutputKind outputKind) { + return outputMap.get(outputKind); + } + + /** + * Returns the content of named streams as a list of lines. + * @param outputKinds the kinds of the selected streams + * @return the content that was written to the given streams when the tool + * was executed. + */ + public List getOutputLines(OutputKind... outputKinds) { + List result = new ArrayList<>(); + for (OutputKind outputKind : outputKinds) { + result.addAll(Arrays.asList(outputMap.get(outputKind).split(lineSeparator))); + } + return result; + } + + /** + * Writes the content of the specified stream to the log. + * @param kind the kind of the selected stream + * @return this Result object + */ + public Result write(OutputKind kind) { + PrintStream out = toolBox.out; + String text = getOutput(kind); + if (text == null || text.isEmpty()) + out.println("[" + task.name() + ":" + kind + "]: empty"); + else { + out.println("[" + task.name() + ":" + kind + "]:"); + out.print(text); + } + return this; + } + + /** + * Writes the content of all streams with any content to the log. + * @return this Result object + */ + public Result writeAll() { + PrintStream out = toolBox.out; + outputMap.forEach((name, text) -> { + if (!text.isEmpty()) { + out.println("[" + name + "]:"); + out.print(text); + } + }); + return this; + } + } +} + diff --git a/test/junit/toolbox/ToolBox.java b/test/junit/toolbox/ToolBox.java new file mode 100644 index 0000000..942a643 --- /dev/null +++ b/test/junit/toolbox/ToolBox.java @@ -0,0 +1,740 @@ +/* + * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package toolbox; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.FilterWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; + +/** + * Utility methods and classes for writing jtreg tests for + * javac, javap, and sjavac. + * (For javadoc support, see JavadocTester.) + * + *

                There is support for common file operations similar to + * shell commands like cat, cp, diff, mv, rm, grep. + * + *

                There is also support for invoking various tools, like + * javac, javap, jar, java and other JDK tools. + * + *

                File separators: for convenience, many operations accept strings + * to represent file names. On all platforms on which JDK is supported, + * "/" is a legal filename component separator. In particular, even + * on Windows, where the official file separator is "\", "/" is a legal + * alternative. It is therefore recommended that any client code using + * strings to specify file names should use "/". + * + * @author Vicente Romero (original) + * @author Jonathan Gibbons (revised) + */ +public class ToolBox { + /** The platform line separator. */ + public static final String lineSeparator = System.getProperty("line.separator"); + /** The platform OS name. */ + public static final String osName = System.getProperty("os.name"); + + /** The location of the class files for this test, or null if not set. */ + public static final String testClasses = System.getProperty("test.classes"); + /** The location of the source files for this test, or null if not set. */ + public static final String testSrc = System.getProperty("test.src"); + /** The location of the test JDK for this test, or null if not set. */ + public static final String testJDK = System.getProperty("test.jdk"); + + /** The current directory. */ + public static final Path currDir = Paths.get("."); + + /** The stream used for logging output. */ + public PrintStream out = System.err; + + /** + * Checks if the host OS is some version of Windows. + * @return true if the host OS is some version of Windows + */ + public boolean isWindows() { + return osName.toLowerCase(Locale.ENGLISH).startsWith("windows"); + } + + /** + * Splits a string around matches of the given regular expression. + * If the string is empty, an empty list will be returned. + * @param text the string to be split + * @param sep the delimiting regular expression + * @return the strings between the separators + */ + public List split(String text, String sep) { + if (text.isEmpty()) + return Collections.emptyList(); + return Arrays.asList(text.split(sep)); + } + + /** + * Checks if two lists of strings are equal. + * @param l1 the first list of strings to be compared + * @param l2 the second list of strings to be compared + * @throws Error if the lists are not equal + */ + public void checkEqual(List l1, List l2) throws Error { + if (!Objects.equals(l1, l2)) { + // l1 and l2 cannot both be null + if (l1 == null) + throw new Error("comparison failed: l1 is null"); + if (l2 == null) + throw new Error("comparison failed: l2 is null"); + // report first difference + for (int i = 0; i < Math.min(l1.size(), l2.size()); i++) { + String s1 = l1.get(i); + String s2 = l2.get(i); + if (!Objects.equals(s1, s2)) { + throw new Error("comparison failed, index " + i + + ", (" + s1 + ":" + s2 + ")"); + } + } + throw new Error("comparison failed: l1.size=" + l1.size() + ", l2.size=" + l2.size()); + } + } + + /** + * Filters a list of strings according to the given regular expression. + * @param regex the regular expression + * @param lines the strings to be filtered + * @return the strings matching the regular expression + */ + public List grep(String regex, List lines) { + return grep(Pattern.compile(regex), lines); + } + + /** + * Filters a list of strings according to the given regular expression. + * @param pattern the regular expression + * @param lines the strings to be filtered + * @return the strings matching the regular expression + */ + public List grep(Pattern pattern, List lines) { + return lines.stream() + .filter(s -> pattern.matcher(s).find()) + .collect(Collectors.toList()); + } + + /** + * Copies a file. + * If the given destination exists and is a directory, the copy is created + * in that directory. Otherwise, the copy will be placed at the destination, + * possibly overwriting any existing file. + *

                Similar to the shell "cp" command: {@code cp from to}. + * @param from the file to be copied + * @param to where to copy the file + * @throws IOException if any error occurred while copying the file + */ + public void copyFile(String from, String to) throws IOException { + copyFile(Paths.get(from), Paths.get(to)); + } + + /** + * Copies a file. + * If the given destination exists and is a directory, the copy is created + * in that directory. Otherwise, the copy will be placed at the destination, + * possibly overwriting any existing file. + *

                Similar to the shell "cp" command: {@code cp from to}. + * @param from the file to be copied + * @param to where to copy the file + * @throws IOException if an error occurred while copying the file + */ + public void copyFile(Path from, Path to) throws IOException { + if (Files.isDirectory(to)) { + to = to.resolve(from.getFileName()); + } else { + Files.createDirectories(to.getParent()); + } + Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); + } + + /** + * Creates one of more directories. + * For each of the series of paths, a directory will be created, + * including any necessary parent directories. + *

                Similar to the shell command: {@code mkdir -p paths}. + * @param paths the directories to be created + * @throws IOException if an error occurred while creating the directories + */ + public void createDirectories(String... paths) throws IOException { + if (paths.length == 0) + throw new IllegalArgumentException("no directories specified"); + for (String p : paths) + Files.createDirectories(Paths.get(p)); + } + + /** + * Creates one or more directories. + * For each of the series of paths, a directory will be created, + * including any necessary parent directories. + *

                Similar to the shell command: {@code mkdir -p paths}. + * @param paths the directories to be created + * @throws IOException if an error occurred while creating the directories + */ + public void createDirectories(Path... paths) throws IOException { + if (paths.length == 0) + throw new IllegalArgumentException("no directories specified"); + for (Path p : paths) + Files.createDirectories(p); + } + + /** + * Deletes one or more files. + * Any directories to be deleted must be empty. + *

                Similar to the shell command: {@code rm files}. + * @param files the files to be deleted + * @throws IOException if an error occurred while deleting the files + */ + public void deleteFiles(String... files) throws IOException { + if (files.length == 0) + throw new IllegalArgumentException("no files specified"); + for (String file : files) + Files.delete(Paths.get(file)); + } + + /** + * Deletes all content of a directory (but not the directory itself). + * @param root the directory to be cleaned + * @throws IOException if an error occurs while cleaning the directory + */ + public Path cleanDirectory(Path root) throws IOException { + if (!Files.isDirectory(root)) { + throw new IOException(root + " is not a directory"); + } + Files.walkFileTree(root, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes a) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { + if (e != null) { + throw e; + } + if (!dir.equals(root)) { + Files.delete(dir); + } + return FileVisitResult.CONTINUE; + } + }); + return root; + } + + /** + * Moves a file. + * If the given destination exists and is a directory, the file will be moved + * to that directory. Otherwise, the file will be moved to the destination, + * possibly overwriting any existing file. + *

                Similar to the shell "mv" command: {@code mv from to}. + * @param from the file to be moved + * @param to where to move the file + * @throws IOException if an error occurred while moving the file + */ + public void moveFile(String from, String to) throws IOException { + moveFile(Paths.get(from), Paths.get(to)); + } + + /** + * Moves a file. + * If the given destination exists and is a directory, the file will be moved + * to that directory. Otherwise, the file will be moved to the destination, + * possibly overwriting any existing file. + *

                Similar to the shell "mv" command: {@code mv from to}. + * @param from the file to be moved + * @param to where to move the file + * @throws IOException if an error occurred while moving the file + */ + public void moveFile(Path from, Path to) throws IOException { + if (Files.isDirectory(to)) { + to = to.resolve(from.getFileName()); + } else { + Files.createDirectories(to.getParent()); + } + Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); + } + + /** + * Reads the lines of a file. + * The file is read using the default character encoding. + * @param path the file to be read + * @return the lines of the file + * @throws IOException if an error occurred while reading the file + */ + public List readAllLines(String path) throws IOException { + return readAllLines(path, null); + } + + /** + * Reads the lines of a file. + * The file is read using the default character encoding. + * @param path the file to be read + * @return the lines of the file + * @throws IOException if an error occurred while reading the file + */ + public List readAllLines(Path path) throws IOException { + return readAllLines(path, null); + } + + /** + * Reads the lines of a file using the given encoding. + * @param path the file to be read + * @param encoding the encoding to be used to read the file + * @return the lines of the file. + * @throws IOException if an error occurred while reading the file + */ + public List readAllLines(String path, String encoding) throws IOException { + return readAllLines(Paths.get(path), encoding); + } + + /** + * Reads the lines of a file using the given encoding. + * @param path the file to be read + * @param encoding the encoding to be used to read the file + * @return the lines of the file + * @throws IOException if an error occurred while reading the file + */ + public List readAllLines(Path path, String encoding) throws IOException { + return Files.readAllLines(path, getCharset(encoding)); + } + + private Charset getCharset(String encoding) { + return (encoding == null) ? Charset.defaultCharset() : Charset.forName(encoding); + } + + /** + * Find .java files in one or more directories. + *

                Similar to the shell "find" command: {@code find paths -name \*.java}. + * @param paths the directories in which to search for .java files + * @return the .java files found + * @throws IOException if an error occurred while searching for files + */ + public Path[] findJavaFiles(Path... paths) throws IOException { + return findFiles(".java", paths); + } + + /** + * Find files matching the file extension, in one or more directories. + *

                Similar to the shell "find" command: {@code find paths -name \*.ext}. + * @param fileExtension the extension to search for + * @param paths the directories in which to search for files + * @return the files matching the file extension + * @throws IOException if an error occurred while searching for files + */ + public Path[] findFiles(String fileExtension, Path... paths) throws IOException { + Set files = new TreeSet<>(); // use TreeSet to force a consistent order + for (Path p : paths) { + Files.walkFileTree(p, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (file.getFileName().toString().endsWith(fileExtension)) { + files.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + } + return files.toArray(new Path[0]); + } + + /** + * Writes a file containing the given content. + * Any necessary directories for the file will be created. + * @param path where to write the file + * @param content the content for the file + * @throws IOException if an error occurred while writing the file + */ + public void writeFile(String path, String content) throws IOException { + writeFile(Paths.get(path), content); + } + + /** + * Writes a file containing the given content. + * Any necessary directories for the file will be created. + * @param path where to write the file + * @param content the content for the file + * @throws IOException if an error occurred while writing the file + */ + public void writeFile(Path path, String content) throws IOException { + Path dir = path.getParent(); + if (dir != null) + Files.createDirectories(dir); + try (BufferedWriter w = Files.newBufferedWriter(path)) { + w.write(content); + } + } + + /** + * Writes one or more files containing Java source code. + * For each file to be written, the filename will be inferred from the + * given base directory, the package declaration (if present) and from the + * the name of the first class, interface or enum declared in the file. + *

                For example, if the base directory is /my/dir/ and the content + * contains "package p; class C { }", the file will be written to + * /my/dir/p/C.java. + *

                Note: the content is analyzed using regular expressions; + * errors can occur if any contents have initial comments that might trip + * up the analysis. + * @param dir the base directory + * @param contents the contents of the files to be written + * @throws IOException if an error occurred while writing any of the files. + */ + public void writeJavaFiles(Path dir, String... contents) throws IOException { + if (contents.length == 0) + throw new IllegalArgumentException("no content specified for any files"); + for (String c : contents) { + new JavaSource(c).write(dir); + } + } + + /** + * Returns the path for the binary of a JDK tool within {@link #testJDK}. + * @param tool the name of the tool + * @return the path of the tool + */ + public Path getJDKTool(String tool) { + return Paths.get(testJDK, "bin", tool); + } + + /** + * Returns a string representing the contents of an {@code Iterable} as a list. + * @param the type parameter of the {@code Iterable} + * @param items the iterable + * @return the string + */ + String toString(Iterable items) { + return StreamSupport.stream(items.spliterator(), false) + .map(Objects::toString) + .collect(Collectors.joining(",", "[", "]")); + } + + + /** + * An in-memory Java source file. + * It is able to extract the file name from simple source text using + * regular expressions. + */ + public static class JavaSource extends SimpleJavaFileObject { + private final String source; + + /** + * Creates a in-memory file object for Java source code. + * @param className the name of the class + * @param source the source text + */ + public JavaSource(String className, String source) { + super(URI.create(className), JavaFileObject.Kind.SOURCE); + this.source = source; + } + + /** + * Creates a in-memory file object for Java source code. + * The name of the class will be inferred from the source code. + * @param source the source text + */ + public JavaSource(String source) { + super(URI.create(getJavaFileNameFromSource(source)), + JavaFileObject.Kind.SOURCE); + this.source = source; + } + + /** + * Writes the source code to a file in the current directory. + * @throws IOException if there is a problem writing the file + */ + public void write() throws IOException { + write(currDir); + } + + /** + * Writes the source code to a file in a specified directory. + * @param dir the directory + * @throws IOException if there is a problem writing the file + */ + public void write(Path dir) throws IOException { + Path file = dir.resolve(getJavaFileNameFromSource(source)); + Files.createDirectories(file.getParent()); + try (BufferedWriter out = Files.newBufferedWriter(file)) { + out.write(source.replace("\n", lineSeparator)); + } + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return source; + } + + private static Pattern commentPattern = + Pattern.compile("(?s)(\\s+//.*?\n|/\\*.*?\\*/)"); + private static Pattern modulePattern = + Pattern.compile("module\\s+((?:\\w+\\.)*)"); + private static Pattern packagePattern = + Pattern.compile("package\\s+(((?:\\w+\\.)*)(?:\\w+))"); + private static Pattern classPattern = + Pattern.compile("(?:class|enum|interface|record)\\s+(\\w+)"); + + /** + * Extracts the Java file name from the class declaration. + * This method is intended for simple files and uses regular expressions. + * Comments in the source are stripped before looking for the + * declarations from which the name is derived. + */ + static String getJavaFileNameFromSource(String source) { + StringBuilder sb = new StringBuilder(); + Matcher matcher = commentPattern.matcher(source); + int start = 0; + while (matcher.find()) { + sb.append(source, start, matcher.start()); + start = matcher.end(); + } + sb.append(source.substring(start)); + source = sb.toString(); + + String packageName = null; + + matcher = modulePattern.matcher(source); + if (matcher.find()) + return "module-info.java"; + + matcher = packagePattern.matcher(source); + if (matcher.find()) + packageName = matcher.group(1).replace(".", "/"); + + matcher = classPattern.matcher(source); + if (matcher.find()) { + String className = matcher.group(1) + ".java"; + return (packageName == null) ? className : packageName + "/" + className; + } else if (packageName != null) { + return packageName + "/package-info.java"; + } else { + throw new Error("Could not extract the java class " + + "name from the provided source"); + } + } + } + + /** + * Extracts the Java file name from the class declaration. + * This method is intended for simple files and uses regular expressions, + * so comments matching the pattern can make the method fail. + * @deprecated This is a legacy method for compatibility with ToolBox v1. + * Use {@link JavaSource#getName JavaSource.getName} instead. + * @param source the source text + * @return the Java file name inferred from the source + */ + @Deprecated + public static String getJavaFileNameFromSource(String source) { + return JavaSource.getJavaFileNameFromSource(source); + } + + /** + * A memory file manager, for saving generated files in memory. + * The file manager delegates to a separate file manager for listing and + * reading input files. + */ + public static class MemoryFileManager extends ForwardingJavaFileManager { + private interface Content { + byte[] getBytes(); + String getString(); + } + + /** + * Maps binary class names to generated content. + */ + private final Map> files; + + /** + * Construct a memory file manager which stores output files in memory, + * and delegates to a default file manager for input files. + */ + public MemoryFileManager() { + this(ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null)); + } + + /** + * Construct a memory file manager which stores output files in memory, + * and delegates to a specified file manager for input files. + * @param fileManager the file manager to be used for input files + */ + public MemoryFileManager(JavaFileManager fileManager) { + super(fileManager); + files = new HashMap<>(); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, + String name, + JavaFileObject.Kind kind, + FileObject sibling) + { + return new MemoryFileObject(location, name, kind); + } + + /** + * Returns the set of names of files that have been written to a given + * location. + * @param location the location + * @return the set of file names + */ + public Set getFileNames(Location location) { + Map filesForLocation = files.get(location); + return (filesForLocation == null) + ? Collections.emptySet() : filesForLocation.keySet(); + } + + /** + * Returns the content written to a file in a given location, + * or null if no such file has been written. + * @param location the location + * @param name the name of the file + * @return the content as an array of bytes + */ + public byte[] getFileBytes(Location location, String name) { + Content content = getFile(location, name); + return (content == null) ? null : content.getBytes(); + } + + /** + * Returns the content written to a file in a given location, + * or null if no such file has been written. + * @param location the location + * @param name the name of the file + * @return the content as a string + */ + public String getFileString(Location location, String name) { + Content content = getFile(location, name); + return (content == null) ? null : content.getString(); + } + + private Content getFile(Location location, String name) { + Map filesForLocation = files.get(location); + return (filesForLocation == null) ? null : filesForLocation.get(name); + } + + private void save(Location location, String name, Content content) { + Map filesForLocation = files.computeIfAbsent(location, k -> new HashMap<>()); + filesForLocation.put(name, content); + } + + /** + * A writable file object stored in memory. + */ + private class MemoryFileObject extends SimpleJavaFileObject { + private final Location location; + private final String name; + + /** + * Constructs a memory file object. + * @param name binary name of the class to be stored in this file object + */ + MemoryFileObject(Location location, String name, JavaFileObject.Kind kind) { + super(URI.create("mfm:///" + name.replace('.','/') + kind.extension), + Kind.CLASS); + this.location = location; + this.name = name; + } + + @Override + public OutputStream openOutputStream() { + return new FilterOutputStream(new ByteArrayOutputStream()) { + @Override + public void close() throws IOException { + out.close(); + byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); + save(location, name, new Content() { + @Override + public byte[] getBytes() { + return bytes; + } + @Override + public String getString() { + return new String(bytes); + } + + }); + } + }; + } + + @Override + public Writer openWriter() { + return new FilterWriter(new StringWriter()) { + @Override + public void close() throws IOException { + out.close(); + String text = out.toString(); + save(location, name, new Content() { + @Override + public byte[] getBytes() { + return text.getBytes(); + } + @Override + public String getString() { + return text; + } + + }); + } + }; + } + } + } +} + diff --git a/test/junit/toolbox/package-info.java b/test/junit/toolbox/package-info.java new file mode 100644 index 0000000..06eafdf --- /dev/null +++ b/test/junit/toolbox/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Utilities to assist in writing tests that generate and compile code, + * and related tasks. + */ +package toolbox; diff --git a/test/showDocs/README.md b/test/showDocs/README.md new file mode 100644 index 0000000..6719bc2 --- /dev/null +++ b/test/showDocs/README.md @@ -0,0 +1,22 @@ +# APIDiff `showDocs` + +`showDocs` is a utility to generate HTML files that show the parts of +API documentation files that are extracted by the `APIReader` component of APIDiff. + +The utility can be built by the rules in `showDocs.gmk`, to generate `showDocs.jar`. + +## show-demo-docs + +The utility can be run on the classes in `demo-src` by the rules in `showDocs.gmk`. +The rules require that the makefile variable `JDK`_N_`HOME` variable must be set +for any JDK for which the output is to be generated. + +`show-demo-docs.sh` is a demonstration script to show how the `JDK`_N_`HOME` +variables can be set up and the makefile rules invoked. + +## show-jdk-docs + +`show-jdk-docs.sh` is a script to run `showDocs` on JDK API documentation +for specified versions of JDK. The documentation must be provided in an +enclosing directory, containing subdirectories which each contain the +documentation for a version of JDK. \ No newline at end of file diff --git a/test/showDocs/demo-src/m/module-info.java b/test/showDocs/demo-src/m/module-info.java new file mode 100644 index 0000000..6b4f27c --- /dev/null +++ b/test/showDocs/demo-src/m/module-info.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * This is module {@code m}. + * This is additional information. + * @since 1.0 + */ +module m { + exports p; +} \ No newline at end of file diff --git a/test/showDocs/demo-src/m/p/C.java b/test/showDocs/demo-src/m/p/C.java new file mode 100644 index 0000000..205482d --- /dev/null +++ b/test/showDocs/demo-src/m/p/C.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package p; + +/** + * This is class {@code C}. + * This is additional information. + * @since 1.0 + */ +public class C { + /** + * This is field {@code f}. + * This is additional information. + * @since 1.0 + */ + public int f; + + /** + * This is method {@code m1}. + * This is additional information. + * @since 1.0 + */ + public void m1() { } + + /** + * This is method {@code m2}. + * This is additional information. + * @param a an argument + * @return a result + * @since 1.0 + */ + public int m2(int a) { return a; } +} \ No newline at end of file diff --git a/test/showDocs/demo-src/m/p/E.java b/test/showDocs/demo-src/m/p/E.java new file mode 100644 index 0000000..e8d4907 --- /dev/null +++ b/test/showDocs/demo-src/m/p/E.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package p; + +/** + * This is class {@code E}. + * This is additional information. + * @since 1.0 + */ +public class E extends Exception { + private static final long serialVersionUID = 123; + + /** + * This is private field {@code f1}; + * This is additional information. + * @since 1.0 + */ + private int f1; + + /** + * This is private transient field {@code f2}; + * It is not part of the serial form. + * @since 1.0 + */ + private transient int f2; +} \ No newline at end of file diff --git a/test/showDocs/demo-src/m/p/S.java b/test/showDocs/demo-src/m/p/S.java new file mode 100644 index 0000000..276422d --- /dev/null +++ b/test/showDocs/demo-src/m/p/S.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package p; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * This is class {@code S}. + * This is additional information. + * @since 1.0 + */ +public class S implements Serializable { + private static final long serialVersionUID = 456; + + /** + * This is private field {@code f1}; + * This is additional information. + * @since 1.0 + */ + private int f1; + + /** + * This is private transient field {@code f2}; + * It is not part of the serial form. + * @since 1.0 + */ + private transient int f2; + + /** + * This is private method {@code readObject}. + * @param stream the serial input stream + * @throws IOException if an IO exception occurs + * @throws ClassNotFoundException if a class cannot be found + * @serialData This is a description of the serial data provided in {@code readObject}. + * This is additional information. + * @since 1.0 + */ + private void readObject(ObjectInputStream stream) + throws IOException, ClassNotFoundException { + + } + + /** + * This is private method {@code writeObject}. + * @param stream the serial input stream + * @throws IOException if an IO exception occurs + * @serialData This is a description of the serial data provided in {@code writeObject}. + * This is additional information. + * @since 1.0 + */ + private void writeObject(ObjectOutputStream stream) + throws IOException { + + } +} \ No newline at end of file diff --git a/test/showDocs/demo-src/m/p/package-info.java b/test/showDocs/demo-src/m/p/package-info.java new file mode 100644 index 0000000..a24b83a --- /dev/null +++ b/test/showDocs/demo-src/m/p/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * This is package {@code p}. + * This is additional information. + * @since 1.0 + */ +package p; diff --git a/test/showDocs/show-demo-docs.sh b/test/showDocs/show-demo-docs.sh new file mode 100644 index 0000000..843fd0d --- /dev/null +++ b/test/showDocs/show-demo-docs.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. + + +mydir="$(dirname ${BASH_SOURCE[0]})" +BUILDDIR=${mydir}/../../build + +WORK=$HOME/Work +JDK=${WORK}/jdk + +sh ${BUILDDIR}/make.sh showDocs-demo \ + JDK11HOME=${JDK}/jdk-11.jdk/Contents/Home/ \ + JDK12HOME=${JDK}/jdk-12.jdk/Contents/Home/ \ + JDK13HOME=${JDK}/jdk-13.jdk/Contents/Home/ \ + JDK14HOME=${JDK}/jdk-14.jdk/Contents/Home/ \ + JDK15HOME=${JDK}/jdk-15.jdk/Contents/Home/ \ + JDK16HOME=${JDK}/jdk-16.jdk/Contents/Home/ \ + JDK17HOME=${JDK}/jdk-17.jdk/Contents/Home/ \ + JDK18HOME=${JDK}/jdk-18.jdk/Contents/Home/ \ + JDK19HOME=${JDK}/jdk-19.jdk/Contents/Home/ \ + JDK20HOME=${JDK}/jdk-20.jdk/Contents/Home/ \ + JDK21HOME=${JDK}/jdk-21.jdk/Contents/Home/ \ + JDK22HOME=${JDK}/jdk.ref/build/macosx-aarch64/images/jdk/ \ + JDKHOME=${JDK}/jdk-17.jdk/Contents/Home/ + diff --git a/test/showDocs/show-jdk-docs.sh b/test/showDocs/show-jdk-docs.sh new file mode 100644 index 0000000..1a7610a --- /dev/null +++ b/test/showDocs/show-jdk-docs.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. + +# Analyses JDK docs with APIReader and generates a hierarchy of +# pages displaying the API descriptions read from declaration +# and serialized-form pages. + +mydir="$(dirname ${BASH_SOURCE[0]})" +BUILDDIR=${mydir}/../../build + +WORK=${WORK:-${HOME}/Work} +JDK=${JDK:-${WORK}/jdk} + +# used to run the showDocs tool +JDKHOME=${JDKHOME:-${JDK}/jdk-17.jdk/Contents/Home} + +# where to find the docs to analyze; +# the directory must be populated with the API docs for the desired JDK versions +JDKDOCS=${JDK}/docs + +OUTDIR=${OUTDIR:-${BUILDDIR}/show-jdk-docs} + +showdocs() { + $JDKHOME/bin/java -jar ${BUILDDIR}/showDocs.jar $* +} + +for v in 11 12 13 14 15 16 17 18 19 20 21 jdk.ref; do + echo $v + rm -rf ${OUTDIR}/$v + showdocs -d ${OUTDIR}/$v $JDKDOCS/$v/docs/api +done diff --git a/test/showDocs/showDocs.gmk b/test/showDocs/showDocs.gmk new file mode 100644 index 0000000..b68cc57 --- /dev/null +++ b/test/showDocs/showDocs.gmk @@ -0,0 +1,123 @@ +# +# Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +# Invoke with a command like the following: +# +# make -C make show-demo-docs \ +# JDK11HOME=${JDK}/jdk-11.jdk/Contents/Home/ \ +# JDK12HOME=${JDK}/jdk-12.jdk/Contents/Home/ \ +# JDK13HOME=${JDK}/jdk-13.jdk/Contents/Home/ \ +# JDK14HOME=${JDK}/jdk-14.jdk/Contents/Home/ \ +# JDK15HOME=${JDK}/jdk-15.jdk/Contents/Home/ \ +# JDK16HOME=${JDK}/jdk-16.jdk/Contents/Home/ \ +# JDK17HOME=${JDK}/jdk-17.jdk/Contents/Home/ \ +# JDK18HOME=${JDK}/jdk-18.jdk/Contents/Home/ \ +# JDK19HOME=${JDK}/jdk-19.jdk/Contents/Home/ \ +# JDK20HOME=${JDK}/jdk-20.jdk/Contents/Home/ \ +# JDK21HOME=${JDK}/jdk-21.jdk/Contents/Home/ \ +# JDK22HOME=${JDK}/jdk.ref/build/macosx-aarch64/images/jdk/ \ +# JDKHOME=${JDK}/jdk-17.jdk/Contents/Home/ + +FILES.showdocs = $(shell $(FIND) $(TESTDIR)/showDocs/src -type f -print) + +$(BUILDTESTDIR)/showDocs/showDocs.jar: \ + $(FILES.showdocs) \ + $(BUILDDIR)/images/apidiff/lib/apidiff.jar + echo $(FILES.showdocs) + $(JAVAC) \ + -d $(BUILDTESTDIR)/showDocs/classes \ + -sourcepath $(TESTDIR)/showDocs/src \ + -classpath $(BUILDDIR)/classes \ + $(TESTDIR)/showDocs/src/jdk/codetools/showdocs/Main.java + $(CP) $(TESTDIR)/showDocs/src/jdk/codetools/showdocs/showDocs.css $(BUILDTESTDIR)/showDocs/classes/jdk/codetools/showdocs/ + $(JAR) \ + --create \ + --file=$@ \ + --main-class=jdk.codetools.showdocs.Main \ + -C $(BUILDTESTDIR)/showDocs/classes . \ + -C $(BUILDDIR)/classes . + +#-------------------------- +# +# Create demo output for demo source files + +FILES.demofiles = $(shell $(FIND) $(TESTDIR)/showDocs/demo-src -type f -print) + +# Setup rule to run javadoc from JDK to generate API output for demo src +# $1: simple version number +# $2: JDK home for version $(1) +define SETUP_JAVADOC_DEMO_DOCS +$$(BUILDTESTDIR)/show-demo-docs/$(1)/api/index.html: $$(FILES.demofiles) + $(2)/bin/javadoc \ + -quiet \ + -Xdoclint:none \ + -d $$(@D) \ + --module-source-path $$(TESTDIR)/showDocs/demo-src \ + --module m +endef + +# Setup rule to run showDocs to generate demo output from demo API files generated by javadoc +# and add targets into show-demo-docs +# $1: simple version number +# $2: 'text', 'html' or 'mixed'' +define SETUP_SHOW_DEMO_DOCS +$$(BUILDTESTDIR)/show-demo-docs/$(1)/$(2)/index.html: \ + $$(BUILDTESTDIR)/show-demo-docs/$(1)/api/index.html \ + $$(BUILDTESTDIR)/showDocs/showDocs.jar + $$(JDKHOME)/bin/java -jar $$(BUILDTESTDIR)/showDocs/showDocs.jar \ + -d $$(@D) \ + --$(2) \ + $$(BUILDTESTDIR)/show-demo-docs/$(1)/api/ + +show-demo-docs: \ + $$(BUILDTESTDIR)/show-demo-docs/$(1)/$(2)/index.html +endef + +# Setup rules to run javadoc for a given version, and then run showDocs +# to generate demo text and html output. +# $1: simple version number +define SETUP_FULL_SHOW_DEMO_DOCS +ifdef $(1) +$(eval $(call SETUP_JAVADOC_DEMO_DOCS,$(1),$(JDK$(1)HOME)) ) +$(eval $(call SETUP_SHOW_DEMO_DOCS,$(1),text) ) +$(eval $(call SETUP_SHOW_DEMO_DOCS,$(1),html) ) +$(eval $(call SETUP_SHOW_DEMO_DOCS,$(1),mixed) ) +endif +endef + +# for each version for which JDKHOME is defined: +# - run JDKHOME/bin/javadoc to generate demo API dir +# - run showDocs to generate text version of extracts from API files +# - run showDocs to generate html version of extracts from API files +VERSIONS = 11 12 13 14 15 16 17 18 19 20 21 22 +$(foreach v,$(VERSIONS),$(eval $(call SETUP_FULL_SHOW_DEMO_DOCS,$(v)))) + +show-demo-docs: + ${JDKHOME}/bin/java $(TESTDIR)/showDocs/src/jdk/codetools/showdocs/GenerateIndex.java \ + $(BUILDTESTDIR)/show-demo-docs \ + > $(BUILDTESTDIR)/show-demo-docs/index.html + + + + + diff --git a/test/showDocs/src/jdk/codetools/showdocs/GenerateIndex.java b/test/showDocs/src/jdk/codetools/showdocs/GenerateIndex.java new file mode 100644 index 0000000..f6034c3 --- /dev/null +++ b/test/showDocs/src/jdk/codetools/showdocs/GenerateIndex.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.showdocs; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.TreeMap; + +public class GenerateIndex { + public static void main(String... args) throws IOException { + new GenerateIndex().run(args); + } + + public void run(String... args) throws IOException { + Path dir = Path.of(args[0]); + run(dir, System.out); + } + + public void run(Path dir, PrintStream out) throws IOException { + String title = dir.toString(); + out.println(""); + out.println(""); + out.println(""); + out.println("" + title + ""); + out.println(""); + out.println(""); + out.println("

                " + title + "

                "); + + listFiles(dir, dir, out); + + out.println(""); + out.println(""); + } + + void listFiles(Path root, Path dir, PrintStream out) { + Map dirs = new TreeMap<>(); + Map files = new TreeMap<>(); + + try (var ds = Files.newDirectoryStream(dir)) { + for (var p : ds) { + String fn = p.getFileName().toString(); + if (Files.isDirectory(p)) { + dirs.put(fn, p); + } else { + if (fn.endsWith(".html") && !p.equals(root.resolve("index.html"))) { + files.put(p.getFileName().toString(), p); + } + } + } + } catch (IOException e) { + System.err.println("Error in listFiles: root=" + root + " dir=" + dir + ": " + e); + } + + dirs.forEach((fn, p) -> { + out.println("
              • "); + out.println("" + fn + ""); + out.println("
                  "); + listFiles(root, p, out); + out.println("
                "); + out.println("
                "); + }); + + files.forEach((fn, p) -> + out.println("
              • " + fn + "")); + } +} \ No newline at end of file diff --git a/test/showDocs/src/jdk/codetools/showdocs/Main.java b/test/showDocs/src/jdk/codetools/showdocs/Main.java new file mode 100644 index 0000000..d3ad8fa --- /dev/null +++ b/test/showDocs/src/jdk/codetools/showdocs/Main.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.codetools.showdocs; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import jdk.codetools.apidiff.Log; +import jdk.codetools.apidiff.html.Content; +import jdk.codetools.apidiff.html.HtmlAttr; +import jdk.codetools.apidiff.html.HtmlTree; +import jdk.codetools.apidiff.html.RawHtml; +import jdk.codetools.apidiff.html.TagName; +import jdk.codetools.apidiff.html.Text; +import jdk.codetools.apidiff.model.APIDocs; +import jdk.codetools.apidiff.model.SerializedFormDocs; + +/** + * A utility program to display the descriptions of the primary element and + * member elements in a page of API documentation generated by javadoc. + * + */ +public class Main { + /** + * The command-line entry point. + * + * @param args the command-line arguments + * @throws Exception if an error occurred while running the program. + */ + public static void main(String... args) throws Exception { + new Main().run(args); + } + + enum Mode { HTML, TEXT, MIXED } + private Mode mode = Mode.MIXED; // default + boolean verbose = false; + + /** + * Execute the program. + * + * @param args the command-line arguments + * @throws Exception if an error occurred while running the program. + */ + public void run(String... args) throws Exception { + Path inDir = null; + Path outDir = null; + + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.equals("-d")) { + if (++i < args.length) { + outDir = Path.of(args[i]); + } else { + throw new Exception("no arg for -d"); + } + } else if (arg.equals("-h") || arg.equals("--html")) { + mode = Mode.HTML; + } else if (arg.equals("-t") || arg.equals("--text")) { + mode = Mode.TEXT; + } else if (arg.equals("--mixed")) { + mode = Mode.MIXED; + } else if (arg.equals("-v") || arg.equals("--verbose")) { + verbose = true; + } else if (arg.startsWith("-")) { + throw new Exception("unknown option: " + arg); + } else if (inDir == null) { + inDir = Path.of(arg); + } else { + throw new Exception("unknown argument: " + arg); + } + } + + if (inDir == null) { + throw new Exception("no input directory specified"); + } + + if (outDir == null) { + throw new Exception("no output directory specified"); + } + + PrintWriter out = new PrintWriter(System.out) { + @Override + public void close() { + flush(); + } + }; + PrintWriter err = new PrintWriter(System.err, true){ + @Override + public void close() { + flush(); + } + }; + + Log log = new Log(out, err); + try { + showDocs(log, inDir, outDir); + } finally { + log.out.flush(); + log.err.flush(); + } + + } + + Map allSerialFormDocs; + + void showDocs(Log log, Path inFile, Path outDir) throws IOException { + if (Files.isDirectory(inFile)) { + Path sfFile = inFile.resolve("serialized-form.html"); + if (Files.exists(sfFile)) { + allSerialFormDocs = SerializedFormDocs.read(log, sfFile); + } + } + + Pattern p = Pattern.compile("(module-summary|package-summary|[A-Z].*|serialized-form)\\.html"); + Files.walkFileTree(inFile, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (p.matcher(file.getFileName().toString()).matches()) { + if (verbose) { + log.err.println("file: " + file); + } + Path relFile = file.equals(inFile) ? file.getFileName() : inFile.relativize(file); + String title = relFile.toString(); + Path pathToRoot = pathToRoot(relFile); + Path outFile = outDir.resolve(relFile); + try { + if (file.getFileName().toString().equals("serialized-form.html")) { + showSerializedFormFile(log, file, outFile, "Serialized Forms", pathToRoot); + } else { + showAPIFile(log, file, outFile, title, pathToRoot); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + if (verbose) { + log.err.println("dir: " + dir); + } + switch (dir.getFileName().toString()) { + case "jquery": + case "resources": + return FileVisitResult.SKIP_SUBTREE; + default: + return FileVisitResult.CONTINUE; + } + } + }); + + copyResource("showDocs.css", outDir); + } + + + private void copyResource(String name, Path dir) { + try (InputStream in = getClass().getResourceAsStream(name)) { + Files.createDirectories(dir); + Files.copy(in, dir.resolve(name), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + System.err.println("Error writing stylesheet: " + e); + } + } + + void showAPIFile(Log log, Path inFile, Path outFile, String title, Path pathToRoot) throws IOException { + APIDocs apiDocs = APIDocs.read(log, inFile); + + if (verbose) { + log.err.println("APIDocs: " + shortText(apiDocs.getDescription())); + log.err.println("APIDocs: " + apiDocs.getMemberDescriptions().keySet()); + apiDocs.getMemberDescriptions().forEach((k, v) -> log.err.println(shortText(k) + ": " + shortText(v))); + } + + Path stylesheet = pathToRoot.resolve("showDocs.css"); + Files.createDirectories(outFile.getParent()); + try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) { + HtmlTree head = HtmlTree.HEAD("UTF-8", title) + // TODO: stylesheet link should allow stylesheet name to be overridden + .add(HtmlTree.LINK("stylesheet", stylesheet.toString())) + .add(HtmlTree.META("generator", "showDocs")); + + Map declNames = apiDocs.getDeclarationNames(); + String decl = declNames.entrySet().stream() + .map(e -> e.getKey() + ": " + e.getValue()) + .collect(Collectors.joining(", ", "Declaration: ", "")); + + HtmlTree dl = HtmlTree.DL().setClass("api-descriptions"); + dl.add(HtmlTree.DT(Text.of("Main Description"))); + String desc = apiDocs.getDescription(); + dl.add(HtmlTree.DD(desc == null + ? HtmlTree.SPAN(Text.of("(not found)")).set(HtmlAttr.STYLE, "color:gray") + : getContent(desc))); + TreeMap members = new TreeMap<>(apiDocs.getMemberDescriptions()); + for (Map.Entry e : members.entrySet()) { + dl.add(HtmlTree.DT(Text.of(e.getKey()))); + dl.add(HtmlTree.DD(getContent(e.getValue()))); + } + + HtmlTree body = HtmlTree.BODY() + .add(HtmlTree.H1(Text.of(title))) + .add(HtmlTree.P().add(new Text(decl))) + .add(dl); + + if (allSerialFormDocs != null && declNames.containsKey("class")) { + StringBuilder sb = new StringBuilder(); + String pkg = declNames.get("package"); + if (pkg != null) { + sb.append(pkg).append("."); + } + sb.append(declNames.get("class")); + SerializedFormDocs sfDocs = allSerialFormDocs.get(sb.toString()); + if (sfDocs != null) { + body.add(new HtmlTree(TagName.HR).setClass("serialized-form-rule")) + .add(HtmlTree.H2(new Text("Serialized Form"))) + .add(buildSerializedForm(sfDocs)); + } + } + + HtmlTree html = new HtmlTree(TagName.HTML, head, body).set(HtmlAttr.LANG, "en_US"); + html.write(out); + } + } + + void showSerializedFormFile(Log log, Path inFile, Path outFile, String title, Path pathToRoot) throws IOException { + Map allSerialFormDocs = SerializedFormDocs.read(log, inFile); + showSerializedFormFile(log, allSerialFormDocs, outFile, title, pathToRoot); + } + + void showSerializedFormFile(Log log, Map allSerialFormDocs, + Path outFile, String title, Path pathToRoot) throws IOException { + + if (verbose) { + allSerialFormDocs.forEach((k, v) -> { + List list = new ArrayList<>(); + if (v.getOverview() != null) { + list.add("overview"); + } + if (v.getSerialVersionUID() != null) { + list.add("svuid"); + } + list.addAll(v.getFieldDescriptions().keySet()); + list.addAll(v.getMethodDescriptions().keySet()); + log.err.println(k + ": " + shortText(String.join(",", list))); + }); + } + + Path stylesheet = pathToRoot.resolve("showDocs.css"); + Files.createDirectories(outFile.getParent()); + try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) { + HtmlTree head = HtmlTree.HEAD("UTF-8", title) + // TODO: stylesheet link should allow stylesheet name to be overridden + .add(HtmlTree.LINK("stylesheet", stylesheet.toString())) + .add(HtmlTree.META("generator", "showDocs")); + + HtmlTree dl = HtmlTree.DL().setClass("all-serialized-forms"); + for (Map.Entry entry : allSerialFormDocs.entrySet()) { + String typeName = entry.getKey(); + SerializedFormDocs sfDocs = entry.getValue(); + dl.add(HtmlTree.DT(Text.of(typeName))); + dl.add(HtmlTree.DD(buildSerializedForm(sfDocs))); + } + HtmlTree body = HtmlTree.BODY() + .add(HtmlTree.H1(Text.of(title))) + .add(dl); + HtmlTree html = new HtmlTree(TagName.HTML, head, body).set(HtmlAttr.LANG, "en_US"); + html.write(out); + } + } + + private HtmlTree buildSerializedForm(SerializedFormDocs sfDocs) { + HtmlTree dl = HtmlTree.DL().setClass("serialized-form-descriptions"); + String overview = sfDocs.getOverview(); + if (overview != null) { + dl.add(HtmlTree.DT(Text.of("Overview"))) + .add(HtmlTree.DD(getContent(overview))); + } + String svuid = sfDocs.getSerialVersionUID(); + if (svuid != null) { + dl.add(HtmlTree.DT(Text.of("SerialVersionUID"))) + .add(HtmlTree.DD(getContent(svuid))); + } + sfDocs.getFieldDescriptions().forEach((f, description) -> { + dl.add(HtmlTree.DT(Text.of(f))) + .add(HtmlTree.DD(getContent(description))); + + }); + sfDocs.getMethodDescriptions().forEach((m, description) -> { + dl.add(HtmlTree.DT(Text.of(m))) + .add(HtmlTree.DD(getContent(description))); + + }); + return dl; + } + + private Content getContent(String desc) { + switch (mode) { + case HTML: + return HtmlTree.DIV(new RawHtml(desc)).setClass("html"); + + case TEXT: + return HtmlTree.PRE(Text.of(desc)).setClass("text"); + + case MIXED: + return HtmlTree.DIV(List.of( + HtmlTree.DIV(new RawHtml(desc)).setClass("html"), + HtmlTree.DETAILS( + HtmlTree.SUMMARY(Text.of("Source")), + HtmlTree.PRE(Text.of(desc)).setClass("text") + ) + + )); + default: + throw new IllegalStateException(); + } + } + + + private Path pathToRoot(Path relFile) { + if (relFile.getParent() == null) { + return Path.of("."); + } else { + return Path.of(relFile.getParent().toString().replaceAll("[^/\\\\]+", "..")); + } + } + + private String shortText(String s) { + if (s == null) + return null; + else if (s.length() < 10) + return s; + else + return s.substring(0,10); + } +} diff --git a/test/showDocs/src/jdk/codetools/showdocs/package-info.java b/test/showDocs/src/jdk/codetools/showdocs/package-info.java new file mode 100644 index 0000000..00d9fbb --- /dev/null +++ b/test/showDocs/src/jdk/codetools/showdocs/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * A simple utility to generate a hierarchy of pages, + * each showing the descriptions derived from the pages + * in a corresponding input hierarchy. + */ +package jdk.codetools.showdocs; diff --git a/test/showDocs/src/jdk/codetools/showdocs/showDocs.css b/test/showDocs/src/jdk/codetools/showdocs/showDocs.css new file mode 100644 index 0000000..400c964 --- /dev/null +++ b/test/showDocs/src/jdk/codetools/showdocs/showDocs.css @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +/* Global/default settings. */ + +body { + font-family:'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size:14px; +} + +/* Headings. */ + +h1 { + font-size:20px; +} +h2 { + font-size:18px; +} +h3 { + font-size:16px; + font-style:italic; +} +h4 { + font-size:13px; +} +h5 { + font-size:12px; +} +h6 { + font-size:11px; +} + +/* Links. */ + +a:link, a:visited { + text-decoration:none; + color:#4A6782; +} +a[href]:hover, a[href]:focus { + text-decoration:none; + color:#bb7a2a; +} + +dl.all-serialized-forms > dt { + font-size: larger; + font-weight: bold; + margin-top: 1em; + border-top: 1px solid gray; + padding-top: 1em; +} + +dl.api-descriptions > dt, +dl.serialized-form-descriptions > dt { + font-weight: bold; +} + +div.html { + border: 1px solid blue; + border-radius: 5px; + margin-top: 1em; + margin-bottom: 1em; + padding: 5px; +} + +pre.text { + border: 1px solid magenta; + border-radius: 5px; + margin-top: 1em; + margin-bottom: 1em; + padding: 5px; + white-space: pre-wrap; +} + +hr.serialized-form-rule { + margin-top: 2em; + color: gray; +} + +dd > div > details { + margin-bottom: 2em; +} diff --git a/test/toolProvider/ToolProviderTest.gmk b/test/toolProvider/ToolProviderTest.gmk new file mode 100644 index 0000000..97a25e9 --- /dev/null +++ b/test/toolProvider/ToolProviderTest.gmk @@ -0,0 +1,43 @@ +# +# Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + + +$(BUILDTESTDIR)/ToolProviderTest_List.ok: \ + $(BUILDDIR)/images/apidiff/lib/apidiff.jar + $(JAVA) \ + --class-path $< \ + $(TESTDIR)/toolProvider/ToolProviderTest.java > $(@:%.ok=%.out) + $(GREP) apidiff $(@:%.ok=%.out) + echo $@ passed at `date` > $@ + +$(BUILDTESTDIR)/ToolProviderTest_Version.ok: \ + $(BUILDDIR)/images/apidiff/lib/apidiff.jar + $(JAVA) \ + --class-path $< \ + $(TESTDIR)/toolProvider/ToolProviderTest.java apidiff --version > $(@:%.ok=%.out) + $(GREP) "apidiff.*version" $(@:%.ok=%.out) + echo $@ passed at `date` > $@ + +TESTS += \ + $(BUILDTESTDIR)/ToolProviderTest_List.ok \ + $(BUILDTESTDIR)/ToolProviderTest_Version.ok \ No newline at end of file diff --git a/test/toolProvider/ToolProviderTest.java b/test/toolProvider/ToolProviderTest.java new file mode 100644 index 0000000..3a8af1c --- /dev/null +++ b/test/toolProvider/ToolProviderTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.Arrays; +import java.util.Iterator; +import java.util.ServiceLoader; +import java.util.spi.ToolProvider; + +public class ToolProviderTest { + public static void main(String... args) throws Exception { + if (args.length == 0) { + ServiceLoader sl = ServiceLoader.load(ToolProvider.class); + Iterator iter = sl.iterator(); + while (iter.hasNext()) { + ToolProvider tp = iter.next(); + System.out.println(tp.name() + ": " + tp.getClass()); + } + } else { + String toolName = args[0]; + String[] toolArgs = Arrays.copyOfRange(args, 1, args.length); + ToolProvider tool = ToolProvider.findFirst(toolName).orElseThrow(); + int rc = tool.run(System.out, System.err, toolArgs); + if (rc != 0) { + throw new Exception("unexpected exit: " + rc); + } + } + } +} \ No newline at end of file