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