diff --git a/changes.xml b/changes.xml
index 6979831..5eff9c0 100644
--- a/changes.xml
+++ b/changes.xml
@@ -24,6 +24,9 @@
+
+ Add JsonOsgiConfigPostProcessor to support reading a combined set of OSGi configuration for run modes from .osgiconfig.json files.
+
ProvisioningOsgiConfigPostProcessor: Write OSGi configurations as .cfg.json files instead of .config files.
diff --git a/conga-sling-plugin/pom.xml b/conga-sling-plugin/pom.xml
index 3a9cab8..9b7917d 100644
--- a/conga-sling-plugin/pom.xml
+++ b/conga-sling-plugin/pom.xml
@@ -79,6 +79,13 @@
compile
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.16.1
+ compile
+
+
org.apache.felix
org.apache.felix.cm.json
diff --git a/conga-sling-plugin/src/it/example/src/main/roles/sling.yaml b/conga-sling-plugin/src/it/example/src/main/roles/sling.yaml
index b8cdd79..f0fbc94 100644
--- a/conga-sling-plugin/src/it/example/src/main/roles/sling.yaml
+++ b/conga-sling-plugin/src/it/example/src/main/roles/sling.yaml
@@ -13,6 +13,11 @@ files:
postProcessors:
- sling-provisioning-osgiconfig
+- file: config-sample.osgiconfig.json
+ dir: osgi-config-from-json
+ template: config-sample.osgiconfig.json.hbs
+ postProcessors:
+ - sling-json-osgiconfig
# Defines configuration parameters and default values
config:
diff --git a/conga-sling-plugin/src/it/example/src/main/templates/sling/config-sample.osgiconfig.json.hbs b/conga-sling-plugin/src/it/example/src/main/templates/sling/config-sample.osgiconfig.json.hbs
new file mode 100644
index 0000000..ae9bb32
--- /dev/null
+++ b/conga-sling-plugin/src/it/example/src/main/templates/sling/config-sample.osgiconfig.json.hbs
@@ -0,0 +1,17 @@
+{
+ "configurations": {
+ "my.pid": {
+ "heapspaceMax": "{{jvm.heapspace.max}}",
+ "booleanProp": true,
+ "numberProp": 123,
+ "arrayProp": ["v1","v2","v3"],
+ "numberArrayProp": [1,2]
+ }
+ },
+ "configurations:mode1": {
+ "my.pid2": {
+ "stringProperty": "{{var1}}",
+ "stringProperty2": "{{var2}}"
+ }
+ }
+}
diff --git a/conga-sling-plugin/src/it/example/src/main/templates/sling/sling-provisioning.provisioning.hbs b/conga-sling-plugin/src/it/example/src/main/templates/sling/sling-provisioning.provisioning.hbs
index 79f53dc..056c0f6 100644
--- a/conga-sling-plugin/src/it/example/src/main/templates/sling/sling-provisioning.provisioning.hbs
+++ b/conga-sling-plugin/src/it/example/src/main/templates/sling/sling-provisioning.provisioning.hbs
@@ -4,6 +4,10 @@
my.pid
heapspaceMax="{{jvm.heapspace.max}}"
+ booleanProp=B"true"
+ numberProp=I"123"
+ arrayProp=["v1","v2","v3"]
+ numberArrayProp=I["1","2"]
[configurations runModes=mode1]
diff --git a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/postprocessor/JsonOsgiConfigPostProcessor.java b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/postprocessor/JsonOsgiConfigPostProcessor.java
new file mode 100644
index 0000000..46c5aaa
--- /dev/null
+++ b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/postprocessor/JsonOsgiConfigPostProcessor.java
@@ -0,0 +1,85 @@
+/*
+ * #%L
+ * wcm.io
+ * %%
+ * Copyright (C) 2024 wcm.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package io.wcm.devops.conga.plugins.sling.postprocessor;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.provisioning.model.Model;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.wcm.devops.conga.generator.GeneratorException;
+import io.wcm.devops.conga.generator.spi.PostProcessorPlugin;
+import io.wcm.devops.conga.generator.spi.context.FileContext;
+import io.wcm.devops.conga.generator.spi.context.PostProcessorContext;
+import io.wcm.devops.conga.plugins.sling.util.JsonOsgiConfigUtil;
+import io.wcm.devops.conga.plugins.sling.util.ProvisioningUtil;
+
+/**
+ * Transforms a combined JSON file containing OSGi configurations into individual OSGi configuration files.
+ */
+public class JsonOsgiConfigPostProcessor implements PostProcessorPlugin {
+
+ /**
+ * Plugin name
+ */
+ public static final String NAME = "sling-json-osgiconfig";
+
+ /**
+ * File extension
+ */
+ public static final String FILE_EXTENSION = ".osgiconfig.json";
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public boolean accepts(FileContext file, PostProcessorContext context) {
+ return StringUtils.endsWith(file.getFile().getName(), FILE_EXTENSION);
+ }
+
+ @Override
+ @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
+ public List apply(FileContext fileContext, PostProcessorContext context) {
+ File file = fileContext.getFile();
+ try {
+ // read JSON file with combined configurations
+ Model model = JsonOsgiConfigUtil.readToProvisioningModel(file);
+
+ // generate OSGi configurations
+ List files = ProvisioningUtil.generateOsgiConfigurations(model, file.getParentFile(), context);
+
+ // delete provisioning file after transformation
+ Files.delete(file.toPath());
+
+ // return list of generated osgi configuration files
+ return files;
+ }
+ catch (IOException ex) {
+ throw new GeneratorException("Unable to parse JSON file with OSGi configurations.", ex);
+ }
+ }
+
+}
diff --git a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessor.java b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessor.java
index 9a759e7..9f855bf 100644
--- a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessor.java
+++ b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessor.java
@@ -20,10 +20,8 @@
package io.wcm.devops.conga.plugins.sling.postprocessor;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Dictionary;
+import java.nio.file.Files;
import java.util.List;
import org.apache.sling.provisioning.model.Model;
@@ -33,12 +31,11 @@
import io.wcm.devops.conga.generator.spi.PostProcessorPlugin;
import io.wcm.devops.conga.generator.spi.context.FileContext;
import io.wcm.devops.conga.generator.spi.context.PostProcessorContext;
-import io.wcm.devops.conga.plugins.sling.util.ConfigConsumer;
-import io.wcm.devops.conga.plugins.sling.util.OsgiConfigUtil;
import io.wcm.devops.conga.plugins.sling.util.ProvisioningUtil;
/**
- * Transforms a Sling Provisioning file into OSGi configurations (ignoring all other provisioning contents).
+ * Transforms a Sling Provisioning file into OSGi configuration files (.cfg.json).
+ * Repoinit statements are supported as well, all other provisioning contents are ignored
*/
public class ProvisioningOsgiConfigPostProcessor implements PostProcessorPlugin {
@@ -64,41 +61,17 @@ public List apply(FileContext fileContext, PostProcessorContext con
try {
// generate OSGi configurations
Model model = ProvisioningUtil.getModel(fileContext);
- List files = generateOsgiConfigurations(model, file.getParentFile(), context);
+ List files = ProvisioningUtil.generateOsgiConfigurations(model, file.getParentFile(), context);
// delete provisioning file after transformation
- file.delete();
+ Files.delete(file.toPath());
// return list of generated osgi configuration files
return files;
}
catch (IOException ex) {
- throw new GeneratorException("Unable to post-process sling provisioning OSGi configurations.", ex);
+ throw new GeneratorException("Unable to post-process Sling Provisioning OSGi configurations.", ex);
}
}
- /**
- * Generate OSGi configuration for all feature and run modes.
- * @param model Provisioning Model
- * @param dir Target directory
- * @param context Post processor context
- */
- private List generateOsgiConfigurations(Model model, File dir, PostProcessorContext context) throws IOException {
- return ProvisioningUtil.visitOsgiConfigurations(model, new ConfigConsumer() {
- @Override
- @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
- public FileContext accept(String path, Dictionary properties) throws IOException {
- context.getLogger().info(" Generate {}", path);
-
- File confFile = new File(dir, path);
- confFile.getParentFile().mkdirs();
- try (FileOutputStream os = new FileOutputStream(confFile)) {
- OsgiConfigUtil.write(os, properties);
- }
-
- return new FileContext().file(confFile).charset(StandardCharsets.UTF_8);
- }
- });
- }
-
}
diff --git a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtil.java b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtil.java
new file mode 100644
index 0000000..bb01a63
--- /dev/null
+++ b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtil.java
@@ -0,0 +1,212 @@
+/*
+ * #%L
+ * wcm.io
+ * %%
+ * Copyright (C) 2024 wcm.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package io.wcm.devops.conga.plugins.sling.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.provisioning.model.Configuration;
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.RunMode;
+import org.apache.sling.provisioning.model.Section;
+import org.jetbrains.annotations.Nullable;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.MapType;
+
+import io.wcm.devops.conga.plugins.sling.postprocessor.JsonOsgiConfigPostProcessor;
+
+/**
+ * Transforms a combined JSON file to provisioning model with OSGi configurations and repoinit statements.
+ */
+public final class JsonOsgiConfigUtil {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
+ .enable(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_COMMENTS);
+ private static final MapType MAP_TYPE = OBJECT_MAPPER.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
+
+ private static final Pattern KEY_PATTERN_CONFIGURATIONS = Pattern.compile("^configurations(:(.*))?$");
+ private static final Pattern KEY_PATTERN_REPOINIT = Pattern.compile("^repoinit(:(.*))?$");
+ private static final int RUNMODES_INDEX = 2;
+
+ private JsonOsgiConfigUtil() {
+ // static methods only
+ }
+
+ /**
+ * Read JSON file content to a map.
+ * @param file JSON file
+ * @return Map containing JSON content
+ * @throws IOException I/O exception
+ */
+ static Map readToMap(File file) throws IOException {
+ String jsonString = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
+ Map result = OBJECT_MAPPER.readValue(jsonString, MAP_TYPE);
+ return convertListsToArrays(result);
+ }
+
+ /**
+ * Jackson converts arrays in JSON to lists. We want to keep them represented as arrays for conversion
+ * to OSGi configuration, so we convert them recursively back to arrays.
+ */
+ @SuppressWarnings("unchecked")
+ private static Map convertListsToArrays(Map map) {
+ Map result = new LinkedHashMap<>();
+ for (Map.Entry entry : map.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ if (value instanceof Collection) {
+ Collection> collection = ((Collection)value);
+ value = collection.toArray((Object[])Array.newInstance(detectArrayType(collection), collection.size()));
+ }
+ else if (value instanceof Map) {
+ value = convertListsToArrays((Map)value);
+ }
+ result.put(key, value);
+ }
+ return result;
+ }
+
+ /**
+ * Detect type from list of items. If all items have the same type this is returned, otherwise Object.class.
+ */
+ private static Class> detectArrayType(Collection> list) {
+ Class> type = null;
+ for (Object item : list) {
+ if (item != null) {
+ if (type == null) {
+ type = item.getClass();
+ }
+ else if (type != item.getClass()) {
+ type = Object.class;
+ }
+ }
+ }
+ if (type == null) {
+ type = Object.class;
+ }
+ return type;
+ }
+
+ /**
+ * Read JSON file content to a map.
+ * @param file JSON file
+ * @return Map containing JSON content
+ * @throws IOException I/O exception
+ */
+ public static Model readToProvisioningModel(File file) throws IOException {
+ Model model = new Model();
+ String featureName = StringUtils.substringBeforeLast(file.getName(), JsonOsgiConfigPostProcessor.FILE_EXTENSION);
+ Feature feature = model.getOrCreateFeature(featureName);
+
+ Map data = readToMap(file);
+ for (Map.Entry entry : data.entrySet()) {
+ processEntry(feature, entry.getKey(), entry.getValue());
+ }
+
+ return model;
+ }
+
+ /**
+ * Detect entries describing OSGi configurations and repoinit statements.
+ */
+ @SuppressWarnings("unchecked")
+ private static void processEntry(Feature feature, String key, Object value) throws IOException {
+ Matcher configurationsKeyMatcher = KEY_PATTERN_CONFIGURATIONS.matcher(key);
+ if (configurationsKeyMatcher.matches()) {
+ if (value instanceof Map) {
+ String[] runModes = toRunModes(configurationsKeyMatcher.group(RUNMODES_INDEX));
+ processOsgiConfiguration(feature, runModes, (Map)value);
+ }
+ else {
+ throw new IOException("Unexpected data for key " + key + ": " + value.getClass().getName());
+ }
+ }
+ else {
+ Matcher repoinitKeyMatcher = KEY_PATTERN_REPOINIT.matcher(key);
+ if (repoinitKeyMatcher.matches()) {
+ if (value.getClass().isArray()) {
+ String[] runModes = toRunModes(repoinitKeyMatcher.group(RUNMODES_INDEX));
+ processRepoInit(feature, runModes, (Object[])value);
+ }
+ else {
+ throw new IOException("Unexpected data for key " + key + ": " + value.getClass().getName());
+ }
+ }
+ else {
+ throw new IOException("Invalid toplevel key in JSON file: " + key);
+ }
+ }
+ }
+
+ private static String @Nullable [] toRunModes(String runModesString) {
+ if (StringUtils.isBlank(runModesString)) {
+ return null;
+ }
+ return StringUtils.split(runModesString, ",");
+ }
+
+ /**
+ * Convert OSGi configurations to Provisioning model configurations with associated run modes.
+ */
+ @SuppressWarnings("unchecked")
+ private static void processOsgiConfiguration(Feature feature, String[] runModes, Map configurations) throws IOException {
+ RunMode runMode = feature.getOrCreateRunMode(runModes);
+ for (Map.Entry entry : configurations.entrySet()) {
+ String pid = entry.getKey();
+ Object value = entry.getValue();
+ if (value instanceof Map) {
+ Map configProperties = (Map)value;
+ Configuration config = runMode.getOrCreateConfiguration(pid, null);
+ Dictionary properties = config.getProperties();
+ for (Map.Entry configProperty : configProperties.entrySet()) {
+ properties.put(configProperty.getKey(), configProperty.getValue());
+ }
+ }
+ else {
+ throw new IOException("Unexpected configurations data for " + pid + ": " + value.getClass().getName());
+ }
+ }
+ }
+
+ /**
+ * Convert repoinit statements to Provisioning model additional sections with associated run modes.
+ */
+ private static void processRepoInit(Feature feature, String[] runModes, Object[] repoinits) {
+ Section section = new Section(ProvisioningUtil.REPOINIT_SECTION);
+ feature.getAdditionalSections().add(section);
+ if (runModes != null) {
+ section.getAttributes().put(ProvisioningUtil.REPOINIT_PROPERTY_RUNMODES, StringUtils.join(runModes, ","));
+ }
+ section.setContents(StringUtils.join(repoinits, "\n"));
+ }
+
+}
diff --git a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/ProvisioningUtil.java b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/ProvisioningUtil.java
index 80aeb32..0ddb467 100644
--- a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/ProvisioningUtil.java
+++ b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/ProvisioningUtil.java
@@ -20,13 +20,17 @@
package io.wcm.devops.conga.plugins.sling.util;
import java.io.BufferedInputStream;
+import java.io.File;
import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Dictionary;
import java.util.List;
import java.util.Objects;
import java.util.SortedSet;
@@ -43,7 +47,9 @@
import org.apache.sling.provisioning.model.Section;
import org.apache.sling.provisioning.model.io.ModelReader;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.wcm.devops.conga.generator.spi.context.FileContext;
+import io.wcm.devops.conga.generator.spi.context.PostProcessorContext;
import io.wcm.devops.conga.generator.util.FileUtil;
/**
@@ -61,7 +67,8 @@ public final class ProvisioningUtil {
*/
public static final String TEXT_FILE_EXTENSION = "txt";
- private static final String REPOINIT_SECTION = "repoinit";
+ static final String REPOINIT_SECTION = "repoinit";
+ static final String REPOINIT_PROPERTY_RUNMODES = "runModes";
private static final String REPOINIT_PID = "org.apache.sling.jcr.repoinit.RepositoryInitializer";
private ProvisioningUtil() {
@@ -135,7 +142,7 @@ public static List visitOsgiConfigurations(Model model, ConfigConsumer
}
// associated run modes
- String runModesString = section.getAttributes().get("runModes");
+ String runModesString = section.getAttributes().get(REPOINIT_PROPERTY_RUNMODES);
RunMode runMode;
if (runModesString != null) {
runMode = new RunMode(StringUtils.split(runModesString, ","));
@@ -184,4 +191,31 @@ private static String getPathForConfiguration(Configuration configuration, RunMo
return path.toString();
}
+ /**
+ * Generate OSGi configuration for all feature and run modes.
+ * @param model Provisioning Model
+ * @param dir Target directory
+ * @param context Post processor context
+ * @return Generated files
+ * @throws IOException I/O exception
+ */
+ public static List generateOsgiConfigurations(Model model, File dir, PostProcessorContext context) throws IOException {
+ return ProvisioningUtil.visitOsgiConfigurations(model, new ConfigConsumer() {
+
+ @Override
+ @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
+ public FileContext accept(String path, Dictionary properties) throws IOException {
+ context.getLogger().info(" Generate {}", path);
+
+ File confFile = new File(dir, path);
+ confFile.getParentFile().mkdirs();
+ try (FileOutputStream os = new FileOutputStream(confFile)) {
+ OsgiConfigUtil.write(os, properties);
+ }
+
+ return new FileContext().file(confFile).charset(StandardCharsets.UTF_8);
+ }
+ });
+ }
+
}
diff --git a/conga-sling-plugin/src/main/resources/META-INF/services/io.wcm.devops.conga.generator.spi.PostProcessorPlugin b/conga-sling-plugin/src/main/resources/META-INF/services/io.wcm.devops.conga.generator.spi.PostProcessorPlugin
index 6d33cbb..890b2dd 100644
--- a/conga-sling-plugin/src/main/resources/META-INF/services/io.wcm.devops.conga.generator.spi.PostProcessorPlugin
+++ b/conga-sling-plugin/src/main/resources/META-INF/services/io.wcm.devops.conga.generator.spi.PostProcessorPlugin
@@ -1 +1,2 @@
+io.wcm.devops.conga.plugins.sling.postprocessor.JsonOsgiConfigPostProcessor
io.wcm.devops.conga.plugins.sling.postprocessor.ProvisioningOsgiConfigPostProcessor
\ No newline at end of file
diff --git a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/postprocessor/JsonOsgiConfigPostProcessorTest.java b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/postprocessor/JsonOsgiConfigPostProcessorTest.java
new file mode 100644
index 0000000..089c4af
--- /dev/null
+++ b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/postprocessor/JsonOsgiConfigPostProcessorTest.java
@@ -0,0 +1,128 @@
+/*
+ * #%L
+ * wcm.io
+ * %%
+ * Copyright (C) 2024 wcm.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package io.wcm.devops.conga.plugins.sling.postprocessor;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Dictionary;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.felix.cm.json.io.Configurations;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.slf4j.LoggerFactory;
+
+import io.wcm.devops.conga.generator.spi.PostProcessorPlugin;
+import io.wcm.devops.conga.generator.spi.context.FileContext;
+import io.wcm.devops.conga.generator.spi.context.PluginContextOptions;
+import io.wcm.devops.conga.generator.spi.context.PostProcessorContext;
+import io.wcm.devops.conga.generator.util.PluginManagerImpl;
+
+class JsonOsgiConfigPostProcessorTest {
+
+ private PostProcessorPlugin underTest;
+
+ private File targetDir;
+
+ @BeforeEach
+ void setUp(TestInfo testInfo) throws IOException {
+ underTest = new PluginManagerImpl().get(JsonOsgiConfigPostProcessor.NAME, PostProcessorPlugin.class);
+
+ // prepare target directory
+ targetDir = new File("target/JsonOsgiConfigPostProcessorTest_" + testInfo.getDisplayName());
+ if (targetDir.exists()) {
+ FileUtils.deleteDirectory(targetDir);
+ }
+ }
+
+ @Test
+ void testJsonFile() throws Exception {
+
+ // post process example JSON file
+ File provisioningFile = new File(targetDir, "sample.osgiconfig.json");
+ FileUtils.copyFile(new File(getClass().getResource("/osgi-config-json/sample.osgiconfig.json").toURI()), provisioningFile);
+ postProcess(provisioningFile);
+
+ // validate generated configs
+ Dictionary, ?> config = readConfig("my.pid.cfg.json");
+ assertEquals("value1", config.get("stringProperty"));
+ assertArrayEquals(new String[] {
+ "v1", "v2", "v3"
+ }, (String[])config.get("stringArrayProperty"));
+ assertEquals(true, config.get("booleanProperty"));
+ assertEquals(999999999999L, config.get("longProperty"));
+
+ assertExists("my.factory-my.pid.cfg.json");
+ assertExists("mode1/my.factory-my.pid2.cfg.json");
+ assertExists("mode2/my.pid2.cfg.json");
+ assertExists("publish.prod/my.pid2.cfg.json");
+
+ // validate repoinit statements
+ config = readConfig("org.apache.sling.jcr.repoinit.RepositoryInitializer-sample.cfg.json");
+ assertArrayEquals(new String[] { "create path /repoinit/test1\n" +
+ "create path /repoinit/test2" }, (String[])config.get("scripts"));
+
+ config = readConfig("mode1/org.apache.sling.jcr.repoinit.RepositoryInitializer-sample-mode1.cfg.json");
+ assertArrayEquals(new String[] { "create service user mode1" }, (String[])config.get("scripts"));
+
+ config = readConfig("mode1.mode2/org.apache.sling.jcr.repoinit.RepositoryInitializer-sample-mode1-mode2.cfg.json");
+ assertArrayEquals(new String[] { "create service user mode1_mode2" }, (String[])config.get("scripts"));
+ }
+
+ private void postProcess(File provisioningFile) {
+ // post-process
+ FileContext fileContext = new FileContext()
+ .file(provisioningFile)
+ .charset(StandardCharsets.UTF_8);
+ PluginContextOptions pluginContextOptions = new PluginContextOptions()
+ .pluginManager(new PluginManagerImpl())
+ .logger(LoggerFactory.getLogger(ProvisioningOsgiConfigPostProcessor.class));
+ PostProcessorContext context = new PostProcessorContext()
+ .pluginContextOptions(pluginContextOptions);
+
+ assertTrue(underTest.accepts(fileContext, context));
+ underTest.apply(fileContext, context);
+
+ // validate
+ assertFalse(provisioningFile.exists(), "Combined JSON file deleted");
+ }
+
+ private Dictionary, ?> readConfig(String fileName) throws IOException {
+ assertExists(fileName);
+ File file = new File(targetDir, fileName);
+ try (FileReader reader = new FileReader(file, StandardCharsets.UTF_8)) {
+ return Configurations.buildReader().build(reader).readConfiguration();
+ }
+ }
+
+ private void assertExists(String fileName) throws IOException {
+ File file = new File(targetDir, fileName);
+ assertTrue(file.exists(), "Config file found: " + file.getCanonicalPath());
+ }
+
+}
diff --git a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessorTest.java b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessorTest.java
index 1c0b609..34f4275 100644
--- a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessorTest.java
+++ b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/postprocessor/ProvisioningOsgiConfigPostProcessorTest.java
@@ -54,8 +54,8 @@ class ProvisioningOsgiConfigPostProcessorTest {
void setUp(TestInfo testInfo) throws IOException {
underTest = new PluginManagerImpl().get(ProvisioningOsgiConfigPostProcessor.NAME, PostProcessorPlugin.class);
- // prepare target dirctory
- targetDir = new File("target/postprocessor-test_" + testInfo.getDisplayName());
+ // prepare target directory
+ targetDir = new File("target/ProvisioningOsgiConfigPostProcessorTest_" + testInfo.getDisplayName());
if (targetDir.exists()) {
FileUtils.deleteDirectory(targetDir);
}
diff --git a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtilTest.java b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtilTest.java
new file mode 100644
index 0000000..3ba5384
--- /dev/null
+++ b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtilTest.java
@@ -0,0 +1,49 @@
+/*
+ * #%L
+ * wcm.io
+ * %%
+ * Copyright (C) 2024 wcm.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package io.wcm.devops.conga.plugins.sling.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+class JsonOsgiConfigUtilTest {
+
+ @Test
+ void testReadToMap() throws IOException {
+ Map content = JsonOsgiConfigUtil.readToMap(new File("src/test/resources/osgi-config-json/sample.osgiconfig.json"));
+ assertArrayEquals(new String[] { "create service user mode1" }, (String[])content.get("repoinit:mode1"));
+ }
+
+ @Test
+ void testListToArrayConversion() throws IOException {
+ Map content = JsonOsgiConfigUtil.readToMap(new File("src/test/resources/arrayTypes.json"));
+ assertArrayEquals(new String[] { "v1", "v2", "v3" }, (String[])content.get("stringArray"));
+ assertArrayEquals(new Integer[] { 1, 2, 3 }, (Integer[])content.get("intArray"));
+ assertArrayEquals(new Boolean[] { true, false }, (Boolean[])content.get("boolArray"));
+ assertArrayEquals(new Object[] { "v1", 1, true }, (Object[])content.get("mixedArray"));
+ assertArrayEquals(new Object[0], (Object[])content.get("emptyArray"));
+ assertArrayEquals(new String[] { "v1" }, (String[])((Map)content.get("nested")).get("stringArray"));
+ }
+
+}
diff --git a/conga-sling-plugin/src/test/resources/arrayTypes.json b/conga-sling-plugin/src/test/resources/arrayTypes.json
new file mode 100644
index 0000000..dc6551a
--- /dev/null
+++ b/conga-sling-plugin/src/test/resources/arrayTypes.json
@@ -0,0 +1,10 @@
+{
+ "stringArray": ["v1","v2","v3"],
+ "intArray": [1,2,3],
+ "boolArray": [true,false],
+ "mixedArray": ["v1",1,true],
+ "emptyArray": [],
+ "nested": {
+ "stringArray": ["v1"]
+ }
+}
diff --git a/conga-sling-plugin/src/test/resources/osgi-config-json/sample.osgiconfig.json b/conga-sling-plugin/src/test/resources/osgi-config-json/sample.osgiconfig.json
new file mode 100644
index 0000000..3bc24d6
--- /dev/null
+++ b/conga-sling-plugin/src/test/resources/osgi-config-json/sample.osgiconfig.json
@@ -0,0 +1,47 @@
+/*
+ * Example comment.
+ */
+{
+ "configurations": {
+ "my.pid": {
+ "stringProperty": "value1",
+ "stringArrayProperty": ["v1","v2","v3"],
+ "booleanProperty": true,
+ "longProperty": 999999999999
+ },
+ "my.factory-my.pid": {
+ "stringProperty": "value2"
+ }
+ },
+
+ "configurations:mode1": {
+ "my.factory-my.pid2": {
+ "stringProperty": "value3"
+ }
+ },
+
+ "configurations:mode2": {
+ "my.pid2": {
+ "stringProperty": "value4"
+ }
+ },
+
+ "configurations:publish,prod": {
+ "my.pid2": {
+ "stringProperty": "value5"
+ }
+ },
+
+ "repoinit": [
+ "create path /repoinit/test1",
+ "create path /repoinit/test2"
+ ],
+
+ "repoinit:mode1": [
+ "create service user mode1"
+ ],
+
+ "repoinit:mode1,mode2": [
+ "create service user mode1_mode2"
+ ]
+}
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index fb34d85..6a12c9a 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -9,6 +9,7 @@ wcm.io DevOps CONGA Plugin for [Apache Sling][sling].
* [Usage][usage]
* [CONGA Extensions][extensions]
+* [Combined JSON file for defining OSGi Configurations][osgi-config-combined-json]
* [API documentation][apidocs]
* [Changelog][changelog]
@@ -17,8 +18,8 @@ wcm.io DevOps CONGA Plugin for [Apache Sling][sling].
This plugin extends [CONGA][conga] with:
-* Manage OSGi configuration templates in [Apache Sling Provisioning][sling-provisioning] file format
-* Generate OSGi configurations in [Apache Felix Config Admin][felix-configadmin] file format
+* Manage OSGi configuration templates in [Combined JSON files][osgi-config-combined-json] or [Apache Sling Provisioning][sling-provisioning] file format
+* Generate OSGi configurations in `.cfg.json` files (as used by [Apache Sling Configuration Installer Factory][sling-configuration-installer-factory-cfg-json])
### Further Resources
@@ -31,9 +32,10 @@ This plugin extends [CONGA][conga] with:
[usage]: usage.html
[extensions]: extensions.html
+[osgi-config-combined-json]: osgi-config-combined-json.html
[apidocs]: conga-sling-plugin/apidocs/
[changelog]: changes-report.html
[conga]: https://devops.wcm.io/conga/
[sling]: http://sling.apache.org/
[sling-provisioning]: https://sling.apache.org/documentation/development/slingstart.html
-[felix-configadmin]: http://felix.apache.org/documentation/subprojects/apache-felix-config-admin.html
+[sling-configuration-installer-factory-cfg-json]: https://sling.apache.org/documentation/bundles/configuration-installer-factory.html#configuration-files-cfgjson
diff --git a/src/site/markdown/osgi-config-combined-json.md b/src/site/markdown/osgi-config-combined-json.md
new file mode 100644
index 0000000..f71585b
--- /dev/null
+++ b/src/site/markdown/osgi-config-combined-json.md
@@ -0,0 +1,54 @@
+## Combined JSON file for defining OSGi Configurations
+
+Similar to the (deprecated) [Apache Sling Provisioning File Format][sling-provisioning] it is possible to define a set of OSGi configurations using a CONGA template in a single JSON file which contains a set of OSGi configurations and [repoinit][sling-repoinit] statements, optionally mapped to run modes.
+
+From this combined JSON file, CONGA generates individually `.cfg.json` files as used by [Apache Sling Configuration Installer Factory][sling-configuration-installer-factory-cfg-json]. With this approach it is easy to define all configurations in one file, and add/remove configurations based on CONGA environment variables using Handlebars logic.
+
+### JSON file example
+
+```json
+{
+ "configurations": {
+ "my.pid": {
+ "prop1": "value1",
+ "prop2": [1,2,3],
+ "prop3": true
+ }
+ },
+
+ "configurations:dev": {
+ "my.pid2": {
+ "prop1": "value-for-dev"
+ }
+ },
+
+ "configurations:publish,prod": {
+ "my.pid2": {
+ "prop1": "value-for-publish-prod"
+ }
+ }
+
+ "repoinit": [
+ "create path /repoinit/test1",
+ "create path /repoinit/test2"
+ ],
+ "repoinit:dev": [
+ "create service user dev-user"
+ ]
+}
+```
+
+The following keys are allowed on toplevel of the JSON file:
+
+* `configurations` - Configurations that are always active
+* `configurations:runmode` - Configurations that are active only for the given run mode.
+* `repoinit` - List of repoinit statements to be always applied
+* `repoinit:runmode` - List of repoinit statements to be applied for the given run mode.
+
+`runmode` can be a comma-separate strings, e.g. `prod,publish`. In this case all given run modes have to be active. On AEM as a Cloud Service, only the [officially supported run modes][aemaacs-runmodes] are allowed.
+
+
+[sling-provisioning]: https://sling.apache.org/documentation/development/slingstart.html
+[sling-repoinit]: https://sling.apache.org/documentation/bundles/repository-initialization.html
+[sling-configuration-installer-factory-cfg-json]: https://sling.apache.org/documentation/bundles/configuration-installer-factory.html#configuration-files-cfgjson
+[aemaacs-runmodes]: https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/implementing/deploying/overview.html?lang=en#runmodes
\ No newline at end of file