All Downloads are FREE. Search and download functionalities are using the official Maven repository.

aQute.bnd.wstemplates.FragmentTemplateEngine Maven / Gradle / Ivy

The newest version!
package aQute.bnd.wstemplates;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.build.Workspace;
import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.http.HttpClient;
import aQute.bnd.osgi.Instruction;
import aQute.bnd.osgi.Instructions;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.Resource;
import aQute.bnd.result.Result;
import aQute.bnd.service.url.TaggedData;
import aQute.bnd.stream.MapStream;
import aQute.lib.collections.MultiMap;
import aQute.lib.io.IO;
import aQute.lib.strings.Strings;

/**
 * Manages a set of workspace template fragments. A template fragment is a zip
 * file or a Github repo. A template index file (parameters format) can be used
 * to provide the meta information. For example, a file on Github can hold the
 * overview of available templates. This class is optimized to add multiple
 * indexes and provide a unified overview of templates. It is expected that the
 * users will select what templates to apply.
 * 

* Once the set of templates is know, an {@link TemplateUpdater} is created. * This takes a folder and analyzes the given templates. Since there can be * multiple templates, there could be conflicts. The caller can remove * {@link Update} objects if they conflict. Otherwise, multiple updates will be * concatenated during {@link TemplateUpdater#commit()} */ public class FragmentTemplateEngine { private static final String TAG = "tag"; private static final String REQUIRE = "require"; private static final String DESCRIPTION = "description"; private static final String WORKSPACE_TEMPLATES = "-workspace-templates"; private static final String NAME = "name"; private static final Pattern COMMIT_SHA = Pattern.compile("[a-f0-9]{40}$"); final static Logger log = LoggerFactory.getLogger(FragmentTemplateEngine.class); final List templates = new ArrayList<>(); final HttpClient httpClient; final Workspace workspace; /** * The conflict status. */ public enum UpdateStatus { WRITE, CONFLICT } /** * Info about a template, comes from the index files. */ public record TemplateInfo(TemplateID id, String name, String description, String[] require, String... tag) implements Comparable { @Override public int compareTo(TemplateInfo o) { return id.compareTo(o.id); } /** * @return true if this template is from the github * bndtools organisation, which we consider "officially provided * by us". */ public boolean isOfficial() { return "bndtools".equals(id.organisation()); } /** * Check if the id points to a specific commit SHA e.g. * githuborg/myrepo/subfolder/workspace-template#b96e0a8877bad1c68cdc050d5854829253ef63bb * In this case b96e0a8877bad1c68cdc050d5854829253ef63bb would be the * SHA. * * @return true if the repoUrl points to a specific commit * SHA. */ public boolean isCommitSHA() { String ref = id.ref(); return ref != null && COMMIT_SHA.matcher(ref) .matches(); } } public enum Action { skip, append, exec, preprocess, delete } /** * A single update operation */ public record Update(UpdateStatus status, File to, Resource from, Set actions, TemplateInfo info) implements Comparable { @Override public int compareTo(Update o) { return info.compareTo(o.info()); } } /** * Constructor * * @param workspace */ public FragmentTemplateEngine(Workspace workspace) { this.workspace = workspace; HttpClient httpClient = workspace.getPlugin(HttpClient.class); this.httpClient = httpClient; } /** * Read a template index from a URL. The result is **not** added to this * class. See {@link #read(String)} for the file's format * * @param url to read. * @return the result */ public Result> read(URL url) { try { TaggedData index = httpClient.build() .asTag() .go(url); if (index.isOk()) { return read(IO.collect(index.getInputStream())); } else { return Result.err(index.toString()); } } catch (Exception e) { return Result.err("failed to read %s: %s", url, e); } } /** *

	 * Parse the file from the source. The format is:
	 *
	 * * key – see {@link TemplateID}
	 * * name – A human readable name
	 * * description – An optional human readable description
	 * * require – An optional comma separated list of {@link TemplateID} that will be included
	 * * tags – An optional comma separated list of tags
	 * 
* * @param source the source (Parameters format) * @return the result. */ public Result> read(String source) { try (Processor processor = new Processor(workspace)) { processor.setProperties(new StringReader(source)); processor.setBase(workspace.getBase()); Parameters ps = new Parameters(processor.getProperty(WORKSPACE_TEMPLATES)); List templates = read(ps); return Result.ok(templates); } catch (IOException e1) { return Result.err("failed to read source %s", e1); } } /** * Read the templates from a Parameters * * @param ps the parameters * @return the list of template info */ public List read(Parameters ps) { List templates = new ArrayList<>(); for (Map.Entry e : ps.entrySet()) { String id = Processor.removeDuplicateMarker(e.getKey()); Attrs attrs = e.getValue(); TemplateID templateId = TemplateID.from(id); String name = attrs.getOrDefault(NAME, id.toString()); String description = attrs.getOrDefault(DESCRIPTION, ""); String require[] = toArray(attrs.get(REQUIRE)); String tags[] = toArray(attrs.get(TAG)); templates.add(new TemplateInfo(templateId, name, description, require, tags)); } return templates; } /** * Convenience method. Add a {@link TemplateInfo} to a list of available * templates, see {@link #getAvailableTemplates()} internally maintained. */ public void add(TemplateInfo info) { if (!templates.contains(info)) { this.templates.add(info); } } /** * Get the list of available templates */ public List getAvailableTemplates() { return new ArrayList(templates); } /** * Used to edit the updates. A TemplateUpdater maintains a list of Update * objects indexed by file they affect. The intention that this structure is * used to resolve any conflicts. Calling {@link #commit()} will then * execute the updates. *

* An instance must be closed when no longer used to release the JARs. */ public class TemplateUpdater implements AutoCloseable { private static final String TOOL_BND = "tool.bnd"; final List templates; final File folder; final MultiMap updates = new MultiMap<>(); final List closeables = new ArrayList<>(); TemplateUpdater(File folder, List templates) { this.folder = folder; this.templates = templates; templates.forEach(templ -> { make(templ).forEach(u -> updates.add(u.to, u)); }); } /** * Remove an update */ public TemplateUpdater remove(Update update) { updates.remove(update.to, update); return this; } /** * Commit the updates */ public void commit() { try (Processor processor = new Processor(workspace)) { updates.forEach((k, us) -> { if (us.isEmpty()) return; k.getParentFile() .mkdirs(); try (FileOutputStream fout = new FileOutputStream(k)) { for (Update r : us) { if (r.actions.contains(Action.delete)) { IO.delete(r.to); } if (r.actions.contains(Action.preprocess)) { String s = IO.collect(r.from.openInputStream()); String preprocessed = processor.getReplacer() .process(s); fout.write(preprocessed.getBytes(StandardCharsets.UTF_8)); } else { IO.copy(r.from.openInputStream(), fout); } if (r.actions.contains(Action.exec)) { k.setExecutable(true); } } } catch (Exception e) { throw Exceptions.duck(e); } }); } catch (IOException e1) { throw Exceptions.duck(e1); } } /** * Get the current set of updates */ public Map> updaters() { return updates; } List make(TemplateInfo template) { TemplateID id = template.id(); Jar jar = getFiles(id .uri()); closeables.add(jar); String prefix = fixup(id .path()); List updates = new ArrayList<>(); Map resources = MapStream.of(jar.getResources()) .filterKey(k -> !k.startsWith("META-INF/")) .mapKey(k -> adjust(prefix, k)) .filterKey(Objects::nonNull) .collect(MapStream.toMap()); try (Processor processing = new Processor(workspace)) { processing.setBase(folder); Resource r = resources.remove(TOOL_BND); if (r != null) { processing.setProperties(r.openInputStream()); } Instructions copyInstructions = new Instructions(processing.mergeProperties("-tool")); Set used = new HashSet<>(); for (Map.Entry e : resources.entrySet()) { String path = e.getKey(); Resource resource = e.getValue(); Instruction matcher = copyInstructions.matcher(path); Attrs attrs; if (matcher != null) { used.add(matcher); if (matcher.isNegated()) continue; attrs = copyInstructions.get(matcher); } else attrs = new Attrs(); Set actions = Stream.of(Action.values()) .filter(action -> attrs.containsKey(action.name())) .collect(Collectors.toSet()); if (actions.contains(Action.skip)) continue; File to = processing.getFile(path); UpdateStatus us; if (to.isFile()) us = UpdateStatus.CONFLICT; else us = UpdateStatus.WRITE; Update update = new Update(us, to, resource, actions, template); updates.add(update); } copyInstructions.keySet() .removeAll(used); copyInstructions.forEach((k, v) -> { if (k.isNegated()) return; if (k.isLiteral()) { File file = IO.getFile(folder, k.getLiteral()); if (file.exists()) { Update update = new Update(UpdateStatus.CONFLICT, file, null, EnumSet.of(Action.delete), template); updates.add(update); } } }); } catch (Exception e) { log.error("unexpected exception in templates {}", e, e); } return updates; } String fixup(String path) { if (path.isEmpty() || path.endsWith("/")) return path; return path + "/"; } String adjust(String prefix, String resourcePath) { int n = resourcePath.indexOf('/'); if (n < 0) { log.error("expected at least one segment at start. Github repos start with `repo-ref/`: {}", resourcePath); return null; } String path = resourcePath.substring(n + 1); if (!path.startsWith(prefix)) { return null; } return path.substring(prefix.length()); } Jar getFiles(URI uri) { try { File file = httpClient.build() .useCache() .go(uri); return new Jar(file); } catch (Exception e) { throw Exceptions.duck(e); } } @Override public void close() throws Exception { closeables.forEach(IO::close); } } /** * Create a TemplateUpdater */ public TemplateUpdater updater(File folder, List templates) { return new TemplateUpdater(folder, templates); } String[] toArray(String string) { if (string == null || string.isBlank()) return new String[0]; return Strings.split(string) .toArray(String[]::new); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy