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