This is an automated email from the ASF dual-hosted git repository. davidb pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/felix-dev.git
The following commit(s) were added to refs/heads/master by this push: new d381c9c FELIX-6444 Contribute a compatible implementation of OSGi Features new 6eff0d0 Merge pull request #88 from bosschaert/features-contribution d381c9c is described below commit d381c9c52b2148606c79a1c10d410a1a2a98b9ea Author: David Bosschaert <dav...@apache.org> AuthorDate: Thu Aug 12 16:20:55 2021 +0100 FELIX-6444 Contribute a compatible implementation of OSGi Features This implementation was initially made in the Apache Sling Whiteboard component at https://github.com/apache/sling-whiteboard/tree/master/osgi-featuremodel --- features/pom.xml | 141 ++++++++ .../felix/feature/impl/ArtifactBuilderImpl.java | 95 +++++ .../felix/feature/impl/BuilderFactoryImpl.java | 59 +++ .../felix/feature/impl/BundleBuilderImpl.java | 95 +++++ .../feature/impl/ConfigurationBuilderImpl.java | 125 +++++++ .../felix/feature/impl/ExtensionBuilderImpl.java | 151 ++++++++ .../felix/feature/impl/FeatureBuilderImpl.java | 275 ++++++++++++++ .../felix/feature/impl/FeatureServiceImpl.java | 399 +++++++++++++++++++++ .../java/org/apache/felix/feature/impl/IDImpl.java | 214 +++++++++++ .../felix/feature/impl/FeatureServiceImplTest.java | 295 +++++++++++++++ .../src/test/resources/features/test-exfeat1.json | 26 ++ .../src/test/resources/features/test-exfeat2.json | 9 + .../src/test/resources/features/test-feature.json | 28 ++ .../src/test/resources/features/test-feature2.json | 19 + 14 files changed, 1931 insertions(+) diff --git a/features/pom.xml b/features/pom.xml new file mode 100644 index 0000000..cd4f4d6 --- /dev/null +++ b/features/pom.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- 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. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.felix</groupId> + <artifactId>felix-parent</artifactId> + <version>7</version> + <relativePath /> + </parent> + + <artifactId>org.apache.felix.feature</artifactId> + <version>0.0.1-SNAPSHOT</version> + <packaging>jar</packaging> + + <name>OSGi Feature Model API</name> + + <properties> + <felix.java.version>11</felix.java.version> + </properties> + + <repositories> + <repository> + <id>sonatype.snapshots</id> + <name>OSGi Snapshot</name> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> + </repository> + </repositories> + + <build> + <plugins> + <plugin> + <groupId>biz.aQute.bnd</groupId> + <artifactId>bnd-maven-plugin</artifactId> + <version>5.3.0</version> + <executions> + <execution> + <goals> + <goal>bnd-process</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <archive> + <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile> + </archive> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.rat</groupId> + <artifactId>apache-rat-plugin</artifactId> + <configuration> + <excludes> + <exclude>*.md</exclude> + <exclude>src/main/resources/META-INF/services/*</exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>osgi.annotation</artifactId> + <version>8.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>osgi.core</artifactId> + <version>8.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.service.feature</artifactId> + <version>1.0.0-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.util.function</artifactId> + <version>1.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-json_1.1_spec</artifactId> + <version>1.3</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.converter</artifactId> + <version>1.0.18</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.cm.json</artifactId> + <version>1.0.6</version> + <scope>provided</scope> + </dependency> + + <!-- Testing --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.13.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>2.8.9</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.johnzon</groupId> + <artifactId>johnzon-core</artifactId> + <version>1.2.2</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/features/src/main/java/org/apache/felix/feature/impl/ArtifactBuilderImpl.java b/features/src/main/java/org/apache/felix/feature/impl/ArtifactBuilderImpl.java new file mode 100644 index 0000000..4fe6584 --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/ArtifactBuilderImpl.java @@ -0,0 +1,95 @@ +/* + * 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.felix.feature.impl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import org.osgi.service.feature.FeatureArtifact; +import org.osgi.service.feature.FeatureArtifactBuilder; +import org.osgi.service.feature.ID; + +class ArtifactBuilderImpl implements FeatureArtifactBuilder { + private final ID id; + + private final Map<String,Object> metadata = new LinkedHashMap<>(); + + ArtifactBuilderImpl(ID id) { + this.id = id; + } + + @Override + public FeatureArtifactBuilder addMetadata(String key, Object value) { + this.metadata.put(key, value); + return this; + } + + @Override + public FeatureArtifactBuilder addMetadata(Map<String,Object> md) { + this.metadata.putAll(md); + return this; + } + + @Override + public FeatureArtifact build() { + return new ArtifactImpl(id, metadata); + } + + private static class ArtifactImpl implements FeatureArtifact { + private final ID id; + private final Map<String, Object> metadata; + + private ArtifactImpl(ID id, Map<String, Object> metadata) { + this.id = id; + this.metadata = Collections.unmodifiableMap(metadata); + } + + @Override + public ID getID() { + return id; + } + + @Override + public Map<String, Object> getMetadata() { + return metadata; + } + + @Override + public int hashCode() { + return Objects.hash(id, metadata); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ArtifactImpl other = (ArtifactImpl) obj; + return Objects.equals(id, other.id) && Objects.equals(metadata, other.metadata); + } + + @Override + public String toString() { + return "ArtifactImpl [getID()=" + getID() + "]"; + } + } +} diff --git a/features/src/main/java/org/apache/felix/feature/impl/BuilderFactoryImpl.java b/features/src/main/java/org/apache/felix/feature/impl/BuilderFactoryImpl.java new file mode 100644 index 0000000..7622aed --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/BuilderFactoryImpl.java @@ -0,0 +1,59 @@ +/* + * 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.felix.feature.impl; + +import org.osgi.service.feature.BuilderFactory; +import org.osgi.service.feature.FeatureArtifactBuilder; +import org.osgi.service.feature.FeatureBuilder; +import org.osgi.service.feature.FeatureBundleBuilder; +import org.osgi.service.feature.FeatureConfigurationBuilder; +import org.osgi.service.feature.FeatureExtension.Kind; +import org.osgi.service.feature.FeatureExtension.Type; +import org.osgi.service.feature.FeatureExtensionBuilder; +import org.osgi.service.feature.ID; + +class BuilderFactoryImpl implements BuilderFactory { + @Override + public FeatureArtifactBuilder newArtifactBuilder(ID id) { + return new ArtifactBuilderImpl(id); + } + + @Override + public FeatureBundleBuilder newBundleBuilder(ID id) { + return new BundleBuilderImpl(id); + } + + @Override + public FeatureConfigurationBuilder newConfigurationBuilder(String pid) { + return new ConfigurationBuilderImpl(pid); + } + + @Override + public FeatureConfigurationBuilder newConfigurationBuilder(String factoryPid, String name) { + return new ConfigurationBuilderImpl(factoryPid, name); + } + + @Override + public FeatureBuilder newFeatureBuilder(ID id) { + return new FeatureBuilderImpl(id); + } + + @Override + public FeatureExtensionBuilder newExtensionBuilder(String name, Type type, Kind kind) { + return new ExtensionBuilderImpl(name, type, kind); + } +} diff --git a/features/src/main/java/org/apache/felix/feature/impl/BundleBuilderImpl.java b/features/src/main/java/org/apache/felix/feature/impl/BundleBuilderImpl.java new file mode 100644 index 0000000..e19a2b1 --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/BundleBuilderImpl.java @@ -0,0 +1,95 @@ +/* + * 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.felix.feature.impl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import org.osgi.service.feature.FeatureBundle; +import org.osgi.service.feature.FeatureBundleBuilder; +import org.osgi.service.feature.ID; + +class BundleBuilderImpl implements FeatureBundleBuilder { + private final ID id; + + private final Map<String,Object> metadata = new LinkedHashMap<>(); + + BundleBuilderImpl(ID id) { + this.id = id; + } + + @Override + public FeatureBundleBuilder addMetadata(String key, Object value) { + this.metadata.put(key, value); + return this; + } + + @Override + public FeatureBundleBuilder addMetadata(Map<String,Object> md) { + this.metadata.putAll(md); + return this; + } + + @Override + public FeatureBundle build() { + return new BundleImpl(id, metadata); + } + + private static class BundleImpl implements FeatureBundle { + private final ID id; + private final Map<String, Object> metadata; + + private BundleImpl(ID id, Map<String, Object> metadata) { + this.id = id; + this.metadata = Collections.unmodifiableMap(metadata); + } + + @Override + public ID getID() { + return id; + } + + @Override + public Map<String, Object> getMetadata() { + return metadata; + } + + @Override + public int hashCode() { + return Objects.hash(id, metadata); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BundleImpl other = (BundleImpl) obj; + return Objects.equals(id, other.id) && Objects.equals(metadata, other.metadata); + } + + @Override + public String toString() { + return "BundleImpl [getID()=" + getID() + "]"; + } + } +} diff --git a/features/src/main/java/org/apache/felix/feature/impl/ConfigurationBuilderImpl.java b/features/src/main/java/org/apache/felix/feature/impl/ConfigurationBuilderImpl.java new file mode 100644 index 0000000..62da552 --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/ConfigurationBuilderImpl.java @@ -0,0 +1,125 @@ +/* + * 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.felix.feature.impl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.osgi.service.feature.FeatureConfiguration; +import org.osgi.service.feature.FeatureConfigurationBuilder; + +class ConfigurationBuilderImpl implements FeatureConfigurationBuilder { + private final String p; + private final String name; + + private final Map<String,Object> values = new LinkedHashMap<>(); + + ConfigurationBuilderImpl(String pid) { + this.p = pid; + this.name = null; + } + + ConfigurationBuilderImpl(String factoryPid, String name) { + this.p = factoryPid; + this.name = name; + } + + ConfigurationBuilderImpl(FeatureConfiguration c) { + if (c.getFactoryPid() == null) { + p = c.getPid(); + name = null; + } else { + // TODO + p = null; + name = null; + } + + addValues(c.getValues()); + } + + @Override + public FeatureConfigurationBuilder addValue(String key, Object value) { + // TODO can do some validation on the configuration + this.values.put(key, value); + return this; + } + + @Override + public FeatureConfigurationBuilder addValues(Map<String, Object> cfg) { + // TODO can do some validation on the configuration + this.values.putAll(cfg); + return this; + } + + @Override + public FeatureConfiguration build() { + if (name == null) { + return new ConfigurationImpl(p, null, values); + } else { + return new ConfigurationImpl(p + "~" + name, p, values); + } + } + + private static class ConfigurationImpl implements FeatureConfiguration { + private final String pid; + private final Optional<String> factoryPid; + private final Map<String, Object> values; + + private ConfigurationImpl(String pid, String factoryPid, + Map<String, Object> values) { + this.pid = pid; + this.factoryPid = Optional.ofNullable(factoryPid); + this.values = Collections.unmodifiableMap(values); + } + + public String getPid() { + return pid; + } + + public Optional<String> getFactoryPid() { + return factoryPid; + } + + public Map<String, Object> getValues() { + return values; + } + + @Override + public int hashCode() { + return Objects.hash(factoryPid, pid, values); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ConfigurationImpl)) + return false; + ConfigurationImpl other = (ConfigurationImpl) obj; + return Objects.equals(factoryPid, other.factoryPid) && Objects.equals(pid, other.pid) + && Objects.equals(values, other.values); + } + + @Override + public String toString() { + return "ConfigurationImpl [pid=" + pid + "]"; + } + } +} diff --git a/features/src/main/java/org/apache/felix/feature/impl/ExtensionBuilderImpl.java b/features/src/main/java/org/apache/felix/feature/impl/ExtensionBuilderImpl.java new file mode 100644 index 0000000..81da44c --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/ExtensionBuilderImpl.java @@ -0,0 +1,151 @@ +/* + * 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.felix.feature.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.osgi.service.feature.FeatureArtifact; +import org.osgi.service.feature.FeatureExtension; +import org.osgi.service.feature.FeatureExtension.Kind; +import org.osgi.service.feature.FeatureExtension.Type; +import org.osgi.service.feature.FeatureExtensionBuilder; + +class ExtensionBuilderImpl implements FeatureExtensionBuilder { + private final String name; + private final Type type; + private final Kind kind; + + private final List<String> content = new ArrayList<>(); + private final List<FeatureArtifact> artifacts = new ArrayList<>(); + + ExtensionBuilderImpl(String name, Type type, Kind kind) { + this.name = name; + this.type = type; + this.kind = kind; + } + + @Override + public FeatureExtensionBuilder addText(String text) { + if (type != Type.TEXT) + throw new IllegalStateException("Cannot add text to extension of type " + type); + + content.add(text); + return this; + } + + @Override + public FeatureExtensionBuilder setJSON(String json) { + if (type != Type.JSON) + throw new IllegalStateException("Cannot add text to extension of type " + type); + + content.clear(); // Clear any previous value + content.add(json); + return this; + } + + @Override + public FeatureExtensionBuilder addArtifact(FeatureArtifact art) { + if (type != Type.ARTIFACTS) + throw new IllegalStateException("Cannot add artifacts to extension of type " + type); + + artifacts.add(art); + return this; + } + + @Override + public FeatureExtension build() { + return new ExtensionImpl(name, type, kind, content, artifacts); + } + + private static class ExtensionImpl implements FeatureExtension { + private final String name; + private final Type type; + private final Kind kind; + private final List<String> content; + private final List<FeatureArtifact> artifacts; + + private ExtensionImpl(String name, Type type, Kind kind, List<String> content, List<FeatureArtifact> artifacts) { + this.name = name; + this.type = type; + this.kind = kind; + this.content = Collections.unmodifiableList(content); + this.artifacts = Collections.unmodifiableList(artifacts); + } + + public String getName() { + return name; + } + + public Type getType() { + return type; + } + + public Kind getKind() { + return kind; + } + + public String getJSON() { + if (type != Type.JSON) + throw new IllegalStateException("Extension is not of type JSON " + type); + + if (content.isEmpty()) + return null; + + return content.get(0); + } + + public List<String> getText() { + if (type != Type.TEXT) + throw new IllegalStateException("Extension is not of type Text " + type); + + return content; + } + + public List<FeatureArtifact> getArtifacts() { + if (type != Type.ARTIFACTS) + throw new IllegalStateException("Extension is not of type Text " + type); + + return artifacts; + } + + @Override + public int hashCode() { + return Objects.hash(artifacts, content, kind, name, type); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ExtensionImpl other = (ExtensionImpl) obj; + return Objects.equals(artifacts, other.artifacts) && Objects.equals(content, other.content) + && kind == other.kind && Objects.equals(name, other.name) && type == other.type; + } + + @Override + public String toString() { + return "ExtensionImpl [name=" + name + ", type=" + type + "]"; + } + } +} diff --git a/features/src/main/java/org/apache/felix/feature/impl/FeatureBuilderImpl.java b/features/src/main/java/org/apache/felix/feature/impl/FeatureBuilderImpl.java new file mode 100644 index 0000000..2f86860 --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/FeatureBuilderImpl.java @@ -0,0 +1,275 @@ +/* + * 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.felix.feature.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.osgi.service.feature.Feature; +import org.osgi.service.feature.FeatureBuilder; +import org.osgi.service.feature.FeatureBundle; +import org.osgi.service.feature.FeatureConfiguration; +import org.osgi.service.feature.FeatureExtension; +import org.osgi.service.feature.ID; + +class FeatureBuilderImpl implements FeatureBuilder { + private final ID id; + + private String name; + private String description; + private String docURL; + private String license; + private String scm; + private String vendor; + private boolean complete; + + private final List<FeatureBundle> bundles = new ArrayList<>(); + private final List<String> categories = new ArrayList<>(); + private final Map<String,FeatureConfiguration> configurations = new LinkedHashMap<>(); + private final Map<String,FeatureExtension> extensions = new LinkedHashMap<>(); + private final Map<String,String> variables = new LinkedHashMap<>(); + + FeatureBuilderImpl(ID id) { + this.id = id; + } + + @Override + public FeatureBuilder setName(String name) { + this.name = name; + return this; + } + + @Override + public FeatureBuilder setDocURL(String url) { + this.docURL = url; + return this; + } + + @Override + public FeatureBuilder setVendor(String vendor) { + this.vendor = vendor; + return this; + } + + @Override + public FeatureBuilder setLicense(String license) { + this.license = license; + return this; + } + + @Override + public FeatureBuilder setComplete(boolean complete) { + this.complete = complete; + return this; + } + + @Override + public FeatureBuilder setDescription(String description) { + this.description = description; + return this; + } + + @Override + public FeatureBuilder setSCM(String scm) { + this.scm = scm; + return this; + } + + @Override + public FeatureBuilder addBundles(FeatureBundle ... bundles) { + this.bundles.addAll(Arrays.asList(bundles)); + return this; + } + + + @Override + public FeatureBuilder addCategories(String ...categories) { + this.categories.addAll(Arrays.asList(categories)); + return this; + } + + @Override + public FeatureBuilder addConfigurations(FeatureConfiguration ... configs) { + for (FeatureConfiguration cfg : configs) { + this.configurations.put(cfg.getPid(), cfg); + } + return this; + } + + @Override + public FeatureBuilder addExtensions(FeatureExtension ... extensions) { + for (FeatureExtension ex : extensions) { + this.extensions.put(ex.getName(), ex); + } + return this; + } + + @Override + public FeatureBuilder addVariable(String key, String value) { + this.variables.put(key, value); + return this; + } + + @Override + public FeatureBuilder addVariables(Map<String,String> variables) { + this.variables.putAll(variables); + return this; + } + + @Override + public Feature build() { + return new FeatureImpl(id, name, description, docURL, + license, scm, vendor, complete, + bundles, categories, configurations, extensions, variables); + } + + private static class FeatureImpl implements Feature { + private final ID id; + private final Optional<String> name; + private final Optional<String> description; + private final Optional<String> docURL; + private final Optional<String> license; + private final Optional<String> scm; + private final Optional<String> vendor; + private final boolean complete; + + private final List<FeatureBundle> bundles; + private final List<String> categories; + private final Map<String,FeatureConfiguration> configurations; + private final Map<String,FeatureExtension> extensions; + private final Map<String,String> variables; + + private FeatureImpl(ID id, String aName, String desc, String docs, String lic, String sc, String vnd, + boolean comp, List<FeatureBundle> bs, List<String> cats, Map<String,FeatureConfiguration> cs, + Map<String,FeatureExtension> es, Map<String,String> vars) { + this.id = id; + name = Optional.ofNullable(aName); + description = Optional.ofNullable(desc); + docURL = Optional.ofNullable(docs); + license = Optional.ofNullable(lic); + scm = Optional.ofNullable(sc); + vendor = Optional.ofNullable(vnd); + complete = comp; + + bundles = Collections.unmodifiableList(bs); + categories = Collections.unmodifiableList(cats); + configurations = Collections.unmodifiableMap(cs); + extensions = Collections.unmodifiableMap(es); + variables = Collections.unmodifiableMap(vars); + } + + @Override + public ID getID() { + return id; + } + + @Override + public Optional<String> getName() { + return name; + } + + @Override + public Optional<String> getDescription() { + return description; + } + + @Override + public Optional<String> getVendor() { + return vendor; + } + + @Override + public Optional<String> getLicense() { + return license; + } + + @Override + public Optional<String> getDocURL() { + return docURL; + } + + @Override + public Optional<String> getSCM() { + return scm; + } + + @Override + public boolean isComplete() { + return complete; + } + + @Override + public List<FeatureBundle> getBundles() { + return bundles; + } + + @Override + public List<String> getCategories() { + return categories; + } + + @Override + public Map<String,FeatureConfiguration> getConfigurations() { + return configurations; + } + + @Override + public Map<String,FeatureExtension> getExtensions() { + return extensions; + } + + @Override + public Map<String,String> getVariables() { + return variables; + } + + @Override + public int hashCode() { + return Objects.hash(bundles, categories, complete, configurations, description, docURL, + extensions, id, license, name, scm, variables, vendor); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FeatureImpl other = (FeatureImpl) obj; + return Objects.equals(bundles, other.bundles) && Objects.equals(categories, other.categories) + && complete == other.complete && Objects.equals(configurations, other.configurations) + && Objects.equals(description, other.description) + && Objects.equals(docURL, other.docURL) && Objects.equals(extensions, other.extensions) + && Objects.equals(id, other.id) && Objects.equals(license, other.license) + && Objects.equals(name, other.name) && Objects.equals(scm, other.scm) + && Objects.equals(variables, other.variables) && Objects.equals(vendor, other.vendor); + } + + @Override + public String toString() { + return "FeatureImpl [getID()=" + getID() + "]"; + } + } +} diff --git a/features/src/main/java/org/apache/felix/feature/impl/FeatureServiceImpl.java b/features/src/main/java/org/apache/felix/feature/impl/FeatureServiceImpl.java new file mode 100644 index 0000000..10dc999 --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/FeatureServiceImpl.java @@ -0,0 +1,399 @@ +/* + * 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.felix.feature.impl; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; +import javax.json.JsonValue; +import javax.json.stream.JsonGenerator; +import javax.json.stream.JsonGeneratorFactory; + +import org.apache.felix.cm.json.impl.JsonSupport; +import org.apache.felix.cm.json.impl.TypeConverter; +import org.osgi.service.feature.BuilderFactory; +import org.osgi.service.feature.Feature; +import org.osgi.service.feature.FeatureArtifact; +import org.osgi.service.feature.FeatureArtifactBuilder; +import org.osgi.service.feature.FeatureBuilder; +import org.osgi.service.feature.FeatureBundle; +import org.osgi.service.feature.FeatureBundleBuilder; +import org.osgi.service.feature.FeatureConfiguration; +import org.osgi.service.feature.FeatureConfigurationBuilder; +import org.osgi.service.feature.FeatureExtension; +import org.osgi.service.feature.FeatureExtensionBuilder; +import org.osgi.service.feature.FeatureService; +import org.osgi.service.feature.ID; + +public class FeatureServiceImpl implements FeatureService { + private final BuilderFactoryImpl builderFactory = new BuilderFactoryImpl(); + + public BuilderFactory getBuilderFactory() { + return builderFactory; + } + + @Override + public ID getIDfromMavenCoordinates(String mavenID) { + return IDImpl.fromMavenID(mavenID); + } + + @Override + public ID getID(String groupId, String artifactId, String version) { + return new IDImpl(groupId, artifactId, version, null, null); + } + + @Override + public ID getID(String groupId, String artifactId, String version, String type) { + return new IDImpl(groupId, artifactId, version, type, null); + } + + @Override + public ID getID(String groupId, String artifactId, String version, String type, String classifier) { + return new IDImpl(groupId, artifactId, version, type, classifier); + } + + public Feature readFeature(Reader jsonReader) throws IOException { + JsonObject json = Json.createReader( + JsonSupport.createCommentRemovingReader(jsonReader)).readObject(); + + String id = json.getString("id"); + FeatureBuilder builder = builderFactory.newFeatureBuilder(getIDfromMavenCoordinates(id)); + + builder.setName(json.getString("name", null)); + builder.setDescription(json.getString("description", null)); + builder.setDocURL(json.getString("docURL", null)); + builder.setLicense(json.getString("license", null)); + builder.setSCM(json.getString("scm", null)); + builder.setVendor(json.getString("vendor", null)); + + builder.setComplete(json.getBoolean("complete", false)); + + builder.addBundles(getBundles(json)); + builder.addCategories(getCategories(json)); + builder.addConfigurations(getConfigurations(json)); + builder.addExtensions(getExtensions(json)); + + return builder.build(); + } + + private FeatureBundle[] getBundles(JsonObject json) { + JsonArray ja = json.getJsonArray("bundles"); + if (ja == null) + return new FeatureBundle[] {}; + + List<FeatureBundle> bundles = new ArrayList<>(); + + for (JsonValue val : ja) { + if (val.getValueType() == JsonValue.ValueType.OBJECT) { + JsonObject jo = val.asJsonObject(); + String bid = jo.getString("id"); + FeatureBundleBuilder builder = builderFactory.newBundleBuilder(getIDfromMavenCoordinates(bid)); + + for (Map.Entry<String, JsonValue> entry : jo.entrySet()) { + if (entry.getKey().equals("id")) + continue; + + JsonValue value = entry.getValue(); + + Object v; + switch (value.getValueType()) { + case NUMBER: + v = ((JsonNumber) value).longValueExact(); + break; + case STRING: + v = ((JsonString) value).getString(); + break; + default: + v = value.toString(); + } + builder.addMetadata(entry.getKey(), v); + } + bundles.add(builder.build()); + } + } + + return bundles.toArray(new FeatureBundle[0]); + } + + private String[] getCategories(JsonObject json) { + JsonArray ja = json.getJsonArray("categories"); + if (ja == null) + return new String[] {}; + + List<String> cats = ja.getValuesAs(JsonString::getString); + return cats.toArray(new String[] {}); + } + + private FeatureConfiguration[] getConfigurations(JsonObject json) { + JsonObject jo = json.getJsonObject("configurations"); + if (jo == null) + return new FeatureConfiguration[] {}; + + List<FeatureConfiguration> configs = new ArrayList<>(); + + for (Map.Entry<String, JsonValue> entry : jo.entrySet()) { + + String p = entry.getKey(); + String factoryPid = null; + int idx = p.indexOf('~'); + if (idx > 0) { + factoryPid = p.substring(0, idx); + p = p.substring(idx + 1); + } + + FeatureConfigurationBuilder builder; + if (factoryPid == null) { + builder = builderFactory.newConfigurationBuilder(p); + } else { + builder = builderFactory.newConfigurationBuilder(factoryPid, p); + } + + JsonObject values = entry.getValue().asJsonObject(); + for (Map.Entry<String, JsonValue> value : values.entrySet()) { + String key = value.getKey(); + String typeInfo = null; + int cidx = key.indexOf(':'); + if (cidx > 0) { + typeInfo = key.substring(cidx + 1); + key = key.substring(0, cidx); + } + + JsonValue val = value.getValue(); + // TODO ensure that binary support works as well + Object v = TypeConverter.convertObjectToType(val, typeInfo); + builder.addValue(key, v); + } + configs.add(builder.build()); + } + + return configs.toArray(new FeatureConfiguration[] {}); + } + + private FeatureExtension[] getExtensions(JsonObject json) { + JsonObject jo = json.getJsonObject("extensions"); + if (jo == null) + return new FeatureExtension[] {}; + + List<FeatureExtension> extensions = new ArrayList<>(); + + for (Map.Entry<String,JsonValue> entry : jo.entrySet()) { + JsonObject exData = entry.getValue().asJsonObject(); + FeatureExtension.Type type; + if (exData.containsKey("text")) { + type = FeatureExtension.Type.TEXT; + } else if (exData.containsKey("artifacts")) { + type = FeatureExtension.Type.ARTIFACTS; + } else if (exData.containsKey("json")) { + type = FeatureExtension.Type.JSON; + } else { + throw new IllegalStateException("Invalid extension: " + entry); + } + String k = exData.getString("kind", "optional"); + FeatureExtension.Kind kind = FeatureExtension.Kind.valueOf(k.toUpperCase()); + + FeatureExtensionBuilder builder = builderFactory.newExtensionBuilder(entry.getKey(), type, kind); + + switch (type) { + case TEXT: + exData.getJsonArray("text") + .stream() + .filter(jv -> jv.getValueType() == JsonValue.ValueType.STRING) + .map(jv -> ((JsonString) jv).getString()) + .forEach(builder::addText); + + break; + case ARTIFACTS: + exData.getJsonArray("artifacts") + .stream() + .filter(jv -> jv.getValueType() == JsonValue.ValueType.OBJECT) + .map(jv -> (JsonObject) jv) + .forEach(md -> { + Map<String, JsonValue> v = new HashMap<>(md); + JsonString idVal = (JsonString) v.remove("id"); + + ID id = getIDfromMavenCoordinates(idVal.getString()); + FeatureArtifactBuilder fab = builderFactory.newArtifactBuilder(id); + + for (Map.Entry<String,JsonValue> mde : v.entrySet()) { + JsonValue val = mde.getValue(); + switch (val.getValueType()) { + case STRING: + fab.addMetadata(mde.getKey(), ((JsonString) val).getString()); + break; + case FALSE: + fab.addMetadata(mde.getKey(), false); + break; + case TRUE: + fab.addMetadata(mde.getKey(), true); + break; + case NUMBER: + JsonNumber num = (JsonNumber) val; + if (num.toString().contains(".")) { + fab.addMetadata(mde.getKey(), num.doubleValue()); + } else { + fab.addMetadata(mde.getKey(), num.longValue()); + } + break; + default: + // do nothing + break; + } + } + + builder.addArtifact(fab.build()); + }); + + break; + case JSON: + builder.setJSON(exData.getJsonObject("json").toString()); + break; + } + extensions.add(builder.build()); + } + + return extensions.toArray(new FeatureExtension[] {}); + } + + public void writeFeature(Feature feature, Writer jsonWriter) throws IOException { + // LinkedHashMap to give it some order, we'd like 'id' and 'name' first. + Map<String,Object> attrs = new LinkedHashMap<>(); + + attrs.put("id", feature.getID().toString()); + feature.getName().ifPresent(n -> attrs.put("name", n)); + feature.getDescription().ifPresent(d -> attrs.put("description", d)); + feature.getDocURL().ifPresent(d -> attrs.put("docURL", d)); + feature.getLicense().ifPresent(l -> attrs.put("license", l)); + feature.getSCM().ifPresent(s -> attrs.put("scm", s)); + feature.getVendor().ifPresent(v -> attrs.put("vendor", v)); + + JsonObjectBuilder json = Json.createObjectBuilder(attrs); + + JsonArray bundles = getBundles(feature); + if (bundles != null) { + json.add("bundles", bundles); + } + + JsonObject configs = getConfigurations(feature); + if (configs != null) { + json.add("configurations", configs); + } + + JsonObject extensions = getExtensions(feature); + if (extensions != null) { + json.add("extensions", extensions); + } + + // TODO add variables + // TODO add frameworkproperties + + JsonObject fo = json.build(); + + JsonGeneratorFactory gf = Json.createGeneratorFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true)); + try (JsonGenerator gr = gf.createGenerator(jsonWriter)) { + gr.write(fo); + } + } + + private JsonArray getBundles(Feature feature) { + List<FeatureBundle> bundles = feature.getBundles(); + if (bundles == null || bundles.size() == 0) + return null; + + JsonArrayBuilder ab = Json.createArrayBuilder(); + + for (FeatureBundle bundle : bundles) { + Map<String, Object> attrs = new LinkedHashMap<>(); + attrs.put("id", bundle.getID().toString()); + attrs.putAll(bundle.getMetadata()); + ab.add(Json.createObjectBuilder(attrs)); + } + + return ab.build(); + } + + private JsonObject getConfigurations(Feature feature) { + Map<String, FeatureConfiguration> configs = feature.getConfigurations(); + if (configs == null || configs.size() == 0) + return null; + + JsonObjectBuilder ob = Json.createObjectBuilder(); + + for (Map.Entry<String,FeatureConfiguration> cfg : configs.entrySet()) { + JsonObjectBuilder cb = Json.createObjectBuilder(); + + for (Map.Entry<String,Object> prop : cfg.getValue().getValues().entrySet()) { + Map.Entry<String, JsonValue> je = TypeConverter.convertObjectToTypedJsonValue(prop.getValue()); + String tk = je.getKey(); + cb.add(TypeConverter.NO_TYPE_INFO.equals(tk) ? prop.getKey() : prop.getKey() + ":" + tk, je.getValue()); + } + ob.add(cfg.getKey(), cb.build()); + } + return ob.build(); + } + + private JsonObject getExtensions(Feature feature) { + Map<String, FeatureExtension> extensions = feature.getExtensions(); + if (extensions == null || extensions.size() == 0) + return null; + + JsonObjectBuilder ob = Json.createObjectBuilder(); + + for (Map.Entry<String,FeatureExtension> entry : extensions.entrySet()) { + FeatureExtension extVal = entry.getValue(); + + JsonObjectBuilder vb = Json.createObjectBuilder(); + vb.add("kind", extVal.getKind().toString().toLowerCase()); + + switch (extVal.getType()) { + case TEXT: + vb.add("text", Json.createArrayBuilder(extVal.getText()).build()); + break; + case ARTIFACTS: + JsonArrayBuilder arr = Json.createArrayBuilder(); + for (FeatureArtifact art : extVal.getArtifacts()) { + Map<String,Object> attrs = new LinkedHashMap<>(); + attrs.put("id", art.getID().toString()); + attrs.putAll(art.getMetadata()); + arr.add(Json.createObjectBuilder(attrs)).build(); + } + + vb.add("artifacts", arr.build()); + break; + case JSON: + vb.add("json", Json.createReader(new StringReader(extVal.getJSON())).readValue()); + break; + } + ob.add(entry.getKey(), vb.build()); + } + return ob.build(); + } +} diff --git a/features/src/main/java/org/apache/felix/feature/impl/IDImpl.java b/features/src/main/java/org/apache/felix/feature/impl/IDImpl.java new file mode 100644 index 0000000..76ef473 --- /dev/null +++ b/features/src/main/java/org/apache/felix/feature/impl/IDImpl.java @@ -0,0 +1,214 @@ +/* + * 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.felix.feature.impl; + +import java.util.Objects; +import java.util.Optional; + +import org.osgi.service.feature.ID; + +public class IDImpl implements ID { + private final String groupId; + private final String artifactId; + private final String version; // The Artifact Version may not follow OSGi version rules + private final String type; + private final String classifier; + + /** + * Construct an ID from a Maven ID. Maven IDs have the following syntax: + * <p> + * {@code group-id ':' artifact-id [ ':' [type] [ ':' classifier ] ] ':' version} + * + * @param mavenID + * @return The ID + * @throws IllegalArgumentException if the mavenID does not match the Syntax + */ + public static IDImpl fromMavenID(String mavenID) + throws IllegalArgumentException { + String[] parts = mavenID.split(":"); + + if (parts.length < 3 || parts.length > 5) + throw new IllegalArgumentException("Not a valid maven ID" + mavenID); + + String gid = parts[0]; + String aid = parts[1]; + String ver = null; + String t = null; + String c = null; + + if (parts.length == 3) { + ver = parts[2]; + } else if (parts.length == 4) { + t = parts[2]; + ver = parts[3]; + } else { + t = parts[2]; + c = parts[3]; + ver = parts[4]; + } + return new IDImpl(gid, aid, ver, t, c); + } + + /** + * Construct an ID + * + * @param groupId The group ID. + * @param artifactId The artifact ID. + * @param version The version. + * @param type The type identifier. + * @param classifier The classifier. + * @throws NullPointerException if one of the parameters (groupId, + * artifactId, version) is null. + * @throws IllegalArgumentException if one of the parameters is empty or + * contains an colon `:` or if a classifier is used without a + * type. + */ + public IDImpl(String groupId, String artifactId, String version, String type, + String classifier) + throws NullPointerException, IllegalArgumentException { + + Objects.requireNonNull(groupId, "groupId"); + Objects.requireNonNull(artifactId, "artifact"); + Objects.requireNonNull(version, "version"); + + if (groupId.isEmpty()) { + throw new IllegalArgumentException("groupId must not be empty"); + } + if (artifactId.isEmpty()) { + throw new IllegalArgumentException("artifactId must not be empty"); + } + if (version.isEmpty()) { + throw new IllegalArgumentException("version must not be empty"); + } + + if (type != null && type.isEmpty()) { + throw new IllegalArgumentException("type must not be empty"); + } + + if (classifier != null && classifier.isEmpty()) { + throw new IllegalArgumentException("classifier must not be empty"); + } + + if (groupId.contains(":")) { + throw new IllegalArgumentException( + "groupId must not contain a colon `:`"); + } + if (artifactId.contains(":")) { + throw new IllegalArgumentException( + "artifactId must not contain a colon `:`"); + } + if (version.contains(":")) { + throw new IllegalArgumentException( + "version must not contain a colon `:`"); + } + if (type != null && type.contains(":")) { + throw new IllegalArgumentException( + "type must not contain a colon `:`"); + } + if (classifier != null && classifier.contains(":")) { + throw new IllegalArgumentException( + "classifier must not contain a colon `:`"); + } + if (type == null && classifier != null) { + throw new IllegalArgumentException( + "type must not be `null` if a classifier is set"); + } + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.type = type; + this.classifier = classifier; + } + + /** + * Get the group ID. + * @return The group ID. + */ + public String getGroupId() { + return groupId; + } + + /** + * Get the artifact ID. + * @return The artifact ID. + */ + public String getArtifactId() { + return artifactId; + } + + /** + * Get the version. + * @return The version. + */ + public String getVersion() { + return version; + } + + /** + * Get the type identifier. + * @return The type identifier. + */ + public Optional<String> getType() { + return Optional.ofNullable(type); + } + + /** + * Get the classifier. + * @return The classifier. + */ + public Optional<String> getClassifier() { + return Optional.ofNullable(classifier); + } + + @Override + public int hashCode() { + return Objects.hash(artifactId, classifier, groupId, type, version); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof IDImpl)) + return false; + IDImpl other = (IDImpl) obj; + return Objects.equals(artifactId, other.artifactId) && Objects.equals(classifier, other.classifier) + && Objects.equals(groupId, other.groupId) && Objects.equals(type, other.type) + && Objects.equals(version, other.version); + } + + /** + * Returns the the mavenID. Maven IDs have the following syntax: + * <p> + * {@code group-id ':' artifact-id [ ':' [type] [ ':' classifier ] ] ':' version} + * + * @return the mavenID. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(groupId).append(":") + .append(artifactId); + + if (type != null) { + sb = sb.append(":").append(type); + if (classifier != null) { + sb = sb.append(":").append(classifier); + } + } + return sb.append(":").append(version).toString(); + } +} diff --git a/features/src/test/java/org/apache/felix/feature/impl/FeatureServiceImplTest.java b/features/src/test/java/org/apache/felix/feature/impl/FeatureServiceImplTest.java new file mode 100644 index 0000000..9b2334b --- /dev/null +++ b/features/src/test/java/org/apache/felix/feature/impl/FeatureServiceImplTest.java @@ -0,0 +1,295 @@ +/* + * 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.felix.feature.impl; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.net.URL; +import java.util.List; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; + +import org.apache.felix.feature.impl.FeatureServiceImpl; +import org.junit.Before; +import org.junit.Test; +import org.osgi.service.feature.BuilderFactory; +import org.osgi.service.feature.Feature; +import org.osgi.service.feature.FeatureArtifact; +import org.osgi.service.feature.FeatureBuilder; +import org.osgi.service.feature.FeatureBundle; +import org.osgi.service.feature.FeatureConfiguration; +import org.osgi.service.feature.FeatureExtension; +import org.osgi.service.feature.FeatureService; + +public class FeatureServiceImplTest { + FeatureServiceImpl features; + + @Before + public void setUp() { + features = new FeatureServiceImpl(); + } + + @Test + public void testReadFeature() throws IOException { + BuilderFactory bf = features.getBuilderFactory(); + + URL res = getClass().getResource("/features/test-feature.json"); + + Feature f; + try (Reader r = new InputStreamReader(res.openStream())) { + f = features.readFeature(r); + + assertTrue(f.getName().isEmpty()); + assertEquals("The feature description", f.getDescription().get()); + assertFalse(f.getDocURL().isPresent()); + assertFalse(f.getLicense().isPresent()); + assertFalse(f.getSCM().isPresent()); + assertFalse(f.getVendor().isPresent()); + + List<FeatureBundle> bundles = f.getBundles(); + assertEquals(3, bundles.size()); + + FeatureBundle bundle = bf.newBundleBuilder(features.getID("org.osgi", "osgi.promise", "7.0.1")) + .addMetadata("hash", "4632463464363646436") + .addMetadata("start-order", 1L) + .build(); + + FeatureBundle ba = bundles.get(0); + ba.equals(bundle); + + assertTrue(bundles.contains(bundle)); + assertTrue(bundles.contains(bf.newBundleBuilder(features.getID("org.slf4j", "slf4j-api", "1.7.29")).build())); + assertTrue(bundles.contains(bf.newBundleBuilder(features.getID("org.slf4j", "slf4j-simple", "1.7.29")).build())); + + Map<String, FeatureConfiguration> configs = f.getConfigurations(); + assertEquals(2, configs.size()); + + FeatureConfiguration cfg1 = configs.get("my.pid"); + assertEquals("my.pid", cfg1.getPid()); + assertFalse(cfg1.getFactoryPid().isPresent()); + Map<String, Object> values1 = cfg1.getValues(); + assertEquals(3, values1.size()); + assertEquals(Long.valueOf(5), values1.get("foo")); + assertEquals("test", values1.get("bar")); + assertEquals(Integer.valueOf(7), values1.get("number")); + + FeatureConfiguration cfg2 = configs.get("my.factory.pid~name"); + assertEquals("my.factory.pid~name", cfg2.getPid()); + assertEquals("my.factory.pid", cfg2.getFactoryPid().get()); + Map<String, Object> values2 = cfg2.getValues(); + assertEquals(1, values2.size()); + assertArrayEquals(new String[] {"yeah", "yeah", "yeah"}, (String[]) values2.get("a.value")); + } + + testWriteFeature(f, res); + } + + @Test + public void testReadFeature2() throws Exception { + URL res = getClass().getResource("/features/test-feature2.json"); + try (Reader r = new InputStreamReader(res.openStream())) { + Feature f = features.readFeature(r); + + assertEquals("org.apache.sling:test-feature2:osgifeature:cls_abc:1.1", f.getID().toString()); + assertEquals("test-feature2", f.getName().get()); + assertEquals("The feature description", f.getDescription().get()); + assertEquals(List.of("foo", "bar"), f.getCategories()); + assertEquals("http://foo.bar.com/abc", f.getDocURL().get()); + assertEquals("Apache-2.0; link=\"http://opensource.org/licenses/apache2.0.php\"", f.getLicense().get()); + assertEquals("url=https://github.com/apache/sling-aggregator, connection=scm:git:https://github.com/apache/sling-aggregator.git, developerConnection=scm:git:g...@github.com:apache/sling-aggregator.git", + f.getSCM().get()); + assertEquals("The Apache Software Foundation", f.getVendor().get()); + } + } + + @Test + public void testWriteFeature() throws Exception { + BuilderFactory factory = features.getBuilderFactory(); + + String desc = "This is the main ACME app, from where all functionality can be reached."; + + FeatureBuilder builder = factory.newFeatureBuilder(features.getID("org.acme", "acmeapp", "1.0.0")); + builder.setName("The ACME app"); + builder.setDescription(desc); + + Feature f = builder.build(); + StringWriter sw = new StringWriter(); + features.writeFeature(f, sw); + + // Now check the generated JSON + JsonReader jr = Json.createReader(new StringReader(sw.toString())); + JsonObject fo = jr.readObject(); + assertEquals("org.acme:acmeapp:1.0.0", fo.getString("id")); + assertEquals("The ACME app", fo.getString("name")); + assertEquals(desc, fo.getString("description")); + assertFalse(fo.containsKey("docURL")); + assertFalse(fo.containsKey("license")); + assertFalse(fo.containsKey("scm")); + assertFalse(fo.containsKey("vendor")); + } + + @Test + public void testFeatureWithExtension() throws Exception { + URL res = getClass().getResource("/features/test-exfeat1.json"); + Feature f; + try (Reader r = new InputStreamReader(res.openStream())) { + f = features.readFeature(r); + + Map<String, FeatureExtension> extensions = f.getExtensions(); + assertEquals(3, extensions.size()); + + FeatureExtension textEx = extensions.get("my-text-ex"); + assertEquals(FeatureExtension.Kind.OPTIONAL, textEx.getKind()); + assertEquals(FeatureExtension.Type.TEXT, textEx.getType()); + assertEquals(List.of("ABC", "DEF"), textEx.getText()); + + FeatureExtension artEx = extensions.get("my-art-ex"); + assertEquals(FeatureExtension.Kind.MANDATORY, artEx.getKind()); + assertEquals(FeatureExtension.Type.ARTIFACTS, artEx.getType()); + List<FeatureArtifact> arts = artEx.getArtifacts(); + assertEquals(2, arts.size()); + + FeatureArtifact art1 = arts.get(0); + assertEquals("g:a:1", art1.getID().toString()); + assertEquals(1, art1.getMetadata().size()); + assertEquals(12345L, art1.getMetadata().get("my-md")); + + FeatureArtifact art2 = arts.get(1); + assertEquals("g:a:zip:foobar:2", art2.getID().toString()); + assertEquals(0, art2.getMetadata().size()); + + FeatureExtension jsonEx = extensions.get("my-json-ex"); + assertEquals(FeatureExtension.Kind.TRANSIENT, jsonEx.getKind()); + assertEquals(FeatureExtension.Type.JSON, jsonEx.getType()); + assertEquals("{\"foo\":[1,2,3]}", jsonEx.getJSON()); + } + + testWriteFeature(f, res); + } + + private void testWriteFeature(Feature feature, URL expectedURL) throws IOException { + StringWriter sw = new StringWriter(); + features.writeFeature(feature, sw); + + String expected = new String(expectedURL.openStream().readAllBytes()).replaceAll("\\s",""); + String actual = sw.toString().replaceAll("\\s",""); + assertEquals(expected, actual); + } + + @Test + public void testCreateFeatureBundle() { + BuilderFactory factory = features.getBuilderFactory(); + + FeatureBuilder builder = factory.newFeatureBuilder( + features.getID("org.acme", "acmeapp", "1.0.1")); + builder.setName("The Acme Application"); + builder.setLicense("https://opensource.org/licenses/Apache-2.0"); + builder.setComplete(true); + + FeatureBundle b1 = factory.newBundleBuilder( + features.getIDfromMavenCoordinates("org.osgi:org.osgi.util.function:1.1.0")) + .build(); + FeatureBundle b2 = factory.newBundleBuilder( + features.getIDfromMavenCoordinates("org.osgi:org.osgi.util.promise:1.1.1")) + .build(); + + FeatureBundle b3 = factory.newBundleBuilder( + features.getIDfromMavenCoordinates("org.apache.commons:commons-email:1.1.5")) + .addMetadata("org.acme.javadoc.link", + "https://commons.apache.org/proper/commons-email/javadocs/api-1.5") + .build(); + + FeatureBundle b4 = factory.newBundleBuilder( + features.getIDfromMavenCoordinates("com.acme:acmelib:1.7.2")) + .build(); + + builder.addBundles(b1, b2, b3, b4); + + Feature f = builder.build(); + System.out.println("***" + f); + } + + @Test + public void testCreateFeature() { + FeatureService fs = features; + BuilderFactory factory = fs.getBuilderFactory(); + + FeatureBuilder builder = factory.newFeatureBuilder(fs.getID("org.acme", "acmeapp", "1.0.0")); + builder.setName("The ACME app"); + builder.setDescription("This is the main ACME app, from where all functionality can be reached."); + Feature f = builder.build(); + + assertEquals(fs.getIDfromMavenCoordinates("org.acme:acmeapp:1.0.0"), f.getID()); + assertEquals("The ACME app", f.getName().get()); + assertEquals("This is the main ACME app, from where all functionality can be reached.", f.getDescription().get()); + } + + @Test + public void testCreateFeatureWithBundles() { + FeatureService fs = features; + BuilderFactory factory = fs.getBuilderFactory(); + FeatureBuilder builder = factory.newFeatureBuilder(fs.getID("org.acme", "acmeapp", "1.0.1")); + builder.setName("The Acme Application"); + builder.setLicense("https://opensource.org/licenses/Apache-2.0"); + builder.setComplete(true); + FeatureBundle b1 = factory + .newBundleBuilder(fs.getIDfromMavenCoordinates("org.osgi:org.osgi.util.function:1.1.0")).build(); + FeatureBundle b2 = factory + .newBundleBuilder(fs.getIDfromMavenCoordinates("org.osgi:org.osgi.util.promise:1.1.1")).build(); + FeatureBundle b3 = factory + .newBundleBuilder(fs.getIDfromMavenCoordinates("org.apache.commons:commons-email:1.1.5")) + .addMetadata("org.acme.javadoc.link", + "https://commons.apache.org/proper/commons-email/javadocs/api-1.5") + .build(); + FeatureBundle b4 = factory.newBundleBuilder(fs.getIDfromMavenCoordinates("com.acme:acmelib:1.7.2")).build(); + builder.addBundles(b1, b2, b3, b4); + Feature f = builder.build(); + + assertEquals("https://opensource.org/licenses/Apache-2.0", f.getLicense().get()); + assertTrue(f.isComplete()); + + assertEquals(4, f.getBundles().size()); + + FeatureBundle fb1 = f.getBundles().get(0); + assertEquals("org.osgi:org.osgi.util.function:1.1.0", fb1.getID().toString()); + assertEquals(0, fb1.getMetadata().size()); + + FeatureBundle fb2 = f.getBundles().get(1); + assertEquals("org.osgi:org.osgi.util.promise:1.1.1", fb2.getID().toString()); + assertEquals(0, fb2.getMetadata().size()); + + FeatureBundle fb3 = f.getBundles().get(2); + assertEquals("org.apache.commons:commons-email:1.1.5", fb3.getID().toString()); + assertEquals(1, fb3.getMetadata().size()); + assertEquals("https://commons.apache.org/proper/commons-email/javadocs/api-1.5", fb3.getMetadata().get("org.acme.javadoc.link")); + + FeatureBundle fb4 = f.getBundles().get(3); + assertEquals("com.acme:acmelib:1.7.2", fb4.getID().toString()); + assertEquals(0, fb4.getMetadata().size()); + } +} diff --git a/features/src/test/resources/features/test-exfeat1.json b/features/src/test/resources/features/test-exfeat1.json new file mode 100644 index 0000000..7112392 --- /dev/null +++ b/features/src/test/resources/features/test-exfeat1.json @@ -0,0 +1,26 @@ +{ + "id" : "org.apache.sling:test-extension-feature:1.0.0", + "extensions" : { + "my-text-ex": { + "kind": "optional", + "text": [ + "ABC", "DEF" + ] + }, + "my-art-ex": { + "kind": "mandatory", + "artifacts": [ + { + "id": "g:a:1", + "my-md": 12345 + }, { + "id": "g:a:zip:foobar:2" + } + ] + }, + "my-json-ex": { + "kind": "transient", + "json": {"foo": [1, 2, 3] } + } + } +} \ No newline at end of file diff --git a/features/src/test/resources/features/test-exfeat2.json b/features/src/test/resources/features/test-exfeat2.json new file mode 100644 index 0000000..5fa93e4 --- /dev/null +++ b/features/src/test/resources/features/test-exfeat2.json @@ -0,0 +1,9 @@ +{ + "id" : "org.apache.sling:test-extension-feature2:1.0.0", + "extensions" : { + "my-text-ex": + { + "text": "DEF" + } + } +} \ No newline at end of file diff --git a/features/src/test/resources/features/test-feature.json b/features/src/test/resources/features/test-feature.json new file mode 100644 index 0000000..323fb79 --- /dev/null +++ b/features/src/test/resources/features/test-feature.json @@ -0,0 +1,28 @@ +{ + "id" : "org.apache.sling:test-feature:1.1", + "description": "The feature description", + + "bundles" :[ + { + "id" : "org.osgi:osgi.promise:7.0.1", + "hash" : "4632463464363646436", + "start-order" : 1 + }, + { + "id" : "org.slf4j:slf4j-api:1.7.29" + }, + { + "id" : "org.slf4j:slf4j-simple:1.7.29" + } + ], + "configurations" : { + "my.pid" : { + "foo" : 5, + "bar" : "test", + "number:Integer" : 7 + }, + "my.factory.pid~name" : { + "a.value" : ["yeah", "yeah", "yeah"] + } + } +} \ No newline at end of file diff --git a/features/src/test/resources/features/test-feature2.json b/features/src/test/resources/features/test-feature2.json new file mode 100644 index 0000000..e12d2d8 --- /dev/null +++ b/features/src/test/resources/features/test-feature2.json @@ -0,0 +1,19 @@ +{ + /** This is a JSMin comment */ + "id" : "org.apache.sling:test-feature2:osgifeature:cls_abc:1.1", + "name": "test-feature2", + "description": "The feature description", + "categories": ["foo", "bar"], + "docURL": "http://foo.bar.com/abc", + "license": "Apache-2.0; link=\"http://opensource.org/licenses/apache2.0.php\"", + "scm": "url=https://github.com/apache/sling-aggregator, connection=scm:git:https://github.com/apache/sling-aggregator.git, developerConnection=scm:git:g...@github.com:apache/sling-aggregator.git", + "vendor": "The Apache Software Foundation", + + // complex entities below here + + "configurations" : { + "my.pid" : { + "bar" : "toast" + } + } +} \ No newline at end of file