Skip to content

Commit

Permalink
feat: organize with Older and prerelease packages (#192)
Browse files Browse the repository at this point in the history
* feat: organize with Older and prerelease packages

* chore: clarity and use 'equals'

* chore: explicit recommendation error conditions

* chore: add 2-release test case to ApiVersionTest
  • Loading branch information
burkedavison committed Sep 20, 2023
1 parent 2b838be commit caf61a8
Show file tree
Hide file tree
Showing 14 changed files with 1,723 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.google.docfx.doclet;

import com.google.common.base.MoreObjects;
import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -52,6 +54,21 @@ private static int safeParseInt(@Nullable String input) {
return Integer.parseInt(input);
}

public static ApiVersion getRecommended(Collection<ApiVersion> versions) {
if (versions.size() == 1) {
return versions.iterator().next();
}

Optional<ApiVersion> latestReleaseVersion =
versions.stream().filter(ApiVersion::isStable).max(Comparator.naturalOrder());

return latestReleaseVersion.orElseGet(
() -> // No (stable) release version found
versions.stream()
.max(Comparator.naturalOrder()) // Select latest prerelease version
.orElseThrow(() -> new IllegalArgumentException("Versions must not be empty.")));
}

private final int major;
private final int minor;
private final String stability;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import static com.microsoft.build.BuilderUtil.populateUidValues;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableListMultimap;
import com.microsoft.lookup.ClassItemsLookup;
import com.microsoft.lookup.ClassLookup;
import com.microsoft.lookup.PackageLookup;
import com.microsoft.lookup.PackageLookup.PackageGroup;
import com.microsoft.model.MetadataFile;
import com.microsoft.model.MetadataFileItem;
import com.microsoft.model.TocFile;
Expand All @@ -19,16 +22,18 @@
import jdk.javadoc.doclet.DocletEnvironment;

public class YmlFilesBuilder {
private DocletEnvironment environment;
private String outputPath;
private ElementUtil elementUtil;
private PackageLookup packageLookup;
private String projectName;
private boolean disableChangelog;
private ProjectBuilder projectBuilder;
private PackageBuilder packageBuilder;
private ClassBuilder classBuilder;
private ReferenceBuilder referenceBuilder;
private static final String OLDER_AND_PRERELEASE = "Older and prerelease packages";

private final DocletEnvironment environment;
private final String outputPath;
private final ElementUtil elementUtil;
private final PackageLookup packageLookup;
private final String projectName;
private final boolean disableChangelog;
private final ProjectBuilder projectBuilder;
private final PackageBuilder packageBuilder;
private final ClassBuilder classBuilder;
private final ReferenceBuilder referenceBuilder;

public YmlFilesBuilder(
DocletEnvironment environment,
Expand Down Expand Up @@ -57,60 +62,84 @@ public YmlFilesBuilder(
}

public boolean build() {
Processor processor = new Processor();
processor.process();

// write to yaml files
FileUtil.dumpToFile(processor.projectMetadataFile);
processor.packageMetadataFiles.forEach(FileUtil::dumpToFile);
processor.classMetadataFiles.forEach(FileUtil::dumpToFile);
FileUtil.dumpToFile(processor.tocFile);

return true;
}

@VisibleForTesting
class Processor {
// table of contents
TocFile tocFile = new TocFile(outputPath, projectName, disableChangelog);
final TocFile tocFile = new TocFile(outputPath, projectName, disableChangelog);
// overview page
MetadataFile projectMetadataFile = new MetadataFile(outputPath, "overview.yml");
final MetadataFile projectMetadataFile = new MetadataFile(outputPath, "overview.yml");
// package summary pages
List<MetadataFile> packageMetadataFiles = new ArrayList<>();
final List<MetadataFile> packageMetadataFiles = new ArrayList<>();
// packages
List<MetadataFileItem> packageItems = new ArrayList<>();
final List<MetadataFileItem> packageItems = new ArrayList<>();
// class/enum/interface/etc. pages
List<MetadataFile> classMetadataFiles = new ArrayList<>();
final List<MetadataFile> classMetadataFiles = new ArrayList<>();

for (PackageElement packageElement :
elementUtil.extractPackageElements(environment.getIncludedElements())) {
String packageUid = packageLookup.extractUid(packageElement);
String packageStatus = packageLookup.extractStatus(packageElement);
TocItem packageTocItem = new TocItem(packageUid, packageUid, packageStatus);
// build package summary
packageMetadataFiles.add(packageBuilder.buildPackageMetadataFile(packageElement));
// add package summary to toc
packageTocItem.getItems().add(new TocItem(packageUid, "Package summary"));
tocFile.addTocItem(packageTocItem);
@VisibleForTesting
void process() {
ImmutableListMultimap<PackageGroup, PackageElement> organizedPackages =
packageLookup.organize(
elementUtil.extractPackageElements(environment.getIncludedElements()));

// build classes/interfaces/enums/exceptions/annotations
TocTypeMap typeMap = new TocTypeMap();
classBuilder.buildFilesForInnerClasses(packageElement, typeMap, classMetadataFiles);
packageTocItem.getItems().addAll(joinTocTypeItems(typeMap));
}
for (PackageElement element : organizedPackages.get(PackageGroup.VISIBLE)) {
tocFile.addTocItem(buildPackage(element));
}

for (MetadataFile packageFile : packageMetadataFiles) {
packageItems.addAll(packageFile.getItems());
String packageFileName = packageFile.getFileName();
for (MetadataFile classFile : classMetadataFiles) {
String classFileName = classFile.getFileName();
if (packageFileName.equalsIgnoreCase(classFileName)) {
packageFile.setFileName(packageFileName.replaceAll("\\.yml$", "(package).yml"));
classFile.setFileName(classFileName.replaceAll("\\.yml$", "(class).yml"));
break;
TocItem older = new TocItem(OLDER_AND_PRERELEASE, OLDER_AND_PRERELEASE, null);
for (PackageElement element : organizedPackages.get(PackageGroup.OLDER_AND_PRERELEASE)) {
older.getItems().add(buildPackage(element));
}
tocFile.addTocItem(older);

for (MetadataFile packageFile : packageMetadataFiles) {
packageItems.addAll(packageFile.getItems());
String packageFileName = packageFile.getFileName();
for (MetadataFile classFile : classMetadataFiles) {
String classFileName = classFile.getFileName();
if (packageFileName.equalsIgnoreCase(classFileName)) {
packageFile.setFileName(packageFileName.replaceAll("\\.yml$", "(package).yml"));
classFile.setFileName(classFileName.replaceAll("\\.yml$", "(class).yml"));
break;
}
}
}
// build project summary page
projectBuilder.buildProjectMetadataFile(packageItems, projectMetadataFile);

// post-processing
populateUidValues(packageMetadataFiles, classMetadataFiles);
referenceBuilder.updateExternalReferences(classMetadataFiles);
}
// build project summary page
projectBuilder.buildProjectMetadataFile(packageItems, projectMetadataFile);

// post-processing
populateUidValues(packageMetadataFiles, classMetadataFiles);
referenceBuilder.updateExternalReferences(classMetadataFiles);
private TocItem buildPackage(PackageElement element) {
String packageUid = packageLookup.extractUid(element);
String packageStatus = packageLookup.extractStatus(element);

// write to yaml files
FileUtil.dumpToFile(projectMetadataFile);
packageMetadataFiles.forEach(FileUtil::dumpToFile);
classMetadataFiles.forEach(FileUtil::dumpToFile);
FileUtil.dumpToFile(tocFile);
TocItem packageTocItem = new TocItem(packageUid, packageUid, packageStatus);
packageTocItem.getItems().add(new TocItem(packageUid, "Package summary"));

return true;
// build package summary
packageMetadataFiles.add(packageBuilder.buildPackageMetadataFile(element));

// build classes/interfaces/enums/exceptions/annotations
TocTypeMap typeMap = new TocTypeMap();
classBuilder.buildFilesForInnerClasses(element, typeMap, classMetadataFiles);
packageTocItem.getItems().addAll(joinTocTypeItems(typeMap));

return packageTocItem;
}
}

List<TocItem> joinTocTypeItems(TocTypeMap tocTypeMap) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
package com.microsoft.lookup;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import com.google.docfx.doclet.ApiVersion;
import com.microsoft.lookup.model.ExtendedMetadataFileItem;
import com.microsoft.model.Status;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import javax.lang.model.element.PackageElement;
import jdk.javadoc.doclet.DocletEnvironment;

Expand Down Expand Up @@ -51,4 +66,112 @@ public String extractJavaType(PackageElement element) {
}
return null;
}

/** Compare PackageElements by their parsed ApiVersion */
private final Comparator<PackageElement> byComparingApiVersion =
Comparator.comparing(pkg -> extractApiVersion(pkg).orElse(ApiVersion.NONE));

public Optional<ApiVersion> extractApiVersion(PackageElement pkg) {
String name = String.valueOf(pkg.getQualifiedName());
int lastPackageIndex = name.lastIndexOf('.');
String leafPackage = name.substring(lastPackageIndex + 1);
return ApiVersion.parse(leafPackage);
}

public enum PackageGroup {
VISIBLE,
OLDER_AND_PRERELEASE
}

/**
* Organize packages into PackageGroups, making some VISIBLE the rest hidden under the
* OLDER_AND_PRERELEASE category.
*/
public ImmutableListMultimap<PackageGroup, PackageElement> organize(
List<PackageElement> packages) {

ListMultimap<PackageGroup, PackageElement> organized =
MultimapBuilder.enumKeys(PackageGroup.class).arrayListValues().build();

Multimap<String, PackageElement> packagesGroups = groupVersions(packages);
ImmutableList<String> alphabetizedPackageGroups =
packagesGroups.keySet().stream().sorted().collect(ImmutableList.toImmutableList());

for (String name : alphabetizedPackageGroups) {
Collection<PackageElement> versions = packagesGroups.get(name);

// The recommended package of each group is made visible.
PackageElement recommendedVersion = getRecommended(versions);
organized.put(PackageGroup.VISIBLE, recommendedVersion);

// All others are added to "Older and prerelease versions"
versions.stream()
.filter(version -> !version.equals(recommendedVersion))
.sorted(byComparingApiVersion)
.forEach(
version -> {
organized.put(PackageGroup.OLDER_AND_PRERELEASE, version);
});
}

return ImmutableListMultimap.copyOf(organized);
}

/**
* This 'grouping' logic combines all versioned packages together in a single `a.b.c.v#` group.
*
* <p>For example: a.b.v1 and a.b.v2 will be in the same group, but a.b and a.b.c will be in their
* own groups.
*
* <p>When packages are grouped, only one package within the group will be VISIBLE and the rest
* will be placed in the OLDER_AND_PRERELEASE category.
*/
@VisibleForTesting
Multimap<String, PackageElement> groupVersions(List<PackageElement> packages) {
return Multimaps.index(
packages,
(pkg) -> {
String name = String.valueOf(pkg.getQualifiedName());
int lastPackageIndex = name.lastIndexOf('.');
String leafPackage = name.substring(lastPackageIndex + 1);
// For package a.b.c.d, the value of leafPackage is 'd'.

boolean packageIsApiVersion = ApiVersion.parse(leafPackage).isPresent();
if (packageIsApiVersion) {
String packageWithoutVersion = name.substring(0, lastPackageIndex);
// For package a.b.c.v1, the value of packageWithoutVersion is a.b.c
return packageWithoutVersion + ".v#"; // Use "v#" package to group all versions
}
// Using 'name' ensures this package is placed in a group of size 1.
return name;
});
}

/**
* @throws java.lang.IllegalStateException if the collections has multiple entries, and any of the
* packages are not versioned.
* @throws java.lang.IllegalArgumentException if the collection is empty or contains entries with
* duplicate API versions.
*/
@VisibleForTesting
PackageElement getRecommended(Collection<PackageElement> packages) {
Preconditions.checkArgument(!packages.isEmpty(), "Packages must not be empty.");

if (packages.size() == 1) {
return packages.iterator().next();
}

ImmutableMap<ApiVersion, PackageElement> versions =
Maps.uniqueIndex(
packages,
(pkg) ->
extractApiVersion(pkg)
.orElseThrow(
() ->
new IllegalStateException(
"Unable to parse version from package " + pkg)));

ApiVersion recommended = ApiVersion.getRecommended(versions.keySet());
return versions.get(recommended);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class TocFile extends ArrayList<TocItem> implements YmlFile {
Expand All @@ -26,7 +25,7 @@ public void addTocItem(TocItem packageTocItem) {
}

protected void sortByUid() {
Collections.sort(this, Comparator.comparing(TocItem::getUid));
Collections.sort(this, (a, b) -> a.getUid().compareToIgnoreCase(b.getUid()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.microsoft.model;

import com.google.common.base.MoreObjects;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -45,4 +46,9 @@ public String getStatus() {
public String getHeading() {
return heading;
}

@Override
public String toString() {
return MoreObjects.toStringHelper(TocItem.class).add("uid", uid).toString();
}
}
Loading

0 comments on commit caf61a8

Please sign in to comment.