diff --git a/changes.xml b/changes.xml index 611239d..6202d89 100644 --- a/changes.xml +++ b/changes.xml @@ -23,6 +23,15 @@ xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/plugins/maven-changes-plugin/xsd/changes-1.0.0.xsd"> + + + 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. + + + Generate run modes for configurations in order as expected by AEM Analyser Plugin. diff --git a/conga-sling-plugin/pom.xml b/conga-sling-plugin/pom.xml index fc27846..e6f356c 100644 --- a/conga-sling-plugin/pom.xml +++ b/conga-sling-plugin/pom.xml @@ -25,13 +25,13 @@ io.wcm.devops.conga.plugins io.wcm.devops.conga.plugins.sling.parent - 1.3.4 + 1.4.0 ../parent/pom.xml io.wcm.devops.conga.plugins io.wcm.devops.conga.plugins.sling - 1.3.4 + 1.4.0 jar CONGA Sling Plugin @@ -60,7 +60,7 @@ io.wcm.devops.conga io.wcm.devops.conga.generator - 1.16.2 + 1.17.0 compile @@ -68,7 +68,6 @@ org.apache.felix org.apache.felix.configadmin - 1.9.26 compile @@ -80,6 +79,38 @@ compile + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + compile + + + + org.apache.felix + org.apache.felix.cm.json + 2.0.6 + compile + + + jakarta.json + jakarta.json-api + 2.1.3 + compile + + + org.eclipse.parsson + parsson + 1.1.5 + compile + + + org.osgi + org.osgi.util.converter + 1.0.9 + compile + + org.slf4j slf4j-simple diff --git a/conga-sling-plugin/src/it/example/pom.xml b/conga-sling-plugin/src/it/example/pom.xml index 097b51d..f041aab 100644 --- a/conga-sling-plugin/src/it/example/pom.xml +++ b/conga-sling-plugin/src/it/example/pom.xml @@ -45,7 +45,7 @@ io.wcm.devops.conga conga-maven-plugin - 1.16.2 + 1.17.0 true 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/fileheader/OsgiConfigFileHeader.java b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/fileheader/OsgiConfigFileHeader.java index df0a1fa..4932236 100644 --- a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/fileheader/OsgiConfigFileHeader.java +++ b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/fileheader/OsgiConfigFileHeader.java @@ -20,13 +20,12 @@ package io.wcm.devops.conga.plugins.sling.fileheader; import java.io.IOException; +import java.util.Arrays; import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; -import com.google.common.collect.ImmutableList; - import io.wcm.devops.conga.generator.GeneratorException; import io.wcm.devops.conga.generator.plugins.fileheader.AbstractFileHeader; import io.wcm.devops.conga.generator.spi.context.FileContext; @@ -95,7 +94,7 @@ public FileHeaderContext extract(FileContext file) { String[] contentLines = StringUtils.split(content, "\n"); if (contentLines.length > 0 && StringUtils.startsWith(contentLines[0], getCommentLinePrefix())) { String fullComment = StringUtils.trim(StringUtils.substringAfter(contentLines[0], getCommentBlockStart())); - List lines = ImmutableList.of(fullComment); + List lines = Arrays.asList(fullComment); return new FileHeaderContext().commentLines(lines); } } 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/ConfigurationHandler_ConfigAdmin184.java b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/ConfigurationHandler_ConfigAdmin184.java deleted file mode 100644 index c2a1852..0000000 --- a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/ConfigurationHandler_ConfigAdmin184.java +++ /dev/null @@ -1,731 +0,0 @@ -/* - * #%L - * wcm.io - * %% - * Copyright (C) 2019 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.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PushbackReader; -import java.io.Writer; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.Collection; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import org.apache.felix.cm.file.FilePersistenceManager; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/* - * This file is COPIED from the sources of org.apache.felix.configadmin 1.8.4 - * to write the configuration using the old (single line) array style, which is required to support - * deploying configurations to old AEM version like AEM 6.1 and lower. - */ -/** - * The ConfigurationHandler class implements configuration reading - * form a java.io.InputStream and writing to a - * java.io.OutputStream on behalf of the - * {@link FilePersistenceManager} class. - * - *
- * cfg = prop "=" value .
- *  prop = symbolic-name . // 1.4.2 of OSGi Core Specification
- *  symbolic-name = token { "." token } .
- *  token = { [ 0..9 ] | [ a..z ] | [ A..Z ] | '_' | '-' } .
- *  value = [ type ] ( "[" values "]" | "(" values ")" | simple ) .
- *  values = simple { "," simple } .
- *  simple = """ stringsimple """ .
- *  type = // 1-char type code .
- *  stringsimple = // quoted string representation of the value .
- * 
- */ -//CHECKSTYLE:OFF -@SuppressWarnings({ "unchecked", "unused", "javadoc", "PMD" }) -@SuppressFBWarnings({ "PERFORMANCE", "STYLE" }) -class ConfigurationHandler_ConfigAdmin184 { - - protected static final String ENCODING = "UTF-8"; - - protected static final int TOKEN_NAME = 'N'; - protected static final int TOKEN_EQ = '='; - protected static final int TOKEN_ARR_OPEN = '['; - protected static final int TOKEN_ARR_CLOS = ']'; - protected static final int TOKEN_VEC_OPEN = '('; - protected static final int TOKEN_VEC_CLOS = ')'; - protected static final int TOKEN_COMMA = ','; - protected static final int TOKEN_VAL_OPEN = '"'; // '{'; - protected static final int TOKEN_VAL_CLOS = '"'; // '}'; - - // simple types (string & primitive wrappers) - protected static final int TOKEN_SIMPLE_STRING = 'T'; - protected static final int TOKEN_SIMPLE_INTEGER = 'I'; - protected static final int TOKEN_SIMPLE_LONG = 'L'; - protected static final int TOKEN_SIMPLE_FLOAT = 'F'; - protected static final int TOKEN_SIMPLE_DOUBLE = 'D'; - protected static final int TOKEN_SIMPLE_BYTE = 'X'; - protected static final int TOKEN_SIMPLE_SHORT = 'S'; - protected static final int TOKEN_SIMPLE_CHARACTER = 'C'; - protected static final int TOKEN_SIMPLE_BOOLEAN = 'B'; - - // primitives - protected static final int TOKEN_PRIMITIVE_INT = 'i'; - protected static final int TOKEN_PRIMITIVE_LONG = 'l'; - protected static final int TOKEN_PRIMITIVE_FLOAT = 'f'; - protected static final int TOKEN_PRIMITIVE_DOUBLE = 'd'; - protected static final int TOKEN_PRIMITIVE_BYTE = 'x'; - protected static final int TOKEN_PRIMITIVE_SHORT = 's'; - protected static final int TOKEN_PRIMITIVE_CHAR = 'c'; - protected static final int TOKEN_PRIMITIVE_BOOLEAN = 'b'; - - protected static final String CRLF = "\r\n"; - - protected static final Map code2Type; - protected static final Map type2Code; - - // set of valid characters for "symblic-name" - private static final BitSet NAME_CHARS; - private static final BitSet TOKEN_CHARS; - - static { - type2Code = new HashMap(); - - // simple (exclusive String whose type code is not written) - type2Code.put(Integer.class, Integer.valueOf(TOKEN_SIMPLE_INTEGER)); - type2Code.put(Long.class, Integer.valueOf(TOKEN_SIMPLE_LONG)); - type2Code.put(Float.class, Integer.valueOf(TOKEN_SIMPLE_FLOAT)); - type2Code.put(Double.class, Integer.valueOf(TOKEN_SIMPLE_DOUBLE)); - type2Code.put(Byte.class, Integer.valueOf(TOKEN_SIMPLE_BYTE)); - type2Code.put(Short.class, Integer.valueOf(TOKEN_SIMPLE_SHORT)); - type2Code.put(Character.class, Integer.valueOf(TOKEN_SIMPLE_CHARACTER)); - type2Code.put(Boolean.class, Integer.valueOf(TOKEN_SIMPLE_BOOLEAN)); - - // primitives - type2Code.put(Integer.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_INT)); - type2Code.put(Long.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_LONG)); - type2Code.put(Float.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_FLOAT)); - type2Code.put(Double.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_DOUBLE)); - type2Code.put(Byte.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_BYTE)); - type2Code.put(Short.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_SHORT)); - type2Code.put(Character.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_CHAR)); - type2Code.put(Boolean.TYPE, Integer.valueOf(TOKEN_PRIMITIVE_BOOLEAN)); - - // reverse map to map type codes to classes, string class mapping - // to be added manually, as the string type code is not written and - // hence not included in the type2Code map - code2Type = new HashMap(); - for (Iterator ti = type2Code.entrySet().iterator(); ti.hasNext();) { - Map.Entry entry = (Map.Entry)ti.next(); - code2Type.put(entry.getValue(), entry.getKey()); - } - code2Type.put(Integer.valueOf(TOKEN_SIMPLE_STRING), String.class); - - NAME_CHARS = new BitSet(); - for (int i = '0'; i <= '9'; i++) { - NAME_CHARS.set(i); - } - for (int i = 'a'; i <= 'z'; i++) { - NAME_CHARS.set(i); - } - for (int i = 'A'; i <= 'Z'; i++) { - NAME_CHARS.set(i); - } - NAME_CHARS.set('_'); - NAME_CHARS.set('-'); - NAME_CHARS.set('.'); - NAME_CHARS.set('\\'); - - TOKEN_CHARS = new BitSet(); - TOKEN_CHARS.set(TOKEN_EQ); - TOKEN_CHARS.set(TOKEN_ARR_OPEN); - TOKEN_CHARS.set(TOKEN_ARR_CLOS); - TOKEN_CHARS.set(TOKEN_VEC_OPEN); - TOKEN_CHARS.set(TOKEN_VEC_CLOS); - TOKEN_CHARS.set(TOKEN_COMMA); - TOKEN_CHARS.set(TOKEN_VAL_OPEN); - TOKEN_CHARS.set(TOKEN_VAL_CLOS); - TOKEN_CHARS.set(TOKEN_SIMPLE_STRING); - TOKEN_CHARS.set(TOKEN_SIMPLE_INTEGER); - TOKEN_CHARS.set(TOKEN_SIMPLE_LONG); - TOKEN_CHARS.set(TOKEN_SIMPLE_FLOAT); - TOKEN_CHARS.set(TOKEN_SIMPLE_DOUBLE); - TOKEN_CHARS.set(TOKEN_SIMPLE_BYTE); - TOKEN_CHARS.set(TOKEN_SIMPLE_SHORT); - TOKEN_CHARS.set(TOKEN_SIMPLE_CHARACTER); - TOKEN_CHARS.set(TOKEN_SIMPLE_BOOLEAN); - - // primitives - TOKEN_CHARS.set(TOKEN_PRIMITIVE_INT); - TOKEN_CHARS.set(TOKEN_PRIMITIVE_LONG); - TOKEN_CHARS.set(TOKEN_PRIMITIVE_FLOAT); - TOKEN_CHARS.set(TOKEN_PRIMITIVE_DOUBLE); - TOKEN_CHARS.set(TOKEN_PRIMITIVE_BYTE); - TOKEN_CHARS.set(TOKEN_PRIMITIVE_SHORT); - TOKEN_CHARS.set(TOKEN_PRIMITIVE_CHAR); - TOKEN_CHARS.set(TOKEN_PRIMITIVE_BOOLEAN); - } - - - /** - * Writes the configuration data from the Dictionary to the - * given OutputStream. - *

