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 extends Dependency> 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:
+ *
+ *
{@code a.equals(b) && of(a, b) == 0) || (!a.equals(b) && of(a, b) > 0)}
+ *
{@code of(a, b) == of(b, a)}
+ *
{@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 extends ZipEntry> 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.
+ *
+ *
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.
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 extends Content> contents) {
+ return new HtmlTree(TagName.DIV, contents);
+ }
+ public static HtmlTree SECTION(Content... contents) {
+ return new HtmlTree(TagName.SECTION, contents);
+ }
+
+ public static HtmlTree SECTION(List extends Content> 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 extends Content> 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