├── .git-blame-ignore-revs ├── .sling-module.json ├── docs ├── diagrams │ ├── Develop.jpg │ └── RunningSystem.jpg ├── aggregation.md ├── feature-ref-files.md ├── feature-archives.md ├── extensions.md └── feature-model.json ├── src ├── test │ ├── resources │ │ ├── features │ │ │ ├── complete.json │ │ │ ├── repoinit.json │ │ │ ├── final.json │ │ │ ├── repoinit2.json │ │ │ ├── test2.json │ │ │ ├── test3.json │ │ │ ├── test4.json │ │ │ ├── artifacts-extension.json │ │ │ ├── internal-prop.json │ │ │ ├── test-metadata.json │ │ │ ├── test.json │ │ │ └── feature-model.json │ │ └── m2 │ │ │ └── org │ │ │ └── apache │ │ │ └── felix │ │ │ └── org.apache.felix.framework │ │ │ └── 7.0.1 │ │ │ └── org.apache.felix.framework-7.0.1.jar │ └── java │ │ └── org │ │ └── apache │ │ └── sling │ │ └── feature │ │ ├── ConfigurationsTest.java │ │ ├── BundlesTest.java │ │ ├── ExtensionTest.java │ │ ├── io │ │ ├── json │ │ │ ├── U.java │ │ │ ├── ArtifactsExtensions.java │ │ │ ├── FeatureJSONWriterTest.java │ │ │ └── FeatureJSONReaderTest.java │ │ ├── artifacts │ │ │ ├── ArtifactManagerConfigTest.java │ │ │ └── ArtifactManagerTest.java │ │ ├── archive │ │ │ └── ArchiveWriterTest.java │ │ └── IOUtilsTest.java │ │ ├── ArtifactTest.java │ │ ├── ExecutionEnvironmentExtensionTest.java │ │ ├── FeatureTest.java │ │ └── ConfigurationTest.java └── main │ ├── java │ └── org │ │ └── apache │ │ └── sling │ │ └── feature │ │ ├── package-info.java │ │ ├── io │ │ ├── package-info.java │ │ ├── json │ │ │ ├── package-info.java │ │ │ ├── ConfigurationJSONWriter.java │ │ │ ├── JSONConstants.java │ │ │ └── ConfigurationJSONReader.java │ │ ├── archive │ │ │ ├── package-info.java │ │ │ ├── ArchiveReader.java │ │ │ └── ArchiveWriter.java │ │ └── artifacts │ │ │ ├── package-info.java │ │ │ ├── spi │ │ │ ├── package-info.java │ │ │ ├── ArtifactProviderContext.java │ │ │ └── ArtifactProvider.java │ │ │ ├── ArtifactHandler.java │ │ │ └── ArtifactManagerConfig.java │ │ ├── osgi │ │ └── package-info.java │ │ ├── builder │ │ ├── package-info.java │ │ ├── ArtifactProvider.java │ │ ├── FeatureProvider.java │ │ ├── PostProcessHandler.java │ │ ├── HandlerContext.java │ │ └── MergeHandler.java │ │ ├── ExtensionType.java │ │ ├── ExtensionState.java │ │ ├── MatchingRequirement.java │ │ ├── Extensions.java │ │ ├── Configurations.java │ │ ├── Bundles.java │ │ ├── MapWithMetadata.java │ │ ├── Artifacts.java │ │ ├── ExecutionEnvironmentExtension.java │ │ ├── Prototype.java │ │ └── Extension.java │ └── resources │ └── META-INF │ └── feature │ └── Feature-1.0.0.schema.json ├── bnd.bnd ├── apicontroller.md ├── .gitignore ├── .asf.yaml ├── Jenkinsfile ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── readme.md └── pom.xml /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | a2d4005381a6d28d978fc552dd1e11a452b950be 2 | 3 | -------------------------------------------------------------------------------- /.sling-module.json: -------------------------------------------------------------------------------- 1 | { 2 | "jenkins": { 3 | "jdks": [17, 21] 4 | } 5 | } -------------------------------------------------------------------------------- /docs/diagrams/Develop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/sling-org-apache-sling-feature/HEAD/docs/diagrams/Develop.jpg -------------------------------------------------------------------------------- /docs/diagrams/RunningSystem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/sling-org-apache-sling-feature/HEAD/docs/diagrams/RunningSystem.jpg -------------------------------------------------------------------------------- /src/test/resources/features/complete.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling/test-feature-complete/1.0.0", 3 | "complete": true 4 | } -------------------------------------------------------------------------------- /bnd.bnd: -------------------------------------------------------------------------------- 1 | Import-Package: org.osgi.service.feature;resolution:=dynamic,* 2 | -noimportjava: true 3 | -conditionalpackage: org.apache.felix.utils.* 4 | -------------------------------------------------------------------------------- /src/test/resources/features/repoinit.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test/repoinit/1.0.0", 3 | "repoinit:TEXT|false": "some repo init\ntext" 4 | } 5 | -------------------------------------------------------------------------------- /src/test/resources/features/final.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling/final-feature/1.1", 3 | "description": "The feature description", 4 | "final" : true 5 | } -------------------------------------------------------------------------------- /src/test/resources/features/repoinit2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test/repoinit2/1.0.0", 3 | "repoinit:TEXT|false": [ 4 | "some repo init", 5 | "text" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/features/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling/test-feature2/1.0.0.0", 3 | "variables": { 4 | "foo": "bar", 5 | "zar": null 6 | } 7 | } -------------------------------------------------------------------------------- /src/test/resources/features/test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling/test-feature3/2", 3 | "bundles": [ 4 | "org.apache.sling/foo/1.2.3", 5 | "org.apache.sling/foo/4.5.6" 6 | ] 7 | } -------------------------------------------------------------------------------- /apicontroller.md: -------------------------------------------------------------------------------- 1 | # API Controller 2 | 3 | This documentation has moved to the [API Region Extension](https://github.com/apache/sling-org-apache-sling-feature-extension-apiregions/blob/master/docs/api-regions.md) 4 | -------------------------------------------------------------------------------- /src/test/resources/features/test4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling/test-feature4/1", 3 | "configurations": { 4 | "a.b.c": { 5 | "foo": 123, 6 | "bar": 456, 7 | "Foo": 789 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .classpath 4 | .metadata 5 | .project 6 | .settings 7 | .externalToolBuilders 8 | maven-eclipse.xml 9 | *.swp 10 | *.iml 11 | *.ipr 12 | *.iws 13 | *.bak 14 | .vlt 15 | .DS_Store 16 | jcr.log 17 | .vscode 18 | atlassian-ide-plugin.xml 19 | -------------------------------------------------------------------------------- /src/test/resources/m2/org/apache/felix/org.apache.felix.framework/7.0.1/org.apache.felix.framework-7.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/sling-org-apache-sling-feature/HEAD/src/test/resources/m2/org/apache/felix/org.apache.felix.framework/7.0.1/org.apache.felix.framework-7.0.1.jar -------------------------------------------------------------------------------- /src/test/resources/features/artifacts-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test/artifacts-extension/1.0.0", 3 | "my-extension1:ARTIFACTS|false":[ 4 | "org.apache.sling/my-extension1/1.2.3" 5 | ], 6 | "my-extension2:ARTIFACTS|true":[ 7 | "org.apache.sling/my-extension2/1.2.3" 8 | ] 9 | } -------------------------------------------------------------------------------- /src/test/resources/features/internal-prop.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling/test-feature/1.1", 3 | 4 | "configurations" : { 5 | "my.pid" : { 6 | "foo" : 5, 7 | ":configurator:feature-origins" : ["org.apache.sling/test-feature/1.1"], 8 | ":configurator:feature-number" : 7 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /.asf.yaml: -------------------------------------------------------------------------------- 1 | github: 2 | description: "Apache Sling OSGi Feature Model" 3 | homepage: "https://sling.apache.org/" 4 | labels: 5 | - "java" 6 | - "sling" 7 | - "osgi" 8 | - "osgi-feature-model" 9 | autolink_jira: 10 | - "SLING" 11 | - "OAK" 12 | - "JCR" 13 | - "JCRVLT" 14 | - "INFRA" 15 | - "FELIX" 16 | - "MNG" 17 | pull_requests: 18 | del_branch_on_merge: true 19 | protected_branches: 20 | master: {} 21 | notifications: 22 | jira_options: "link" 23 | pullrequests_bot_sonarcloud: "commits@sling.apache.org" 24 | -------------------------------------------------------------------------------- /src/test/resources/features/test-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling:test-feature:1.1", 3 | 4 | "variables" : { 5 | "bar" : "hello" 6 | }, 7 | "framework-properties" : { 8 | "foo" : "1" 9 | }, 10 | "feature-internal-data:JSON|false" : { 11 | "framework-properties-metadata" : { 12 | "foo" : { 13 | "bool" : true 14 | } 15 | }, 16 | "variables-metadata" : { 17 | "bar" : { 18 | "string" : "hello world" 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /docs/aggregation.md: -------------------------------------------------------------------------------- 1 | # Feature Aggregation 2 | 3 | When creating aggregates, extensions are merged into the resulting aggregate feature. There are 4 | default rules for aggregating extension content, which essentially is appending 5 | all the extension content of a given type. However custom merge plugins can also 6 | be provided. After the merge a postprocessor is always run which can perform 7 | additional operations based on the extension content. Note that both the 8 | aggregate task of the `slingfeature-maven-plugin` as well as the launcher perform 9 | merge operations on all the feature models these are provided with. 10 | 11 | TBD 12 | -------------------------------------------------------------------------------- /docs/feature-ref-files.md: -------------------------------------------------------------------------------- 1 | # Feature Reference Files 2 | 3 | Sometimes it is necessary to pass around a list of features. For this purpose feature *ref*erence files can be used. These files are simple text files where each non empty line which is not a comment, is treated as Maven coordinates. The coordinates can be specified as a Maven id or a Maven url. 4 | 5 | Ref files should use the extension *.ref*. 6 | 7 | ``` 8 | # This is a comment and below are three features 9 | org.apache.sling:org.apache.sling.core.feature:slingosgifeature:1.0.0 10 | org.apache.sling:org.apache.sling.core.servlets:slingosgifeature:1.3.0 11 | org.apache.sling:org.apache.sling.core.scripting:slingosgifeature:1.1.0 12 | ``` 13 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | slingOsgiBundleBuild() 21 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("2.0.0") 21 | package org.apache.sling.feature; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("2.0.0") 21 | package org.apache.sling.feature.io; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/osgi/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("1.0.1") 21 | package org.apache.sling.feature.osgi; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/builder/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("2.0.0") 21 | package org.apache.sling.feature.builder; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/json/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("2.0.0") 21 | package org.apache.sling.feature.io.json; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/archive/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("1.0.0") 21 | package org.apache.sling.feature.io.archive; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/artifacts/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("1.2.0") 21 | package org.apache.sling.feature.io.artifacts; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/artifacts/spi/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | @org.osgi.annotation.versioning.Version("1.0.0") 21 | package org.apache.sling.feature.io.artifacts.spi; 22 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/ExtensionType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | /** 22 | * Enumeration for {@link Extension} types. 23 | */ 24 | public enum ExtensionType { 25 | ARTIFACTS, 26 | TEXT, 27 | JSON 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/ExtensionState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | /** 22 | * Enumeration for {@link Extension} states. 23 | */ 24 | public enum ExtensionState { 25 | TRANSIENT, 26 | OPTIONAL, 27 | REQUIRED 28 | } 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 19 | Contributing 20 | ==== 21 | 22 | Thanks for choosing to contribute! 23 | 24 | You will find all the necessary details about how you can do this at https://sling.apache.org/contributing.html. 25 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/MatchingRequirement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import org.osgi.annotation.versioning.ConsumerType; 22 | import org.osgi.resource.Capability; 23 | import org.osgi.resource.Requirement; 24 | 25 | @ConsumerType 26 | public interface MatchingRequirement extends Requirement { 27 | 28 | boolean matches(Capability cap); 29 | } 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 19 | Apache Software Foundation Code of Conduct 20 | ==== 21 | 22 | Being an Apache project, Apache Sling adheres to the Apache Software Foundation's [Code of Conduct](https://www.apache.org/foundation/policies/conduct.html). 23 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/builder/ArtifactProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.builder; 20 | 21 | import java.net.URL; 22 | 23 | import org.apache.sling.feature.ArtifactId; 24 | import org.osgi.annotation.versioning.ConsumerType; 25 | 26 | /** 27 | * The artifact provider provides a URL for an artifact. 28 | */ 29 | @ConsumerType 30 | @FunctionalInterface 31 | public interface ArtifactProvider { 32 | 33 | /** 34 | * Provide the artifact with the given id. 35 | * 36 | * @param id The artifact id 37 | * @return The URL or {@code null} 38 | */ 39 | URL provide(ArtifactId id); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/builder/FeatureProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.builder; 20 | 21 | import org.apache.sling.feature.ArtifactId; 22 | import org.apache.sling.feature.Feature; 23 | import org.osgi.annotation.versioning.ConsumerType; 24 | 25 | /** 26 | * The feature provider is used to find features while assembling using {@link FeatureBuilder}. 27 | */ 28 | @ConsumerType 29 | @FunctionalInterface 30 | public interface FeatureProvider { 31 | 32 | /** 33 | * Provide the feature with the given id. 34 | * @param id The feature id 35 | * @return The feature or {@code null} 36 | */ 37 | Feature provide(ArtifactId id); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/Extensions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.ArrayList; 22 | 23 | /** 24 | * A container for extensions 25 | * 26 | * This class is not thread-safe. 27 | */ 28 | public class Extensions extends ArrayList { 29 | 30 | private static final long serialVersionUID = -3850006820840607498L; 31 | 32 | /** 33 | * Get an extension by name 34 | * @param name The name 35 | * @return The {@link Extension} or {@code null} 36 | */ 37 | public Extension getByName(final String name) { 38 | for (final Extension ext : this) { 39 | if (ext.getName().equals(name)) { 40 | return ext; 41 | } 42 | } 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/feature-archives.md: -------------------------------------------------------------------------------- 1 | # Feature Archives 2 | 3 | A feature file contains only references to artifacts like bundles. There are some use cases where it is useful to distribute a feature with all its artifacts, for example when distributing a feature to downstream users or when distributing a complete OSGi application. This can be done through feature archives. 4 | 5 | A feature archive is an uncompressed zip file which contains one or more features together with the artifacts. The zip file uses the jar file format and has a manifest as the first entry in the archive. The manifest must contain these headers: 6 | 7 | * 'Feature-Archive-Version' : The version of the format of the feature archive. Currently the value of this must be *1*. 8 | * 'Feature-Archive-Contents' : The features contained in this archive. This is a comma separated list containing Maven ids or Maven urls. 9 | 10 | All artifacts including the features are written to the archive in a maven repository like structure, that is the group id is converted to a path, the next path segment is the artifact id, followed by a path segment for the version. And the actual files are written into that directory with the typical maven convention. For example a feature with the id *org.apache.sling:org.apache.sling.core.feature:slingosgifeature:1.0.0* containing a bundle with the id *org.apache.sling:org.apache.sling.api:2.3.0* will result in the following archive: 11 | 12 | ``` 13 | / META-INF / MANIFEST.MF 14 | / org / apache / sling / org.apache.sling.core.feature / 1.0.0 / org.apache.sling.core.feature-1.0.0.slingosgifeature 15 | / org.apache.sling.api / 2.3.0 / org.apache.sling.api-2.3.0.jar 16 | ``` 17 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/artifacts/spi/ArtifactProviderContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.artifacts.spi; 20 | 21 | import java.io.File; 22 | 23 | import org.osgi.annotation.versioning.ProviderType; 24 | 25 | /** 26 | * This is the context for the artifact providers 27 | */ 28 | @ProviderType 29 | public interface ArtifactProviderContext { 30 | 31 | /** 32 | * Get the cache directory 33 | * @return The cache directory. 34 | */ 35 | File getCacheDirectory(); 36 | 37 | /** 38 | * Inform about an artifact found in the cache. 39 | */ 40 | void incCachedArtifacts(); 41 | 42 | /** 43 | * Inform about an artifact being downloaded 44 | */ 45 | void incDownloadedArtifacts(); 46 | 47 | /** 48 | * Inform about an artifact found locally. 49 | */ 50 | void incLocalArtifacts(); 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/ConfigurationsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.Collection; 22 | import java.util.HashSet; 23 | import java.util.Set; 24 | 25 | import org.junit.Test; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.assertTrue; 29 | 30 | public class ConfigurationsTest { 31 | 32 | @Test 33 | public void testGetFactoryConfigurations() { 34 | final Configurations cfgs = new Configurations(); 35 | 36 | cfgs.add(new Configuration("factory~a")); 37 | cfgs.add(new Configuration("factory~b")); 38 | cfgs.add(new Configuration("pid")); 39 | 40 | final Collection result = cfgs.getFactoryConfigurations("factory"); 41 | assertEquals(2, result.size()); 42 | final Set names = new HashSet<>(); 43 | for (final Configuration c : result) { 44 | names.add(c.getName()); 45 | } 46 | assertTrue(names.contains("a")); 47 | assertTrue(names.contains("b")); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/extensions.md: -------------------------------------------------------------------------------- 1 | # Available Feature Extensions 2 | 3 | The Feature Model is extensible, meaning that it can be augmented with custom content in a number of ways. Some extensions are supported out of the box. Other extensions are available through additional modules. 4 | 5 | ## Built-in extension: content-packages 6 | 7 | This extension of type `ARTIFACTS` allows listing content packages which will 8 | be installed by the launcher. Example: 9 | 10 | ``` 11 | "content-packages:ARTIFACTS|required":[ 12 | "org.apache.sling.myapp:my-content-package:zip:1.0.0" 13 | ] 14 | ``` 15 | 16 | ## Built-in extension: repoinit 17 | 18 | This extension is of type `TEXT`. It allows the specification of Sling Repository 19 | Initialization statements which will be executed on the repository at startup. 20 | Example: 21 | 22 | ``` 23 | "repoinit:TEXT|required":[ 24 | "create path /content/example.com(mixin mix:referenceable)", 25 | "create path (nt:unstructured) /var" 26 | ] 27 | ``` 28 | 29 | As initializing the repository is usually important for Sling based applications 30 | the extension should be marked as required as in the example above. 31 | 32 | ## Built-in extension: execution-environment 33 | 34 | This extension is of type `JSON` and allows to specify the execution environment for a feature. 35 | Right now it only supports to set the artifact for the framework to launch the feature. The framework can either be specified as a string or a JSON object with an `id` property: 36 | 37 | The execution environment - if provided - is used by tooling like the feature launcher or the feature analysers. 38 | 39 | ``` 40 | "execution-environment:JSON|optional" : { 41 | "framework" : { 42 | "id" : "org.apache.felix:org.apache.felix.framework:6.0.3" 43 | } 44 | } 45 | ``` 46 | 47 | 48 | ## Further extensions 49 | 50 | * [API Controller and API Regions](https://github.com/apache/sling-org-apache-sling-feature-extension-apiregions/blob/master/docs/api-regions.md) 51 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/builder/PostProcessHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.builder; 20 | 21 | import org.apache.sling.feature.Extension; 22 | import org.apache.sling.feature.Feature; 23 | import org.osgi.annotation.versioning.ConsumerType; 24 | 25 | /** 26 | * A Post Process Handler processes features after a merge operation. The 27 | * handlers are passed in to the {@link FeatureBuilder} via 28 | * {@link BuilderContext#addPostProcessExtensions(PostProcessHandler...)}. Once 29 | * all extensions are merged, all post processor handlers are called for each 30 | * extension in the target feature. 31 | */ 32 | @ConsumerType 33 | public interface PostProcessHandler { 34 | /** 35 | * Post process the feature with respect to the extension. 36 | * Post processing is invoked after all extensions have been merged. 37 | * 38 | * @param context Context for the handler 39 | * @param feature The feature 40 | * @param extension The extension 41 | * @throws IllegalStateException If post processing failed 42 | */ 43 | void postProcess(HandlerContext context, Feature feature, Extension extension); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/builder/HandlerContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.builder; 20 | 21 | import java.util.Map; 22 | 23 | import org.osgi.annotation.versioning.ProviderType; 24 | 25 | /** 26 | * Context for an extension handler. 27 | */ 28 | @ProviderType 29 | public interface HandlerContext { 30 | 31 | /** 32 | * Provide the optional artifact provider. 33 | * @return The artifact provider or {@code null} 34 | */ 35 | ArtifactProvider getArtifactProvider(); 36 | 37 | /** 38 | * Configuration for the handler 39 | * @return Map of provided configuration, or an empty map if there is no configuration. 40 | * Never {@code null}. 41 | */ 42 | Map getConfiguration(); 43 | 44 | /** 45 | * Is this merging a prototype into the defining feature? 46 | * @return {@code true} if it is prototype processing 47 | * @since 1.3.0 48 | */ 49 | boolean isPrototypeMerge(); 50 | 51 | /** 52 | * Is this the first feature being merged in? 53 | * @return {@code true} if it is the first feature 54 | * @since 1.3.0 55 | */ 56 | boolean isInitialMerge(); 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/artifacts/spi/ArtifactProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.artifacts.spi; 20 | 21 | import java.io.IOException; 22 | import java.net.URL; 23 | 24 | import org.osgi.annotation.versioning.ConsumerType; 25 | 26 | /** 27 | * The artifact provider is an extension point for providing artifacts 28 | * from different sources, like for example s3. 29 | */ 30 | @ConsumerType 31 | public interface ArtifactProvider { 32 | 33 | /** 34 | * The protocol name of the provider, e.g. "s3" 35 | * @return The protocol name. 36 | */ 37 | String getProtocol(); 38 | 39 | /** 40 | * Initialize the provider. 41 | * @param context The context 42 | * @throws IOException If the provider can't be initialized. 43 | */ 44 | void init(ArtifactProviderContext context) throws IOException; 45 | 46 | /** 47 | * Shutdown the provider. 48 | */ 49 | void shutdown(); 50 | 51 | /** 52 | * Get a local file for the artifact URL. 53 | * 54 | * @param url Artifact url 55 | * @param relativeCachePath A relative path that can be used as a cache path 56 | * by the provider. The path does not start with a slash. 57 | * @return A local url if the artifact exists or {@code null} 58 | */ 59 | URL getArtifact(String url, String relativeCachePath); 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/BundlesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | import org.junit.Test; 25 | 26 | import static org.junit.Assert.assertEquals; 27 | 28 | public class BundlesTest { 29 | 30 | @Test 31 | public void testIterator() { 32 | final Bundles bundles = new Bundles(); 33 | bundles.add(createBundle("1/a/1", 1)); 34 | bundles.add(createBundle("5/a/5", 5)); 35 | bundles.add(createBundle("5/b/6", 5)); 36 | bundles.add(createBundle("2/b/2", 2)); 37 | bundles.add(createBundle("2/a/3", 2)); 38 | bundles.add(createBundle("4/x/4", 4)); 39 | 40 | int index = 1; 41 | for (final Map.Entry> entry : 42 | bundles.getBundlesByStartOrder().entrySet()) { 43 | for (final Artifact a : entry.getValue()) { 44 | assertEquals(entry.getKey().toString(), a.getId().getGroupId()); 45 | assertEquals(index, a.getId().getOSGiVersion().getMajor()); 46 | index++; 47 | } 48 | } 49 | assertEquals(7, index); 50 | } 51 | 52 | public static Artifact createBundle(final String id, final int startOrder) { 53 | final Artifact a = new Artifact(ArtifactId.parse(id)); 54 | a.getMetadata().put(Artifact.KEY_START_ORDER, String.valueOf(startOrder)); 55 | 56 | return a; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/Configurations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.List; 24 | 25 | /** 26 | * A container for configurations. 27 | * 28 | * This class is not thread-safe. 29 | */ 30 | public class Configurations extends ArrayList { 31 | 32 | private static final long serialVersionUID = -7243822886707856704L; 33 | 34 | /** 35 | * Get the configuration 36 | * @param pid The pid of the configuration 37 | * @return The configuration or {@code null} 38 | */ 39 | public Configuration getConfiguration(final String pid) { 40 | for (final Configuration cfg : this) { 41 | if (pid.equals(cfg.getPid())) { 42 | return cfg; 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | /** 49 | * Get all factory configurations matching the factory pid. 50 | * @param factoryPid The factory pid of the configurations 51 | * @return The configurations - the collection might be empty 52 | * @since 1.5 53 | */ 54 | public Collection getFactoryConfigurations(final String factoryPid) { 55 | final List result = new ArrayList<>(); 56 | for (final Configuration cfg : this) { 57 | if (factoryPid.equals(cfg.getFactoryPid())) { 58 | result.add(cfg); 59 | } 60 | } 61 | return result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/ExtensionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import org.junit.Test; 22 | 23 | import static org.junit.Assert.assertEquals; 24 | 25 | public class ExtensionTest { 26 | @Test 27 | public void testCopyTextExtension() { 28 | Extension ex = new Extension(ExtensionType.TEXT, "t1", ExtensionState.OPTIONAL); 29 | ex.setText("foo"); 30 | Extension ex2 = ex.copy(); 31 | 32 | assertEquals(ex.getType(), ex2.getType()); 33 | assertEquals("foo", ex2.getText()); 34 | } 35 | 36 | @Test 37 | public void testCopyJSONExtension() { 38 | Extension ex = new Extension(ExtensionType.JSON, "t1", ExtensionState.OPTIONAL); 39 | ex.setJSON("[123]"); 40 | Extension ex2 = ex.copy(); 41 | 42 | assertEquals(ex.getType(), ex2.getType()); 43 | assertEquals("[123]", ex2.getJSON()); 44 | } 45 | 46 | @Test 47 | public void testCopyArtifactsExtension() { 48 | Extension ex = new Extension(ExtensionType.ARTIFACTS, "t1", ExtensionState.OPTIONAL); 49 | Artifact art = new Artifact(ArtifactId.fromMvnId("g:a:123")); 50 | art.getMetadata().put("test", "blah"); 51 | ex.getArtifacts().add(art); 52 | Extension ex2 = ex.copy(); 53 | 54 | assertEquals(ex.getType(), ex2.getType()); 55 | assertEquals(1, ex2.getArtifacts().size()); 56 | Artifact art2 = ex2.getArtifacts().iterator().next(); 57 | assertEquals("g:a:123", art2.getId().toMvnId()); 58 | assertEquals(1, art2.getMetadata().size()); 59 | assertEquals("blah", art2.getMetadata().get("test")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/artifacts/ArtifactHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.artifacts; 20 | 21 | import java.io.File; 22 | import java.net.MalformedURLException; 23 | import java.net.URL; 24 | import java.nio.file.Path; 25 | 26 | /** 27 | * A handler provides a file object for an artifact. 28 | */ 29 | public class ArtifactHandler { 30 | 31 | private final String url; 32 | 33 | private final URL localURL; 34 | 35 | /** 36 | * Create a new handler. 37 | * 38 | * @param url The url of the artifact 39 | * @param localURL The local URL for the artifact 40 | */ 41 | public ArtifactHandler(final String url, final URL localURL) { 42 | this.url = url; 43 | this.localURL = localURL; 44 | } 45 | 46 | /** 47 | * Create a new handler. 48 | * 49 | * @param file The file for the artifact 50 | * @throws MalformedURLException If the file name cannot be converted to a URL. 51 | * @since 1.1.0 52 | */ 53 | public ArtifactHandler(final File file) throws MalformedURLException { 54 | this(file.toURI().toString(), file.toURI().toURL()); 55 | } 56 | 57 | /** 58 | * Create a new handler. 59 | * 60 | * @param file The file for the artifact 61 | * @throws MalformedURLException If the file name cannot be converted to a URL. 62 | * @since 1.2.0 63 | */ 64 | public ArtifactHandler(final Path file) throws MalformedURLException { 65 | this(file.toUri().toString(), file.toUri().toURL()); 66 | } 67 | 68 | /** 69 | * Get the url of the artifact 70 | * 71 | * @return The url. 72 | */ 73 | public String getUrl() { 74 | return url; 75 | } 76 | 77 | /** 78 | * Get a local url for the artifact 79 | * 80 | * @return The file 81 | */ 82 | public URL getLocalURL() { 83 | return localURL; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/builder/MergeHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.builder; 20 | 21 | import org.apache.sling.feature.Extension; 22 | import org.apache.sling.feature.Feature; 23 | import org.osgi.annotation.versioning.ConsumerType; 24 | 25 | /** 26 | * A merge handler can merge an extension of a particular type. The handlers are 27 | * passed in to the {@link FeatureBuilder} via 28 | * {@link BuilderContext#addMergeExtensions(MergeHandler...)}. When the feature 29 | * builder is merging features, the first handler that returns {@code true} for 30 | * an extension in {@link #canMerge(Extension)} merges the extension. Further 31 | * handlers are not tested anymore. 32 | */ 33 | @ConsumerType 34 | public interface MergeHandler { 35 | /** 36 | * Checks whether this merger can merge the given extension. 37 | * 38 | * @param extension The extension 39 | * @return {@code true} if merger can handle this 40 | */ 41 | boolean canMerge(Extension extension); 42 | 43 | /** 44 | * Merge the source extension into the target extension. 45 | * 46 | * Only called if {@link #canMerge(Extension)} for the extension returned 47 | * {@code true}. If the target does not yet contain this extension, then the 48 | * targetEx argument is {@code null}. In that case the handler should add the 49 | * extension to the target. 50 | * 51 | * @param context Context for the handler 52 | * @param target The target feature 53 | * @param source The source feature 54 | * @param targetEx The target extension or {@code null} if the extension does 55 | * not exist in the target. 56 | * @param sourceEx The source extension 57 | * @throws IllegalStateException If the extensions can't be merged 58 | */ 59 | void merge(HandlerContext context, Feature target, Feature source, Extension targetEx, Extension sourceEx); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/Bundles.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Collections; 23 | import java.util.Comparator; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.TreeMap; 27 | 28 | /** 29 | * Groups a list of bundles {@code Artifact} and provides a means to sort them 30 | * based on start order. 31 | * This class is not thread-safe. 32 | */ 33 | public class Bundles extends Artifacts { 34 | 35 | private static final long serialVersionUID = 5134067638024826299L; 36 | 37 | /** 38 | * Get the map of all bundles sorted by start order. The map is sorted 39 | * and iterating over the keys is done in start order. Bundles without a start 40 | * order (having the value 0) are returned last. 41 | * @return The map of bundles. The map is unmodifiable. 42 | */ 43 | public Map> getBundlesByStartOrder() { 44 | final Map> startOrderMap = new TreeMap<>(new Comparator() { 45 | 46 | @Override 47 | public int compare(final Integer o1, final Integer o2) { 48 | if (o1.compareTo(o2) == 0) { 49 | return 0; 50 | } 51 | if (o1 == 0) { 52 | return 1; 53 | } 54 | if (o2 == 0) { 55 | return -1; 56 | } 57 | return o1 - o2; 58 | } 59 | }); 60 | 61 | for (final Artifact bundle : this) { 62 | final int startOrder = bundle.getStartOrder(); 63 | List list = startOrderMap.get(startOrder); 64 | if (list == null) { 65 | list = new ArrayList<>(); 66 | startOrderMap.put(startOrder, list); 67 | } 68 | list.add(bundle); 69 | } 70 | return Collections.unmodifiableMap(startOrderMap); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/json/U.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.json; 20 | 21 | import java.io.InputStreamReader; 22 | import java.io.Reader; 23 | import java.util.List; 24 | 25 | import org.apache.sling.feature.Configuration; 26 | import org.apache.sling.feature.Feature; 27 | import org.osgi.resource.Capability; 28 | import org.osgi.resource.Requirement; 29 | 30 | import static org.junit.Assert.fail; 31 | 32 | /** Test utilities */ 33 | public class U { 34 | 35 | /** Read the feature from the provided resource 36 | */ 37 | public static Feature readFeature(final String name) throws Exception { 38 | try (final Reader reader = 39 | new InputStreamReader(U.class.getResourceAsStream("/features/" + name + ".json"), "UTF-8")) { 40 | return FeatureJSONReader.read(reader, name); 41 | } 42 | } 43 | 44 | public static Configuration findConfiguration(final List cfgs, final String pid) { 45 | for (final Configuration c : cfgs) { 46 | if (pid.equals(c.getPid())) { 47 | return c; 48 | } 49 | } 50 | fail("Configuration not found " + pid); 51 | return null; 52 | } 53 | 54 | public static Capability findCapability(List capabilities, final String namespace) { 55 | for (Capability capability : capabilities) { 56 | if (capability.getNamespace().equals(namespace)) { 57 | return capability; 58 | } 59 | } 60 | 61 | fail(String.format("No Capability with namespace '%s' found", namespace)); 62 | return null; 63 | } 64 | 65 | public static Requirement findRequirement(List requirements, final String namespace) { 66 | for (Requirement requirement : requirements) { 67 | if (requirement.getNamespace().equals(namespace)) { 68 | return requirement; 69 | } 70 | } 71 | 72 | fail(String.format("No Requirement with namespace '%s' found", namespace)); 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/ArtifactTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import org.junit.Test; 22 | 23 | import static org.junit.Assert.assertEquals; 24 | import static org.junit.Assert.assertNotNull; 25 | import static org.junit.Assert.assertNull; 26 | 27 | public class ArtifactTest { 28 | 29 | @Test 30 | public void testFeatureOrigins() { 31 | final ArtifactId self = ArtifactId.parse("self:self:1"); 32 | 33 | final Artifact art = new Artifact(ArtifactId.parse("art:art:1")); 34 | assertEquals(0, art.getFeatureOrigins().length); 35 | assertNull(art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS)); 36 | assertEquals(1, art.getFeatureOrigins(self).length); 37 | assertEquals(self, art.getFeatureOrigins(self)[0]); 38 | 39 | // single id 40 | final ArtifactId id = ArtifactId.parse("g:a:1"); 41 | art.setFeatureOrigins(id); 42 | assertEquals(1, art.getFeatureOrigins().length); 43 | assertEquals(id, art.getFeatureOrigins()[0]); 44 | assertEquals(1, art.getFeatureOrigins(self).length); 45 | assertEquals(id, art.getFeatureOrigins(self)[0]); 46 | 47 | assertNotNull(art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS)); 48 | assertEquals(id.toMvnId(), art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS)); 49 | 50 | // add another id 51 | final ArtifactId id2 = ArtifactId.parse("g:b:2"); 52 | art.setFeatureOrigins(id, id2); 53 | assertEquals(2, art.getFeatureOrigins().length); 54 | assertEquals(id, art.getFeatureOrigins()[0]); 55 | assertEquals(id2, art.getFeatureOrigins()[1]); 56 | assertEquals(2, art.getFeatureOrigins(self).length); 57 | assertEquals(id, art.getFeatureOrigins(self)[0]); 58 | assertEquals(id2, art.getFeatureOrigins(self)[1]); 59 | 60 | assertNotNull(art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS)); 61 | assertEquals( 62 | id.toMvnId().concat(",").concat(id2.toMvnId()), 63 | art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/resources/features/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "org.apache.sling/test-feature/1.1", 3 | "description": "The feature description", 4 | 5 | "prototype" : 6 | { 7 | "id" : "org.apache.sling/sling/9", 8 | "removals" : { 9 | "configurations" : [ 10 | ], 11 | "bundles" : [ 12 | ], 13 | "framework-properties" : [ 14 | ] 15 | } 16 | } 17 | , 18 | "requirements" : [ 19 | { 20 | "namespace" : "osgi.contract", 21 | "directives" : { 22 | "filter" : "(&(osgi.contract=JavaServlet)(&(version>=3.0)(!(version>=4.0))))" 23 | } 24 | } 25 | ], 26 | "capabilities" : [ 27 | { 28 | "namespace" : "osgi.implementation", 29 | "attributes" : { 30 | "osgi.implementation" : "osgi.http", 31 | "version:Version" : "1.1" 32 | }, 33 | "directives" : { 34 | "uses" : "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard" 35 | } 36 | }, 37 | { 38 | "namespace" : "osgi.service", 39 | "attributes" : { 40 | "objectClass:List" : "org.osgi.service.http.runtime.HttpServiceRuntime" 41 | }, 42 | "directives" : { 43 | "uses" : "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto" 44 | } 45 | }, 46 | { 47 | "namespace" : "osgi.contract", 48 | "attributes" : { 49 | "osgi.contract" : "JavaServlet", 50 | "osgi.implementation" : "osgi.http", 51 | "version:Version" : "3.1" 52 | }, 53 | "directives" : { 54 | "uses" : "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto" 55 | } 56 | } 57 | ], 58 | "framework-properties" : { 59 | "foo" : 1, 60 | "brave" : "something", 61 | "org.apache.felix.scr.directory" : "launchpad/scr" 62 | }, 63 | "bundles" :[ 64 | { 65 | "id" : "org.apache.sling/oak-server/1.0.0", 66 | "hash" : "4632463464363646436", 67 | "start-order" : 1 68 | }, 69 | { 70 | "id" : "org.apache.sling/application-bundle/2.0.0", 71 | "start-order" : 1 72 | }, 73 | { 74 | "id" : "org.apache.sling/another-bundle/2.1.0", 75 | "start-order" : 1 76 | }, 77 | { 78 | "id" : "org.apache.sling/foo-xyz/1.2.3", 79 | "start-order" : 2 80 | } 81 | ], 82 | "configurations" : { 83 | "my.pid" : { 84 | "foo" : 5, 85 | "bar" : "test", 86 | "number:Integer" : 7 87 | }, 88 | "my.factory.pid~name" : { 89 | "a.value" : "yeah" 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.json; 20 | 21 | import java.io.IOException; 22 | import java.io.Writer; 23 | import java.util.Collections; 24 | import java.util.Hashtable; 25 | 26 | import org.apache.felix.cm.json.io.ConfigurationResource; 27 | import org.apache.felix.cm.json.io.ConfigurationWriter; 28 | import org.apache.sling.feature.Configuration; 29 | import org.apache.sling.feature.Configurations; 30 | 31 | /** JSON writer for configurations */ 32 | public class ConfigurationJSONWriter { 33 | 34 | /** Writes the configurations to the writer. The writer is not closed. 35 | * 36 | * @param writer Writer 37 | * @param configs List of configurations 38 | * @throws IOException If writing fails */ 39 | public static void write(final Writer writer, final Configurations configs) throws IOException { 40 | final ConfigurationJSONWriter w = new ConfigurationJSONWriter(); 41 | w.writeConfigurations(writer, configs); 42 | } 43 | 44 | private void writeConfigurations(final Writer writer, final Configurations configs) throws IOException { 45 | 46 | final ConfigurationWriter cfgWriter = 47 | org.apache.felix.cm.json.io.Configurations.buildWriter().build(writer); 48 | 49 | final ConfigurationResource rsrc = new ConfigurationResource(); 50 | for (final Configuration cfg : configs) { 51 | final Hashtable properties; 52 | if (cfg.getProperties() instanceof Hashtable 53 | && cfg.getProperties().get(Configuration.PROP_ARTIFACT_ID) == null) { 54 | properties = (Hashtable) cfg.getProperties(); 55 | } else { 56 | properties = org.apache.felix.cm.json.io.Configurations.newConfiguration(); 57 | for (final String name : Collections.list(cfg.getProperties().keys())) { 58 | if (!Configuration.PROP_ARTIFACT_ID.equals(name)) { 59 | properties.put(name, cfg.getProperties().get(name)); 60 | } 61 | } 62 | } 63 | rsrc.getConfigurations().put(cfg.getPid(), properties); 64 | } 65 | cfgWriter.writeConfigurationResource(rsrc); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/json/ArtifactsExtensions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.json; 20 | 21 | import org.apache.sling.feature.ArtifactId; 22 | import org.apache.sling.feature.Extension; 23 | import org.apache.sling.feature.ExtensionState; 24 | import org.apache.sling.feature.Extensions; 25 | import org.apache.sling.feature.Feature; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.assertNotNull; 29 | 30 | /* 31 | * Licensed to the Apache Software Foundation (ASF) under one or more 32 | * contributor license agreements. See the NOTICE file distributed with 33 | * this work for additional information regarding copyright ownership. 34 | * The ASF licenses this file to You under the Apache License, Version 2.0 35 | * (the "License"); you may not use this file except in compliance with 36 | * the License. You may obtain a copy of the License at 37 | * 38 | * http://www.apache.org/licenses/LICENSE-2.0 39 | * 40 | * Unless required by applicable law or agreed to in writing, software 41 | * distributed under the License is distributed on an "AS IS" BASIS, 42 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 43 | * See the License for the specific language governing permissions and 44 | * limitations under the License. 45 | */ 46 | public class ArtifactsExtensions { 47 | 48 | public static void testReadArtifactsExtensions(Feature feature) { 49 | Extensions extensions = feature.getExtensions(); 50 | 51 | assertEquals(2, extensions.size()); 52 | 53 | Extension extension1 = extensions.getByName("my-extension1"); 54 | assertNotNull(extension1); 55 | assertEquals(extension1.getName(), "my-extension1"); 56 | assertEquals(extension1.getState(), ExtensionState.OPTIONAL); 57 | assertEquals(1, extension1.getArtifacts().size()); 58 | 59 | ArtifactId artifactId1 = extension1.getArtifacts().get(0).getId(); 60 | assertEquals(artifactId1.getGroupId(), "org.apache.sling"); 61 | assertEquals(artifactId1.getArtifactId(), "my-extension1"); 62 | assertEquals(artifactId1.getVersion(), "1.2.3"); 63 | 64 | Extension extension2 = extensions.getByName("my-extension2"); 65 | assertNotNull(extension2); 66 | assertEquals(extension2.getName(), "my-extension2"); 67 | assertEquals(extension2.getState(), ExtensionState.REQUIRED); 68 | assertEquals(1, extension2.getArtifacts().size()); 69 | 70 | ArtifactId artifactId2 = extension2.getArtifacts().get(0).getId(); 71 | assertEquals(artifactId2.getGroupId(), "org.apache.sling"); 72 | assertEquals(artifactId2.getArtifactId(), "my-extension2"); 73 | assertEquals(artifactId2.getVersion(), "1.2.3"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/artifacts/ArtifactManagerConfigTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.artifacts; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.nio.charset.StandardCharsets; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | 27 | import org.apache.commons.io.FileUtils; 28 | import org.junit.After; 29 | import org.junit.Before; 30 | import org.junit.Test; 31 | 32 | import static org.apache.sling.feature.io.artifacts.ArtifactManagerConfig.toFileUrl; 33 | import static org.junit.Assert.assertArrayEquals; 34 | 35 | public class ArtifactManagerConfigTest { 36 | 37 | private String userHomePreviousValue; 38 | private Path tempUserHome; 39 | 40 | @Before 41 | public void setUp() throws IOException { 42 | userHomePreviousValue = System.getProperty("user.home"); 43 | 44 | // set user.home to temp directory 45 | tempUserHome = Files.createTempDirectory("ArtifactManagerConfigTest").toAbsolutePath(); 46 | System.setProperty("user.home", tempUserHome.toString()); 47 | } 48 | 49 | @After 50 | public void tearDown() { 51 | // restore previous user home value 52 | System.setProperty("user.home", userHomePreviousValue); 53 | } 54 | 55 | @Test 56 | public void testDefaultRepositoryUrlsWithoutMavenSettings() { 57 | ArtifactManagerConfig underTest = new ArtifactManagerConfig(); 58 | assertArrayEquals( 59 | new String[] { 60 | toFileUrl(tempUserHome + "/.m2/repository"), 61 | "https://repo.maven.apache.org/maven2", 62 | "https://repository.apache.org/content/groups/snapshots", 63 | }, 64 | underTest.getRepositoryUrls()); 65 | } 66 | 67 | @Test 68 | public void testDefaultRepositoryUrlsWithMavenSettings() throws IOException { 69 | FileUtils.write( 70 | new File(tempUserHome.toString() + "/.m2/settings.xml"), 71 | "\n" 72 | + "\n" 74 | + " " + tempUserHome + "/other-repo-path\n" 75 | + "", 76 | StandardCharsets.UTF_8); 77 | 78 | ArtifactManagerConfig underTest = new ArtifactManagerConfig(); 79 | assertArrayEquals( 80 | new String[] { 81 | toFileUrl(tempUserHome + "/other-repo-path"), 82 | "https://repo.maven.apache.org/maven2", 83 | "https://repository.apache.org/content/groups/snapshots", 84 | }, 85 | underTest.getRepositoryUrls()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/json/JSONConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.json; 20 | 21 | import java.util.Arrays; 22 | import java.util.List; 23 | 24 | import org.apache.sling.feature.Configuration; 25 | 26 | public abstract class JSONConstants { 27 | 28 | static final String FEATURE_ID = "id"; 29 | 30 | static final String FEATURE_VARIABLES = "variables"; 31 | 32 | static final String FEATURE_BUNDLES = "bundles"; 33 | 34 | static final String FEATURE_FRAMEWORK_PROPERTIES = "framework-properties"; 35 | 36 | static final String FEATURE_CONFIGURATIONS = "configurations"; 37 | 38 | static final String FEATURE_PROTOTYPE = "prototype"; 39 | 40 | static final String FEATURE_REQUIREMENTS = "requirements"; 41 | 42 | static final String FEATURE_CAPABILITIES = "capabilities"; 43 | 44 | static final String FEATURE_TITLE = "title"; 45 | 46 | static final String FEATURE_DESCRIPTION = "description"; 47 | 48 | static final String FEATURE_VENDOR = "vendor"; 49 | 50 | static final String FEATURE_LICENSE = "license"; 51 | 52 | static final String FEATURE_FINAL = "final"; 53 | 54 | static final String FEATURE_COMPLETE = "complete"; 55 | 56 | static final String FEATURE_MODEL_VERSION = "model-version"; 57 | 58 | static final String FEATURE_CATEGORIES = "categories"; 59 | 60 | static final String FEATURE_DOC_URL = "doc-url"; 61 | 62 | static final String FEATURE_SCM_INFO = "scm-info"; 63 | 64 | static final List FEATURE_KNOWN_PROPERTIES = Arrays.asList( 65 | FEATURE_ID, 66 | FEATURE_MODEL_VERSION, 67 | FEATURE_VARIABLES, 68 | FEATURE_BUNDLES, 69 | FEATURE_FRAMEWORK_PROPERTIES, 70 | FEATURE_CONFIGURATIONS, 71 | FEATURE_PROTOTYPE, 72 | FEATURE_REQUIREMENTS, 73 | FEATURE_CAPABILITIES, 74 | FEATURE_TITLE, 75 | FEATURE_DESCRIPTION, 76 | FEATURE_VENDOR, 77 | FEATURE_FINAL, 78 | FEATURE_COMPLETE, 79 | FEATURE_LICENSE, 80 | FEATURE_CATEGORIES, 81 | FEATURE_DOC_URL, 82 | FEATURE_SCM_INFO); 83 | 84 | static final String ARTIFACT_ID = "id"; 85 | 86 | static final List ARTIFACT_KNOWN_PROPERTIES = 87 | Arrays.asList(ARTIFACT_ID, Configuration.PROP_ARTIFACT_ID, FEATURE_CONFIGURATIONS); 88 | 89 | static final String PROTOTYPE_REMOVALS = "removals"; 90 | 91 | static final String PROTOTYPE_EXTENSION_REMOVALS = "extensions"; 92 | 93 | static final String REQCAP_NAMESPACE = "namespace"; 94 | static final String REQCAP_ATTRIBUTES = "attributes"; 95 | static final String REQCAP_DIRECTIVES = "directives"; 96 | 97 | static final String FRAMEWORK_PROPERTIES_METADATA = "framework-properties-metadata"; 98 | 99 | static final String VARIABLES_METADATA = "variables-metadata"; 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.json; 20 | 21 | import java.io.IOException; 22 | import java.io.Reader; 23 | import java.util.Hashtable; 24 | import java.util.Map; 25 | 26 | import org.apache.felix.cm.json.io.ConfigurationReader; 27 | import org.apache.felix.cm.json.io.ConfigurationReader.ConfiguratorPropertyHandler; 28 | import org.apache.felix.cm.json.io.ConfigurationResource; 29 | import org.apache.sling.feature.Configuration; 30 | import org.apache.sling.feature.Configurations; 31 | 32 | /** 33 | * JSON Reader for configurations. 34 | */ 35 | public class ConfigurationJSONReader { 36 | 37 | /** 38 | * Read a map of configurations from the reader 39 | * The reader is not closed. It is up to the caller to close the reader. 40 | * 41 | * @param reader The reader for the configuration 42 | * @param location Optional location 43 | * @return The read configurations 44 | * @throws IOException If an IO errors occurs or the JSON is invalid. 45 | */ 46 | public static Configurations read(final Reader reader, final String location) throws IOException { 47 | try { 48 | final ConfigurationJSONReader mr = new ConfigurationJSONReader(); 49 | return mr.readConfigurations(location, reader); 50 | } catch (final IllegalStateException | IllegalArgumentException e) { 51 | throw new IOException(e); 52 | } 53 | } 54 | 55 | Configurations readConfigurations(final String location, final Reader reader) throws IOException { 56 | final Configurations result = new Configurations(); 57 | 58 | final ConfigurationReader cfgReader = org.apache.felix.cm.json.io.Configurations.buildReader() 59 | .withIdentifier(location) 60 | .verifyAsBundleResource(true) 61 | .withConfiguratorPropertyHandler(new ConfiguratorPropertyHandler() { 62 | 63 | @Override 64 | public void handleConfiguratorProperty( 65 | final String pid, final String property, final Object value) { 66 | Configuration cfg = result.getConfiguration(pid); 67 | if (cfg == null) { 68 | cfg = new Configuration(pid); 69 | result.add(cfg); 70 | } 71 | cfg.getProperties().put(Configuration.CONFIGURATOR_PREFIX.concat(property), value); 72 | } 73 | }) 74 | .build(reader); 75 | final ConfigurationResource rsrc = cfgReader.readConfigurationResource(); 76 | for (Map.Entry> entry : 77 | rsrc.getConfigurations().entrySet()) { 78 | Configuration cf = result.getConfiguration(entry.getKey()); 79 | if (cf == null) { 80 | cf = new Configuration(entry.getKey()); 81 | result.add(cf); 82 | } 83 | for (final Map.Entry prop : entry.getValue().entrySet()) { 84 | cf.getProperties().put(prop.getKey(), prop.getValue()); 85 | } 86 | } 87 | 88 | return result; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/MapWithMetadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.io.Serializable; 22 | import java.util.Collection; 23 | import java.util.HashMap; 24 | import java.util.LinkedHashMap; 25 | import java.util.Map; 26 | import java.util.Set; 27 | 28 | /** 29 | * Helper class to maintain metadata for a map. 30 | * This class is not thread-safe. 31 | * @since 1.7.0 32 | */ 33 | class MapWithMetadata implements Map, Serializable { 34 | 35 | private static final long serialVersionUID = 2L; 36 | 37 | private final Map values = new LinkedHashMap<>(); 38 | 39 | private final Map> metadata = new HashMap<>(); 40 | 41 | public Map getMetadata(final String key) { 42 | if (values.containsKey(key)) { 43 | return metadata.computeIfAbsent(key, id -> new LinkedHashMap<>()); 44 | } 45 | metadata.remove(key); 46 | return null; 47 | } 48 | 49 | @Override 50 | public void clear() { 51 | this.values.clear(); 52 | this.metadata.clear(); 53 | } 54 | 55 | @Override 56 | public boolean containsKey(final Object key) { 57 | return this.values.containsKey(key); 58 | } 59 | 60 | @Override 61 | public boolean containsValue(final Object value) { 62 | return this.values.containsValue(value); 63 | } 64 | 65 | @Override 66 | public Set> entrySet() { 67 | return this.values.entrySet(); 68 | } 69 | 70 | @Override 71 | public String get(final Object key) { 72 | return this.values.get(key); 73 | } 74 | 75 | @Override 76 | public boolean isEmpty() { 77 | return this.values.isEmpty(); 78 | } 79 | 80 | @Override 81 | public Set keySet() { 82 | return this.values.keySet(); 83 | } 84 | 85 | @Override 86 | public String put(final String key, final String value) { 87 | return this.values.put(key, value); 88 | } 89 | 90 | @Override 91 | public void putAll(final Map m) { 92 | this.values.putAll(m); 93 | } 94 | 95 | @Override 96 | public String remove(final Object key) { 97 | this.metadata.remove(key); 98 | return this.values.remove(key); 99 | } 100 | 101 | @Override 102 | public int size() { 103 | return this.values.size(); 104 | } 105 | 106 | @Override 107 | public Collection values() { 108 | return this.values.values(); 109 | } 110 | 111 | /* (non-Javadoc) 112 | * @see java.lang.Object#hashCode() 113 | */ 114 | @Override 115 | public int hashCode() { 116 | return values.hashCode(); 117 | } 118 | 119 | /* (non-Javadoc) 120 | * @see java.lang.Object#equals(java.lang.Object) 121 | */ 122 | @Override 123 | public boolean equals(final Object obj) { 124 | if (this == obj) { 125 | return true; 126 | } 127 | if (obj instanceof MapWithMetadata) { 128 | return this.values.equals((MapWithMetadata) obj); 129 | } 130 | if (!(obj instanceof Map)) { 131 | return false; 132 | } 133 | return this.values.equals(obj); 134 | } 135 | 136 | /* (non-Javadoc) 137 | * @see java.lang.Object#toString() 138 | */ 139 | @Override 140 | public String toString() { 141 | return values.toString(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/Artifacts.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.ArrayList; 22 | 23 | /** 24 | * Groups a list of {@code Artifact}s. 25 | * This class is not thread-safe. 26 | */ 27 | public class Artifacts extends ArrayList { 28 | 29 | private static final long serialVersionUID = 240141452817960076L; 30 | 31 | /** 32 | * Add an artifact. If the exact artifact is already contained in the 33 | * collection, it is not added again. 34 | * 35 | * @param artifact The artifact 36 | * @return {@code true} if this collection changed as a result of the call 37 | */ 38 | @Override 39 | public boolean add(final Artifact artifact) { 40 | if (this.containsExact(artifact.getId())) { 41 | return false; 42 | } 43 | return super.add(artifact); 44 | } 45 | 46 | /** 47 | * Remove the exact artifact. The first one found is removed. 48 | * 49 | * @param id The artifact id 50 | * @return {@code true} if the artifact has been removed 51 | */ 52 | public boolean removeExact(final ArtifactId id) { 53 | for (final Artifact artifact : this) { 54 | if (artifact.getId().equals(id)) { 55 | return this.remove(artifact); 56 | } 57 | } 58 | return false; 59 | } 60 | 61 | /** 62 | * Remove the same artifact, neglecting the version. The first one found is 63 | * removed. 64 | * 65 | * @param id The artifact id 66 | * @return {@code true} if the artifact has been removed 67 | */ 68 | public boolean removeSame(final ArtifactId id) { 69 | for (final Artifact artifact : this) { 70 | if (artifact.getId().isSame(id)) { 71 | return this.remove(artifact); 72 | } 73 | } 74 | return false; 75 | } 76 | 77 | /** 78 | * Get the artifact for the given id, neglecting the version 79 | * 80 | * @param id The artifact id 81 | * @return The artifact or {@code null} otherwise 82 | */ 83 | public Artifact getSame(final ArtifactId id) { 84 | for (final Artifact artifact : this) { 85 | if (artifact.getId().isSame(id)) { 86 | return artifact; 87 | } 88 | } 89 | return null; 90 | } 91 | 92 | /** 93 | * Get the artifact for the given id 94 | * 95 | * @param id The artifact id 96 | * @return The artifact or {@code null} otherwise 97 | */ 98 | public Artifact getExact(final ArtifactId id) { 99 | for (final Artifact artifact : this) { 100 | if (artifact.getId().equals(id)) { 101 | return artifact; 102 | } 103 | } 104 | return null; 105 | } 106 | 107 | /** 108 | * Checks whether the exact artifact is available 109 | * @param id The artifact id. 110 | * @return {@code true} if the artifact exists 111 | */ 112 | public boolean containsExact(final ArtifactId id) { 113 | for (final Artifact entry : this) { 114 | if (entry.getId().equals(id)) { 115 | return true; 116 | } 117 | } 118 | return false; 119 | } 120 | 121 | /** 122 | * Checks whether the same artifact is available, neglecting the version 123 | * @param id The artifact id. 124 | * @return {@code true} if the artifact exists 125 | */ 126 | public boolean containsSame(final ArtifactId id) { 127 | for (final Artifact entry : this) { 128 | if (entry.getId().isSame(id)) { 129 | return true; 130 | } 131 | } 132 | return false; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /docs/feature-model.json: -------------------------------------------------------------------------------- 1 | { 2 | // this is a comment 3 | "id": "org.apache.sling:my.app:slingosgifeature:my-classifier:1.0", 4 | 5 | "title": "A title for the feature. (optional)", 6 | "description": "A description for the feature. (optional)", 7 | "vendor": "The feature vendor, for example 'Apache Software Foundation'. (optional)", 8 | "license": "The license of this feature file, for example 'ASL-2'. (optional)", 9 | 10 | // A complete feature has no external dependencies 11 | "complete": true, 12 | 13 | // A final feature cannot be used as a prototype for another feature 14 | "final": false, 15 | 16 | // variables used in configuration and framework properties are substituted at launch time. 17 | "variables": { 18 | "cfgvar": "somedefault", 19 | "org.abc.xyz": "1.2.3", 20 | 21 | // When converting to provisioning model, if you need a special name 22 | "provisioning.model.name": ":boot" 23 | }, 24 | 25 | // A prototype is another feature that is used as a prototype for this one 26 | // Bundles, configurations and framework properties can be removed from the 27 | //prototype. Bundles with the same artifact ID defined in the feature override 28 | // bundles with this artifact ID in the Prototype 29 | "prototype": 30 | { 31 | "id": "org.apache.sling:some-other-feature:1.2.3", 32 | "removals": { 33 | "configurations": [], 34 | "bundles": [], 35 | "framework-properties": [] 36 | } 37 | }, 38 | 39 | // Requirements over and above the requirements in the bundles referenced by 40 | // feature. 41 | "requirements": [ 42 | { 43 | "namespace": "osgi.contract", 44 | "directives": { 45 | "filter": "(&(osgi.contract=JavaServlet)(version=3.1))" 46 | } 47 | } 48 | ], 49 | 50 | // Capabilities over and above the capabilities provided by the bundles referenced 51 | // by the feature. 52 | "capabilities": [ 53 | { 54 | "namespace": "osgi.implementation", 55 | "attributes": { 56 | "osgi.implementation": "osgi.http", 57 | "version:Version": "1.1" 58 | }, 59 | "directives": { 60 | "uses": "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard" 61 | } 62 | }, 63 | { 64 | "namespace": "osgi.service", 65 | "attributes": { 66 | "objectClass:List": "org.osgi.service.http.runtime.HttpServiceRuntime" 67 | }, 68 | "directives": { 69 | "uses": "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto" 70 | } 71 | } 72 | ], 73 | 74 | // Framework properties to be provided to the running OSGi Framework 75 | "framework-properties": { 76 | "foo": 1, 77 | "org.osgi.framework.storage": "${tempdir}", 78 | "org.apache.felix.scr.directory": "launchpad/scr" 79 | }, 80 | 81 | // The bundles that are part of the feature. Bundles are referenced using Maven 82 | // coordinates and can have additional metadata associated with them. Bundles can 83 | // specified as either a simple string (the Maven coordinates of the bundle) or 84 | // as an object with 'id' and additional metadata. 85 | "bundles": [ 86 | { 87 | "id": "org.apache.sling:security-server:2.2.0", 88 | "hash": "4632463464363646436", 89 | 90 | // This is the relative start order inside the feature 91 | "start-order": 5 92 | }, 93 | { 94 | "id": "org.apache.sling:application-bundle:2.0.0", 95 | "start-order": 10 96 | }, 97 | "org.apache.sling:foo-xyz:1.2.3" 98 | ], 99 | 100 | // The configurations are specified following the format defined by the OSGi Configurator 101 | // specification: https://osgi.org/specification/osgi.cmpn/7.0.0/service.configurator.html 102 | // Variables declared in the variables section can be used for late binding of variables 103 | // they can be specified with the Launcher, or the default from the variables section is used. 104 | // Factory configurations can be specified using the named factory syntax, which separates 105 | // The factory PID and the name with a tilde '~' 106 | "configurations": { 107 | "my.pid": { 108 | "foo": 5, 109 | "something-enabled": false, 110 | "bar": "${cfgvar}", 111 | 112 | // The tempdir variable is not specified at the variables section. 113 | // It needs to be provided at launch, otherwise the launch will stop. 114 | "tempdir": "${tempdir}", 115 | 116 | 117 | "number:Integer": 7 118 | }, 119 | "my.factory.pid~name": { 120 | "a.value":"yeah" 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/resources/features/feature-model.json: -------------------------------------------------------------------------------- 1 | { 2 | // this is a comment 3 | "id": "org.apache.sling:my.app:slingosgifeature:my-classifier:1.0", 4 | 5 | "title": "A title for the feature. (optional)", 6 | "description": "A description for the feature. (optional)", 7 | "vendor": "The feature vendor, for example 'Apache Software Foundation'. (optional)", 8 | "license": "The license of this feature file, for example 'ASL-2'. (optional)", 9 | 10 | // A complete feature has no external dependencies 11 | "complete": true, 12 | 13 | // A final feature cannot be used as a prototype for another feature 14 | "final": false, 15 | 16 | // variables used in configuration and framework properties are substituted at launch time. 17 | "variables": { 18 | "cfgvar": "somedefault", 19 | "org.abc.xyz": "1.2.3", 20 | 21 | // When converting to provisioning model, if you need a special name 22 | "provisioning.model.name": ":boot" 23 | }, 24 | 25 | // A prototype is another feature that is used as a prototype for this one 26 | // Bundles, configurations and framework properties can be removed from the 27 | //prototype. Bundles with the same artifact ID defined in the feature override 28 | // bundles with this artifact ID in the Prototype 29 | "prototype": 30 | { 31 | "id": "org.apache.sling:some-other-feature:1.2.3", 32 | "removals": { 33 | "configurations": [], 34 | "bundles": [], 35 | "framework-properties": [] 36 | } 37 | }, 38 | 39 | // Requirements over and above the requirements in the bundles referenced by 40 | // feature. 41 | "requirements": [ 42 | { 43 | "namespace": "osgi.contract", 44 | "directives": { 45 | "filter": "(&(osgi.contract=JavaServlet)(version=3.1))" 46 | } 47 | } 48 | ], 49 | 50 | // Capabilities over and above the capabilities provided by the bundles referenced 51 | // by the feature. 52 | "capabilities": [ 53 | { 54 | "namespace": "osgi.implementation", 55 | "attributes": { 56 | "osgi.implementation": "osgi.http", 57 | "version:Version": "1.1" 58 | }, 59 | "directives": { 60 | "uses": "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard" 61 | } 62 | }, 63 | { 64 | "namespace": "osgi.service", 65 | "attributes": { 66 | "objectClass:List": "org.osgi.service.http.runtime.HttpServiceRuntime" 67 | }, 68 | "directives": { 69 | "uses": "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto" 70 | } 71 | } 72 | ], 73 | 74 | // Framework properties to be provided to the running OSGi Framework 75 | "framework-properties": { 76 | "foo": 1, 77 | "org.osgi.framework.storage": "${tempdir}", 78 | "org.apache.felix.scr.directory": "launchpad/scr" 79 | }, 80 | 81 | // The bundles that are part of the feature. Bundles are referenced using Maven 82 | // coordinates and can have additional metadata associated with them. Bundles can 83 | // specified as either a simple string (the Maven coordinates of the bundle) or 84 | // as an object with 'id' and additional metadata. 85 | "bundles": [ 86 | { 87 | "id": "org.apache.sling:security-server:2.2.0", 88 | "hash": "4632463464363646436", 89 | 90 | // This is the relative start order inside the feature 91 | "start-order": 5 92 | }, 93 | { 94 | "id": "org.apache.sling:application-bundle:2.0.0", 95 | "start-order": 10 96 | }, 97 | "org.apache.sling:foo-xyz:1.2.3" 98 | ], 99 | 100 | // The configurations are specified following the format defined by the OSGi Configurator 101 | // specification: https://osgi.org/specification/osgi.cmpn/7.0.0/service.configurator.html 102 | // Variables declared in the variables section can be used for late binding of variables 103 | // they can be specified with the Launcher, or the default from the variables section is used. 104 | // Factory configurations can be specified using the named factory syntax, which separates 105 | // The factory PID and the name with a tilde '~' 106 | "configurations": { 107 | "my.pid": { 108 | "foo": 5, 109 | "something-enabled": false, 110 | "bar": "${cfgvar}", 111 | 112 | // The tempdir variable is not specified at the variables section. 113 | // It needs to be provided at launch, otherwise the launch will stop. 114 | "tempdir": "${tempdir}", 115 | 116 | 117 | "number:Integer": 7 118 | }, 119 | "my.factory.pid~name": { 120 | "a.value":"yeah" 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/archive/ArchiveWriterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.archive; 20 | 21 | import java.io.ByteArrayInputStream; 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.io.Reader; 26 | import java.io.StringReader; 27 | import java.nio.charset.StandardCharsets; 28 | import java.util.ArrayList; 29 | import java.util.HashSet; 30 | import java.util.List; 31 | import java.util.Set; 32 | 33 | import org.apache.sling.feature.Artifact; 34 | import org.apache.sling.feature.ArtifactId; 35 | import org.apache.sling.feature.Feature; 36 | import org.apache.sling.feature.io.json.FeatureJSONReader; 37 | import org.junit.Test; 38 | 39 | import static org.junit.Assert.assertArrayEquals; 40 | import static org.junit.Assert.assertEquals; 41 | import static org.junit.Assert.assertTrue; 42 | 43 | public class ArchiveWriterTest { 44 | 45 | public static final String ARTIFACT = "/features/final.json"; 46 | 47 | @Test 48 | public void testArchiveWrite() throws IOException { 49 | final Feature f = new Feature(ArtifactId.parse("g:f:1")); 50 | f.getBundles().add(new Artifact(ArtifactId.parse("g:a:2"))); 51 | 52 | byte[] archive = null; 53 | final Set ids = new HashSet<>(); 54 | try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { 55 | ArchiveWriter.write( 56 | out, 57 | null, 58 | id -> { 59 | ids.add(id); 60 | return ArchiveWriterTest.class.getResource(ARTIFACT); 61 | }, 62 | f) 63 | .finish(); 64 | out.flush(); 65 | archive = out.toByteArray(); 66 | } 67 | assertEquals(1, ids.size()); 68 | assertTrue(ids.contains(ArtifactId.parse("g:a:2"))); 69 | 70 | // read "artifact" 71 | final byte[] artifactBytes; 72 | try (final InputStream is = ArchiveWriterTest.class.getResourceAsStream(ARTIFACT)) { 73 | artifactBytes = readFromStream(is); 74 | } 75 | 76 | final Set readIds = new HashSet<>(); 77 | final Set readFeatureIds = new HashSet<>(); 78 | 79 | final List features = new ArrayList<>(); 80 | try (final InputStream in = new ByteArrayInputStream(archive)) { 81 | features.addAll(ArchiveReader.read(in, (id, is) -> { 82 | 83 | // read contents 84 | byte[] read = readFromStream(is); 85 | 86 | // is feature? 87 | if (id.equals(f.getId())) { 88 | try (final Reader reader = new StringReader(new String(read, StandardCharsets.UTF_8))) { 89 | final Feature readFeature = FeatureJSONReader.read(reader, id.toString()); 90 | assertEquals(f.getId(), readFeature.getId()); 91 | readFeatureIds.add(f.getId()); 92 | } 93 | } else { 94 | readIds.add(id); 95 | assertEquals(artifactBytes.length, read.length); 96 | assertArrayEquals(artifactBytes, read); 97 | } 98 | })); 99 | } 100 | 101 | assertEquals(1, readFeatureIds.size()); 102 | assertTrue(readFeatureIds.contains(f.getId())); 103 | 104 | assertEquals(1, readIds.size()); 105 | assertTrue(readIds.contains(ArtifactId.parse("g:a:2"))); 106 | 107 | assertEquals(1, features.size()); 108 | final Feature g = features.get(0); 109 | assertEquals(f.getId(), g.getId()); 110 | } 111 | 112 | private byte[] readFromStream(final InputStream is) throws IOException { 113 | byte[] read; 114 | try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 115 | byte[] buf = new byte[1024]; 116 | int l; 117 | while ((l = is.read(buf)) > 0) { 118 | baos.write(buf, 0, l); 119 | } 120 | baos.flush(); 121 | read = baos.toByteArray(); 122 | } 123 | return read; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/ExecutionEnvironmentExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import jakarta.json.JsonString; 22 | import jakarta.json.JsonStructure; 23 | import jakarta.json.JsonValue; 24 | import jakarta.json.JsonValue.ValueType; 25 | import org.osgi.framework.Version; 26 | 27 | /** 28 | * Execution environment extension. 29 | * This class is thread-safe. 30 | * @since 1.4.0 31 | */ 32 | public class ExecutionEnvironmentExtension { 33 | 34 | /** 35 | * Extension name containing the execution environment. The execution 36 | * environment can specify the framework to launch 37 | * This extension is of type {@link ExtensionType#JSON} and is optional. 38 | */ 39 | public static final String EXTENSION_NAME = "execution-environment"; 40 | 41 | /** 42 | * Get the execution environment from the feature - if it exists. 43 | * @param feature The feature 44 | * @return The execution environment or {@code null}. 45 | * @throws IllegalArgumentException If the extension is wrongly formatted 46 | */ 47 | public static ExecutionEnvironmentExtension getExecutionEnvironmentExtension(final Feature feature) { 48 | final Extension ext = feature == null ? null : feature.getExtensions().getByName(EXTENSION_NAME); 49 | return getExecutionEnvironmentExtension(ext); 50 | } 51 | 52 | /** 53 | * Get the execution environment from the extension. 54 | * @param ext The extension 55 | * @return The execution environment or {@code null}. 56 | * @throws IllegalArgumentException If the extension is wrongly formatted 57 | */ 58 | public static ExecutionEnvironmentExtension getExecutionEnvironmentExtension(final Extension ext) { 59 | if (ext == null) { 60 | return null; 61 | } 62 | if (ext.getType() != ExtensionType.JSON) { 63 | throw new IllegalArgumentException("Extension " + ext.getName() + " must have JSON type"); 64 | } 65 | return new ExecutionEnvironmentExtension(ext.getJSONStructure()); 66 | } 67 | 68 | /** Optional framework artifact. */ 69 | private final Artifact framework; 70 | 71 | /** Optional java version */ 72 | private final Version javaVersion; 73 | 74 | /** Optional java options */ 75 | private final String javaOptions; 76 | 77 | private ExecutionEnvironmentExtension(final JsonStructure structure) { 78 | // get framework 79 | final JsonValue fwk = structure.asJsonObject().getOrDefault("framework", null); 80 | if (fwk != null) { 81 | this.framework = new Artifact(fwk); 82 | } else { 83 | this.framework = null; 84 | } 85 | // get version 86 | final JsonValue jv = structure.asJsonObject().getOrDefault("javaVersion", null); 87 | if (jv != null) { 88 | if (jv.getValueType() != ValueType.STRING) { 89 | throw new IllegalArgumentException("javaVersion is not of type String"); 90 | } 91 | this.javaVersion = Version.parseVersion(((JsonString) jv).getString()); 92 | } else { 93 | this.javaVersion = null; 94 | } 95 | // get options 96 | final JsonValue jo = structure.asJsonObject().getOrDefault("javaOptions", null); 97 | if (jo != null) { 98 | if (jo.getValueType() != ValueType.STRING) { 99 | throw new IllegalArgumentException("javaOptions is not of type String"); 100 | } 101 | this.javaOptions = ((JsonString) jo).getString(); 102 | } else { 103 | this.javaOptions = null; 104 | } 105 | } 106 | 107 | /** 108 | * Get the specified framework 109 | * @return The framework or {@code null} 110 | */ 111 | public Artifact getFramework() { 112 | return this.framework; 113 | } 114 | 115 | /** 116 | * Get the specified java version 117 | * @return The version or {@code null} 118 | * @since 1.5.0 119 | */ 120 | public Version getJavaVersion() { 121 | return javaVersion; 122 | } 123 | 124 | /** 125 | * Get the specified java options 126 | * @return The options or {@code null} 127 | * @since 1.5.0 128 | */ 129 | public String getJavaOptions() { 130 | return javaOptions; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/ExecutionEnvironmentExtensionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import org.junit.Test; 22 | import org.osgi.framework.Version; 23 | 24 | import static org.junit.Assert.assertEquals; 25 | import static org.junit.Assert.assertNotNull; 26 | import static org.junit.Assert.assertNull; 27 | 28 | public class ExecutionEnvironmentExtensionTest { 29 | 30 | @Test 31 | public void testNullFeature() { 32 | assertNull(ExecutionEnvironmentExtension.getExecutionEnvironmentExtension((Feature) null)); 33 | } 34 | 35 | @Test 36 | public void testNullExtension() { 37 | assertNull(ExecutionEnvironmentExtension.getExecutionEnvironmentExtension((Extension) null)); 38 | final Feature f = new Feature(ArtifactId.parse("g:a:1.0")); 39 | assertNull(ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(f)); 40 | } 41 | 42 | @Test(expected = IllegalArgumentException.class) 43 | public void testWrongExtensionType() { 44 | final Feature f = new Feature(ArtifactId.parse("g:a:1.0")); 45 | final Extension e = new Extension( 46 | ExtensionType.TEXT, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 47 | f.getExtensions().add(e); 48 | ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(f); 49 | } 50 | 51 | @Test 52 | public void testNoFramework() { 53 | final Extension e = new Extension( 54 | ExtensionType.JSON, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 55 | e.setJSON("{}"); 56 | 57 | assertNull(ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(e) 58 | .getFramework()); 59 | } 60 | 61 | @Test 62 | public void testFrameworkAsString() { 63 | final Extension e = new Extension( 64 | ExtensionType.JSON, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 65 | e.setJSON("{ \"framework\" : \"g:a:1\" }"); 66 | 67 | final ExecutionEnvironmentExtension eee = ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(e); 68 | assertNotNull(eee.getFramework()); 69 | assertEquals(ArtifactId.parse("g:a:1"), eee.getFramework().getId()); 70 | } 71 | 72 | @Test 73 | public void testFrameworkAsObject() { 74 | final Extension e = new Extension( 75 | ExtensionType.JSON, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 76 | e.setJSON("{ \"framework\" : { \"id\" : \"g:a:1\", \"p\" : \"v\" } }"); 77 | 78 | final ExecutionEnvironmentExtension eee = ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(e); 79 | assertNotNull(eee.getFramework()); 80 | assertEquals(ArtifactId.parse("g:a:1"), eee.getFramework().getId()); 81 | assertEquals("v", eee.getFramework().getMetadata().get("p")); 82 | } 83 | 84 | @Test 85 | public void testJavaOptions() { 86 | final Extension e = new Extension( 87 | ExtensionType.JSON, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 88 | e.setJSON("{ \"javaOptions\" : \"options\" }"); 89 | 90 | final ExecutionEnvironmentExtension eee = ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(e); 91 | assertEquals("options", eee.getJavaOptions()); 92 | } 93 | 94 | @Test(expected = IllegalArgumentException.class) 95 | public void testWrongJavaOptions() { 96 | final Extension e = new Extension( 97 | ExtensionType.JSON, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 98 | e.setJSON("{ \"javaOptions\" : true }"); 99 | 100 | ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(e); 101 | } 102 | 103 | @Test 104 | public void testJavaVersion() { 105 | final Extension e = new Extension( 106 | ExtensionType.JSON, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 107 | e.setJSON("{ \"javaVersion\" : \"11\" }"); 108 | 109 | final ExecutionEnvironmentExtension eee = ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(e); 110 | assertEquals(new Version("11"), eee.getJavaVersion()); 111 | } 112 | 113 | @Test(expected = IllegalArgumentException.class) 114 | public void testWrongJavaVersion() { 115 | final Extension e = new Extension( 116 | ExtensionType.JSON, ExecutionEnvironmentExtension.EXTENSION_NAME, ExtensionState.OPTIONAL); 117 | e.setJSON("{ \"javaVersion\" : true }"); 118 | 119 | ExecutionEnvironmentExtension.getExecutionEnvironmentExtension(e); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/Prototype.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.io.Serializable; 22 | import java.util.ArrayList; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import org.osgi.resource.Capability; 28 | 29 | /** 30 | * A prototype is a blueprint of a feature with optional removals of 31 | *
    32 | *
  • Configurations / configuration properties 33 | *
  • Bundles 34 | *
  • Framework properties 35 | *
  • Extensions or artifacts from extensions 36 | *
  • Capabilities 37 | *
  • Requirements 38 | *
