package biz.netcentric.cq.tools.actool.configreader;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.Node;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeType;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.jackrabbit.vault.fs.io.Archive;
import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.settings.SlingSettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
@Component(label = "AC Config Files Retriever", description = "Provides a map path->yamlConfigContent of relevant configs")
public class ConfigFilesRetrieverImpl implements ConfigFilesRetriever {
private static final Logger LOG = LoggerFactory.getLogger(ConfigFilesRetrieverImpl.class);
@Reference
private SlingSettingsService slingSettingsService;
@Reference
private SlingRepository repository;
@Override
public Map<String, String> getConfigFileContentFromNode(String rootPath, Session session) throws Exception {
Node rootNode = session.getNode(rootPath);
if (rootNode == null) {
throw new IllegalArgumentException("No configuration path configured! please check the configuration of AcService!");
}
Map<String, String> configurations = getConfigurations(new NodeInJcr(rootNode));
return configurations;
}
@Override
public Map<String, String> getConfigFileContentFromPackage(Archive archive) throws Exception {
Entry rootEntry = archive.getJcrRoot();
if (rootEntry == null) {
throw new IllegalStateException("Invalid package: It does not contain a JCR root element");
}
Map<String, String> configurations = getConfigurations(new EntryInPackage(archive, "/", rootEntry));
return configurations;
}
private Map<String, String> getConfigurations(PackageEntryOrNode configFileOrDir) throws Exception {
Map<String, String> configs = new HashMap<String, String>();
Set<String> currentRunModes = slingSettingsService.getRunModes();
for (PackageEntryOrNode entry : configFileOrDir.getChildren()) {
if (entry.isDirectory()) {
Map<String, String> resultsFromDir = getConfigurations(entry);
configs.putAll(resultsFromDir);
continue;
}
if (isRelevantConfiguration(entry.getName(), configFileOrDir.getName(), currentRunModes)) {
LOG.debug("Found relevant YAML file {}", entry.getName());
configs.put(entry.getPath(), entry.getContentAsString());
}
}
return configs;
}
static boolean isRelevantConfiguration(final String entryName, final String parentName, final Set<String> currentRunModes) {
if (!entryName.endsWith(".yaml") && !entryName.equals("config") /* name 'config' without .yaml allowed for backwards compatibility */) {
return false;
}
// extract runmode from parent name (if parent has "." in it)
Set<Set<String>> requiredRunModes = extractRunModesFromName(parentName);
if (requiredRunModes.isEmpty()) {
LOG.debug("Install file '{}', because parent name '{}' does not have a run mode specified.",
entryName, parentName);
return true;
}
// check the OR concatenated run modes
for (Set<String> andRunModes : requiredRunModes) {
// within each OR section is a number of AND concatenated run modes
boolean restrictionFulfilled = true;
for (String andRunMode : andRunModes) {
// all must be fulfilled
if (!currentRunModes.contains(andRunMode)) {
restrictionFulfilled = false;
break;
}
}
if (restrictionFulfilled) {
LOG.debug("The following run modes are all set: {}, there proceed installing file '{}'", StringUtils.join(andRunModes,","), entryName);
return true;
}
}
LOG.debug("The run mode restrictions could not be fullfilled, therefore not installing file '{}'", entryName);
return false;
}
/**
*
* @param name a name containing a number of runmodes concatenated with AND and OR. The name must stick to the following grammar
* <pre><somename>['.'{<runmode>'.'|<runmode>','}]</pre>
* The separator '.' means AND, "," means OR.
* As usual in most programming languages the AND has a higher precendence.
* @return the run modes being extracted from the given name
* (the outer set of run modes represent OR concatenated run modes, the inner set AND concatenated run modes)
*/
static Set<Set<String>> extractRunModesFromName(final String name) {
Set<Set<String>> requiredRunModes = new HashSet<Set<String>>();
// strip prefix as the name starts usually with config.
int positionDot = name.indexOf(".");
if (positionDot == -1) {
return requiredRunModes;
}
String allSegments = name.substring(positionDot + 1);
String[] orSegments = allSegments.split(",");
for (String orSegment : orSegments) {
Set<String> andRunModes = new HashSet<String>();
String[] andSegments = orSegment.split("\\.");
for (String andSegment : andSegments) {
andRunModes.add(andSegment);
}
requiredRunModes.add(andRunModes);
}
return requiredRunModes;
}
/** Internal representation of either a package entry or a node to be able to use one algorithm for both InstallHook/JMX scenarios. */
private static interface PackageEntryOrNode {
String getName() throws Exception;
String getPath() throws Exception;
List<PackageEntryOrNode> getChildren() throws Exception;
boolean isDirectory() throws Exception;
String getContentAsString() throws Exception;
}
private static class NodeInJcr implements PackageEntryOrNode {
private final Node node;
public NodeInJcr(Node node) {
this.node = node;
}
@Override
public String getName() throws Exception {
return node.getName();
}
@Override
public String getPath() throws Exception {
return node.getPath();
}
@Override
@SuppressWarnings("unchecked")
public List<PackageEntryOrNode> getChildren() throws Exception {
List<PackageEntryOrNode> children = new LinkedList<PackageEntryOrNode>();
Iterator<Node> childNodesIt = node.getNodes();
while (childNodesIt.hasNext()) {
children.add(new NodeInJcr(childNodesIt.next()));
}
return children;
}
@Override
public boolean isDirectory() throws Exception {
return node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER);
}
@Override
public String getContentAsString() throws Exception {
final InputStream configInputStream = JcrUtils.readFile(node);
try {
StringWriter writer = new StringWriter();
IOUtils.copy(configInputStream, writer, "UTF-8");
String configData = writer.toString();
if (StringUtils.isNotBlank(configData)) {
LOG.debug("found configuration data of node: {}", node.getPath());
return configData;
} else {
throw new IllegalStateException("File " + node.getPath() + " is empty!");
}
} finally {
configInputStream.close();
}
}
}
private static class EntryInPackage implements PackageEntryOrNode {
private final Entry entry;
private final String path;
private final Archive archive;
public EntryInPackage(Archive archive, String parentPath, Entry entry) {
this.archive = archive;
this.entry = entry;
path = parentPath + "/" + entry.getName();
}
@Override
public String getName() throws Exception {
return entry.getName();
}
@Override
public String getPath() {
return path;
}
@Override
@SuppressWarnings("unchecked")
public List<PackageEntryOrNode> getChildren() throws Exception {
List<PackageEntryOrNode> children = new LinkedList<PackageEntryOrNode>();
Collection<? extends Entry> entryChildren = entry.getChildren();
for (Entry childEntry : entryChildren) {
children.add(new EntryInPackage(archive, getPath(), childEntry));
}
return children;
}
@Override
public boolean isDirectory() throws Exception {
return entry.isDirectory();
}
@Override
public String getContentAsString() throws Exception {
LOG.debug("Reading YAML file {}", getPath());
InputStream input = archive.getInputSource(entry).getByteStream();
if (input == null) {
throw new IllegalStateException("Could not get input stream from entry " + getPath());
}
StringWriter writer = new StringWriter();
IOUtils.copy(input, writer, "UTF-8");
return writer.toString();
}
}
}