blob: ea6b035606b57b192fea983ec1aacd7b567df919 [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.apache.sling.feature;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import jakarta.json.JsonValue.ValueType;
/**
* An artifact consists of
* <ul>
* <li>An id
* <li>metadata
* <li>optional alias and start order properties (which are part of the metadata)
* </ul>
*
* This class is not thread-safe.
*/
public class Artifact implements Comparable<Artifact> {
/** Can be used in artifact metadata to specify an alias. Multiple aliases can be comma-separated. */
public static final String KEY_ALIAS = "alias";
/** This key might be used by bundles to define the start order. */
public static final String KEY_START_ORDER = "start-order";
public static final String KEY_FEATURE_ORIGINS = "feature-origins";
private static final String KEY_ID = "id";
/** The artifact id. */
private final ArtifactId id;
/** Artifact metadata. */
private final Map<String,String> metadata = new HashMap<>();
/**
* Construct a new artifact
* @param id The id of the artifact.
* @throws IllegalArgumentException If id is {@code null}.
*/
public Artifact(final ArtifactId id) {
if ( id == null ) {
throw new IllegalArgumentException("id must not be null.");
}
this.id = id;
}
/**
* Construct a new artifact
* @param json The json for the artifact
* @throws IllegalArgumentException If json is {@code null} or wrongly formatted.
* @since 1.4
*/
public Artifact(final JsonValue json) {
if ( json == null ) {
throw new IllegalArgumentException("json must not be null.");
}
if ( json.getValueType() == ValueType.STRING ) {
this.id = ArtifactId.parse(((JsonString)json).getString());
} else if ( json.getValueType() == ValueType.OBJECT) {
String id = json.asJsonObject().getString(KEY_ID, null);
if ( id == null ) {
throw new IllegalArgumentException("JSON for artifact is missing id property");
}
this.id = ArtifactId.parse(id);
for(final Map.Entry<String, JsonValue> metadataEntry : json.asJsonObject().entrySet()) {
final String key = metadataEntry.getKey();
if ( KEY_ID.equals(key) ) {
continue;
}
final JsonValue value = metadataEntry.getValue();
if ( value.getValueType() == ValueType.STRING || value.getValueType() == ValueType.NUMBER || value.getValueType() == ValueType.FALSE || value.getValueType() == ValueType.TRUE ) {
this.getMetadata().put(key, org.apache.felix.cm.json.io.Configurations.convertToObject(value).toString());
} else {
throw new IllegalArgumentException("Key ".concat(key).concat(" is not one of the allowed types string, number or boolean : ").concat(value.getValueType().name()));
}
}
} else {
throw new IllegalArgumentException("JSON for artifact must be of type object or string");
}
}
/**
* Get the id of the artifact.
* @return The id.
*/
public ArtifactId getId() {
return this.id;
}
/**
* Get the metadata of the artifact.
* The metadata can be modified.
* @return The metadata.
*/
public Map<String,String> getMetadata() {
return this.metadata;
}
/**
* Obtain the alias or aliases for the artifact.
* @param includeMain Whether to include the main ID in the result.
* @return The aliases or an empty set if there are none.
*/
public Set<ArtifactId> getAliases(boolean includeMain) {
Set<ArtifactId> artifactIds = new HashSet<>();
if (includeMain)
artifactIds.add(getId());
String aliases = getMetadata().get(KEY_ALIAS);
if (aliases != null) {
for (String alias : aliases.split(",")) {
alias = alias.trim();
if (alias.indexOf(':') == alias.lastIndexOf(':')) {
// No version provided, set to version zero
alias += ":0.0.0";
}
artifactIds.add(ArtifactId.fromMvnId(alias));
}
}
return artifactIds;
}
/**
* Get the start order of the artifact.
* This is a convenience method which gets the value for the property named
* {@code #KEY_START_ORDER} from the metadata.
* @return The start order, if no start order is defined, {@code 0} is returned.
* @throws NumberFormatException If the stored metadata is not a number
* @throws IllegalStateException If the stored metadata is a negative number
*/
public int getStartOrder() {
final String order = this.getMetadata().get(Artifact.KEY_START_ORDER);
final int startOrder;
if ( order != null ) {
startOrder = Integer.parseInt(order);
if ( startOrder < 0 ) {
throw new IllegalStateException("Start order must be >= 0 but is " + order);
}
} else {
startOrder = 0;
}
return startOrder;
}
/**
* Set the start order of the artifact
* This is a convenience method which sets the value of the property named
* {@code #KEY_START_ORDER} from the metadata.
* @param startOrder The start order
* @throws IllegalArgumentException If the number is negative
*/
public void setStartOrder(final int startOrder) {
if ( startOrder < 0 ) {
throw new IllegalArgumentException("Start order must be >= 0 but is " + startOrder);
}
if ( startOrder == 0 ) {
this.getMetadata().remove(KEY_START_ORDER);
} else {
this.getMetadata().put(KEY_START_ORDER, String.valueOf(startOrder));
}
}
/**
* Get the feature origins - if recorded
*
* @return A array of feature artifact ids - array might be empty
* @throws IllegalArgumentException If the stored values are not valid artifact ids
*/
public ArtifactId[] getFeatureOrigins() {
String origins = this.getMetadata().get(KEY_FEATURE_ORIGINS);
Set<ArtifactId> originFeatures;
if (origins == null || origins.trim().isEmpty()) {
originFeatures = Collections.emptySet();
} else {
originFeatures = new LinkedHashSet<>();
for (String origin : origins.split(",")) {
if (!origin.trim().isEmpty()) {
originFeatures.add(ArtifactId.parse(origin));
}
}
}
return originFeatures.toArray(new ArtifactId[0]);
}
/**
* Get the feature origins
* If no origins are recorded, the provided artifact id is returned
*
* @param self The id of the current feature
* @return An array of feature artifact ids
* @throws IllegalArgumentException If the stored values are not valid artifact ids
* @since 1.7.0
*/
public ArtifactId[] getFeatureOrigins(final ArtifactId self) {
String origins = this.getMetadata().get(KEY_FEATURE_ORIGINS);
Set<ArtifactId> originFeatures;
if (origins == null || origins.trim().isEmpty()) {
originFeatures = Collections.singleton(self);
} else {
originFeatures = new LinkedHashSet<>();
for (String origin : origins.split(",")) {
if (!origin.trim().isEmpty()) {
originFeatures.add(ArtifactId.parse(origin));
}
}
}
return originFeatures.toArray(new ArtifactId[0]);
}
/**
* Set the feature origins
* @param featureOrigins the array of artifact ids or null to remove the info from this object
*/
public void setFeatureOrigins(ArtifactId... featureOrigins) {
String origins;
if (featureOrigins != null && featureOrigins.length > 0) {
origins = Stream.of(featureOrigins).filter(Objects::nonNull).map(ArtifactId::toMvnId).distinct().collect(Collectors.joining(","));
} else {
origins = "";
}
if (!origins.trim().isEmpty()) {
this.getMetadata().put(KEY_FEATURE_ORIGINS, origins);
} else {
this.getMetadata().remove(KEY_FEATURE_ORIGINS);
}
}
@Override
public int compareTo(final Artifact o) {
return this.id.compareTo(o.id);
}
@Override
public int hashCode() {
return this.id.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return this.id.equals(((Artifact)obj).id);
}
/**
* Create a copy of the artifact with a different id
*
* @param id The new id
* @return The copy of the feature with the new id
*/
public Artifact copy(final ArtifactId id) {
final Artifact result = new Artifact(id);
result.getMetadata().putAll(this.getMetadata());
return result;
}
@Override
public String toString() {
return "Artifact [id=" + id.toMvnId()
+ "]";
}
}