You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by da...@apache.org on 2021/08/18 08:04:07 UTC
[felix-dev] branch master updated: FELIX-6444 Contribute a
compatible implementation of OSGi Features
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 <da...@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:git@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:git@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