39 | * 40 | * This class is not thread-safe. 41 | */ 42 | public class Prototype implements Comparable, Serializable { 43 | 44 | private static final long serialVersionUID = 2L; 45 | 46 | private final ArtifactId id; 47 | 48 | private final List configurationRemovals = new ArrayList<>(); 49 | 50 | private final List bundleRemovals = new ArrayList<>(); 51 | 52 | private final List frameworkPropertiesRemovals = new ArrayList<>(); 53 | 54 | private final List extensionRemovals = new ArrayList<>(); 55 | 56 | private final Map> artifactExtensionRemovals = new HashMap<>(); 57 | 58 | private final List requirementRemovals = new ArrayList<>(); 59 | 60 | private final List capabilityRemovals = new ArrayList<>(); 61 | 62 | /** 63 | * Construct a new Include. 64 | * @param id The id of the feature. 65 | * @throws IllegalArgumentException If id is {@code null}. 66 | */ 67 | public Prototype(final ArtifactId id) { 68 | if (id == null) { 69 | throw new IllegalArgumentException("id must not be null."); 70 | } 71 | this.id = id; 72 | } 73 | 74 | /** 75 | * Get the id of the artifact. 76 | * @return The id. 77 | */ 78 | public ArtifactId getId() { 79 | return this.id; 80 | } 81 | 82 | /** 83 | * Get the list of configuration removals The returned object is modifiable. 84 | * 85 | * @return List of {@code PID}s. 86 | */ 87 | public List getConfigurationRemovals() { 88 | return configurationRemovals; 89 | } 90 | 91 | /** 92 | * Get the list of artifact removals The returned object is modifiable. 93 | * 94 | * @return List of artifact ids. 95 | */ 96 | public List getBundleRemovals() { 97 | return bundleRemovals; 98 | } 99 | 100 | /** 101 | * Get the list of framework property removals The returned object is 102 | * modifiable. 103 | * 104 | * @return List of property names 105 | */ 106 | public List getFrameworkPropertiesRemovals() { 107 | return frameworkPropertiesRemovals; 108 | } 109 | 110 | /** 111 | * Get the list of extension removals The returned object is modifiable. 112 | * 113 | * @return List of extension names 114 | */ 115 | public List getExtensionRemovals() { 116 | return extensionRemovals; 117 | } 118 | 119 | /** 120 | * Get the list of artifacts removed from extensions The returned object is 121 | * modifiable. 122 | * 123 | * @return Map where the extension name is the key, and the value is a list of 124 | * artifact ids 125 | */ 126 | public Map> getArtifactExtensionRemovals() { 127 | return artifactExtensionRemovals; 128 | } 129 | 130 | /** 131 | * Get the list of requirement removals. The returned object is modifiable. 132 | * 133 | * @return The list of requirements 134 | * @since 1.3 135 | */ 136 | public List getRequirementRemovals() { 137 | return requirementRemovals; 138 | } 139 | 140 | /** 141 | * Get the list of capability removals. The returned object is modifiable. 142 | * 143 | * @return The list of capabilities 144 | */ 145 | public List getCapabilityRemovals() { 146 | return capabilityRemovals; 147 | } 148 | 149 | @Override 150 | public int compareTo(final Prototype o) { 151 | return this.id.compareTo(o.id); 152 | } 153 | 154 | @Override 155 | public int hashCode() { 156 | return this.id.hashCode(); 157 | } 158 | 159 | @Override 160 | public boolean equals(final Object obj) { 161 | if (this == obj) { 162 | return true; 163 | } 164 | if (obj == null || getClass() != obj.getClass()) { 165 | return false; 166 | } 167 | return this.id.equals(((Prototype) obj).id); 168 | } 169 | 170 | @Override 171 | public String toString() { 172 | return "Include [id=" + id.toMvnId() + "]"; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/feature/Feature-1.0.0.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://sling.apache.org/Feature/1.0.1", 4 | "type": "object", 5 | "properties": { 6 | "model-version": { 7 | "type": "string" 8 | }, 9 | "id": { 10 | "type": "string", 11 | "pattern": "^(([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)|([^/ ]+)/([^/ ]+)(/([^/ ]+))?(/([^/ ]*)(/([^/ ]+))?)?)$" 12 | }, 13 | "final": { 14 | "type": "boolean" 15 | }, 16 | "complete": { 17 | "type": "boolean" 18 | }, 19 | "title": { 20 | "type": "string" 21 | }, 22 | "description": { 23 | "type": "string" 24 | }, 25 | "vendor": { 26 | "type": "string" 27 | }, 28 | "license": { 29 | "type": "string" 30 | }, 31 | "variables": { 32 | "type": "object", 33 | "patternProperties": { 34 | "^(.+)$": { 35 | "type": ["string","null"] 36 | } 37 | } 38 | }, 39 | "bundles": { 40 | "type": "array", 41 | "items": { 42 | "$ref": "#/definitions/Bundle" 43 | } 44 | }, 45 | "framework-properties": { 46 | "type": "object", 47 | "patternProperties": { 48 | "^(.+)$": { 49 | "type": [ "string", "number", "boolean" ] 50 | } 51 | } 52 | }, 53 | "configurations": { 54 | "type": "object", 55 | "patternProperties": { 56 | "^(.+)$": { 57 | "$ref": "#/definitions/Configuration" 58 | } 59 | } 60 | }, 61 | "prototype": { 62 | "$ref": "#/definitions/Prototype" 63 | }, 64 | "requirements": { 65 | " type": "array", 66 | "items": { 67 | "$ref": "#/definitions/Requirement" 68 | } 69 | }, 70 | "capabilities": { 71 | " type": "array", 72 | "items": { 73 | "$ref": "#/definitions/Capability" 74 | } 75 | } 76 | }, 77 | "patternProperties": { 78 | "^[^:]+:ARTIFACTS\\|(true|false)$": { 79 | "type": "array", 80 | "items": { 81 | "$ref": "#/definitions/Bundle" 82 | } 83 | }, 84 | "^[^:]+:TEXT\\|(true|false)$": { 85 | "type": [ "string", "array" ], 86 | "items": { 87 | "type": "string" 88 | } 89 | }, 90 | "^[^:]+:JSON\\|(true|false)$": { 91 | "type": [ 92 | "object", 93 | "array" 94 | ] 95 | } 96 | }, 97 | "definitions": { 98 | "Bundle": { 99 | "$id": "#Bundle", 100 | "type": [ 101 | "string", 102 | "object" 103 | ], 104 | "properties": { 105 | "id": { 106 | "type": "string", 107 | "pattern": "^(([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)|(mvn:)?([^/ ]+)/([^/ ]+)(/([^/ ]+))?(/([^/ ]*)(/([^/ ]+))?)?)$" 108 | }, 109 | "start-order": { 110 | "type": [ "string", "number" ], 111 | "pattern": "^\\d+$" 112 | }, 113 | "run-modes": { 114 | "type": [ 115 | "string", 116 | "array" 117 | ], 118 | "items": { 119 | "type": "string" 120 | } 121 | }, 122 | "configurations": { 123 | "type": "object", 124 | "patternProperties": { 125 | "^(.+)$": { 126 | "$ref": "#/definitions/Configuration" 127 | } 128 | } 129 | } 130 | } 131 | }, 132 | "Configuration": { 133 | "$id": "#Configuration", 134 | "patternProperties": { 135 | "^(.+)$": { 136 | "type": [ 137 | "string", 138 | "number", 139 | "boolean", 140 | "array", 141 | "object" 142 | ] 143 | } 144 | } 145 | }, 146 | "Prototype": { 147 | "$id": "#Prototype", 148 | "type": "object", 149 | "properties": { 150 | "id": { 151 | "type": "string", 152 | "pattern": "^(([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)|(mvn:)?([^/ ]+)/([^/ ]+)(/([^/ ]+))?(/([^/ ]*)(/([^/ ]+))?)?)$" 153 | }, 154 | "removals": { 155 | "$ref": "#/definitions/Removals" 156 | } 157 | } 158 | }, 159 | "Removals": { 160 | "$id": "#Removals", 161 | "type": "object", 162 | "properties": { 163 | "configurations": { 164 | "type": "array", 165 | "items": { 166 | "type": "string" 167 | } 168 | }, 169 | "bundles": { 170 | "type": "array", 171 | "items": { 172 | "type": "string" 173 | } 174 | }, 175 | "framework-properties": { 176 | "type": "array", 177 | "items": { 178 | "type": "string" 179 | } 180 | } 181 | } 182 | }, 183 | "Requirement": { 184 | "$id": "#Requirement", 185 | "type": "object", 186 | "properties": { 187 | "namespace": { 188 | "type": "string" 189 | }, 190 | "directives": { 191 | "type": "object", 192 | "patternProperties": { 193 | "^(.+)$": { 194 | "type": "string" 195 | } 196 | } 197 | } 198 | } 199 | }, 200 | "Capability": { 201 | "$id": "#Capability", 202 | "type": "object", 203 | "properties": { 204 | "namespace": { 205 | "type": "string" 206 | }, 207 | "directives": { 208 | "type": "object", 209 | "patternProperties": { 210 | "^(.+)$": { 211 | "type": "string" 212 | } 213 | } 214 | }, 215 | "attributes": { 216 | "type": "object", 217 | "patternProperties": { 218 | "^(.+)$": { 219 | "type": [ "string", "number", "boolean" ] 220 | } 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/FeatureTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.Map; 24 | 25 | import org.junit.Test; 26 | 27 | import static org.junit.Assert.assertArrayEquals; 28 | import static org.junit.Assert.assertEquals; 29 | import static org.junit.Assert.assertNotNull; 30 | import static org.junit.Assert.assertNull; 31 | import static org.junit.Assert.assertSame; 32 | 33 | public class FeatureTest { 34 | 35 | @Test 36 | public void testVariableMetadata() { 37 | final ArtifactId self = ArtifactId.parse("self:self:1"); 38 | final Feature f = new Feature(self); 39 | 40 | f.getVariables().put("a", "foo"); 41 | final Map metadata = f.getVariableMetadata("a"); 42 | assertNotNull(metadata); 43 | 44 | assertNull(f.getVariableMetadata("b")); 45 | f.getVariables().put("b", "bar"); 46 | assertNotNull(f.getVariableMetadata("b")); 47 | 48 | metadata.put("hello", "world"); 49 | 50 | assertEquals(1, f.getVariableMetadata("a").size()); 51 | 52 | f.getVariables().remove("b"); 53 | assertNull(f.getVariableMetadata("b")); 54 | } 55 | 56 | @Test 57 | public void testFrameworkPropertiesMetadata() { 58 | final ArtifactId self = ArtifactId.parse("self:self:1"); 59 | final Feature f = new Feature(self); 60 | 61 | f.getFrameworkProperties().put("a", "foo"); 62 | final Map metadata = f.getFrameworkPropertyMetadata("a"); 63 | assertNotNull(metadata); 64 | 65 | assertNull(f.getFrameworkPropertyMetadata("b")); 66 | f.getFrameworkProperties().put("b", "bar"); 67 | assertNotNull(f.getFrameworkPropertyMetadata("b")); 68 | 69 | metadata.put("hello", "world"); 70 | 71 | assertEquals(1, f.getFrameworkPropertyMetadata("a").size()); 72 | 73 | f.getFrameworkProperties().remove("b"); 74 | assertNull(f.getFrameworkPropertyMetadata("b")); 75 | } 76 | 77 | @Test 78 | public void testVariableOrigins() { 79 | final ArtifactId self = ArtifactId.parse("self:self:1"); 80 | final Feature f = new Feature(self); 81 | 82 | f.getVariables().put("a", "foo"); 83 | final Map metadata = f.getVariableMetadata("a"); 84 | 85 | assertNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS)); 86 | assertEquals(1, f.getFeatureOrigins(metadata).size()); 87 | assertEquals(self, f.getFeatureOrigins(metadata).get(0)); 88 | 89 | // single id 90 | final ArtifactId id = ArtifactId.parse("g:a:1"); 91 | f.setFeatureOrigins(metadata, Collections.singletonList(id)); 92 | assertEquals(1, f.getFeatureOrigins(metadata).size()); 93 | assertEquals(id, f.getFeatureOrigins(metadata).get(0)); 94 | 95 | assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS)); 96 | final String[] array = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS); 97 | assertArrayEquals(new String[] {id.toMvnId()}, array); 98 | 99 | // add another id 100 | final ArtifactId id2 = ArtifactId.parse("g:b:2"); 101 | f.setFeatureOrigins(metadata, Arrays.asList(id, id2)); 102 | assertEquals(2, f.getFeatureOrigins(metadata).size()); 103 | assertEquals(id, f.getFeatureOrigins(metadata).get(0)); 104 | assertEquals(id2, f.getFeatureOrigins(metadata).get(1)); 105 | 106 | assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS)); 107 | final String[] array2 = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS); 108 | assertArrayEquals(new String[] {id.toMvnId(), id2.toMvnId()}, array2); 109 | 110 | assertSame(metadata, f.getVariableMetadata("a")); 111 | } 112 | 113 | @Test 114 | public void testFrameworkPropertiesOrigins() { 115 | final ArtifactId self = ArtifactId.parse("self:self:1"); 116 | final Feature f = new Feature(self); 117 | 118 | f.getFrameworkProperties().put("a", "foo"); 119 | final Map metadata = f.getFrameworkPropertyMetadata("a"); 120 | 121 | assertNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS)); 122 | assertEquals(1, f.getFeatureOrigins(metadata).size()); 123 | assertEquals(self, f.getFeatureOrigins(metadata).get(0)); 124 | 125 | // single id 126 | final ArtifactId id = ArtifactId.parse("g:a:1"); 127 | f.setFeatureOrigins(metadata, Collections.singletonList(id)); 128 | assertEquals(1, f.getFeatureOrigins(metadata).size()); 129 | assertEquals(id, f.getFeatureOrigins(metadata).get(0)); 130 | 131 | assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS)); 132 | final String[] array = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS); 133 | assertArrayEquals(new String[] {id.toMvnId()}, array); 134 | 135 | // add another id 136 | final ArtifactId id2 = ArtifactId.parse("g:b:2"); 137 | f.setFeatureOrigins(metadata, Arrays.asList(id, id2)); 138 | assertEquals(2, f.getFeatureOrigins(metadata).size()); 139 | assertEquals(id, f.getFeatureOrigins(metadata).get(0)); 140 | assertEquals(id2, f.getFeatureOrigins(metadata).get(1)); 141 | 142 | assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS)); 143 | final String[] array2 = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS); 144 | assertArrayEquals(new String[] {id.toMvnId(), id2.toMvnId()}, array2); 145 | 146 | assertSame(metadata, f.getFrameworkPropertyMetadata("a")); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Apache Sling](https://sling.apache.org/res/logos/sling.png)](https://sling.apache.org) 2 | 3 | [![Build Status](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-feature/job/master/badge/icon)](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-feature/job/master/) [![Test Status](https://img.shields.io/jenkins/tests.svg?jobUrl=https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-feature/job/master/)](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-feature/job/master/test/?width=800&height=600) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=apache_sling-org-apache-sling-feature&metric=coverage)](https://sonarcloud.io/dashboard?id=apache_sling-org-apache-sling-feature) [![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=apache_sling-org-apache-sling-feature&metric=alert_status)](https://sonarcloud.io/dashboard?id=apache_sling-org-apache-sling-feature) [![JavaDoc](https://www.javadoc.io/badge/org.apache.sling/org.apache.sling.feature.svg)](https://www.javadoc.io/doc/org.apache.sling/org-apache-sling-feature) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.sling/org.apache.sling.feature/badge.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.sling%22%20a%3A%22org.apache.sling.feature%22) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 4 | 5 | # The Sling OSGi Feature Model 6 | 7 | # An Introduction to the Sling Feature Model 8 | 9 | See also the [Sling Website Feature Model documentation](http://sling.apache.org/tags/featuremodel.html) which includes tutorials and examples. 10 | 11 | OSGi is a platform capable of running large applications for a variety of purposes, including rich client applications, server-side systems and cloud and container based architectures. Typical OSGi applications are assembled out of bundles and configured through both, OSGi configurations and framework properties (though these are less frequently used than OSGi configurations). Depending on the nature of the application, there might be additional artifact types involved. 12 | 13 | As these applications are generally based on many bundles, describing each bundle individually in the application definition becomes unwieldy once the number of bundles reaches a certain level. Additionally, OSGi has no mechanism to describe other elements of the application definition, such as configuration or custom artifacts. 14 | 15 | While bundles already provide a good way to define rather small, coherent modules, there is often a need to distribute or provision a set of such bundles together with some configuration. Or if you want to build a set of applications (for example microservices) which share certain parts (like a foundation) the need for a higher level building block arises. 16 | 17 | The Sling OSGi Feature Model introduces such a higher level building block to describe OSGi applications or parts of it that encapsulates the details of the various components that the feature is built up from. It allows the description of an entire OSGi-based application based on reusable components and includes everything related to this application, including bundles, configuration, framework properties, capabilities, requirements and custom artifacts. 18 | 19 | The model is a general purpose feature model and in no way tied to Apache Sling. 20 | 21 | ## Features 22 | 23 | In a nutshell, a feature is the central entity for the Feature Model. A feature is described through a JSON object and can contain: 24 | 25 | * Metadata like a unique identifier, description etc. 26 | * OSGi bundles 27 | * OSGi configurations 28 | * OSGi Framework properties 29 | * Extensions - a plugin mechanism to add additional information to the feature 30 | 31 | Read the [documentation about features](docs/features.md) 32 | 33 | ## Feature Extensions 34 | 35 | The Feature Model is extensible, meaning that it can be augmented with custom content in a number of ways. Some extensions are supported out of the box. Other extensions are available through additional modules. 36 | 37 | Read the [documentation about available extensions](docs/extensions.md) 38 | 39 | ## Feature Archives 40 | 41 | Feature archives allow to distribute a feature together with all its referenced binaries. 42 | 43 | Read the [documentation about feature archives](docs/feature-archives.md) 44 | 45 | ## Feature Reference Files 46 | 47 | Reference files can be used to mention a collection of feature files in a single file. 48 | 49 | Read the [documentation about feature reference files](docs/feature-ref-files.md) 50 | 51 | ## Feature Aggregation 52 | 53 | In order to create higher level features or a complete application, usually several features are aggregated into a single final feature. 54 | 55 | Read the [documentation about feature aggregation](docs/aggregation.md) 56 | 57 | # Managing Features 58 | 59 | A Feature Launcher can be used to launch features into a running process with an OSGi Framework. 60 | The launcher is typically fed with a number of feature files that should be launched together. 61 | Overrides for variables defined in the feature models can be provided on the launcher commandline. 62 | 63 | Tooling exists to analyze and validate features, and to aggregate and merge multiple features into a single 64 | feature, which can be used to create higher level features from a combination of lower-level ones. Most of 65 | the tooling is accessible through the [slingfeature-maven-plugin](https://github.com/apache/sling-slingfeature-maven-plugin). 66 | 67 | The following diagrams show a typical workflow when working with feature files: 68 | 69 | 70 | 71 | Features are authored as JSON Feature Files. 72 | The slingfeature-maven-plugin provides analyzers and aggregators that check features and can combine them into larger features. The maven plugin can also be used to publish features to a Maven Repository. 73 | 74 | 75 | 76 | To create a running system from a number of feature files, features are selected from a Maven Repository, 77 | they are validated for completeness and optionally additional features are pulled in through the OSGi Resolver 78 | (not yet implemented). A final system feature has no unresolved dependencies. It is passed to the Feature Launcher 79 | along with optional additional features the provide functionality on top of what is defined in the system feature. 80 | The Feature Launcher creates a running process containing an OSGi Framework provisioned with the feature's contents. 81 | 82 | # Launching 83 | 84 | A launcher for feature models is available in the [Apache Sling Feature Launcher](https://github.com/apache/sling-org-apache-sling-feature-launcher) project. 85 | 86 | # Tooling 87 | 88 | The primary tooling around the feature model is provided through Maven by the [Sling Feature Maven Plugin](https://github.com/apache/sling-slingfeature-maven-plugin) 89 | 90 | See the readme of the plugin for more information. 91 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/json/FeatureJSONWriterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.json; 20 | 21 | import java.io.InputStreamReader; 22 | import java.io.Reader; 23 | import java.io.StringReader; 24 | import java.io.StringWriter; 25 | import java.util.Arrays; 26 | 27 | import jakarta.json.Json; 28 | import jakarta.json.JsonArray; 29 | import jakarta.json.JsonObject; 30 | import jakarta.json.JsonValue; 31 | import jakarta.json.JsonValue.ValueType; 32 | import org.apache.sling.feature.Feature; 33 | import org.junit.Assert; 34 | import org.junit.Test; 35 | 36 | import static org.junit.Assert.assertEquals; 37 | import static org.junit.Assert.assertNotNull; 38 | import static org.junit.Assert.assertNull; 39 | 40 | public class FeatureJSONWriterTest { 41 | 42 | @Test 43 | public void testWrite() throws Exception { 44 | final Feature f = U.readFeature("test"); 45 | final Feature rf; 46 | try (final StringWriter writer = new StringWriter()) { 47 | FeatureJSONWriter.write(writer, f); 48 | try (final StringReader reader = new StringReader(writer.toString())) { 49 | rf = FeatureJSONReader.read(reader, null); 50 | } 51 | } 52 | assertEquals(f.getId(), rf.getId()); 53 | assertEquals("org.apache.sling:test-feature:1.1", rf.getId().toMvnId()); 54 | assertEquals("The feature description", rf.getDescription()); 55 | 56 | assertEquals( 57 | Arrays.asList("org.osgi.service.http.runtime.HttpServiceRuntime"), 58 | U.findCapability(rf.getCapabilities(), "osgi.service") 59 | .getAttributes() 60 | .get("objectClass")); 61 | } 62 | 63 | @Test 64 | public void testWrite2() throws Exception { 65 | final Feature f = U.readFeature("test2"); 66 | 67 | final Feature rf; 68 | try (final StringWriter writer = new StringWriter()) { 69 | FeatureJSONWriter.write(writer, f); 70 | try (final StringReader reader = new StringReader(writer.toString())) { 71 | rf = FeatureJSONReader.read(reader, null); 72 | } 73 | } 74 | 75 | assertEquals(f.getVariables(), rf.getVariables()); 76 | } 77 | 78 | @Test 79 | public void testExtensionsWriteRead() throws Exception { 80 | final Feature f = U.readFeature("artifacts-extension"); 81 | final Feature rf; 82 | try (final StringWriter writer = new StringWriter()) { 83 | FeatureJSONWriter.write(writer, f); 84 | try (final StringReader reader = new StringReader(writer.toString())) { 85 | rf = FeatureJSONReader.read(reader, null); 86 | } 87 | } 88 | 89 | ArtifactsExtensions.testReadArtifactsExtensions(rf); 90 | } 91 | 92 | @Test 93 | public void testPrototypeWriteRead() throws Exception { 94 | final Feature f = U.readFeature("test"); 95 | assertNotNull(f.getPrototype()); 96 | 97 | final Feature rf; 98 | try (final StringWriter writer = new StringWriter()) { 99 | FeatureJSONWriter.write(writer, f); 100 | try (final StringReader reader = new StringReader(writer.toString())) { 101 | rf = FeatureJSONReader.read(reader, null); 102 | } 103 | } 104 | assertEquals(f.getPrototype().getId(), rf.getPrototype().getId()); 105 | } 106 | 107 | @Test 108 | public void testRepoInitWrite() throws Exception { 109 | final Feature f = U.readFeature("repoinit2"); 110 | try (final StringWriter writer = new StringWriter()) { 111 | FeatureJSONWriter.write(writer, f); 112 | final JsonObject refJson = Json.createReader( 113 | new InputStreamReader(U.class.getResourceAsStream("/features/repoinit2.json"))) 114 | .readObject(); 115 | final JsonObject resultJson = 116 | Json.createReader(new StringReader(writer.toString())).readObject(); 117 | 118 | JsonArray refJsonArray = refJson.getJsonArray("repoinit:TEXT|false"); 119 | JsonArray resultJsonArray = resultJson.getJsonArray("repoinit:TEXT|false"); 120 | Assert.assertEquals(refJsonArray, resultJsonArray); 121 | } 122 | } 123 | 124 | @Test 125 | public void testFinalFlag() throws Exception { 126 | // no final flag set in test feature 127 | final Feature featureA = U.readFeature("test"); 128 | try (final StringWriter writer = new StringWriter()) { 129 | FeatureJSONWriter.write(writer, featureA); 130 | final JsonObject resultJson = 131 | Json.createReader(new StringReader(writer.toString())).readObject(); 132 | 133 | assertNull(resultJson.get(JSONConstants.FEATURE_FINAL)); 134 | } 135 | 136 | // final flag set in final feature 137 | final Feature featureB = U.readFeature("final"); 138 | try (final StringWriter writer = new StringWriter()) { 139 | FeatureJSONWriter.write(writer, featureB); 140 | final JsonObject resultJson = 141 | Json.createReader(new StringReader(writer.toString())).readObject(); 142 | 143 | final JsonValue val = resultJson.get(JSONConstants.FEATURE_FINAL); 144 | assertNotNull(val); 145 | assertEquals(ValueType.TRUE, val.getValueType()); 146 | } 147 | } 148 | 149 | @Test 150 | public void testWriteInternalData() throws Exception { 151 | try (final Reader reader = 152 | new InputStreamReader(U.class.getResourceAsStream("/features/test-metadata.json"), "UTF-8")) { 153 | final JsonObject origJson = Json.createReader(reader).readObject(); 154 | 155 | final Feature feature = U.readFeature("test-metadata"); 156 | try (final StringWriter writer = new StringWriter()) { 157 | FeatureJSONWriter.write(writer, feature); 158 | final JsonObject resultJson = 159 | Json.createReader(new StringReader(writer.toString())).readObject(); 160 | 161 | assertEquals(origJson, resultJson); 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 4.0.0 17 | 18 | org.apache.sling 19 | sling-bundle-parent 20 | 62 21 | 22 | 23 | 24 | org.apache.sling.feature 25 | 2.0.5-SNAPSHOT 26 | 27 | Apache Sling Feature Model 28 | A feature describes an OSGi system 29 | 30 | 31 | scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature.git 32 | scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature.git 33 | org.apache.sling.feature-1.3.0 34 | https://github.com/apache/sling-org-apache-sling-feature.git 35 | 36 | 37 | 38 | 1734446536 39 | 8 40 | 41 | 42 | 43 | org.slf4j 44 | slf4j-api 45 | provided 46 | 47 | 48 | org.osgi 49 | org.osgi.annotation.versioning 50 | provided 51 | 52 | 53 | org.osgi 54 | org.osgi.framework 55 | provided 56 | 57 | 58 | org.osgi 59 | org.osgi.resource 60 | provided 61 | 62 | 63 | jakarta.json 64 | jakarta.json-api 65 | 2.0.2 66 | provided 67 | 68 | 69 | org.osgi 70 | org.osgi.util.converter 71 | provided 72 | 73 | 74 | org.osgi 75 | org.osgi.service.feature 76 | 1.0.0 77 | provided 78 | 79 | 80 | org.apache.felix 81 | org.apache.felix.utils 82 | 1.11.8 83 | provided 84 | 85 | 86 | org.apache.felix 87 | org.apache.felix.cm.json 88 | 2.0.0 89 | provided 90 | 91 | 92 | org.jetbrains 93 | annotations 94 | provided 95 | 96 | 97 | 98 | 99 | junit 100 | junit 101 | test 102 | 103 | 104 | 105 | org.mockito 106 | mockito-core 107 | 3.12.4 108 | test 109 | 110 | 111 | org.osgi 112 | org.osgi.util.function 113 | test 114 | 115 | 116 | org.apache.felix 117 | org.apache.felix.feature 118 | 1.0.2 119 | test 120 | 121 | 122 | org.eclipse.parsson 123 | parsson 124 | 1.0.4 125 | test 126 | 127 | 128 | commons-io 129 | commons-io 130 | 2.18.0 131 | test 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.apache.rat 139 | apache-rat-plugin 140 | 141 | 142 | *.md 143 | 144 | 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-shade-plugin 149 | 150 | 151 | 152 | shade 153 | 154 | package 155 | 156 | true 157 | true 158 | 159 | 160 | org.apache.felix.utils 161 | org.apache.sling.feature.impl.felix.utils 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/IOUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io; 20 | 21 | import java.io.BufferedReader; 22 | import java.io.File; 23 | import java.io.FileInputStream; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.io.InputStreamReader; 28 | import java.io.OutputStreamWriter; 29 | import java.io.PrintWriter; 30 | import java.net.URL; 31 | import java.net.URLConnection; 32 | import java.net.URLStreamHandler; 33 | import java.util.ArrayList; 34 | import java.util.Arrays; 35 | import java.util.Collections; 36 | import java.util.List; 37 | import java.util.jar.JarEntry; 38 | import java.util.jar.JarFile; 39 | import java.util.jar.JarOutputStream; 40 | 41 | import org.junit.Test; 42 | 43 | import static org.junit.Assert.assertEquals; 44 | import static org.junit.Assert.assertNotEquals; 45 | import static org.junit.Assert.assertNotNull; 46 | import static org.junit.Assert.assertNull; 47 | import static org.junit.Assert.fail; 48 | 49 | public class IOUtilsTest { 50 | 51 | @Test 52 | public void testFileSort() { 53 | final String[] files = new String[] { 54 | "/different/path/app.json", 55 | "/path/to/base.json", 56 | "/path/to/feature.json", 57 | "/path/to/amode/feature.json", 58 | "/path/to/later/feature.json", 59 | "http://sling.apache.org/features/one.json", 60 | "http://sling.apache.org/features/two.json", 61 | "http://sling.apache.org/features/amode/feature.json" 62 | }; 63 | 64 | final List l = new ArrayList<>(Arrays.asList(files)); 65 | Collections.sort(l, IOUtils.FEATURE_PATH_COMP); 66 | for (int i = 0; i < files.length; i++) { 67 | assertEquals(files[i], l.get(i)); 68 | } 69 | } 70 | 71 | @Test 72 | public void testGetFileFromURL() throws IOException { 73 | File file = File.createTempFile("IOUtilsTest", ".test"); 74 | 75 | try { 76 | try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) { 77 | writer.println("Hello"); 78 | } 79 | 80 | assertEquals(file, IOUtils.getFileFromURL(file.toURI().toURL(), false, null)); 81 | 82 | URL url = new URL(null, "bla:" + file.toURI().toURL(), new URLStreamHandler() { 83 | @Override 84 | protected URLConnection openConnection(URL u) { 85 | return new URLConnection(u) { 86 | @Override 87 | public void connect() {} 88 | 89 | @Override 90 | public InputStream getInputStream() throws IOException { 91 | return new FileInputStream(file); 92 | } 93 | }; 94 | } 95 | }); 96 | 97 | assertNull(IOUtils.getFileFromURL(url, false, null)); 98 | File tmp = IOUtils.getFileFromURL(url, true, null); 99 | 100 | assertNotEquals(file, tmp); 101 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(tmp), "UTF-8"))) { 102 | assertEquals("Hello", reader.readLine()); 103 | } 104 | } finally { 105 | file.delete(); 106 | } 107 | File jarFile = File.createTempFile("IOUtilsTes", ".jar"); 108 | try { 109 | 110 | try (JarOutputStream output = new JarOutputStream(new FileOutputStream(jarFile))) { 111 | output.putNextEntry(new JarEntry("test")); 112 | output.write("Hello".getBytes()); 113 | output.closeEntry(); 114 | } 115 | 116 | assertEquals( 117 | jarFile, 118 | IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/"), false, null)); 119 | assertNull(IOUtils.getFileFromURL(new URL("jar:file:" + jarFile.getPath() + "!/test"), false, null)); 120 | File tmpJar = 121 | IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), true, null); 122 | assertNotNull(tmpJar); 123 | assertNotEquals(jarFile, tmpJar); 124 | } finally { 125 | jarFile.delete(); 126 | } 127 | } 128 | 129 | @Test 130 | public void testGetJarFileFromURL() throws IOException { 131 | File jarFile = File.createTempFile("IOUtilsTest", ".jar"); 132 | 133 | try { 134 | try (JarOutputStream output = new JarOutputStream(new FileOutputStream(jarFile))) { 135 | output.putNextEntry(new JarEntry("test")); 136 | output.write("Hello".getBytes()); 137 | output.closeEntry(); 138 | output.putNextEntry(new JarEntry("test.jar")); 139 | try (JarOutputStream inner = new JarOutputStream(output)) { 140 | inner.putNextEntry(new JarEntry("inner")); 141 | inner.write("Hello".getBytes()); 142 | inner.closeEntry(); 143 | } 144 | } 145 | 146 | JarFile jar = 147 | IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/"), true, null); 148 | assertNotNull(jar); 149 | jar = IOUtils.getJarFileFromURL(jarFile.toURI().toURL(), true, null); 150 | assertNotNull(jar); 151 | 152 | assertNull(IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), false, null)); 153 | 154 | JarFile tmpJar = 155 | IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test.jar"), true, null); 156 | assertNotNull(tmpJar); 157 | assertNotNull(tmpJar.getEntry("inner")); 158 | 159 | try { 160 | IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), true, null); 161 | fail(); 162 | } catch (IOException ex) { 163 | // Expected 164 | } 165 | 166 | try { 167 | IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test.jar"), false, null); 168 | fail(); 169 | } catch (IOException ex) { 170 | // Expected 171 | } 172 | } finally { 173 | jarFile.delete(); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/json/FeatureJSONReaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.json; 20 | 21 | import java.io.IOException; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | 25 | import org.apache.sling.feature.ArtifactId; 26 | import org.apache.sling.feature.Bundles; 27 | import org.apache.sling.feature.Configuration; 28 | import org.apache.sling.feature.Extension; 29 | import org.apache.sling.feature.Extensions; 30 | import org.apache.sling.feature.Feature; 31 | import org.junit.Test; 32 | import org.osgi.resource.Capability; 33 | 34 | import static org.junit.Assert.assertArrayEquals; 35 | import static org.junit.Assert.assertEquals; 36 | import static org.junit.Assert.assertFalse; 37 | import static org.junit.Assert.assertNotNull; 38 | import static org.junit.Assert.assertNull; 39 | import static org.junit.Assert.assertTrue; 40 | 41 | public class FeatureJSONReaderTest { 42 | 43 | @Test 44 | public void testRead() throws Exception { 45 | final Feature feature = U.readFeature("test"); 46 | assertNotNull(feature); 47 | assertNotNull(feature.getId()); 48 | assertEquals("org.apache.sling", feature.getId().getGroupId()); 49 | assertEquals("test-feature", feature.getId().getArtifactId()); 50 | assertEquals("1.1", feature.getId().getVersion()); 51 | assertEquals("jar", feature.getId().getType()); 52 | assertNull(feature.getId().getClassifier()); 53 | 54 | assertEquals(2, feature.getConfigurations().size()); 55 | final Configuration cfg1 = U.findConfiguration(feature.getConfigurations(), "my.pid"); 56 | assertEquals(7, cfg1.getProperties().get("number")); 57 | final Configuration cfg2 = U.findConfiguration(feature.getConfigurations(), "my.factory.pid~name"); 58 | assertEquals("yeah", cfg2.getProperties().get("a.value")); 59 | 60 | assertEquals(3, feature.getCapabilities().size()); 61 | Capability capability = U.findCapability(feature.getCapabilities(), "osgi.service"); 62 | assertNotNull(capability.getAttributes().get("objectClass")); 63 | 64 | assertEquals( 65 | Arrays.asList("org.osgi.service.http.runtime.HttpServiceRuntime"), 66 | capability.getAttributes().get("objectClass")); 67 | } 68 | 69 | @Test 70 | public void testReadRepoInitExtension() throws Exception { 71 | Feature feature = U.readFeature("repoinit"); 72 | Extensions extensions = feature.getExtensions(); 73 | assertEquals(1, extensions.size()); 74 | Extension ext = extensions.iterator().next(); 75 | assertEquals("some repo init\ntext", ext.getText()); 76 | } 77 | 78 | @Test 79 | public void testReadRepoInitExtensionArray() throws Exception { 80 | Feature feature = U.readFeature("repoinit2"); 81 | Extensions extensions = feature.getExtensions(); 82 | assertEquals(1, extensions.size()); 83 | Extension ext = extensions.iterator().next(); 84 | assertEquals("some repo init\ntext\n", ext.getText()); 85 | } 86 | 87 | @Test 88 | public void testReadArtifactsExtensions() throws Exception { 89 | final Feature feature = U.readFeature("artifacts-extension"); 90 | ArtifactsExtensions.testReadArtifactsExtensions(feature); 91 | } 92 | 93 | @Test 94 | public void testFinalFlag() throws Exception { 95 | final Feature featureA = U.readFeature("test"); 96 | assertFalse(featureA.isFinal()); 97 | 98 | final Feature featureB = U.readFeature("final"); 99 | assertTrue(featureB.isFinal()); 100 | } 101 | 102 | @Test 103 | public void testCompleteFlag() throws Exception { 104 | final Feature featureA = U.readFeature("test"); 105 | assertFalse(featureA.isComplete()); 106 | 107 | final Feature featureB = U.readFeature("complete"); 108 | assertTrue(featureB.isComplete()); 109 | } 110 | 111 | @Test 112 | public void testReadMultiBSNVer() throws Exception { 113 | final Feature f = U.readFeature("test3"); 114 | Bundles fb = f.getBundles(); 115 | assertEquals(2, fb.size()); 116 | assertTrue(fb.containsExact(ArtifactId.fromMvnId("org.apache.sling:foo:1.2.3"))); 117 | assertTrue(fb.containsExact(ArtifactId.fromMvnId("org.apache.sling:foo:4.5.6"))); 118 | assertFalse(fb.containsExact(ArtifactId.fromMvnId("org.apache.sling:foo:7.8.9"))); 119 | } 120 | 121 | @Test 122 | public void readComments() throws Exception { 123 | // we only test whether the feature can be read without problems 124 | U.readFeature("feature-model"); 125 | } 126 | 127 | @Test 128 | public void testReadInternalData() throws Exception { 129 | final Feature feature = U.readFeature("test-metadata"); 130 | assertNotNull(feature); 131 | assertNotNull(feature.getId()); 132 | 133 | assertEquals("1", feature.getFrameworkProperties().get("foo")); 134 | assertEquals("hello", feature.getVariables().get("bar")); 135 | 136 | assertEquals(1, feature.getFrameworkPropertyMetadata("foo").size()); 137 | assertEquals(true, feature.getFrameworkPropertyMetadata("foo").get("bool")); 138 | 139 | assertEquals(1, feature.getVariableMetadata("bar").size()); 140 | assertEquals("hello world", feature.getVariableMetadata("bar").get("string")); 141 | 142 | assertNull(feature.getExtensions().getByName(Extension.EXTENSION_NAME_INTERNAL_DATA)); 143 | } 144 | 145 | @Test(expected = IOException.class) 146 | public void testReadConflictingConfigKeys() throws Exception { 147 | // This is expected to throw an exception since the same key is defined twice 148 | U.readFeature("test4"); 149 | } 150 | 151 | @Test 152 | public void testInternalConfigurationProperties() throws Exception { 153 | final Feature f = U.readFeature("internal-prop"); 154 | final Configuration c = f.getConfigurations().get(0); 155 | assertEquals(1, c.getConfigurationProperties().size()); 156 | assertEquals(5L, c.getConfigurationProperties().get("foo")); 157 | 158 | assertEquals(3, c.getProperties().size()); 159 | assertEquals(5L, c.getProperties().get("foo")); 160 | assertEquals(7L, c.getProperties().get(Configuration.PROP_PREFIX.concat("number"))); 161 | assertArrayEquals(new String[] {"org.apache.sling/test-feature/1.1"}, (String[]) 162 | c.getProperties().get(Configuration.PROP_FEATURE_ORIGINS)); 163 | 164 | final List origins = c.getFeatureOrigins(); 165 | assertEquals(1, origins.size()); 166 | assertEquals(ArtifactId.parse("org.apache.sling/test-feature/1.1"), origins.get(0)); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/io/artifacts/ArtifactManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.artifacts; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.net.URL; 24 | import java.net.URLConnection; 25 | import java.net.URLStreamHandler; 26 | import java.net.URLStreamHandlerFactory; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | import java.util.HashMap; 30 | import java.util.Map; 31 | import java.util.Random; 32 | 33 | import org.apache.sling.feature.io.artifacts.spi.ArtifactProvider; 34 | import org.junit.Test; 35 | 36 | import static org.junit.Assert.assertEquals; 37 | import static org.junit.Assert.assertNotNull; 38 | import static org.junit.Assert.assertTrue; 39 | import static org.mockito.Mockito.mock; 40 | import static org.mockito.Mockito.when; 41 | 42 | public class ArtifactManagerTest { 43 | 44 | private static final String METADATA = 45 | "\n" + "org.apache.sling.samples\n" 46 | + "slingshot\n" 47 | + "0-DEFAULT-SNAPSHOT\n" 48 | + "\n" 49 | + "\n" 50 | + "20160321.103951\n" 51 | + "1\n" 52 | + "\n" 53 | + "20160321103951\n" 54 | + "\n" 55 | + "\n" 56 | + "txt\n" 57 | + "0-DEFAULT-20160321.103951-1\n" 58 | + "20160321103951\n" 59 | + "\n" 60 | + "\n" 61 | + "pom\n" 62 | + "0-DEFAULT-20160321.103951-1\n" 63 | + "20160321103951\n" 64 | + "\n" 65 | + "\n" 66 | + ""; 67 | 68 | @Test 69 | public void testMetadataParsing() { 70 | final String version = ArtifactManager.getLatestSnapshot(METADATA); 71 | assertEquals("20160321.103951-1", version); 72 | } 73 | 74 | @Test 75 | public void testSnapshotHandling() throws IOException { 76 | final String REPO = "http://org.apache.sling"; 77 | final ArtifactManagerConfig config = mock(ArtifactManagerConfig.class); 78 | when(config.getRepositoryUrls()).thenReturn(new String[] {REPO}); 79 | 80 | final URL metadataFile = new URL("file:/maven-metadata.xml"); 81 | 82 | final URL artifactFile = new URL("file:/artifact"); 83 | 84 | final ArtifactProvider provider = mock(ArtifactProvider.class); 85 | when(provider.getArtifact( 86 | REPO + "/group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt", 87 | "group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt")) 88 | .thenReturn(null); 89 | when(provider.getArtifact( 90 | REPO + "/group/artifact/1.0.0-SNAPSHOT/maven-metadata.xml", 91 | "org.apache.sling/group/artifact/1.0.0-SNAPSHOT/maven-metadata.xml")) 92 | .thenReturn(metadataFile); 93 | when(provider.getArtifact( 94 | REPO + "/group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-20160321.103951-1.txt", 95 | "group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt")) 96 | .thenReturn(artifactFile); 97 | 98 | final Map providers = new HashMap<>(); 99 | providers.put("*", provider); 100 | 101 | final ArtifactManager mgr = new ArtifactManager(config, providers) { 102 | 103 | @Override 104 | protected String getFileContents(final ArtifactHandler handler) throws IOException { 105 | final String path = handler.getLocalURL().getPath(); 106 | if ("/maven-metadata.xml".equals(path)) { 107 | return METADATA; 108 | } 109 | return super.getFileContents(handler); 110 | } 111 | }; 112 | 113 | final ArtifactHandler handler = mgr.getArtifactHandler("mvn:group/artifact/1.0.0-SNAPSHOT/txt"); 114 | assertNotNull(handler); 115 | assertEquals(artifactFile, handler.getLocalURL()); 116 | } 117 | 118 | @Test 119 | public void testGetArtifactManager() throws Exception { 120 | ArtifactManagerConfig cfg = new ArtifactManagerConfig(); 121 | ArtifactManager am = ArtifactManager.getArtifactManager(cfg); 122 | 123 | am.shutdown(); 124 | } 125 | 126 | @Test 127 | public void testGetArtifactManagerWithCachedir() throws Exception { 128 | ArtifactManagerConfig cfg = new ArtifactManagerConfig(); 129 | Path tempDir = Files.createTempDirectory("testGetArtifactManagerWithCachedir"); 130 | cfg.setCacheDirectory(tempDir.toFile()); 131 | 132 | ArtifactManager am = ArtifactManager.getArtifactManager(cfg); 133 | 134 | am.shutdown(); 135 | assertTrue(tempDir.toFile().isDirectory()); 136 | assertTrue(tempDir.toFile().delete()); 137 | } 138 | 139 | @Test 140 | public void testGetArtifactHandler() throws Exception { 141 | ArtifactManagerConfig cfg = new ArtifactManagerConfig(); 142 | cfg.setRepositoryUrls(new String[] {"https://repo.maven.apache.org/maven2"}); 143 | 144 | ArtifactManager am = ArtifactManager.getArtifactManager(cfg); 145 | 146 | assertNotNull(am.getArtifactHandler( 147 | ":org/apache/felix/org.apache.felix.framework/7.0.1/org.apache.felix.framework-7.0.1.jar")); 148 | assertNotNull(am.getArtifactHandler("mvn:org.apache.felix/org.apache.felix.framework/7.0.1")); 149 | assertNotNull( 150 | am.getArtifactHandler( 151 | "src/test/resources/m2/org/apache/felix/org.apache.felix.framework/7.0.1/org.apache.felix.framework-7.0.1.jar")); 152 | 153 | String fooProtocol = "foo" + new Random().nextLong(); 154 | URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() { 155 | @Override 156 | public URLStreamHandler createURLStreamHandler(String protocol) { 157 | if (fooProtocol.equals(protocol)) { 158 | return new URLStreamHandler() { 159 | @Override 160 | protected URLConnection openConnection(URL u) throws IOException { 161 | return new File( 162 | "src/test/resources/m2/org/apache/felix/org.apache.felix.framework/7.0.1/org.apache.felix.framework-7.0.1.jar") 163 | .toURL() 164 | .openConnection(); 165 | } 166 | }; 167 | } else { 168 | return null; 169 | } 170 | } 171 | }); 172 | assertNotNull(am.getArtifactHandler(fooProtocol + ":/felix.jar")); 173 | 174 | am.shutdown(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/artifacts/ArtifactManagerConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.artifacts; 20 | 21 | import javax.xml.XMLConstants; 22 | import javax.xml.parsers.DocumentBuilder; 23 | import javax.xml.parsers.DocumentBuilderFactory; 24 | import javax.xml.parsers.ParserConfigurationException; 25 | 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.util.concurrent.atomic.AtomicLong; 29 | 30 | import org.apache.sling.feature.io.artifacts.spi.ArtifactProviderContext; 31 | import org.jetbrains.annotations.NotNull; 32 | import org.jetbrains.annotations.Nullable; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | import org.w3c.dom.Document; 36 | import org.w3c.dom.NodeList; 37 | import org.xml.sax.SAXException; 38 | 39 | /** 40 | * This class holds the configuration of artifact manager. 41 | * This class is not thread-safe. 42 | */ 43 | public class ArtifactManagerConfig implements ArtifactProviderContext { 44 | 45 | /** The repository urls. */ 46 | private String[] repositoryUrls; 47 | 48 | /** The cache directory. */ 49 | private File cacheDirectory; 50 | 51 | /** Metrics for artifacts used from the cache. */ 52 | private final AtomicLong cachedArtifacts = new AtomicLong(); 53 | 54 | /** Metrics for artifacts needed to be downloaded. */ 55 | private final AtomicLong downloadedArtifacts = new AtomicLong(); 56 | 57 | /** Metrics for artifacts read locally. */ 58 | private final AtomicLong localArtifacts = new AtomicLong(); 59 | 60 | /** Whether locally mvn command can be used to download artifacts. */ 61 | private boolean useMvn = false; 62 | 63 | /** 64 | * The .m2 directory. 65 | */ 66 | private final @NotNull String repoHome; 67 | 68 | private static final Logger logger = LoggerFactory.getLogger(ArtifactManagerConfig.class); 69 | 70 | /** 71 | * Create a new configuration object. Set the default values 72 | */ 73 | public ArtifactManagerConfig() { 74 | // set defaults 75 | String mvnRepositoryDirectory = getMvnRepositoryDirectory(); 76 | this.repositoryUrls = new String[] { 77 | toFileUrl(mvnRepositoryDirectory), 78 | "https://repo.maven.apache.org/maven2", 79 | "https://repository.apache.org/content/groups/snapshots" 80 | }; 81 | this.repoHome = mvnRepositoryDirectory + "/"; 82 | } 83 | 84 | /** 85 | * Set the repository urls 86 | * @param urls The repository urls 87 | */ 88 | public void setRepositoryUrls(final String[] urls) { 89 | if (urls == null || urls.length == 0) { 90 | this.repositoryUrls = new String[0]; 91 | } else { 92 | this.repositoryUrls = new String[urls.length]; 93 | System.arraycopy(urls, 0, this.repositoryUrls, 0, urls.length); 94 | for (int i = 0; i < this.repositoryUrls.length; i++) { 95 | if (this.repositoryUrls[i].endsWith("/")) { 96 | this.repositoryUrls[i] = this.repositoryUrls[i].substring(0, this.repositoryUrls[i].length() - 1); 97 | } 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Get the repository urls. 104 | * A repository url does not end with a slash. 105 | * @return The repository urls. 106 | */ 107 | public @NotNull String[] getRepositoryUrls() { 108 | return repositoryUrls; 109 | } 110 | 111 | /** 112 | * Get the cache directory 113 | * @return The cache directory or {@code null} if none has been set 114 | */ 115 | @Override 116 | public @Nullable File getCacheDirectory() { 117 | return cacheDirectory; 118 | } 119 | 120 | /** 121 | * Set the cache directory 122 | * @param dir The cache directory 123 | */ 124 | public void setCacheDirectory(final File dir) { 125 | this.cacheDirectory = dir; 126 | } 127 | 128 | @Override 129 | public void incCachedArtifacts() { 130 | this.cachedArtifacts.incrementAndGet(); 131 | } 132 | 133 | @Override 134 | public void incDownloadedArtifacts() { 135 | this.downloadedArtifacts.incrementAndGet(); 136 | } 137 | 138 | @Override 139 | public void incLocalArtifacts() { 140 | this.localArtifacts.incrementAndGet(); 141 | } 142 | 143 | /** 144 | * Get the number of cached artifacts 145 | * @return The number of cached artifacts 146 | */ 147 | public long getCachedArtifacts() { 148 | return this.cachedArtifacts.get(); 149 | } 150 | 151 | /** 152 | * Get the number of downloaded artifacts 153 | * @return The number of downloaded artifacts 154 | */ 155 | public long getDownloadedArtifacts() { 156 | return this.downloadedArtifacts.get(); 157 | } 158 | 159 | /** 160 | * Get the number of local artifacts 161 | * @return The number of local artifacts 162 | */ 163 | public long getLocalArtifacts() { 164 | return this.localArtifacts.get(); 165 | } 166 | 167 | /** 168 | * Should mvn be used if an artifact can't be found in the repositories 169 | * 170 | * @return Whether mvn command should be used. 171 | * @since 1.1.0 172 | */ 173 | public boolean isUseMvn() { 174 | return useMvn; 175 | } 176 | 177 | /** 178 | * Set whether mvn should be used to get artifacts. 179 | * 180 | * @param useMvn flag for enabling mvn 181 | * @since 1.1.0 182 | */ 183 | public void setUseMvn(final boolean useMvn) { 184 | this.useMvn = useMvn; 185 | } 186 | 187 | /** 188 | * Return mvn home 189 | * 190 | * @since 1.1.0 191 | */ 192 | @NotNull 193 | String getMvnHome() { 194 | return this.repoHome; 195 | } 196 | 197 | /** 198 | * Get the .m2 directory for the current user. 199 | * If a ~/.m2/settings.xml exists, it is checked for a localRepository configuration. 200 | * Otherwise ~/.m2/repository is used as default value. 201 | * @return Maven repository directory 202 | */ 203 | private static final String getMvnRepositoryDirectory() { 204 | String mavenDirectory = System.getProperty("user.home") + "/.m2"; 205 | File settings = new File(mavenDirectory, "settings.xml"); 206 | if (settings.exists()) { 207 | try { 208 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 209 | factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 210 | DocumentBuilder builder = factory.newDocumentBuilder(); 211 | Document doc = builder.parse(settings); 212 | NodeList nodes = doc.getElementsByTagName("localRepository"); 213 | if (nodes.getLength() > 0) { 214 | String localRepository = nodes.item(0).getTextContent(); 215 | return new File(localRepository).getAbsolutePath(); 216 | } 217 | } catch (ParserConfigurationException | SAXException | IOException ex) { 218 | logger.warn("Unable to parse " + settings.getAbsolutePath(), ex); 219 | } 220 | } 221 | return mavenDirectory + "/repository"; 222 | } 223 | 224 | static final String toFileUrl(String path) { 225 | return "file://" + new File(path).toURI().getPath(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/archive/ArchiveReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.archive; 20 | 21 | import java.io.ByteArrayInputStream; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.InputStreamReader; 25 | import java.io.Reader; 26 | import java.io.StringReader; 27 | import java.io.StringWriter; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.Arrays; 30 | import java.util.HashSet; 31 | import java.util.List; 32 | import java.util.Set; 33 | import java.util.jar.JarEntry; 34 | import java.util.jar.JarInputStream; 35 | import java.util.jar.Manifest; 36 | import java.util.stream.Collectors; 37 | 38 | import org.apache.sling.feature.Artifact; 39 | import org.apache.sling.feature.ArtifactId; 40 | import org.apache.sling.feature.Extension; 41 | import org.apache.sling.feature.ExtensionType; 42 | import org.apache.sling.feature.Feature; 43 | import org.apache.sling.feature.io.json.FeatureJSONReader; 44 | 45 | /** 46 | * The feature archive reader can be used to read an archive based on a feature 47 | * model. The archive contains the model and all artifacts. 48 | */ 49 | public class ArchiveReader { 50 | 51 | @FunctionalInterface 52 | public interface ArtifactConsumer { 53 | 54 | /** 55 | * Consume the artifact from the archive The input stream must not be closed by 56 | * the consumer. 57 | * 58 | * @param artifactId The artifact id 59 | * @param is The input stream for the artifact 60 | * @throws IOException If the artifact can't be consumed 61 | */ 62 | void consume(ArtifactId artifactId, final InputStream is) throws IOException; 63 | } 64 | 65 | /** 66 | * Read a feature model archive. The input stream is not closed. It is up to the 67 | * caller to close the input stream. 68 | * 69 | * @param in The input stream to read from. 70 | * @param consumer The plugin consuming the binaries, including the features. 71 | * If no consumer is provided, only the features will be returned. 72 | * @return The feature models mentioned in the manifest of the archive 73 | * @throws IOException If anything goes wrong 74 | */ 75 | public static Set read(final InputStream in, final ArtifactConsumer consumer) throws IOException { 76 | final JarInputStream jis = new JarInputStream(in); 77 | 78 | // validate manifest and get feature ids 79 | final String[] featureIds = checkHeaderAndExtractContents(jis.getManifest()); 80 | final List featurePaths = Arrays.asList(featureIds).stream() 81 | .map(id -> ArtifactId.parse(id).toMvnPath()) 82 | .collect(Collectors.toList()); 83 | 84 | // read contents 85 | final Set features = new HashSet<>(); 86 | final Set artifacts = new HashSet<>(); 87 | 88 | JarEntry entry = null; 89 | while ((entry = jis.getNextJarEntry()) != null) { 90 | if (!entry.isDirectory() && !entry.getName().startsWith("META-INF/")) { 91 | final ArtifactId id = ArtifactId.fromMvnPath(entry.getName()); 92 | 93 | if (featurePaths.contains(entry.getName())) { 94 | // feature - read to string first 95 | final String contents; 96 | try (final StringWriter writer = new StringWriter()) { 97 | // don't close the input stream 98 | final Reader reader = new InputStreamReader(jis, "UTF-8"); 99 | final char[] buffer = new char[2048]; 100 | int l; 101 | while ((l = reader.read(buffer)) > 0) { 102 | writer.write(buffer, 0, l); 103 | } 104 | writer.flush(); 105 | contents = writer.toString(); 106 | } 107 | // add to features 108 | try (final Reader reader = new StringReader(contents)) { 109 | final Feature feature = FeatureJSONReader.read(reader, entry.getName()); 110 | features.add(feature); 111 | } 112 | // pass to consumer 113 | if (consumer != null) { 114 | try (final InputStream is = 115 | new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8))) { 116 | consumer.consume(id, is); 117 | } 118 | } 119 | } else { 120 | // artifact 121 | if (consumer != null) { 122 | consumer.consume(id, jis); 123 | } 124 | artifacts.add(id); 125 | } 126 | } 127 | jis.closeEntry(); 128 | } 129 | if (features.isEmpty()) { 130 | throw new IOException("Not a feature model archive - feature file is missing."); 131 | } 132 | 133 | // check whether all artifacts from the models are in the archive 134 | for (final Feature feature : features) { 135 | for (final Artifact a : feature.getBundles()) { 136 | if (!artifacts.contains(a.getId())) { 137 | throw new IOException("Artifact " + a.getId().toMvnId() + " is missing in archive"); 138 | } 139 | } 140 | 141 | for (final Extension e : feature.getExtensions()) { 142 | if (e.getType() == ExtensionType.ARTIFACTS) { 143 | for (final Artifact a : e.getArtifacts()) { 144 | if (!artifacts.contains(a.getId())) { 145 | throw new IOException("Artifact " + a.getId().toMvnId() + " is missing in archive"); 146 | } 147 | } 148 | } 149 | } 150 | } 151 | return features; 152 | } 153 | 154 | private static String[] checkHeaderAndExtractContents(final Manifest manifest) throws IOException { 155 | if (manifest == null) { 156 | throw new IOException("Not a feature model archive - manifest is missing."); 157 | } 158 | // check version header 159 | final String version = manifest.getMainAttributes().getValue(ArchiveWriter.VERSION_HEADER); 160 | if (version == null) { 161 | throw new IOException("Not a feature model archive - version manifest header is missing."); 162 | } 163 | // validate version header 164 | try { 165 | final int number = Integer.valueOf(version); 166 | if (number < 1 || number > ArchiveWriter.ARCHIVE_VERSION) { 167 | throw new IOException("Not a feature model archive - invalid manifest header value: " + version); 168 | } 169 | } catch (final NumberFormatException nfe) { 170 | throw new IOException("Not a feature model archive - invalid manifest header value: " + version); 171 | } 172 | 173 | // check contents header 174 | final String contents = manifest.getMainAttributes().getValue(ArchiveWriter.CONTENTS_HEADER); 175 | if (contents == null) { 176 | throw new IOException("Not a feature model archive - contents manifest header is missing."); 177 | } 178 | 179 | return contents.split(","); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/test/java/org/apache/sling/feature/ConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | 24 | import org.junit.Test; 25 | 26 | import static org.junit.Assert.assertArrayEquals; 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.assertFalse; 29 | import static org.junit.Assert.assertNotNull; 30 | import static org.junit.Assert.assertNull; 31 | import static org.junit.Assert.assertTrue; 32 | import static org.junit.Assert.fail; 33 | 34 | public class ConfigurationTest { 35 | 36 | @Test 37 | public void testNullArgument() { 38 | try { 39 | new Configuration(null); 40 | fail(); 41 | } catch (final IllegalArgumentException iae) { 42 | // expected 43 | } 44 | } 45 | 46 | @Test 47 | public void testFactoryPid() { 48 | final Configuration fc = new Configuration("org.apache.sling.factory~script"); 49 | assertTrue(fc.isFactoryConfiguration()); 50 | assertEquals("org.apache.sling.factory", fc.getFactoryPid()); 51 | assertEquals("script", fc.getName()); 52 | assertEquals("org.apache.sling.factory~script", fc.getPid()); 53 | } 54 | 55 | @Test 56 | public void testPid() { 57 | final Configuration c = new Configuration("org.apache.sling.script"); 58 | assertFalse(c.isFactoryConfiguration()); 59 | assertNull(c.getFactoryPid()); 60 | assertNull(c.getName()); 61 | assertEquals("org.apache.sling.script", c.getPid()); 62 | } 63 | 64 | @Test 65 | public void testStaticFactoryPidMethods() { 66 | final String factoryPid = "org.apache.sling.factory~script"; 67 | final String pid = "org.apache.sling.script"; 68 | 69 | assertTrue(Configuration.isFactoryConfiguration(factoryPid)); 70 | assertEquals("org.apache.sling.factory", Configuration.getFactoryPid(factoryPid)); 71 | assertEquals("script", Configuration.getName(factoryPid)); 72 | 73 | assertFalse(Configuration.isFactoryConfiguration(pid)); 74 | assertNull(Configuration.getFactoryPid(pid)); 75 | assertNull(Configuration.getName(pid)); 76 | } 77 | 78 | @Test 79 | public void testFeatureOrigins() { 80 | final ArtifactId self = ArtifactId.parse("self:self:1"); 81 | 82 | final Configuration cfg = new Configuration("foo"); 83 | assertTrue(cfg.getFeatureOrigins().isEmpty()); 84 | assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS)); 85 | assertNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS)); 86 | assertEquals(1, cfg.getFeatureOrigins(self).size()); 87 | assertEquals(self, cfg.getFeatureOrigins(self).get(0)); 88 | 89 | // single id 90 | final ArtifactId id = ArtifactId.parse("g:a:1"); 91 | cfg.setFeatureOrigins(Collections.singletonList(id)); 92 | assertEquals(1, cfg.getFeatureOrigins().size()); 93 | assertEquals(id, cfg.getFeatureOrigins().get(0)); 94 | assertEquals(1, cfg.getFeatureOrigins(self).size()); 95 | assertEquals(id, cfg.getFeatureOrigins(self).get(0)); 96 | 97 | assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS)); 98 | assertNotNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS)); 99 | final String[] array = (String[]) cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS); 100 | assertArrayEquals(new String[] {id.toMvnId()}, array); 101 | 102 | // add another id 103 | final ArtifactId id2 = ArtifactId.parse("g:b:2"); 104 | cfg.setFeatureOrigins(Arrays.asList(id, id2)); 105 | assertEquals(2, cfg.getFeatureOrigins().size()); 106 | assertEquals(id, cfg.getFeatureOrigins().get(0)); 107 | assertEquals(id2, cfg.getFeatureOrigins().get(1)); 108 | assertEquals(2, cfg.getFeatureOrigins(self).size()); 109 | assertEquals(id, cfg.getFeatureOrigins(self).get(0)); 110 | assertEquals(id2, cfg.getFeatureOrigins(self).get(1)); 111 | 112 | assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS)); 113 | assertNotNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS)); 114 | final String[] array2 = (String[]) cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS); 115 | assertArrayEquals(new String[] {id.toMvnId(), id2.toMvnId()}, array2); 116 | } 117 | 118 | @Test 119 | public void testDuplicateConfigKeys() throws Exception { 120 | Configuration c1 = new Configuration("a.b.c"); 121 | c1.getProperties().put("aaa", "123"); 122 | c1.getProperties().put("AaA", "456"); 123 | 124 | assertEquals( 125 | "As keys are case insensitive, there should just be 1 key", 126 | 1, 127 | c1.getProperties().size()); 128 | } 129 | 130 | @Test 131 | public void testPropertyFeatureOrigins() { 132 | final ArtifactId self = ArtifactId.parse("self:self:1"); 133 | 134 | final Configuration cfg = new Configuration("foo"); 135 | cfg.getProperties().put("a", true); 136 | assertTrue(cfg.getFeatureOrigins("a").isEmpty()); 137 | assertNull(cfg.getConfigurationProperties() 138 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 139 | assertNull(cfg.getProperties() 140 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 141 | assertEquals(1, cfg.getFeatureOrigins("a", self).size()); 142 | assertEquals(self, cfg.getFeatureOrigins("a", self).get(0)); 143 | 144 | // single id 145 | final ArtifactId id = ArtifactId.parse("g:a:1"); 146 | cfg.setFeatureOrigins("a", Collections.singletonList(id)); 147 | assertEquals(1, cfg.getFeatureOrigins("a").size()); 148 | assertEquals(id, cfg.getFeatureOrigins("a").get(0)); 149 | assertEquals(1, cfg.getFeatureOrigins("a", self).size()); 150 | assertEquals(id, cfg.getFeatureOrigins("a", self).get(0)); 151 | 152 | assertNull(cfg.getConfigurationProperties() 153 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 154 | assertNotNull(cfg.getProperties() 155 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 156 | final String[] array = (String[]) cfg.getProperties() 157 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a")); 158 | assertArrayEquals(new String[] {id.toMvnId()}, array); 159 | 160 | // add another id 161 | final ArtifactId id2 = ArtifactId.parse("g:b:2"); 162 | cfg.setFeatureOrigins("a", Arrays.asList(id, id2)); 163 | assertEquals(2, cfg.getFeatureOrigins("a").size()); 164 | assertEquals(id, cfg.getFeatureOrigins("a").get(0)); 165 | assertEquals(id2, cfg.getFeatureOrigins("a").get(1)); 166 | assertEquals(2, cfg.getFeatureOrigins("a", self).size()); 167 | assertEquals(id, cfg.getFeatureOrigins("a", self).get(0)); 168 | assertEquals(id2, cfg.getFeatureOrigins("a", self).get(1)); 169 | 170 | assertNull(cfg.getConfigurationProperties() 171 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 172 | assertNotNull(cfg.getProperties() 173 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 174 | final String[] array2 = (String[]) cfg.getProperties() 175 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a")); 176 | assertArrayEquals(new String[] {id.toMvnId(), id2.toMvnId()}, array2); 177 | 178 | // remove 179 | cfg.getProperties().remove("a"); 180 | cfg.setFeatureOrigins("a", null); 181 | assertTrue(cfg.getFeatureOrigins("a").isEmpty()); 182 | assertNull(cfg.getConfigurationProperties() 183 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 184 | assertNull(cfg.getProperties() 185 | .get(Configuration.PROP_FEATURE_ORIGINS.concat("-").concat("a"))); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/io/archive/ArchiveWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature.io.archive; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.OutputStream; 25 | import java.io.OutputStreamWriter; 26 | import java.io.Reader; 27 | import java.io.StringReader; 28 | import java.io.Writer; 29 | import java.net.URL; 30 | import java.nio.charset.StandardCharsets; 31 | import java.util.Arrays; 32 | import java.util.HashSet; 33 | import java.util.Set; 34 | import java.util.jar.JarEntry; 35 | import java.util.jar.JarOutputStream; 36 | import java.util.jar.Manifest; 37 | import java.util.stream.Collectors; 38 | import java.util.zip.Deflater; 39 | 40 | import org.apache.sling.feature.Artifact; 41 | import org.apache.sling.feature.ArtifactId; 42 | import org.apache.sling.feature.Extension; 43 | import org.apache.sling.feature.ExtensionType; 44 | import org.apache.sling.feature.Feature; 45 | import org.apache.sling.feature.builder.ArtifactProvider; 46 | import org.apache.sling.feature.io.json.FeatureJSONReader; 47 | import org.apache.sling.feature.io.json.FeatureJSONWriter; 48 | 49 | /** 50 | * The feature archive writer can be used to create an archive based on a 51 | * feature model. The archive contains the feature model file and all artifacts 52 | * using a maven repository layout. 53 | */ 54 | public class ArchiveWriter { 55 | 56 | /** The manifest header marking an archive as a feature archive. */ 57 | public static final String VERSION_HEADER = "Feature-Archive-Version"; 58 | 59 | /** The manifest header listing the features in this archive. */ 60 | public static final String CONTENTS_HEADER = "Feature-Archive-Contents"; 61 | 62 | /** Current support version of the feature model archive. */ 63 | public static final int ARCHIVE_VERSION = 1; 64 | 65 | /** 66 | * Create a feature model archive. The output stream will not be closed by this 67 | * method. The caller must call {@link JarOutputStream#close()} 68 | * on the return output stream. The caller can 69 | * add additional files through the return stream. However, the files 70 | * should not be compressed (which is the default for the output stream). 71 | * 72 | * A feature model can be in different states: it might be a partial feature 73 | * model, a complete feature model or an assembled feature model. This method 74 | * takes the feature model as provided and only writes the listed bundles and 75 | * artifacts of this feature model into the archive. In general, the best 76 | * approach for sharing features is to archive {@link Feature#isComplete() 77 | * complete} features. 78 | * 79 | * @param out The output stream to write to 80 | * @param baseManifest Optional base manifest used for creating the manifest. 81 | * @param provider The artifact provider 82 | * @param features The features model to archive 83 | * @return The jar output stream. 84 | * @throws IOException If anything goes wrong 85 | */ 86 | public static JarOutputStream write( 87 | final OutputStream out, 88 | final Manifest baseManifest, 89 | final ArtifactProvider provider, 90 | final Feature... features) 91 | throws IOException { 92 | // create manifest 93 | final Manifest manifest = (baseManifest == null ? new Manifest() : new Manifest(baseManifest)); 94 | manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); 95 | manifest.getMainAttributes().putValue(VERSION_HEADER, String.valueOf(ARCHIVE_VERSION)); 96 | manifest.getMainAttributes() 97 | .putValue( 98 | CONTENTS_HEADER, 99 | String.join( 100 | ",", 101 | Arrays.asList(features).stream() 102 | .map(feature -> feature.getId().toMvnId()) 103 | .collect(Collectors.toList()))); 104 | 105 | final Set artifacts = new HashSet<>(); 106 | final byte[] buffer = new byte[1024 * 1024 * 256]; 107 | 108 | // create archive 109 | final JarOutputStream jos = new JarOutputStream(out, manifest); 110 | 111 | // write everything without compression 112 | jos.setLevel(Deflater.NO_COMPRESSION); 113 | for (final Feature feature : features) { 114 | writeFeature(artifacts, feature, provider, jos, buffer); 115 | } 116 | 117 | for (final Feature feature : features) { 118 | for (final Artifact a : feature.getBundles()) { 119 | writeArtifact(artifacts, provider, a, jos, buffer); 120 | } 121 | 122 | for (final Extension e : feature.getExtensions()) { 123 | if (e.getType() == ExtensionType.ARTIFACTS) { 124 | final boolean isFeature = Extension.EXTENSION_NAME_ASSEMBLED_FEATURES.equals(e.getName()); 125 | for (final Artifact a : e.getArtifacts()) { 126 | if (isFeature) { 127 | writeFeature(artifacts, provider, a.getId(), jos, buffer); 128 | } else { 129 | writeArtifact(artifacts, provider, a, jos, buffer); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | return jos; 136 | } 137 | 138 | private static void writeFeature( 139 | final Set artifacts, 140 | final Feature feature, 141 | final ArtifactProvider provider, 142 | final JarOutputStream jos, 143 | final byte[] buffer) 144 | throws IOException { 145 | if (artifacts.add(feature.getId())) { 146 | final JarEntry entry = new JarEntry(feature.getId().toMvnPath()); 147 | jos.putNextEntry(entry); 148 | final Writer writer = new OutputStreamWriter(jos, StandardCharsets.UTF_8); 149 | FeatureJSONWriter.write(writer, feature); 150 | writer.flush(); 151 | jos.closeEntry(); 152 | 153 | if (feature.getPrototype() != null) { 154 | writeFeature(artifacts, provider, feature.getPrototype().getId(), jos, buffer); 155 | } 156 | } 157 | } 158 | 159 | private static void writeFeature( 160 | final Set artifacts, 161 | final ArtifactProvider provider, 162 | final ArtifactId featureId, 163 | final JarOutputStream jos, 164 | final byte[] buffer) 165 | throws IOException { 166 | if (!artifacts.contains(featureId)) { 167 | final URL url = provider.provide(featureId); 168 | try (final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 169 | final InputStream is = url.openStream()) { 170 | int l = 0; 171 | while ((l = is.read(buffer)) > 0) { 172 | baos.write(buffer, 0, l); 173 | } 174 | final String contents = new String(baos.toByteArray(), StandardCharsets.UTF_8); 175 | try (final Reader reader = new StringReader(contents)) { 176 | final Feature feature = FeatureJSONReader.read(reader, featureId.toMvnId()); 177 | writeFeature(artifacts, feature, provider, jos, buffer); 178 | } 179 | } 180 | } 181 | } 182 | 183 | private static void writeArtifact( 184 | final Set artifacts, 185 | final ArtifactProvider provider, 186 | final Artifact artifact, 187 | final JarOutputStream jos, 188 | final byte[] buffer) 189 | throws IOException { 190 | if (artifacts.add(artifact.getId())) { 191 | final JarEntry artifactEntry = new JarEntry(artifact.getId().toMvnPath()); 192 | jos.putNextEntry(artifactEntry); 193 | 194 | final URL url = provider.provide(artifact.getId()); 195 | if (url == null) { 196 | throw new IOException( 197 | "Unable to find artifact " + artifact.getId().toMvnId()); 198 | } 199 | try (final InputStream is = url.openStream()) { 200 | int l = 0; 201 | while ((l = is.read(buffer)) > 0) { 202 | jos.write(buffer, 0, l); 203 | } 204 | } 205 | jos.closeEntry(); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/org/apache/sling/feature/Extension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package org.apache.sling.feature; 20 | 21 | import java.io.IOException; 22 | import java.io.ObjectInputStream; 23 | import java.io.Serializable; 24 | import java.io.StringReader; 25 | import java.io.StringWriter; 26 | 27 | import jakarta.json.Json; 28 | import jakarta.json.JsonStructure; 29 | import jakarta.json.JsonWriter; 30 | 31 | /** 32 | * An Extension can either be of type 33 | *
    34 | *
  • Artifacts : it contains a list of artifacts 35 | *
  • Text : it contains text 36 | *
  • JSON : it contains a blob of JSON 37 | *
38 | *

39 | * An extension can be in one of these states 40 | *

    41 | *
  • Required : Required extensions need to be processed by tooling 42 | *
  • Optional : Optional extensions might be processed by tooling, for example 43 | * they might contain environment specific parts 44 | *
  • Transient: Transient extensions are cache like extensions where tooling 45 | * can store additional information to avoid reprocessing of down stream 46 | * tooling. However such tooling must work without the transient extension being 47 | * available. 48 | *
49 | *

50 | * This class is not thread-safe. 51 | * 52 | * @see ExtensionType 53 | */ 54 | public class Extension implements Serializable { 55 | 56 | private static final long serialVersionUID = 2L; 57 | 58 | /** 59 | * Common extension name to specify the repoinit part for Apache Sling. This 60 | * extension is of type {@link ExtensionType#TEXT} and is required. 61 | */ 62 | public static final String EXTENSION_NAME_REPOINIT = "repoinit"; 63 | 64 | /** 65 | * Common extension name to specify the content packages for Apache Sling. This 66 | * extension is of type {@link ExtensionType#ARTIFACTS} and is required. 67 | */ 68 | public static final String EXTENSION_NAME_CONTENT_PACKAGES = "content-packages"; 69 | 70 | /** 71 | * Extension name containing the assembled features as produced by 72 | * {@link org.apache.sling.feature.builder.FeatureBuilder#assemble(ArtifactId, BuilderContext, Feature...)}. 73 | * This extension is of type {@link ExtensionType#ARTIFACTS} and is optional. 74 | */ 75 | public static final String EXTENSION_NAME_ASSEMBLED_FEATURES = "assembled-features"; 76 | 77 | /** 78 | * Extension name containing internal data. An extension with this name must not be created by 79 | * hand, it is managed by the feature model implementation. 80 | * This extension is of type {@link ExtensionType#JSON} and is optional. 81 | * @since 1.7.0 82 | */ 83 | public static final String EXTENSION_NAME_INTERNAL_DATA = "feature-internal-data"; 84 | 85 | /** The extension type */ 86 | private final ExtensionType type; 87 | 88 | /** The extension name. */ 89 | private final String name; 90 | 91 | /** The list of artifacts (if type artifacts) */ 92 | private final Artifacts artifacts; 93 | 94 | /** The text or json (if corresponding type) */ 95 | private String text; 96 | 97 | /** The json structure (if corresponding type) */ 98 | private transient JsonStructure json; 99 | 100 | /** Extension state. */ 101 | private final ExtensionState state; 102 | 103 | /** 104 | * Create a new extension 105 | * 106 | * @param type The type of the extension 107 | * @param name The name of the extension 108 | * @param state The state of the extension 109 | * @throws IllegalArgumentException If name, type or state is {@code null} 110 | * @since 1.1 111 | */ 112 | public Extension(final ExtensionType type, final String name, final ExtensionState state) { 113 | if (type == null || name == null || state == null) { 114 | throw new IllegalArgumentException("Argument must not be null"); 115 | } 116 | this.type = type; 117 | this.name = name; 118 | this.state = state; 119 | if (type == ExtensionType.ARTIFACTS) { 120 | this.artifacts = new Artifacts(); 121 | } else { 122 | this.artifacts = null; 123 | } 124 | } 125 | 126 | private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 127 | in.defaultReadObject(); 128 | // initialize json object 129 | if (this.type == ExtensionType.JSON) { 130 | this.setJSON(this.text); 131 | } 132 | } 133 | 134 | /** 135 | * Get the extension type 136 | * @return The type 137 | */ 138 | public ExtensionType getType() { 139 | return this.type; 140 | } 141 | 142 | /** 143 | * Get the extension state 144 | * 145 | * @return The state 146 | * @since 1.1 147 | */ 148 | public ExtensionState getState() { 149 | return this.state; 150 | } 151 | 152 | /** 153 | * Get the extension name 154 | * 155 | * @return The name 156 | */ 157 | public String getName() { 158 | return name; 159 | } 160 | 161 | /** 162 | * Get the text of the extension 163 | * @return The text 164 | * @throws IllegalStateException if the type is not {@code ExtensionType#TEXT} 165 | */ 166 | public String getText() { 167 | if (type != ExtensionType.TEXT) { 168 | throw new IllegalStateException(); 169 | } 170 | return text; 171 | } 172 | 173 | /** 174 | * Set the text of the extension 175 | * @param text The text 176 | * @throws IllegalStateException if the type is not {@code ExtensionType#TEXT} 177 | */ 178 | public void setText(final String text) { 179 | if (type != ExtensionType.TEXT) { 180 | throw new IllegalStateException(); 181 | } 182 | this.text = text; 183 | } 184 | 185 | /** 186 | * Get the JSON of the extension 187 | * 188 | * @return The JSON or {@code null} 189 | * @throws IllegalStateException if the type is not {@code ExtensionType#JSON} 190 | */ 191 | public String getJSON() { 192 | if (type != ExtensionType.JSON) { 193 | throw new IllegalStateException(); 194 | } 195 | return text; 196 | } 197 | 198 | /** 199 | * Set the JSON of the extension 200 | * 201 | * @param text The JSON 202 | * @throws IllegalStateException if the type is not 203 | * {@code ExtensionType#JSON} 204 | * @throws IllegalArgumentException If the structure is not valid 205 | */ 206 | public void setJSON(String text) { 207 | if (type != ExtensionType.JSON) { 208 | throw new IllegalStateException(); 209 | } 210 | this.text = text; 211 | try (final StringReader reader = new StringReader(text)) { 212 | this.json = Json.createReader(reader).read(); 213 | } 214 | } 215 | 216 | /** 217 | * Get the JSON structure of the extension 218 | * 219 | * @return The JSON object or {@code null} 220 | * @throws IllegalStateException if the type is not {@code ExtensionType#JSON} 221 | * @since 1.1 222 | */ 223 | public JsonStructure getJSONStructure() { 224 | if (type != ExtensionType.JSON) { 225 | throw new IllegalStateException(); 226 | } 227 | return json; 228 | } 229 | 230 | /** 231 | * Set the JSON structure of the extension 232 | * 233 | * @param struct The JSON structure 234 | * @throws IllegalStateException if the type is not 235 | * {@code ExtensionType#JSON} 236 | * @throws IllegalArgumentException If the structure is not valid 237 | * @since 1.1 238 | */ 239 | public void setJSONStructure(JsonStructure struct) { 240 | if (type != ExtensionType.JSON) { 241 | throw new IllegalStateException(); 242 | } 243 | this.json = struct; 244 | try (final StringWriter w = new StringWriter()) { 245 | final JsonWriter jw = Json.createWriter(w); 246 | jw.write(struct); 247 | w.flush(); 248 | this.text = w.toString(); 249 | } catch (IOException ioe) { 250 | throw new IllegalArgumentException("Not a json structure: " + struct, ioe); 251 | } 252 | } 253 | 254 | /** 255 | * Get the artifacts of the extension 256 | * 257 | * @return The artifacts 258 | * @throws IllegalStateException if the type is not 259 | * {@code ExtensionType#ARTIFACTS} 260 | */ 261 | public Artifacts getArtifacts() { 262 | if (type != ExtensionType.ARTIFACTS) { 263 | throw new IllegalStateException(); 264 | } 265 | return artifacts; 266 | } 267 | 268 | /** 269 | * Create a copy of the Extension 270 | * @return A copy of the Extension 271 | */ 272 | public Extension copy() { 273 | Extension c = new Extension(type, name, state); 274 | switch (type) { 275 | case TEXT: 276 | c.setText(text); 277 | break; 278 | case JSON: 279 | c.setJSON(text); 280 | break; 281 | case ARTIFACTS: 282 | if (artifacts != null) { 283 | for (Artifact a : artifacts) { 284 | c.getArtifacts().add(a.copy(a.getId())); 285 | } 286 | } 287 | break; 288 | } 289 | return c; 290 | } 291 | 292 | @Override 293 | public int hashCode() { 294 | return name.hashCode(); 295 | } 296 | 297 | @Override 298 | public boolean equals(final Object obj) { 299 | if (this == obj) { 300 | return true; 301 | } 302 | if (obj == null || getClass() != obj.getClass()) { 303 | return false; 304 | } 305 | return name.equals(((Extension) obj).name); 306 | } 307 | 308 | @Override 309 | public String toString() { 310 | return "Extension [type=" + type + ", name=" + name + "]"; 311 | } 312 | } 313 | --------------------------------------------------------------------------------