Skip to content

Commit

Permalink
Add JsonOsgiConfigPostProcessor to support reading a combined set of …
Browse files Browse the repository at this point in the history
…OSGi configuration for run modes from .osgiconfig.json files.
  • Loading branch information
stefanseifert committed Jan 9, 2024
1 parent e7b66a8 commit c73c1b1
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 36 deletions.
3 changes: 3 additions & 0 deletions changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<body>

<release version="1.4.0" date="not released">
<action type="add" dev="sseifert">
Add JsonOsgiConfigPostProcessor to support reading a combined set of OSGi configuration for run modes from .osgiconfig.json files.
</action>
<action type="update" dev="sseifert" issue="6">
ProvisioningOsgiConfigPostProcessor: Write OSGi configurations as .cfg.json files instead of .config files.
</action>
Expand Down
7 changes: 7 additions & 0 deletions conga-sling-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.cm.json</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FileContext> 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<FileContext> 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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -64,41 +61,17 @@ public List<FileContext> apply(FileContext fileContext, PostProcessorContext con
try {
// generate OSGi configurations
Model model = ProvisioningUtil.getModel(fileContext);
List<FileContext> files = generateOsgiConfigurations(model, file.getParentFile(), context);
List<FileContext> 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<FileContext> generateOsgiConfigurations(Model model, File dir, PostProcessorContext context) throws IOException {
return ProvisioningUtil.visitOsgiConfigurations(model, new ConfigConsumer<FileContext>() {
@Override
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
public FileContext accept(String path, Dictionary<String, Object> 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);
}
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* #%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.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Dictionary;
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 com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.wcm.devops.conga.plugins.sling.postprocessor.JsonOsgiConfigPostProcessor;

/**
* Helper class for reading JSON files.
*/
public final class JsonOsgiConfigUtil {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
// default implementation
};

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<String, Object> readToMap(File file) throws IOException {
String jsonString = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
return OBJECT_MAPPER.readValue(jsonString, MAP_TYPE_REFERENCE);
}

/**
* 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<String, Object> data = readToMap(file);
for (Map.Entry<String, Object> entry : data.entrySet()) {
processEntry(feature, entry.getKey(), entry.getValue());
}

return model;
}

@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<String, Object>)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 instanceof Collection) {
String[] runModes = toRunModes(repoinitKeyMatcher.group(RUNMODES_INDEX));
processRepoInit(feature, runModes, (Collection<String>)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[] toRunModes(String runModesString) {
if (StringUtils.isBlank(runModesString)) {
return null;
}
return StringUtils.split(runModesString, ",");
}


@SuppressWarnings("unchecked")
private static void processOsgiConfiguration(Feature feature, String[] runModes, Map<String, Object> configurations) throws IOException {
RunMode runMode = feature.getOrCreateRunMode(runModes);
for (Map.Entry<String, Object> entry : configurations.entrySet()) {
String pid = entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
Map<String, Object> configProperties = (Map<String, Object>)value;
Configuration config = runMode.getOrCreateConfiguration(pid, null);
Dictionary<String, Object> properties = config.getProperties();
for (Map.Entry<String, Object> configProperty : configProperties.entrySet()) {
properties.put(configProperty.getKey(), configProperty.getValue());
}
}
else {
throw new IOException("Unexpected configurations data for " + pid + ": " + value.getClass().getName());
}
}
}

private static void processRepoInit(Feature feature, String[] runModes, Collection<String> 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"));
}

}
Loading

0 comments on commit c73c1b1

Please sign in to comment.