- * This method writes at the current location in the stream and does not - * close the outputstream. - * @param out - * The OutputStream to write the configurtion data - * to. - * @param properties - * The Dictionary to write. - * @throws IOException - * If an error occurrs writing to the output stream. - */ - public static void write(OutputStream out, Dictionary properties) throws IOException { - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, ENCODING)); - - for (Enumeration ce = properties.keys(); ce.hasMoreElements();) { - String key = (String)ce.nextElement(); - - // cfg = prop "=" value "." . - writeQuoted(bw, key); - bw.write(TOKEN_EQ); - writeValue(bw, properties.get(key)); - bw.write(CRLF); - } - - bw.flush(); - } - - - /** - * Reads configuration data from the given InputStream and - * returns a new Dictionary object containing the data. - *

- * This method reads from the current location in the stream upto the end of - * the stream but does not close the stream at the end. - * @param ins - * The InputStream from which to read the - * configuration data. - * @return A Dictionary object containing the configuration - * data. This object may be empty if the stream contains no - * configuration data. - * @throws IOException - * If an error occurrs reading from the stream. This exception - * is also thrown if a syntax error is encountered. - */ - public static Dictionary read(InputStream ins) throws IOException { - return new ConfigurationHandler_ConfigAdmin184().readInternal(ins); - } - - - // private constructor, this class is not to be instantiated from the - // outside - private ConfigurationHandler_ConfigAdmin184() {} - - // ---------- Configuration Input Implementation --------------------------- - - private int token; - private String tokenValue; - private int line; - private int pos; - - - private Dictionary readInternal(InputStream ins) throws IOException { - BufferedReader br = new BufferedReader(new InputStreamReader(ins, ENCODING)); - PushbackReader pr = new PushbackReader(br, 1); - - token = 0; - tokenValue = null; - line = 0; - pos = 0; - - Hashtable configuration = new Hashtable(); - token = 0; - while (nextToken(pr) == TOKEN_NAME) { - String key = tokenValue; - - // expect equal sign - if (nextToken(pr) != TOKEN_EQ) { - throw readFailure(token, TOKEN_EQ); - } - - // expect the token value - Object value = readValue(pr); - if (value != null) { - configuration.put(key, value); - } - } - - return configuration; - } - - - /** - * value = type ( "[" values "]" | "(" values ")" | simple ) . values = - * value { "," value } . simple = "{" stringsimple "}" . type = // 1-char - * type code . stringsimple = // quoted string representation of the value . - * @param pr - * @return - * @throws IOException - */ - private Object readValue(PushbackReader pr) throws IOException { - // read (optional) type code - int type = read(pr); - - // read value kind code if type code is not a value kinde code - int code; - if (code2Type.containsKey(Integer.valueOf(type))) { - code = read(pr); - } - else { - code = type; - type = TOKEN_SIMPLE_STRING; - } - - switch (code) { - case TOKEN_ARR_OPEN: - return readArray(type, pr); - - case TOKEN_VEC_OPEN: - return readCollection(type, pr); - - case TOKEN_VAL_OPEN: - Object value = readSimple(type, pr); - ensureNext(pr, TOKEN_VAL_CLOS); - return value; - - default: - return null; - } - } - - - private Object readArray(int typeCode, PushbackReader pr) throws IOException { - List list = new ArrayList(); - for (;;) { - int c = read(pr); - if (c == TOKEN_VAL_OPEN) { - Object value = readSimple(typeCode, pr); - if (value == null) { - // abort due to error - return null; - } - - ensureNext(pr, TOKEN_VAL_CLOS); - - list.add(value); - - c = read(pr); - } - - if (c == TOKEN_ARR_CLOS) { - Class type = (Class)code2Type.get(Integer.valueOf(typeCode)); - Object array = Array.newInstance(type, list.size()); - for (int i = 0; i < list.size(); i++) { - Array.set(array, i, list.get(i)); - } - return array; - } - else if (c < 0) { - return null; - } - else if (c != TOKEN_COMMA) { - return null; - } - } - } - - - private Collection readCollection(int typeCode, PushbackReader pr) throws IOException { - Collection collection = new ArrayList(); - for (;;) { - int c = read(pr); - if (c == TOKEN_VAL_OPEN) { - Object value = readSimple(typeCode, pr); - if (value == null) { - // abort due to error - return null; - } - - ensureNext(pr, TOKEN_VAL_CLOS); - - collection.add(value); - - c = read(pr); - } - - if (c == TOKEN_VEC_CLOS) { - return collection; - } - else if (c < 0) { - return null; - } - else if (c != TOKEN_COMMA) { - return null; - } - } - } - - - private Object readSimple(int code, PushbackReader pr) throws IOException { - switch (code) { - case -1: - return null; - - case TOKEN_SIMPLE_STRING: - return readQuoted(pr); - - // Simple/Primitive, only use wrapper classes - case TOKEN_SIMPLE_INTEGER: - case TOKEN_PRIMITIVE_INT: - return Integer.valueOf(readQuoted(pr)); - - case TOKEN_SIMPLE_LONG: - case TOKEN_PRIMITIVE_LONG: - return Long.valueOf(readQuoted(pr)); - - case TOKEN_SIMPLE_FLOAT: - case TOKEN_PRIMITIVE_FLOAT: - int fBits = Integer.parseInt(readQuoted(pr)); - return Float.valueOf(Float.intBitsToFloat(fBits)); - - case TOKEN_SIMPLE_DOUBLE: - case TOKEN_PRIMITIVE_DOUBLE: - long dBits = Long.parseLong(readQuoted(pr)); - return Double.valueOf(Double.longBitsToDouble(dBits)); - - case TOKEN_SIMPLE_BYTE: - case TOKEN_PRIMITIVE_BYTE: - return Byte.valueOf(readQuoted(pr)); - - case TOKEN_SIMPLE_SHORT: - case TOKEN_PRIMITIVE_SHORT: - return Short.valueOf(readQuoted(pr)); - - case TOKEN_SIMPLE_CHARACTER: - case TOKEN_PRIMITIVE_CHAR: - String cString = readQuoted(pr); - if (cString != null && cString.length() > 0) { - return Character.valueOf(cString.charAt(0)); - } - return null; - - case TOKEN_SIMPLE_BOOLEAN: - case TOKEN_PRIMITIVE_BOOLEAN: - return Boolean.valueOf(readQuoted(pr)); - - // unknown type code - default: - return null; - } - } - - - private void ensureNext(PushbackReader pr, int expected) throws IOException { - int next = read(pr); - if (next != expected) { - readFailure(next, expected); - } - } - - - private boolean checkNext(PushbackReader pr, int expected) throws IOException { - int next = read(pr); - if (next < 0) { - return false; - } - - if (next == expected) { - return true; - } - - return false; - } - - - private String readQuoted(PushbackReader pr) throws IOException { - StringBuffer buf = new StringBuffer(); - for (;;) { - int c = read(pr); - switch (c) { - // escaped character - case '\\': - c = read(pr); - switch (c) { - // well known escapes - case 'b': - buf.append('\b'); - break; - case 't': - buf.append('\t'); - break; - case 'n': - buf.append('\n'); - break; - case 'f': - buf.append('\f'); - break; - case 'r': - buf.append('\r'); - break; - case 'u':// need 4 characters ! - char[] cbuf = new char[4]; - if (read(pr, cbuf) == 4) { - c = Integer.parseInt(new String(cbuf), 16); - buf.append((char)c); - } - break; - - // just an escaped character, unescape - default: - buf.append((char)c); - } - break; - - // eof - case -1: // fall through - - // separator token - case TOKEN_EQ: - case TOKEN_VAL_CLOS: - pr.unread(c); - return buf.toString(); - - // no escaping - default: - buf.append((char)c); - } - } - } - - - private int nextToken(PushbackReader pr) throws IOException { - int c = ignorableWhiteSpace(pr); - - // immediately return EOF - if (c < 0) { - return (token = c); - } - - // check whether there is a name - if (NAME_CHARS.get(c) || !TOKEN_CHARS.get(c)) { - // read the property name - pr.unread(c); - tokenValue = readQuoted(pr); - return (token = TOKEN_NAME); - } - - // check another token - if (TOKEN_CHARS.get(c)) { - return (token = c); - } - - // unexpected character -> so what ?? - return (token = -1); - } - - - private int ignorableWhiteSpace(PushbackReader pr) throws IOException { - int c = read(pr); - while (c >= 0 && Character.isWhitespace((char)c)) { - c = read(pr); - } - return c; - } - - - private int read(PushbackReader pr) throws IOException { - int c = pr.read(); - if (c == '\r') { - int c1 = pr.read(); - if (c1 != '\n') { - pr.unread(c1); - } - c = '\n'; - } - - if (c == '\n') { - line++; - pos = 0; - } - else { - pos++; - } - - return c; - } - - - private int read(PushbackReader pr, char[] buf) throws IOException { - for (int i = 0; i < buf.length; i++) { - int c = read(pr); - if (c >= 0) { - buf[i] = (char)c; - } - else { - return i; - } - } - - return buf.length; - } - - - private IOException readFailure(int current, int expected) { - return new IOException("Unexpected token " + current + "; expected: " + expected + " (line=" + line + ", pos=" - + pos + ")"); - } - - - // ---------- Configuration Output Implementation -------------------------- - - private static void writeValue(Writer out, Object value) throws IOException { - Class clazz = value.getClass(); - if (clazz.isArray()) { - writeArray(out, value); - } - else if (value instanceof Collection) { - writeCollection(out, (Collection)value); - } - else { - writeType(out, clazz); - writeSimple(out, value); - } - } - - - private static void writeArray(Writer out, Object arrayValue) throws IOException { - int size = Array.getLength(arrayValue); - writeType(out, arrayValue.getClass().getComponentType()); - out.write(TOKEN_ARR_OPEN); - for (int i = 0; i < size; i++) { - if (i > 0) { - out.write(TOKEN_COMMA); - } - writeSimple(out, Array.get(arrayValue, i)); - } - out.write(TOKEN_ARR_CLOS); - } - - - private static void writeCollection(Writer out, Collection collection) throws IOException { - if (collection.isEmpty()) { - out.write(TOKEN_VEC_OPEN); - out.write(TOKEN_VEC_CLOS); - } - else { - Iterator ci = collection.iterator(); - Object firstElement = ci.next(); - - writeType(out, firstElement.getClass()); - out.write(TOKEN_VEC_OPEN); - writeSimple(out, firstElement); - - while (ci.hasNext()) { - out.write(TOKEN_COMMA); - writeSimple(out, ci.next()); - } - out.write(TOKEN_VEC_CLOS); - } - } - - - private static void writeType(Writer out, Class valueType) throws IOException { - Integer code = (Integer)type2Code.get(valueType); - if (code != null) { - out.write((char)code.intValue()); - } - } - - - private static void writeSimple(Writer out, Object value) throws IOException { - if (value instanceof Double) { - double dVal = ((Double)value).doubleValue(); - value = Long.valueOf(Double.doubleToRawLongBits(dVal)); - } - else if (value instanceof Float) { - float fVal = ((Float)value).floatValue(); - value = Integer.valueOf(Float.floatToRawIntBits(fVal)); - } - - out.write(TOKEN_VAL_OPEN); - writeQuoted(out, String.valueOf(value)); - out.write(TOKEN_VAL_CLOS); - } - - - private static void writeQuoted(Writer out, String simple) throws IOException { - if (simple == null || simple.length() == 0) { - return; - } - - char c = 0; - int len = simple.length(); - for (int i = 0; i < len; i++) { - c = simple.charAt(i); - switch (c) { - case '\\': - case TOKEN_VAL_CLOS: - case ' ': - case TOKEN_EQ: - out.write('\\'); - out.write(c); - break; - - // well known escapes - case '\b': - out.write("\\b"); - break; - case '\t': - out.write("\\t"); - break; - case '\n': - out.write("\\n"); - break; - case '\f': - out.write("\\f"); - break; - case '\r': - out.write("\\r"); - break; - - // other escaping - default: - if (c < ' ') { - String t = "000" + Integer.toHexString(c); - out.write("\\u" + t.substring(t.length() - 4)); - } - else { - out.write(c); - } - } - } - } -} 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..825c3a2 --- /dev/null +++ b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtil.java @@ -0,0 +1,216 @@ +/* + * #%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.core.JsonParser; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +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 JsonMapper OBJECT_MAPPER = JsonMapper.builder() + .enable(JsonParser.Feature.ALLOW_COMMENTS) + .enable(JsonReadFeature.ALLOW_TRAILING_COMMA) + .build(); + 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/OsgiConfigUtil.java b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/OsgiConfigUtil.java index e7d42e8..1c64d7b 100644 --- a/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/OsgiConfigUtil.java +++ b/conga-sling-plugin/src/main/java/io/wcm/devops/conga/plugins/sling/util/OsgiConfigUtil.java @@ -21,11 +21,15 @@ import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.Dictionary; +import org.apache.felix.cm.json.io.Configurations; + /** - * Helper class for writing OSGi configurations in Felix ConfigAdmin format. - * This writes the "old" ConfigAdmin format of Felix ConfigAdmin 1.8.4 to be compatible with AEM 6.1 and below. + * Helper class for writing OSGi configurations in JSON format (.cfg.json files). */ public final class OsgiConfigUtil { @@ -42,8 +46,10 @@ private OsgiConfigUtil() { * @param properties The Dictionary to write. * @throws IOException If an error occurs writing to the output stream. */ - public static void write(OutputStream out, Dictionary properties) throws IOException { - ConfigurationHandler_ConfigAdmin184.write(out, properties); + public static void write(OutputStream out, Dictionary properties) throws IOException { + try (Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + Configurations.buildWriter().build(writer).writeConfiguration(properties); + } } } 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 b9a9f09..4d1d164 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() { @@ -110,6 +117,7 @@ public static Model getModel(FileContext file) throws IOException { * @return List of non-null results * @throws IOException I/O exception */ + @SuppressWarnings("java:S3776") // ignore complexity public static List visitOsgiConfigurations(Model model, ConfigConsumer consumer) throws IOException { List results = new ArrayList<>(); for (Feature feature : model.getFeatures()) { @@ -135,7 +143,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, ",")); @@ -179,9 +187,36 @@ private static String getPathForConfiguration(Configuration configuration, RunMo if (configuration.getFactoryPid() != null) { path.append(configuration.getFactoryPid()).append("-"); } - path.append(configuration.getPid()).append(".config"); + path.append(configuration.getPid()).append(".cfg.json"); 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/fileheader/OsgiConfigFileHeaderTest.java b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/fileheader/OsgiConfigFileHeaderTest.java index c17755d..046e9fd 100644 --- a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/fileheader/OsgiConfigFileHeaderTest.java +++ b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/fileheader/OsgiConfigFileHeaderTest.java @@ -31,8 +31,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.google.common.collect.ImmutableList; - import io.wcm.devops.conga.generator.spi.FileHeaderPlugin; import io.wcm.devops.conga.generator.spi.context.FileContext; import io.wcm.devops.conga.generator.spi.context.FileHeaderContext; @@ -55,7 +53,7 @@ void testApply() throws Exception { File file = new File("target/generation-test/fileHeader.config"); FileUtils.write(file, "myscript", StandardCharsets.UTF_8); - List lines = ImmutableList.of("**********", "", "Der Jodelkaiser", "aus dem Oetztal", "ist wieder daheim.", "**********"); + List lines = List.of("**********", "", "Der Jodelkaiser", "aus dem Oetztal", "ist wieder daheim.", "**********"); FileHeaderContext context = new FileHeaderContext().commentLines(lines); FileContext fileContext = new FileContext().file(file); @@ -66,7 +64,7 @@ void testApply() throws Exception { "# Der Jodelkaiser aus dem Oetztal ist wieder daheim.")); FileHeaderContext extractContext = underTest.extract(fileContext); - assertEquals(ImmutableList.of("Der Jodelkaiser aus dem Oetztal ist wieder daheim."), extractContext.getCommentLines()); + assertEquals(List.of("Der Jodelkaiser aus dem Oetztal ist wieder daheim."), extractContext.getCommentLines()); file.delete(); } diff --git a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/fileheader/ProvisioningFileHeaderTest.java b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/fileheader/ProvisioningFileHeaderTest.java index 392f55e..1649809 100644 --- a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/fileheader/ProvisioningFileHeaderTest.java +++ b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/fileheader/ProvisioningFileHeaderTest.java @@ -31,8 +31,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.google.common.collect.ImmutableList; - import io.wcm.devops.conga.generator.spi.FileHeaderPlugin; import io.wcm.devops.conga.generator.spi.context.FileContext; import io.wcm.devops.conga.generator.spi.context.FileHeaderContext; @@ -50,9 +48,9 @@ void setUp() { @Test void testApply() throws Exception { File file = new File("target/generation-test/fileHeader.txt"); - FileUtils.copyFile(new File(getClass().getResource("/validProvisioning.txt").toURI()), file); + FileUtils.copyFile(new File(getClass().getResource("/provisioning/validProvisioning.txt").toURI()), file); - List lines = ImmutableList.of("Der Jodelkaiser", "aus dem Oetztal", "ist wieder daheim."); + List lines = List.of("Der Jodelkaiser", "aus dem Oetztal", "ist wieder daheim."); FileHeaderContext context = new FileHeaderContext().commentLines(lines); FileContext fileContext = new FileContext().file(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 c7ab868..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 @@ -25,16 +25,14 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; +import java.io.FileReader; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Dictionary; import org.apache.commons.io.FileUtils; -import org.apache.felix.cm.file.ConfigurationHandler; +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; @@ -56,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); } @@ -68,11 +66,11 @@ void testProvisioningExample() throws Exception { // post process example valid provisioning file File provisioningFile = new File(targetDir, "provisioningExample.txt"); - FileUtils.copyFile(new File(getClass().getResource("/validProvisioning.txt").toURI()), provisioningFile); + FileUtils.copyFile(new File(getClass().getResource("/provisioning/validProvisioning.txt").toURI()), provisioningFile); postProcess(provisioningFile); // validate generated configs - Dictionary config = readConfig("my.pid.config"); + Dictionary config = readConfig("my.pid.cfg.json"); assertEquals("value1", config.get("stringProperty")); assertArrayEquals(new String[] { "v1", "v2", "v3" @@ -80,20 +78,20 @@ void testProvisioningExample() throws Exception { assertEquals(true, config.get("booleanProperty")); assertEquals(999999999999L, config.get("longProperty")); - assertExists("my.factory-my.pid.config"); - assertExists("mode1/my.factory-my.pid2.config"); - assertExists("mode2/my.pid2.config"); - assertExists("publish.prod/my.pid2.config"); + 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-test.config"); + config = readConfig("org.apache.sling.jcr.repoinit.RepositoryInitializer-test.cfg.json"); assertArrayEquals(new String[] {"create path /repoinit/test1\n" + "create path /repoinit/test2\n" }, (String[])config.get("scripts")); - config = readConfig("mode1/org.apache.sling.jcr.repoinit.RepositoryInitializer-test-mode1.config"); + config = readConfig("mode1/org.apache.sling.jcr.repoinit.RepositoryInitializer-test-mode1.cfg.json"); assertArrayEquals(new String[] { "create service user mode1\n" }, (String[])config.get("scripts")); - config = readConfig("mode1.mode2/org.apache.sling.jcr.repoinit.RepositoryInitializer-test-mode1-mode2.config"); + config = readConfig("mode1.mode2/org.apache.sling.jcr.repoinit.RepositoryInitializer-test-mode1-mode2.cfg.json"); assertArrayEquals(new String[] { "create service user mode1_mode2" }, (String[])config.get("scripts")); } @@ -111,7 +109,7 @@ void testSimpleConfig() throws Exception { postProcess(provisioningFile); // validate generated configs - Dictionary config = readConfig("com.example.ServiceConfiguration.config"); + Dictionary config = readConfig("com.example.ServiceConfiguration.cfg.json"); assertEquals("bar", config.get("bar")); assertEquals("foo", config.get("foo")); } @@ -130,7 +128,7 @@ void testSimpleConfigWithNewline() throws Exception { postProcess(provisioningFile); // validate generated configs - Dictionary config = readConfig("com.example.ServiceConfiguration.config"); + Dictionary config = readConfig("com.example.ServiceConfiguration.cfg.json"); assertNull(config.get("bar")); assertEquals("foo", config.get("foo")); } @@ -140,11 +138,11 @@ void testEscapedVariable() throws Exception { // post process example valid provisioning file File provisioningFile = new File(targetDir, "provisioningExample.txt"); - FileUtils.copyFile(new File(getClass().getResource("/validProvisioningEscapedVariable.txt").toURI()), provisioningFile); + FileUtils.copyFile(new File(getClass().getResource("/provisioning/validProvisioningEscapedVariable.txt").toURI()), provisioningFile); postProcess(provisioningFile); // validate generated configs - Dictionary config = readConfig("my.pid.config"); + Dictionary config = readConfig("my.pid.cfg.json"); assertEquals("${var1} and ${var2}", config.get("stringProperty")); } @@ -169,8 +167,8 @@ private void postProcess(File provisioningFile) { private Dictionary readConfig(String fileName) throws IOException { assertExists(fileName); File file = new File(targetDir, fileName); - try (InputStream is = new BufferedInputStream(new FileInputStream(file))) { - return ConfigurationHandler.read(is); + try (FileReader reader = new FileReader(file, StandardCharsets.UTF_8)) { + return Configurations.buildReader().build(reader).readConfiguration(); } } 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..da65a32 --- /dev/null +++ b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/util/JsonOsgiConfigUtilTest.java @@ -0,0 +1,50 @@ +/* + * #%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 Boolean[] { null }, (Object[])content.get("nullArray")); + assertArrayEquals(new Object[] { "v1", 1, true, null }, (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/java/io/wcm/devops/conga/plugins/sling/validator/ProvisioningValidatorTest.java b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/validator/ProvisioningValidatorTest.java index a0a3ed4..23af1d4 100644 --- a/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/validator/ProvisioningValidatorTest.java +++ b/conga-sling-plugin/src/test/java/io/wcm/devops/conga/plugins/sling/validator/ProvisioningValidatorTest.java @@ -45,7 +45,7 @@ void setUp() { @Test void testValid() throws Exception { - File file = new File(getClass().getResource("/validProvisioning.txt").toURI()); + File file = new File(getClass().getResource("/provisioning/validProvisioning.txt").toURI()); FileContext fileContext = new FileContext().file(file).charset(StandardCharsets.UTF_8); assertTrue(underTest.accepts(fileContext, null)); underTest.apply(fileContext, null); @@ -53,7 +53,7 @@ void testValid() throws Exception { @Test void testInvalid() throws Exception { - File file = new File(getClass().getResource("/invalidProvisioning.txt").toURI()); + File file = new File(getClass().getResource("/provisioning/invalidProvisioning.txt").toURI()); FileContext fileContext = new FileContext().file(file).charset(StandardCharsets.UTF_8); assertTrue(underTest.accepts(fileContext, null)); assertThrows(ValidationException.class, () -> { @@ -63,14 +63,14 @@ void testInvalid() throws Exception { @Test void testInvalidFileExtension() throws Exception { - File file = new File(getClass().getResource("/noProvisioning.txt").toURI()); + File file = new File(getClass().getResource("/provisioning/noProvisioning.txt").toURI()); FileContext fileContext = new FileContext().file(file).charset(StandardCharsets.UTF_8); assertFalse(underTest.accepts(fileContext, null)); } @Test void testEscapedVariable() throws Exception { - File file = new File(getClass().getResource("/validProvisioningEscapedVariable.txt").toURI()); + File file = new File(getClass().getResource("/provisioning/validProvisioningEscapedVariable.txt").toURI()); FileContext fileContext = new FileContext().file(file).charset(StandardCharsets.UTF_8); assertTrue(underTest.accepts(fileContext, null)); underTest.apply(fileContext, null); 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..3be5a2b --- /dev/null +++ b/conga-sling-plugin/src/test/resources/arrayTypes.json @@ -0,0 +1,11 @@ +{ + "stringArray": ["v1","v2","v3",], + "intArray": [1,2,3,], + "boolArray": [true,false,], + "nullArray": [null], + "mixedArray": ["v1",1,true,null], + "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/conga-sling-plugin/src/test/resources/invalidProvisioning.txt b/conga-sling-plugin/src/test/resources/provisioning/invalidProvisioning.txt similarity index 100% rename from conga-sling-plugin/src/test/resources/invalidProvisioning.txt rename to conga-sling-plugin/src/test/resources/provisioning/invalidProvisioning.txt diff --git a/conga-sling-plugin/src/test/resources/noProvisioning.txt b/conga-sling-plugin/src/test/resources/provisioning/noProvisioning.txt similarity index 100% rename from conga-sling-plugin/src/test/resources/noProvisioning.txt rename to conga-sling-plugin/src/test/resources/provisioning/noProvisioning.txt diff --git a/conga-sling-plugin/src/test/resources/validProvisioning.txt b/conga-sling-plugin/src/test/resources/provisioning/validProvisioning.txt similarity index 100% rename from conga-sling-plugin/src/test/resources/validProvisioning.txt rename to conga-sling-plugin/src/test/resources/provisioning/validProvisioning.txt diff --git a/conga-sling-plugin/src/test/resources/validProvisioningEscapedVariable.txt b/conga-sling-plugin/src/test/resources/provisioning/validProvisioningEscapedVariable.txt similarity index 100% rename from conga-sling-plugin/src/test/resources/validProvisioningEscapedVariable.txt rename to conga-sling-plugin/src/test/resources/provisioning/validProvisioningEscapedVariable.txt diff --git a/parent/pom.xml b/parent/pom.xml index e8038dd..a06ffa7 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -25,13 +25,13 @@ io.wcm.devops io.wcm.devops.parent_toplevel - 1.4.2 + 1.4.4 io.wcm.devops.conga.plugins io.wcm.devops.conga.plugins.sling.parent - 1.3.4 + 1.4.0 pom CONGA Sling Plugin diff --git a/pom.xml b/pom.xml index c34f117..5ea1aba 100644 --- a/pom.xml +++ b/pom.xml @@ -23,13 +23,13 @@ io.wcm.devops.conga.plugins io.wcm.devops.conga.plugins.sling.parent - 1.3.4 + 1.4.0 parent/pom.xml io.wcm.devops.conga.plugins io.wcm.devops.conga.plugins.sling.root - 1.3.4 + 1.4.0 pom CONGA Sling Plugin 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..6120e10 --- /dev/null +++ b/src/site/markdown/osgi-config-combined-json.md @@ -0,0 +1,57 @@ +## 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. + +You can use comments in the JSON file, they are stripped out when generating the `.cfg.json` files. It's also valid to use trailing commas in JSON arrays and JSON objects, this eases generating JSON via templates. + +Real world example with HBS template: [wcm-io-samples-aem-cms-config.osgiconfig.json.hbs](https://github.com/wcm-io/io.wcm.samples/blob/develop/config-definition/src/main/templates/wcm-io-samples-aem-cms/wcm-io-samples-aem-cms-config.osgiconfig.json.hbs) + +[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