package org.bndtools.core.templating.repobased;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import org.apache.felix.metatype.AD;
import org.apache.felix.metatype.MetaData;
import org.apache.felix.metatype.MetaDataReader;
import org.apache.felix.metatype.OCD;
import org.bndtools.templating.BytesResource;
import org.bndtools.templating.FolderResource;
import org.bndtools.templating.Resource;
import org.bndtools.templating.ResourceMap;
import org.bndtools.templating.Template;
import org.bndtools.templating.TemplateEngine;
import org.bndtools.templating.util.AttributeDefinitionImpl;
import org.bndtools.templating.util.CompositeOCD;
import org.bndtools.templating.util.ObjectClassDefinitionImpl;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.osgi.framework.Version;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.resource.Capability;
import org.osgi.service.metatype.AttributeDefinition;
import org.osgi.service.metatype.ObjectClassDefinition;
import org.osgi.service.repository.ContentNamespace;
import aQute.bnd.osgi.resource.ResourceUtils;
import aQute.lib.io.IO;
import bndtools.Plugin;
public class CapabilityBasedTemplate implements Template {
private static final String DEFAULT_DIR = "template/";
private final Capability capability;
private final BundleLocator locator;
private final TemplateEngine engine;
private final String name;
private final String category;
private final String description;
private final Version version;
private final String dir;
private final URI iconUri;
private final String metaTypePath;
private final String ocdRef;
private final String helpPath;
private File _bundleFile = null;
private ResourceMap _inputResources = null;
public CapabilityBasedTemplate(Capability capability, BundleLocator locator, TemplateEngine engine) {
this.capability = capability;
this.locator = locator;
this.engine = engine;
Map<String,Object> attrs = capability.getAttributes();
Object nameObj = attrs.get("name");
this.name = nameObj instanceof String ? (String) nameObj : "<<unknown>>";
this.description = "from " + ResourceUtils.getIdentityCapability(capability.getResource()).osgi_identity();
Object categoryObj = attrs.get("category");
category = categoryObj instanceof String ? (String) categoryObj : null;
// Get version from the capability if found, otherwise it comes from the bundle
Object versionObj = attrs.get("version");
if (versionObj instanceof Version)
this.version = (Version) versionObj;
else if (versionObj instanceof String)
this.version = Version.parseVersion((String) versionObj);
else {
String v = ResourceUtils.getIdentityVersion(capability.getResource());
this.version = v != null ? Version.parseVersion(v) : Version.emptyVersion;
}
Object dirObj = attrs.get("dir");
if (dirObj instanceof String) {
String dirStr = ((String) dirObj).trim();
if (dirStr.charAt(dirStr.length() - 1) != '/')
dirStr += '/';
this.dir = dirStr;
} else {
this.dir = DEFAULT_DIR;
}
Object iconObj = attrs.get("icon");
iconUri = iconObj instanceof String ? URI.create((String) iconObj) : null;
Object helpObj = attrs.get("help");
helpPath = helpObj instanceof String ? (String) helpObj : null;
Object metaTypeObj = attrs.get("metaType");
metaTypePath = metaTypeObj instanceof String ? (String) metaTypeObj : null;
Object ocdObj = attrs.get("ocd");
ocdRef = ocdObj instanceof String ? ((String) ocdObj).trim() : null;
}
@Override
public String getName() {
return name;
}
@Override
public String getCategory() {
return category;
}
@Override
public String getShortDescription() {
return description;
}
@Override
public Version getVersion() {
return version;
}
@Override
public int getRanking() {
Object rankingObj = capability.getAttributes().get("ranking");
return rankingObj instanceof Number ? ((Number) rankingObj).intValue() : 0;
}
@Override
public ObjectClassDefinition getMetadata() throws Exception {
return getMetadata(new NullProgressMonitor());
}
@Override
public ObjectClassDefinition getMetadata(IProgressMonitor monitor) throws Exception {
String resourceId = ResourceUtils.getIdentityCapability(capability.getResource()).osgi_identity();
final CompositeOCD compositeOcd = new CompositeOCD(name, description, null);
if (metaTypePath != null) {
try (JarFile bundleJarFile = new JarFile(fetchBundle())) {
JarEntry metaTypeEntry = bundleJarFile.getJarEntry(metaTypePath);
try (InputStream entryInput = bundleJarFile.getInputStream(metaTypeEntry)) {
MetaData metaData = new MetaDataReader().parse(entryInput);
@SuppressWarnings("rawtypes")
Map ocdMap = metaData.getObjectClassDefinitions();
if (ocdMap != null) {
if (ocdMap.size() == 1) {
@SuppressWarnings("unchecked")
Entry<String,OCD> entry = (Entry<String,OCD>) ocdMap.entrySet().iterator().next();
// There is exactly one OCD, but if the capability specified the 'ocd' property then it must match.
if (ocdRef == null || ocdRef.equals(entry.getKey())) {
compositeOcd.addDelegate(new FelixOCDAdapter(entry.getValue()));
} else {
log(IStatus.WARNING, String.format("MetaType entry '%s' from resource '%s' did not contain an Object Class Definition with id '%s'", metaTypePath, resourceId, ocdRef), null);
}
} else {
// There are multiple OCDs in the MetaType record, so the capability must have specified the 'ocd' property.
if (ocdRef != null) {
OCD felixOcd = (OCD) ocdMap.get(ocdRef);
if (felixOcd != null) {
compositeOcd.addDelegate(new FelixOCDAdapter(felixOcd));
} else {
log(IStatus.WARNING, String.format("MetaType entry '%s' from resource '%s' did not contain an Object Class Definition with id '%s'", metaTypePath, resourceId, ocdRef), null);
}
} else {
log(IStatus.WARNING, String.format("MetaType entry '%s' from resource '%s' contains multiple Object Class Definitions, and no 'ocd' property was specified.", metaTypePath, resourceId), null);
}
}
}
}
}
}
// Add attribute definitions for any parameter names found in the templates and not already
// loaded from the Metatype XML.
ObjectClassDefinitionImpl ocdImpl = new ObjectClassDefinitionImpl(name, description, null);
ResourceMap inputs = getInputSources();
Map<String,String> params = engine.getTemplateParameters(inputs, monitor);
for (Entry<String,String> entry : params.entrySet()) {
AttributeDefinitionImpl ad = new AttributeDefinitionImpl(entry.getKey(), entry.getKey(), 0, AttributeDefinition.STRING);
if (entry.getValue() != null)
ad.setDefaultValue(new String[] {
entry.getValue()
});
ocdImpl.addAttribute(ad, true);
}
compositeOcd.addDelegate(ocdImpl);
return compositeOcd;
}
@Override
public ResourceMap generateOutputs(Map<String,List<Object>> parameters) throws Exception {
return generateOutputs(parameters, new NullProgressMonitor());
}
@Override
public ResourceMap generateOutputs(Map<String,List<Object>> parameters, IProgressMonitor monitor) throws Exception {
ResourceMap inputs = getInputSources();
return engine.generateOutputs(inputs, parameters, monitor);
}
@Override
public URI getIcon() {
return iconUri;
}
@Override
public URI getHelpContent() {
URI uri = null;
if (helpPath != null) {
try {
File f = fetchBundle();
uri = new URI("jar:" + f.toURI().toURL() + "!/" + helpPath);
} catch (Exception e) {
// ignore
}
}
return uri;
}
private synchronized ResourceMap getInputSources() throws IOException {
File bundleFile = fetchBundle();
_inputResources = new ResourceMap();
try (JarInputStream in = new JarInputStream(IO.stream(bundleFile))) {
JarEntry jarEntry = in.getNextJarEntry();
while (jarEntry != null) {
String entryPath = jarEntry.getName().trim();
if (entryPath.startsWith(dir)) {
String relativePath = entryPath.substring(dir.length());
if (!relativePath.isEmpty()) { // skip the root folder
Resource resource;
if (relativePath.endsWith("/")) {
// strip the trailing slash
relativePath.substring(0, relativePath.length());
resource = new FolderResource();
} else {
// cannot use IO.collect() because it closes the whole JarInputStream
resource = BytesResource.loadFrom(in);
}
_inputResources.put(relativePath, resource);
}
}
jarEntry = in.getNextJarEntry();
}
}
return _inputResources;
}
private synchronized File fetchBundle() throws IOException {
if (_bundleFile != null && _bundleFile.exists())
return _bundleFile;
Capability idCap = capability.getResource().getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE).get(0);
String id = (String) idCap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE);
Capability contentCap = capability.getResource().getCapabilities(ContentNamespace.CONTENT_NAMESPACE).get(0);
URI location;
Object locationObj = contentCap.getAttributes().get("url");
if (locationObj instanceof URI)
location = (URI) locationObj;
else if (locationObj instanceof String)
location = URI.create((String) locationObj);
else
throw new IOException("Template repository entry is missing url attribute");
if ("file".equals(location.getScheme())) {
_bundleFile = IO.getFile(location.getPath());
return _bundleFile;
}
// Try to locate from the workspace and/or repositories if a BundleLocator was provide
if (locator != null) {
String hashStr = (String) contentCap.getAttributes().get(ContentNamespace.CONTENT_NAMESPACE);
try {
_bundleFile = locator.locate(id, hashStr, "SHA-256", location);
if (_bundleFile != null)
return _bundleFile;
} catch (Exception e) {
throw new IOException("Unable to fetch bundle for template: " + getName(), e);
}
}
throw new IOException("Unable to fetch bundle for template: " + getName());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((capability == null) ? 0 : capability.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CapabilityBasedTemplate other = (CapabilityBasedTemplate) obj;
if (capability == null) {
if (other.capability != null)
return false;
} else if (!capability.equals(other.capability))
return false;
return true;
}
@Override
public void close() throws IOException {
// nothing to do
}
private static void log(int level, String message, Throwable e) {
Plugin.getDefault().getLog().log(new Status(level, Plugin.PLUGIN_ID, 0, message, e));
}
private static class FelixADAdapter implements AttributeDefinition {
private final AD ad;
public FelixADAdapter(AD ad) {
this.ad = ad;
}
@Override
public String getName() {
return ad.getName();
}
@Override
public String getID() {
return ad.getID();
}
@Override
public String getDescription() {
return ad.getDescription();
}
@Override
public int getCardinality() {
return ad.getCardinality();
}
@Override
public int getType() {
return ad.getType();
}
@Override
public String[] getOptionValues() {
return ad.getOptionValues();
}
@Override
public String[] getOptionLabels() {
return ad.getOptionLabels();
}
@Override
public String validate(String value) {
return ad.validate(value);
}
@Override
public String[] getDefaultValue() {
return ad.getDefaultValue();
}
}
private static class FelixOCDAdapter implements ObjectClassDefinition {
private final OCD ocd;
public FelixOCDAdapter(OCD ocd) {
if (ocd == null)
throw new NullPointerException();
this.ocd = ocd;
}
@Override
public String getName() {
return ocd.getName();
}
@Override
public String getID() {
return ocd.getID();
}
@Override
public String getDescription() {
return ocd.getDescription();
}
@SuppressWarnings("unchecked")
@Override
public AttributeDefinition[] getAttributeDefinitions(int filter) {
if (ocd.getAttributeDefinitions() == null)
return null;
Iterator<AD> iter = ocd.getAttributeDefinitions().values().iterator();
if (filter == ObjectClassDefinition.OPTIONAL || filter == ObjectClassDefinition.REQUIRED) {
boolean required = (filter == ObjectClassDefinition.REQUIRED);
iter = new RequiredFilterIterator(iter, required);
} else if (filter != ObjectClassDefinition.ALL) {
return null;
}
if (!iter.hasNext())
return null;
List<AttributeDefinition> result = new ArrayList<>();
while (iter.hasNext()) {
result.add(new FelixADAdapter(iter.next()));
}
return result.toArray(new AttributeDefinition[0]);
}
@Override
public InputStream getIcon(int size) throws IOException {
// TODO
return null;
}
@SuppressWarnings("rawtypes")
private static class RequiredFilterIterator implements Iterator {
private final Iterator base;
private final boolean required;
private AD next;
private RequiredFilterIterator(Iterator base, boolean required) {
this.base = base;
this.required = required;
this.next = seek();
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public Object next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
AD toReturn = next;
next = seek();
return toReturn;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
private AD seek() {
if (base.hasNext()) {
AD next;
do {
next = (AD) base.next();
} while (next.isRequired() != required && base.hasNext());
if (next.isRequired() == required) {
return next;
}
}
// nothing found any more
return null;
}
}
}
}