blob: 1d8a41b1e43bbeb9ec1ee5dfb5f00d2bab999bab [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.project.dependency;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectActionContext;
import org.netbeans.modules.project.dependency.spi.ProjectArtifactsImplementation;
import org.openide.util.*;
/**
* This project query provides access to artifacts produced or managed by a project. An artifact
* is essentially a file (may extend to a collection of files in the future) that is a product of a project build,
* or generally of a project or build system action. A project can have multiple artifacts produced by a build,
* or can have multiple sets of artifacts produced by specific project actions.
* <p>
* By default the query will return artifacts produced by project's compilation (incl. packaging, in maven terminology) -
* but the exact meaning depends on a build system used, and the project's settings and the active configuration.
* <p>
* Different project output are marked by different <b>classifiers</b>. Some special, abstract classifiers may
* be defined that should be handled by implementations specific for each build system.
* <ul>
* <li>{@link Filter#CLASSIFIER_BUNDLED} - describes a product with all dependencies included, such as the output of
* <b>shade or shadow plugins</b> in Gradle or Maven. Since the plugins behave differently (maven replaces the original artifact,
* while gradle attaches a new one), this meta-classiifer allows to pick the appropriate artifact despite its real classifier depends
* on project type. If there are more bundles (shadows),
* </ul>
* @author sdedic
*/
public final class ProjectArtifactsQuery {
/**
* Computes artifacts produced by a project. The returned Result can enumerate artifacts
* from the project, along with their location(s) either actual or supposed. The Result can
* be listened of changes that may change the artifacts reported.
*
* @param project the project to be queried
* @param filter artifact filter
* @return list of artifacts that can be listened on.
*/
public static ArtifactsResult findArtifacts(Project project, Filter filter) {
Parameters.notNull("project", project);
Parameters.notNull("filter", filter);
List<ProjectArtifactsImplementation> impls = new ArrayList<>(project.getLookup().lookupAll(ProjectArtifactsImplementation.class));
SortedMap<Integer, List<E<?>>> buckets = new TreeMap<>();
for (ProjectArtifactsImplementation impl : impls) {
Object r = impl.evaluate(filter);
if (r != null) {
buckets.computeIfAbsent(impl.getOrder(), i -> new ArrayList<>()).add(new E(r, impl));
}
}
List<E<?>> delegates;
if (buckets.size() == 1) {
delegates = buckets.values().iterator().next();
} else {
delegates = buckets.values().stream().flatMap(l -> l.stream()).collect(Collectors.toList());
}
return new ArtifactsResult(delegates);
}
private static final class E<T> {
final T data;
final ProjectArtifactsImplementation<T> impl;
public E(T data, ProjectArtifactsImplementation<T> impl) {
this.data = data;
this.impl = impl;
}
public Project findProject() {
return impl.findProject(data);
}
public List<ArtifactSpec> findArtifacts() {
return impl.findArtifacts(data);
}
public Collection<ArtifactSpec> findExcludedArtifacts() {
return impl.findExcludedArtifacts(data);
}
public void handleChangeListener(ChangeListener l, boolean add) {
impl.handleChangeListener(data, l, add);
}
public boolean computeSupportsChanges() {
return impl.computeSupportsChanges(data);
}
}
public static final class ArtifactsResult {
private final List<E<?>> delegates;
// @GuardedBy(this)
private final List<ChangeListener> listeners = new ArrayList<>();
// @GuardedBy(this)
private ChangeListener delListener;
// @GuardedBy(this)
private List<ArtifactSpec> artifacts;
// @GuardedBy(this)
private Boolean supportsChanges;
ArtifactsResult(List<E<?>> delegates) {
this.delegates = delegates;
}
/**
* Returns project artifacts. The result may become invalida after the result fires
* an event to its {@link #addChangeListener(javax.swing.event.ChangeListener) ChangeListeners}.
* @return artifacts produced by the project.
*/
public List<ArtifactSpec> getArtifacts() {
synchronized (this) {
if (artifacts != null) {
return artifacts;
}
}
return updateResults();
}
List<ArtifactSpec> updateResults() {
boolean changes = false;
// accept only first matching artifact.
Collection<ArtifactSpec> specs = new LinkedHashSet<>();
for (E<?> e : delegates) {
Collection<ArtifactSpec> ex = e.findExcludedArtifacts();
if (ex != null) {
specs.removeAll(ex);
}
Collection<ArtifactSpec> add = e.findArtifacts();
if (add != null) {
specs.addAll(add);
}
changes |= e.computeSupportsChanges();
}
List<ArtifactSpec> copy = new ArrayList<>(specs);
ChangeListener[] ll;
synchronized (this) {
if (null == supportsChanges) {
this.supportsChanges = changes;
}
if (this.artifacts != null && this.artifacts.equals(specs)) {
return copy;
}
this.artifacts = copy;
if (listeners.isEmpty()) {
return copy;
}
ll = listeners.toArray(new ChangeListener[listeners.size()]);
}
ChangeEvent e = new ChangeEvent(this);
for (ChangeListener l : ll) {
l.stateChanged(e);
}
return copy;
}
/**
* Adds a listener that will be informed of changes. An event will fire if
* the project changes in a way that might affect the reported artifacts.
*
* @param l the listener
*/
public void addChangeListener(ChangeListener l) {
synchronized (this) {
if (Boolean.FALSE.equals(supportsChanges)) {
return;
}
if (delListener == null) {
delListener = WeakListeners.change((e) -> updateResults(), null);
for (E d : delegates) {
d.handleChangeListener(delListener, true);
}
}
listeners.add(l);
}
}
/**
* Removes a previously registered Listener.
* @param l the listener to unregister
*/
public void removeChangeListener(ChangeListener l) {
synchronized (this) {
listeners.remove(l);
}
}
}
/**
* Specifies the artifact filter. By default, the default artifact type is returned,
* perhaps determined by the configured packaging with <b>no classifier</b>. It it possible
* to list artifacts of all types and/or artifacts with any classifier in one query.
*/
public static final class Filter {
/**
* Represents all types of artifacts. The query will return all build products
*/
public static final String TYPE_ALL = "<all>"; // NOI18N
/**
* Will return artifacts with any classifier.
*/
public static final String CLASSIFIER_ANY = "<any>"; // NOI18N
private final Set<String> tags;
private final String classifier;
private final String artifactType;
private final ProjectActionContext buildContext;
Filter(String artifactType, String classifier, Set<String> tags, ProjectActionContext buildContext) {
this.classifier = classifier;
this.artifactType = artifactType;
this.buildContext = buildContext;
this.tags = tags == null ? Collections.emptySet() : Collections.unmodifiableSet(tags);
}
/**
* The desired artifact classifier. Only artifact with the specific classifier will be returned. The value
* {@link #CLASSIFIER_ANY} means that any classifier will match. {@code null} (the default) value means
* the default (none) classifier will match.
*
* @return artifact classifier
*/
@CheckForNull
public String getClassifier() {
return classifier;
}
public Set<String> getTags() {
return tags;
}
public boolean hasTag(String t) {
return tags.contains(t);
}
/**
* The desired artifact type. Only artifacts with tha type will be returned. {@link #TYPE_ALL} means that artifacts
* of all types will be returned. {@code null} (the default) means the default type, i.e. defined by project packaging.
* @return artifact type
*/
@CheckForNull
public String getArtifactType() {
return artifactType;
}
/**
* The {@link ProjectActionContext} that shall be used during evaluation.
* @return context instance of {@code null} if none specified.
*/
@CheckForNull
public ProjectActionContext getBuildContext() {
return buildContext;
}
}
/**
* Creates a new simple Filter that returns artifacts of the specified type and
* no classifier.
* @param artifactType the desired type
* @return Filter instance.
*/
@NonNull
public static Filter newQuery(@NullAllowed String artifactType) {
return new Filter(artifactType, null, null, null);
}
/**
* Creates a Filter with the specified properties
* @param artifactType the desired type; use {@code null} for the default artifact type (i.e. defined by packaging)
* @param classifier the desired classifier; use {@code null} for no classifier
* @param buildContext the action context
* @return Filter instance.
*/
@NonNull
public static Filter newQuery(@NullAllowed String artifactType, @NullAllowed String classifier, @NullAllowed ProjectActionContext buildContext) {
return new Filter(artifactType, classifier, null, buildContext);
}
/**
* Creates a Filter with the specified properties
* @param artifactType the desired type; use {@code null} for the default artifact type (i.e. defined by packaging)
* @param classifier the desired classifier; use {@code null} for no classifier
* @param buildContext the action context
* @return Filter instance.
*/
@NonNull
public static Filter newQuery(@NullAllowed String artifactType, @NullAllowed String classifier, @NullAllowed ProjectActionContext buildContext, String... tags) {
return new Filter(artifactType, classifier,
tags == null || tags.length == 0 ? null : new HashSet<>(Arrays.asList(tags)),
buildContext);
}
}