Java tutorial
/* * Copyright 2016 The Bazel Authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.idea.blaze.java.sync.source; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashMultimap; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multiset; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.idea.blaze.base.async.executor.TransientExecutor; import com.google.idea.blaze.base.ideinfo.ArtifactLocation; import com.google.idea.blaze.base.ideinfo.TargetKey; import com.google.idea.blaze.base.model.primitives.WorkspacePath; import com.google.idea.blaze.base.model.primitives.WorkspaceRoot; import com.google.idea.blaze.base.scope.BlazeContext; import com.google.idea.blaze.base.scope.Scope; import com.google.idea.blaze.base.scope.output.IssueOutput; import com.google.idea.blaze.base.scope.scopes.TimingScope; import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder; import com.google.idea.blaze.base.util.PackagePrefixCalculator; import com.google.idea.blaze.java.sync.model.BlazeContentEntry; import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import javax.annotation.Nullable; /** * This is a utility class for calculating the java sources and their package prefixes given a * module and its Blaze {@link ArtifactLocation} list. */ public final class SourceDirectoryCalculator { private static final Logger LOG = Logger.getInstance(SourceDirectoryCalculator.class); private static final Splitter PACKAGE_SPLITTER = Splitter.on('.'); private static final Splitter PATH_SPLITTER = Splitter.on('/'); private static final Joiner PACKAGE_JOINER = Joiner.on('.'); private static final Joiner PATH_JOINER = Joiner.on('/'); private static final JavaPackageReader generatedFileJavaPackageReader = new FilePathJavaPackageReader(); private final ListeningExecutorService executorService = MoreExecutors.sameThreadExecutor(); private final ListeningExecutorService packageReaderExecutorService = MoreExecutors .listeningDecorator(new TransientExecutor(16)); public ImmutableList<BlazeContentEntry> calculateContentEntries(Project project, BlazeContext context, WorkspaceRoot workspaceRoot, ArtifactLocationDecoder artifactLocationDecoder, Collection<WorkspacePath> rootDirectories, Collection<SourceArtifact> sources, Map<TargetKey, ArtifactLocation> javaPackageManifests) { ManifestFilePackageReader manifestFilePackageReader = Scope.push(context, (childContext) -> { childContext.push(new TimingScope("ReadPackageManifests")); Map<TargetKey, Map<ArtifactLocation, String>> manifestMap = PackageManifestReader.getInstance() .readPackageManifestFiles(project, childContext, artifactLocationDecoder, javaPackageManifests, packageReaderExecutorService); return new ManifestFilePackageReader(manifestMap); }); final List<JavaPackageReader> javaPackageReaders = Lists.newArrayList(manifestFilePackageReader, JavaSourcePackageReader.getInstance(), generatedFileJavaPackageReader); Collection<SourceArtifact> nonGeneratedSources = filterGeneratedArtifacts(sources); // Sort artifacts and excludes into their respective workspace paths Multimap<WorkspacePath, SourceArtifact> sourcesUnderDirectoryRoot = sortArtifactLocationsByRootDirectory( context, rootDirectories, nonGeneratedSources); List<BlazeContentEntry> result = Lists.newArrayList(); Scope.push(context, (childContext) -> { childContext.push(new TimingScope("CalculateSourceDirectories")); for (WorkspacePath workspacePath : rootDirectories) { File contentRoot = workspaceRoot.fileForPath(workspacePath); ImmutableList<BlazeSourceDirectory> sourceDirectories = calculateSourceDirectoriesForContentRoot( context, workspaceRoot, artifactLocationDecoder, workspacePath, sourcesUnderDirectoryRoot.get(workspacePath), javaPackageReaders); if (!sourceDirectories.isEmpty()) { result.add(new BlazeContentEntry(contentRoot, sourceDirectories)); } } Collections.sort(result, (lhs, rhs) -> lhs.contentRoot.compareTo(rhs.contentRoot)); }); return ImmutableList.copyOf(result); } private Collection<SourceArtifact> filterGeneratedArtifacts(Collection<SourceArtifact> artifactLocations) { return artifactLocations.stream().filter(sourceArtifact -> sourceArtifact.artifactLocation.isSource()) .collect(Collectors.toList()); } private static Multimap<WorkspacePath, SourceArtifact> sortArtifactLocationsByRootDirectory( BlazeContext context, Collection<WorkspacePath> rootDirectories, Collection<SourceArtifact> sources) { Multimap<WorkspacePath, SourceArtifact> result = ArrayListMultimap.create(); for (SourceArtifact sourceArtifact : sources) { WorkspacePath foundWorkspacePath = rootDirectories.stream() .filter(rootDirectory -> isUnderRootDirectory(rootDirectory, sourceArtifact.artifactLocation.getRelativePath())) .findFirst().orElse(null); if (foundWorkspacePath != null) { result.put(foundWorkspacePath, sourceArtifact); } else if (sourceArtifact.artifactLocation.isSource()) { ArtifactLocation sourceFile = sourceArtifact.artifactLocation; String message = String .format("Did not add %s. You're probably using a java file from outside the workspace" + " that has been exported using export_files. Don't do that.", sourceFile); IssueOutput.warn(message).submit(context); } } return result; } private static boolean isUnderRootDirectory(WorkspacePath rootDirectory, String relativePath) { if (rootDirectory.isWorkspaceRoot()) { return true; } String rootDirectoryString = rootDirectory.toString(); return relativePath.startsWith(rootDirectoryString) && (relativePath.length() == rootDirectoryString.length() || (relativePath.charAt(rootDirectoryString.length()) == '/')); } /** Calculates all source directories for a single content root. */ private ImmutableList<BlazeSourceDirectory> calculateSourceDirectoriesForContentRoot(BlazeContext context, WorkspaceRoot workspaceRoot, ArtifactLocationDecoder artifactLocationDecoder, WorkspacePath directoryRoot, Collection<SourceArtifact> sourceArtifacts, Collection<JavaPackageReader> javaPackageReaders) { // Split out java files List<SourceArtifact> javaArtifacts = Lists.newArrayList(); for (SourceArtifact sourceArtifact : sourceArtifacts) { if (isJavaFile(sourceArtifact.artifactLocation)) { javaArtifacts.add(sourceArtifact); } } List<BlazeSourceDirectory> result = Lists.newArrayList(); // Add java source directories calculateJavaSourceDirectories(context, workspaceRoot, artifactLocationDecoder, directoryRoot, javaArtifacts, javaPackageReaders, result); Collections.sort(result, BlazeSourceDirectory.COMPARATOR); return ImmutableList.copyOf(result); } /** Adds the java source directories. */ private void calculateJavaSourceDirectories(BlazeContext context, WorkspaceRoot workspaceRoot, ArtifactLocationDecoder artifactLocationDecoder, WorkspacePath directoryRoot, Collection<SourceArtifact> javaArtifacts, Collection<JavaPackageReader> javaPackageReaders, Collection<BlazeSourceDirectory> result) { List<SourceRoot> sourceRootsPerFile = Lists.newArrayList(); // Get java sources List<ListenableFuture<SourceRoot>> sourceRootFutures = Lists.newArrayList(); for (final SourceArtifact sourceArtifact : javaArtifacts) { ListenableFuture<SourceRoot> future = executorService.submit(() -> sourceRootForJavaSource(context, artifactLocationDecoder, sourceArtifact, javaPackageReaders)); sourceRootFutures.add(future); } try { for (SourceRoot sourceRoot : Futures.allAsList(sourceRootFutures).get()) { if (sourceRoot != null) { sourceRootsPerFile.add(sourceRoot); } } } catch (ExecutionException | InterruptedException e) { LOG.error(e); throw new IllegalStateException("Could not read sources"); } // Sort source roots into their respective directories Multimap<WorkspacePath, SourceRoot> sourceDirectoryToSourceRoots = HashMultimap.create(); for (SourceRoot sourceRoot : sourceRootsPerFile) { sourceDirectoryToSourceRoots.put(sourceRoot.workspacePath, sourceRoot); } // Create a mapping from directory to package prefix Map<WorkspacePath, SourceRoot> workspacePathToSourceRoot = Maps.newHashMap(); for (WorkspacePath workspacePath : sourceDirectoryToSourceRoots.keySet()) { Collection<SourceRoot> sources = sourceDirectoryToSourceRoots.get(workspacePath); Multiset<String> packages = HashMultiset.create(); for (SourceRoot source : sources) { packages.add(source.packagePrefix); } final String directoryPackagePrefix; // Common case -- all source files agree on a single package if (packages.elementSet().size() == 1) { directoryPackagePrefix = packages.elementSet().iterator().next(); } else { String preferredPackagePrefix = PackagePrefixCalculator.packagePrefixOf(workspacePath); directoryPackagePrefix = pickMostFrequentlyOccurring(packages, preferredPackagePrefix); } SourceRoot candidateRoot = new SourceRoot(workspacePath, directoryPackagePrefix); workspacePathToSourceRoot.put(workspacePath, candidateRoot); } // Add content entry base if it doesn't exist if (!workspacePathToSourceRoot.containsKey(directoryRoot)) { SourceRoot candidateRoot = new SourceRoot(directoryRoot, PackagePrefixCalculator.packagePrefixOf(directoryRoot)); workspacePathToSourceRoot.put(directoryRoot, candidateRoot); } // First, create a graph of the directory structure from root to each source file Map<WorkspacePath, SourceRootDirectoryNode> sourceRootDirectoryNodeMap = Maps.newHashMap(); SourceRootDirectoryNode rootNode = new SourceRootDirectoryNode(directoryRoot, null); sourceRootDirectoryNodeMap.put(directoryRoot, rootNode); for (SourceRoot sourceRoot : workspacePathToSourceRoot.values()) { final String sourcePathRelativeToDirectoryRoot = sourcePathRelativeToDirectoryRoot(directoryRoot, sourceRoot.workspacePath); List<String> pathComponents = !Strings.isNullOrEmpty(sourcePathRelativeToDirectoryRoot) ? PATH_SPLITTER.splitToList(sourcePathRelativeToDirectoryRoot) : ImmutableList.of(); SourceRootDirectoryNode previousNode = rootNode; for (int i = 0; i < pathComponents.size(); ++i) { final WorkspacePath workspacePath = getWorkspacePathFromPathComponents(directoryRoot, pathComponents, i + 1); SourceRootDirectoryNode node = sourceRootDirectoryNodeMap.get(workspacePath); if (node == null) { node = new SourceRootDirectoryNode(workspacePath, pathComponents.get(i)); sourceRootDirectoryNodeMap.put(workspacePath, node); previousNode.children.add(node); } previousNode = node; } } // Add package prefix votes at each directory node for (SourceRoot sourceRoot : workspacePathToSourceRoot.values()) { final String sourcePathRelativeToDirectoryRoot = sourcePathRelativeToDirectoryRoot(directoryRoot, sourceRoot.workspacePath); List<String> packageComponents = PACKAGE_SPLITTER.splitToList(sourceRoot.packagePrefix); List<String> pathComponents = !Strings.isNullOrEmpty(sourcePathRelativeToDirectoryRoot) ? PATH_SPLITTER.splitToList(sourcePathRelativeToDirectoryRoot) : ImmutableList.of(); int packageIndex = packageComponents.size(); int pathIndex = pathComponents.size(); while (pathIndex >= 0 && packageIndex >= 0) { final WorkspacePath workspacePath = getWorkspacePathFromPathComponents(directoryRoot, pathComponents, pathIndex); SourceRootDirectoryNode node = sourceRootDirectoryNodeMap.get(workspacePath); String packagePrefix = PACKAGE_JOINER.join(packageComponents.subList(0, packageIndex)); // If this is the source root containing Java files, we *have* to pick its package prefix // Otherwise just add a vote if (sourceRoot.workspacePath.equals(workspacePath)) { node.forcedPackagePrefix = packagePrefix; } else { node.packagePrefixVotes.add(packagePrefix); } String pathComponent = pathIndex > 0 ? pathComponents.get(pathIndex - 1) : ""; String packageComponent = packageIndex > 0 ? packageComponents.get(packageIndex - 1) : ""; if (!pathComponent.equals(packageComponent)) { break; } --packageIndex; --pathIndex; } } Map<WorkspacePath, SourceRoot> sourceRoots = Maps.newHashMap(); SourceRootDirectoryNode root = sourceRootDirectoryNodeMap.get(directoryRoot); visitDirectoryNode(sourceRoots, root, null); for (SourceRoot sourceRoot : sourceRoots.values()) { result.add(BlazeSourceDirectory.builder(workspaceRoot.fileForPath(sourceRoot.workspacePath)) .setPackagePrefix(sourceRoot.packagePrefix).setGenerated(false).build()); } } private static String sourcePathRelativeToDirectoryRoot(WorkspacePath directoryRoot, WorkspacePath workspacePath) { int directoryRootLength = directoryRoot.relativePath().length(); String relativePath = workspacePath.relativePath(); final String relativeSourcePath; if (relativePath.length() > directoryRootLength) { if (directoryRootLength > 0) { relativeSourcePath = relativePath.substring(directoryRootLength + 1); } else { relativeSourcePath = relativePath; } } else { relativeSourcePath = ""; } return relativeSourcePath; } private static WorkspacePath getWorkspacePathFromPathComponents(WorkspacePath directoryRoot, List<String> pathComponents, int pathIndex) { String directoryRootRelativePath = PATH_JOINER.join(pathComponents.subList(0, pathIndex)); final WorkspacePath workspacePath; if (directoryRootRelativePath.isEmpty()) { workspacePath = directoryRoot; } else if (directoryRoot.isWorkspaceRoot()) { workspacePath = new WorkspacePath(directoryRootRelativePath); } else { workspacePath = new WorkspacePath( PATH_JOINER.join(directoryRoot.relativePath(), directoryRootRelativePath)); } return workspacePath; } private static void visitDirectoryNode(Map<WorkspacePath, SourceRoot> sourceRoots, SourceRootDirectoryNode node, @Nullable String parentCompatiblePackagePrefix) { String packagePrefix = node.forcedPackagePrefix != null ? node.forcedPackagePrefix : pickMostFrequentlyOccurring(node.packagePrefixVotes, PackagePrefixCalculator.packagePrefixOf(node.workspacePath)); packagePrefix = packagePrefix != null ? packagePrefix : parentCompatiblePackagePrefix; if (packagePrefix != null && !packagePrefix.equals(parentCompatiblePackagePrefix)) { sourceRoots.put(node.workspacePath, new SourceRoot(node.workspacePath, packagePrefix)); } for (SourceRootDirectoryNode child : node.children) { String compatiblePackagePrefix = null; if (packagePrefix != null) { compatiblePackagePrefix = Strings.isNullOrEmpty(packagePrefix) ? child.directoryName : packagePrefix + "." + child.directoryName; } visitDirectoryNode(sourceRoots, child, compatiblePackagePrefix); } } @Nullable private static <T> T pickMostFrequentlyOccurring(Multiset<T> set, String prefer) { T best = null; int bestCount = 0; for (T candidate : set.elementSet()) { int candidateCount = set.count(candidate); if (candidateCount > bestCount || (candidateCount == bestCount && candidate.equals(prefer))) { best = candidate; bestCount = candidateCount; } } return best; } @Nullable private SourceRoot sourceRootForJavaSource(BlazeContext context, ArtifactLocationDecoder artifactLocationDecoder, SourceArtifact sourceArtifact, Collection<JavaPackageReader> javaPackageReaders) { String declaredPackage = null; for (JavaPackageReader reader : javaPackageReaders) { declaredPackage = reader.getDeclaredPackageOfJavaFile(context, artifactLocationDecoder, sourceArtifact); if (declaredPackage != null) { break; } } if (declaredPackage == null) { IssueOutput .warn("Failed to inspect the package name of java source file: " + sourceArtifact.artifactLocation) .inFile(artifactLocationDecoder.decode(sourceArtifact.artifactLocation)).submit(context); return null; } return new SourceRoot( new WorkspacePath(new File(sourceArtifact.artifactLocation.getRelativePath()).getParent()), declaredPackage); } static class SourceRoot { final WorkspacePath workspacePath; final String packagePrefix; public SourceRoot(WorkspacePath workspacePath, String packagePrefix) { this.workspacePath = workspacePath; this.packagePrefix = packagePrefix; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } SourceRoot that = (SourceRoot) o; return Objects.equal(workspacePath, that.workspacePath) && Objects.equal(packagePrefix, that.packagePrefix); } @Override public int hashCode() { return Objects.hashCode(workspacePath, packagePrefix); } @Override public String toString() { return "SourceRoot {" + '\n' + " workspacePath: " + workspacePath + '\n' + " packagePrefix: " + packagePrefix + '\n' + '}'; } } static class SourceRootDirectoryNode { final WorkspacePath workspacePath; @Nullable final String directoryName; final Set<SourceRootDirectoryNode> children = Sets.newHashSet(); final Multiset<String> packagePrefixVotes = HashMultiset.create(); String forcedPackagePrefix; public SourceRootDirectoryNode(WorkspacePath workspacePath, @Nullable String directoryName) { this.workspacePath = workspacePath; this.directoryName = directoryName; } } private static boolean isJavaFile(ArtifactLocation artifactLocation) { return artifactLocation.getRelativePath().endsWith(".java"); } }