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

aQute.bnd.osgi.Processor Maven / Gradle / Ivy

The newest version!
package aQute.bnd.osgi;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.Spliterator;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.osgi.util.promise.PromiseFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.exceptions.RunnableWithException;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.OSGiHeader;
import aQute.bnd.header.Parameters;
import aQute.bnd.help.Syntax;
import aQute.bnd.help.SyntaxAnnotation;
import aQute.bnd.http.HttpClient;
import aQute.bnd.memoize.CloseableMemoize;
import aQute.bnd.memoize.Memoize;
import aQute.bnd.service.Plugin;
import aQute.bnd.service.Registry;
import aQute.bnd.service.RegistryPlugin;
import aQute.bnd.stream.MapStream;
import aQute.bnd.unmodifiable.Lists;
import aQute.bnd.version.Version;
import aQute.bnd.version.VersionRange;
import aQute.lib.collections.Iterables;
import aQute.lib.io.IO;
import aQute.lib.io.IOConstants;
import aQute.lib.strings.Strings;
import aQute.lib.utf8properties.UTF8Properties;
import aQute.libg.command.Command;
import aQute.libg.cryptography.Digester;
import aQute.libg.cryptography.SHA1;
import aQute.libg.generics.Create;
import aQute.libg.qtokens.QuotedTokenizer;
import aQute.libg.reporter.ReporterAdapter;
import aQute.service.reporter.Reporter;

public class Processor extends Domain implements Reporter, Registry, Constants, Closeable {
	private static final Logger	logger	= LoggerFactory.getLogger(Processor.class);
	public static Reporter		log;

	static {
		ReporterAdapter reporterAdapter = new ReporterAdapter(System.out);
		reporterAdapter.setTrace(true);
		reporterAdapter.setExceptions(true);
		reporterAdapter.setPedantic(true);
		log = reporterAdapter;
	}

	static final int							BUFFER_SIZE			= IOConstants.PAGE_SIZE * 1;

	final static ThreadLocal			current				= new ThreadLocal<>();
	private static final Memoize	executors			= Memoize.supplier(ExecutorGroup::new);
	private static final Memoize		random				= Memoize.supplier(Random::new);
	public final static String					LIST_SPLITTER		= "\\s*,\\s*";

	final ThreadLocal					bracket				= new ThreadLocal<>();
	private final Set					basicPlugins		= Collections
		.newSetFromMap(new ConcurrentHashMap<>());
	private final Set			toBeClosed			= Collections
		.newSetFromMap(new ConcurrentHashMap<>());

	private volatile CloseableMemoize		pluginLoader		= newPluginLoader();
	private volatile Memoize	pluginsContainer	= newPluginsContainer();

	final MessageReporter						reporter			= new MessageReporter(this);
	boolean										fileMustExist		= true;

	private File								base				= new File("").getAbsoluteFile();
	private URI									baseURI				= base.toURI();

	Properties									properties;
	String										profile;
	private Macro								replacer;
	private long								lastModified;
	private File								propertiesFile;
	private boolean								fixup				= true;
	private Processor							parent;
	private final CopyOnWriteArrayList	included			= new CopyOnWriteArrayList<>();

	Collection							filter;
	Boolean										strict;
	boolean										trace;
	boolean										pedantic;
	boolean										exceptions;

	public static class FileLine {
		public static final FileLine	DUMMY	= new FileLine(null, 0, 0);
		public File						file;
		public int						line;
		public int						length;
		public int						start;
		public int						end;

		public FileLine() {

		}

		public FileLine(File file, int line, int length) {
			this.file = file;
			this.line = line;
			this.length = length;

		}

		public void set(SetLocation sl) {
			sl.file(IO.absolutePath(file));
			sl.line(line);
			sl.length(length);
		}
	}

	public Processor() {
		this(new UTF8Properties(), false);
	}

	public Processor(Properties props) {
		this(props, true);
	}

	public Processor(Processor parent) {
		this(parent, parent.getRawProperties(), true);
	}

	public Processor(Properties props, boolean wrap) {
		this.properties = wrap ? new UTF8Properties(props) : props;
	}

	public Processor(Processor parent, Properties props, boolean wrap) {
		this(props, wrap);
		this.parent = parent;
		if (parent != null) {
			updateModified(parent.lastModified(), "parent");
		}
	}

	public void setParent(Processor parent) {
		this.parent = parent;
		Properties updated = (parent != null) ? new UTF8Properties(parent.getRawProperties()) : new UTF8Properties();
		updated.putAll(getRawProperties());
		properties = updated;
		propertiesChanged();
	}

	public Processor getParent() {
		return parent;
	}

	public Processor getTop() {
		if (getParent() == null)
			return this;
		return getParent().getTop();
	}

	public void getInfo(Reporter processor, String prefix) {
		reporter.getInfo(processor, prefix);
	}

	public void getInfo(Reporter processor) {
		getInfo(processor, "");
	}

	/**
	 * A processor can mark itself current for a thread.
	 */
	Processor current() {
		Processor p = current.get();
		if (p == null)
			return this;
		return p;
	}

	@SuppressWarnings("resource")
	@Override
	public SetLocation warning(String string, Object... args) {
		return current().reporter.warning(string, args);
	}

	@SuppressWarnings("resource")
	@Override
	public SetLocation error(String string, Object... args) {
		return current().reporter.error(string, args);
	}

	/**
	 * @deprecated Use SLF4J Logger.info() instead.
	 */
	@Override
	@Deprecated
	public void progress(float progress, String format, Object... args) {
		Logger l = getLogger();
		if (l.isInfoEnabled()) {
			String message = formatArrays(format, args);
			if (progress > 0)
				l.info("[{}] {}", (int) progress, message);
			else
				l.info("{}", message);
		}
	}

	public void progress(String format, Object... args) {
		progress(-1f, format, args);
	}

	public SetLocation error(String format, Throwable t, Object... args) {
		return exception(t, format, args);
	}

	@Override
	public SetLocation exception(Throwable t, String format, Object... args) {
		Processor p = current();
		if (p.trace) {
			p.getLogger()
				.info("Reported exception {}", Exceptions.causes(t), t);
		} else {
			p.getLogger()
				.debug("Reported exception {}", Exceptions.causes(t), t);
		}
		if (p.exceptions) {
			printExceptionSummary(t, System.err);
		}

		t = Exceptions.unrollCause(t, InvocationTargetException.class);

		String s = formatArrays("Exception: %s", Exceptions.toString(t));
		reporter.error(s);

		return reporter.error(format, args);
	}

	public int printExceptionSummary(Throwable e, PrintStream out) {
		if (e == null) {
			return 0;
		}
		int count = 10;
		int n = printExceptionSummary(e.getCause(), out);

		if (n == 0) {
			out.println("Root cause: " + e.getMessage() + "   :" + e.getClass()
				.getName());
			count = Integer.MAX_VALUE;
		} else {
			out.println("Rethrown from: " + e.toString());
		}
		out.println();
		printStackTrace(e, count, out);
		System.err.println();
		return n + 1;
	}

	public void printStackTrace(Throwable e, int count, PrintStream out) {
		e.printStackTrace(out);
	}

	public void signal() {}

	@Override
	public List getWarnings() {
		return reporter.getWarnings();
	}

	@Override
	public List getErrors() {
		return reporter.getErrors();
	}

	/**
	 * Standard OSGi header parser.
	 *
	 * @param value
	 */
	static public Parameters parseHeader(String value, Processor logger) {
		return new Parameters(value, logger);
	}

	public Parameters parseHeader(String value) {
		return new Parameters(value, this);
	}

	public void addClose(AutoCloseable closeable) {
		assert closeable != null;
		toBeClosed.add(closeable);
	}

	public void removeClose(AutoCloseable closeable) {
		assert closeable != null;
		toBeClosed.remove(closeable);
	}

	public void use(Processor reporter) {
		setPedantic(reporter.isPedantic());
		setTrace(reporter.isTrace());
		setExceptions(reporter.isExceptions());
		setFailOk(reporter.isFailOk());
	}

	public static File getFile(File base, String file) {
		return IO.getFile(base, file);
	}

	public File getFile(String file) {
		return getFile(base, file);
	}

	/**
	 * Return a list of plugins that implement the given class.
	 *
	 * @param clazz Each returned plugin implements this class/interface
	 * @return A list of plugins
	 */
	@Override
	public  List getPlugins(Class clazz) {
		List list = getPlugins().getPlugins(clazz);
		return list;
	}

	/**
	 * Returns the first plugin it can find of the given type.
	 *
	 * @param 
	 * @param clazz
	 */
	@Override
	public  T getPlugin(Class clazz) {
		T plugin = getPlugins().getPlugin(clazz);
		return plugin;
	}

	/**
	 * Return the PluginsContainer. Plugins are defined with the -plugin
	 * command. They are class names, optionally associated with attributes.
	 * Plugins can implement the Plugin interface to see these attributes. Any
	 * object can be a plugin.
	 */
	public PluginsContainer getPlugins() {
		PluginsContainer pc = pluginsContainer.get();
		return pc;
	}

	/**
	 * Return a memoizer for the PluginsContainer.
	 */
	private Memoize newPluginsContainer() {
		Memoize supplier = Memoize.supplier(() -> {
			PluginsContainer pc = new PluginsContainer();
			pc.init(this);
			return pc;
		});
		// do postInit outside of above memoizer to allow reentrant
		// access to the inited PluginsContainer
		AtomicBoolean postInit = new AtomicBoolean(true);
		return Memoize.predicateSupplier(supplier, pc -> {
			// only first caller gets to do postInit
			if (postInit.getAndSet(false)) {
				pc.postInit(this);
			}
			return true; // always memoize the PluginsContainer
		});
	}

	/**
	 * Is called after the PluginsContainer is initialized.
	 *
	 * @param pluginsContainer
	 */
	protected void addExtensions(PluginsContainer pluginsContainer) {

	}

	protected void setTypeSpecificPlugins(PluginsContainer pluginsContainer) {
		pluginsContainer.add(getExecutor());
		pluginsContainer.add(getPromiseFactory());
		pluginsContainer.add(random);
		pluginsContainer.addAll(basicPlugins);
	}

	/**
	 * Set the initial parameters of a plugin
	 *
	 * @param plugin
	 * @param map
	 */
	protected  T customize(T plugin, Attrs map, PluginsContainer pluginsContainer) {
		if (plugin instanceof Plugin pluginPlugin) {
			try {
				pluginPlugin.setReporter(this);
			} catch (Exception e) {
				exception(e, "While setting reporter on plugin %s", pluginPlugin);
			}
			try {
				if (map == null) {
					map = Attrs.EMPTY_ATTRS;
				}
				pluginPlugin.setProperties(map);
			} catch (Exception e) {
				exception(e, "While setting properties %s on plugin %s", map, pluginPlugin);
			}
		}
		if (plugin instanceof RegistryPlugin registryPlugin) {
			try {
				registryPlugin.setRegistry(pluginsContainer);
			} catch (Exception e) {
				exception(e, "While setting registry on plugin %s", registryPlugin);
			}
		}
		return plugin;
	}

	/**
	 * Indicates that this run should ignore errors and succeed anyway
	 *
	 * @return true if this processor should return errors
	 */
	@Override
	public boolean isFailOk() {
		String v = getProperty(Constants.FAIL_OK, null);
		return v != null && v.equalsIgnoreCase("true");
	}

	public File getBase() {
		return base;
	}

	public URI getBaseURI() {
		return baseURI;
	}

	public void setBase(File base) {
		if (base == null) {
			this.base = null;
			baseURI = null;
		} else {
			this.base = base.getAbsoluteFile();
			baseURI = base.toURI();
		}
	}

	public void clear() {
		reporter.clear();
	}

	public Logger getLogger() {
		return logger;
	}

	/**
	 * Used to provide verbose commands
	 */
	@Override
	public void trace(String msg, Object... parms) {
		Processor p = current();
		if (p.trace) {
			String s = formatArrays(msg, parms);
			System.out.println(s);
		}
	}

	public  List newList() {
		return new ArrayList<>();
	}

	public  Set newSet() {
		return new TreeSet<>();
	}

	public static  Map newMap() {
		return new LinkedHashMap<>();
	}

	public static  Map newHashMap() {
		return new LinkedHashMap<>();
	}

	public  List newList(Collection t) {
		return new ArrayList<>(t);
	}

	public  Set newSet(Collection t) {
		return new TreeSet<>(t);
	}

	public  Map newMap(Map t) {
		return new LinkedHashMap<>(t);
	}

	@Override
	public void close() throws IOException {
		toBeClosed.forEach(IO::close);

		clearPlugins();

		toBeClosed.clear();
	}

	private void clearPlugins() {
		CloseableMemoize outgoingPluginLoader = pluginLoader;
		Memoize outgoingPluginsContainer = pluginsContainer;
		pluginLoader = newPluginLoader();
		pluginsContainer = newPluginsContainer();
		outgoingPluginsContainer.ifPresent(PluginsContainer::close);
		IO.close(outgoingPluginLoader);
	}

	public String _basedir(@SuppressWarnings("unused")
	String args[]) {
		if (base == null)
			throw new IllegalArgumentException("No base dir set");

		return IO.absolutePath(base);
	}

	public String _propertiesname(String[] args) {
		if (args.length > 1) {
			error("propertiesname does not take arguments");
			return null;
		}

		File pf = getPropertiesFile();
		if (pf == null)
			return "";

		return pf.getName();
	}

	public String _propertiesdir(String[] args) {
		if (args.length > 1) {
			error("propertiesdir does not take arguments");
			return null;
		}
		File pf = getPropertiesFile();
		if (pf == null)
			return "";

		return IO.absolutePath(pf.getParentFile());
	}

	static final String _uriHelp = "${uri;[;]}, Resolve the uri against the baseuri. baseuri defaults to the processor base.";

	public String _uri(String args[]) throws Exception {
		Macro.verifyCommand(args, _uriHelp, null, 2, 3);

		URI uri = new URI(args[1]);
		if (!uri.isAbsolute() || uri.getScheme()
			.equals("file")) {
			URI base;
			if (args.length > 2) {
				base = new URI(args[2]);
			} else {
				base = getBaseURI();
				if (base == null) {
					throw new IllegalArgumentException("No base dir set");
				}
			}
			uri = base.resolve(uri.getSchemeSpecificPart());
		}
		return uri.toString();
	}

	public String _fileuri(String args[]) throws Exception {
		return getReplacer()._fileuri(args);
	}

	/**
	 * Property handling ...
	 */

	public Properties getProperties() {
		if (fixup) {
			fixup = false;
			begin();
		}
		return getRawProperties();
	}

	/**
	 * This is the primary place where we get the local Properties. No code in
	 * this class should use this variable directory.
	 *
	 * @return the local properties
	 */
	protected Properties getRawProperties() {
		return properties;
	}

	public String getProperty(String key) {
		return getProperty(key, null);
	}

	public String getUnexpandedProperty(String key) {
		if (filter != null && filter.contains(key)) {
			Object raw = getProperties().get(key);
			return (raw instanceof String string) ? string : null;
		}
		return getProperties().getProperty(key);
	}

	public void mergeProperties(File file, boolean overwrite) {
		if (file.isFile()) {
			try {
				Properties properties = loadProperties(file);
				mergeProperties(properties, overwrite);
			} catch (Exception e) {
				error("Error loading properties file: %s", file);
			}
		} else {
			if (!file.exists())
				error("Properties file does not exist: %s", file);
			else
				error("Properties file must a file, not a directory: %s", file);
		}
	}

	public void mergeProperties(Properties properties, boolean overwrite) {
		for (String key : Iterables.iterable(properties.propertyNames(), String.class::cast)) {
			String value = properties.getProperty(key);
			if (overwrite || !getProperties().containsKey(key))
				setProperty(key, value);
		}
	}

	public void setProperties(Properties properties) {
		setProperties(getBase(), properties);
	}

	public void setProperties(InputStream properties) throws IOException {
		UTF8Properties p = new UTF8Properties();
		p.load(properties);
		setProperties(getBase(), p);
	}

	public void setProperties(File base, Properties properties) {
		doIncludes(base, properties);
		getRawProperties().putAll(properties);
		mergeProperties(Constants.INIT); // execute macros in -init
		getRawProperties().remove(Constants.INIT);
		propertiesChanged();
	}

	public void addProperties(File file) throws Exception {
		addIncluded(file);
		Properties p = loadProperties(file);
		setProperties(p);
	}

	public void addProperties(Map properties) {
		properties.forEach((k, v) -> setProperty(k.toString(), String.valueOf(v)));
	}

	public void addIncluded(File file) {
		addIncludedIfAbsent(file);
	}

	private boolean addIncludedIfAbsent(File file) {
		return included.addIfAbsent(file);
	}

	private boolean removeIncluded(File file) {
		return included.remove(file);
	}

	/**
	 * Inspect the properties and if you find -includes parse the line included
	 * manifest files or properties files. The files are relative from the given
	 * base, this is normally the base for the analyzer.
	 *
	 * @param ubase
	 * @param p
	 * @throws IOException
	 */

	private void doIncludes(File ubase, Properties p) {
		String includes = p.getProperty(INCLUDE);
		if (includes != null) {
			includes = getReplacer().process(includes);
			p.remove(INCLUDE);
			Collection clauses = new Parameters(includes, this).keySet();

			for (String value : clauses) {
				boolean fileMustExist = true;
				boolean overwrite = true;
				while (true) {
					if (value.startsWith("-")) {
						fileMustExist = false;
						value = value.substring(1)
							.trim();
					} else if (value.startsWith("~")) {
						// Don't overwrite properties!
						overwrite = false;
						value = value.substring(1)
							.trim();
					} else
						break;
				}
				try {
					File file = getFile(ubase, value).getAbsoluteFile();
					if (!file.isFile()) {
						try {
							URL url = new URL(value);
							int n = value.lastIndexOf('.');
							String ext = ".jar";
							if (n >= 0)
								ext = value.substring(n);

							Path tmp = Files.createTempFile("url", ext);
							try (Resource resource = Resource.fromURL(url, getPlugin(HttpClient.class))) {
								try (OutputStream out = IO.outputStream(tmp)) {
									resource.write(out);
								}
								Files.setLastModifiedTime(tmp, FileTime.fromMillis(resource.lastModified()));
								doIncludeFile(tmp.toFile(), overwrite, p);
							} finally {
								removeIncluded(tmp.toFile());
								IO.delete(tmp);
							}
						} catch (MalformedURLException mue) {
							if (fileMustExist)
								error("Included file %s %s", file,
									(file.isDirectory() ? "is directory" : "does not exist"));
						} catch (Exception e) {
							if (fileMustExist)
								exception(e, "Error in processing included URL: %s", value);
						}
					} else
						doIncludeFile(file, overwrite, p);
				} catch (Exception e) {
					if (fileMustExist)
						exception(e, "Error in processing included file: %s", value);
				}
			}
		}
	}

	/**
	 * @param file
	 * @param overwrite
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void doIncludeFile(File file, boolean overwrite, Properties target) throws Exception {
		doIncludeFile(file, overwrite, target, null);
	}

	/**
	 * @param file
	 * @param overwrite
	 * @param extensionName
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void doIncludeFile(File file, boolean overwrite, Properties target, String extensionName) throws Exception {
		if (!addIncludedIfAbsent(file)) {
			error("Cyclic or multiple include of %s", file);
			return;
		}
		updateModified(file.lastModified(), file.toString());
		Properties sub = magicBnd(file);

		doIncludes(file.getParentFile(), sub);
		// take care regarding overwriting properties
		for (Map.Entry entry : sub.entrySet()) {
			String key = (String) entry.getKey();
			String value = (String) entry.getValue();

			if (overwrite || !target.containsKey(key)) {
				target.setProperty(key, value);
			} else if (extensionName != null) {
				String extensionKey = key + "." + extensionName;
				if (!target.containsKey(extensionKey))
					target.setProperty(extensionKey, value);
			}
		}
	}

	/**
	 * This method allows a sub Processor to override recognized included files.
	 * In general we treat files as bnd files but a sub processor can override
	 * this method to provide additional types. It is a rquirement that the file
	 * must be able to be mapped to a Properties. These properties will be added
	 * to this processor's properties. The default includes bnd, bndrun and
	 * manifest files.
	 *
	 * @param file the file with the information
	 * @return the Properties to include
	 */

	protected Properties magicBnd(File file) throws IOException {
		if (Strings.endsWithIgnoreCase(file.getName(), ".mf")) {
			try (InputStream in = IO.stream(file)) {
				return getManifestAsProperties(in);
			}
		} else
			return loadProperties(file);
	}

	public void unsetProperty(String string) {
		getProperties().remove(string);

	}

	public boolean refresh() {
		clearPlugins(); // We always refresh our plugins

		if (propertiesFile == null)
			return false;

		boolean changed = updateModified(propertiesFile.lastModified(), "properties file");
		for (File file : getIncluded()) {
			changed |= !file.exists() || updateModified(file.lastModified(), "include file: " + file);
		}

		profile = null; // Used in property access

		if (changed) {
			forceRefresh();
			return true;
		}
		return false;
	}

	/**
	 * If strict is true, then extra verification is done.
	 */
	boolean isStrict() {
		if (strict == null)
			strict = isTrue(getProperty(STRICT)); // Used in property access
		return strict;
	}

	/**
	 *
	 */
	public void forceRefresh() {
		included.clear();
		Processor p = getParent();
		properties = (p != null) ? new UTF8Properties(p.getRawProperties()) : new UTF8Properties();

		setProperties(propertiesFile, base);
	}

	public void propertiesChanged() {
		Processor p = getParent();
		if (p != null) {
			updateModified(p.lastModified(), "propertiesChanged");
		}

		clearPlugins(); // force plugins to reload since properties have changed
	}

	/**
	 * Set the properties by file. Setting the properties this way will also set
	 * the base for this analyzer. After reading the properties, this will call
	 * setProperties(Properties) which will handle the includes.
	 *
	 * @param propertiesFile
	 */
	public void setProperties(File propertiesFile) {
		if (propertiesFile == null)
			return;
		propertiesFile = propertiesFile.getAbsoluteFile();
		setProperties(propertiesFile, propertiesFile.getParentFile());
	}

	public void setProperties(File propertiesFile, File base) {
		this.propertiesFile = propertiesFile.getAbsoluteFile();
		setBase(base);
		try {
			if (propertiesFile.isFile()) {
				// System.err.println("Loading properties " + propertiesFile);
				included.clear();
				Properties p = loadProperties(propertiesFile);
				setProperties(p);
			} else {
				if (fileMustExist) {
					error("No such properties file: %s", propertiesFile);
				}
			}
		} catch (IOException e) {
			error("Could not load properties %s", propertiesFile);
		}
	}

	public void setProperties(Reader reader) throws IOException {
		UTF8Properties p = new UTF8Properties();
		p.load(reader);
		setProperties(p);
	}

	protected void begin() {
		if (isTrue(getProperty(PEDANTIC)))
			setPedantic(true);
	}

	public static boolean isTrue(String value) {

		if (value == null)
			return false;

		value = value.trim();
		if (value.isEmpty())
			return false;

		if (value.startsWith("!"))
			if (value.equals("!"))
				return false;
			else
				return !isTrue(value.substring(1));

		if ("false".equalsIgnoreCase(value))
			return false;

		if ("off".equalsIgnoreCase(value))
			return false;

		if ("not".equalsIgnoreCase(value))
			return false;

		return true;
	}

	/**
	 * Get a property without preprocessing it with a proper default
	 *
	 * @param key
	 * @param deflt
	 */

	public String getUnprocessedProperty(String key, String deflt) {
		if (filter != null && filter.contains(key)) {
			Object raw = getProperties().get(key);
			return (raw instanceof String string) ? string : deflt;
		}
		return getProperties().getProperty(key, deflt);
	}

	/**
	 * Get a property with preprocessing it with a proper default
	 *
	 * @param key
	 * @param deflt
	 */
	public String getProperty(String key, String deflt) {
		return getProperty(key, deflt, ",");
	}

	public String getProperty(String key, String deflt, String separator) {
		return getProperty(key, deflt, separator, true);
	}

	@SuppressWarnings("resource")
	private String getProperty(String key, String deflt, String separator, boolean inherit) {

		Instruction ins = new Instruction(key);
		if (ins.isLiteral()) {
			return getLiteralProperty(ins.getLiteral(), deflt, this, inherit);
		}

		return getWildcardProperty(deflt, separator, inherit, ins);
	}

	/**
	 * A Property Key is the pair of a Processor and a key it defines. It also
	 * defines if this is the firsts definition viewed from this Processor. The
	 * floor indicates where the property is defined relative to its parents.
	 * Zero is in the current processor, 1, is its parents, and so on.
	 */
	public record PropertyKey(Processor processor, String key, int floor) implements Comparable {

		/**
		 * Check if this PropertyKey belongs to the given processor
		 *
		 * @param p the processor to check
		 * @return true if our processor is the same as p
		 */
		public boolean isLocalTo(Processor p) {
			return processor == p;
		}

		/**
		 * Get the value of the property key
		 *
		 * @return a processed value
		 */
		public String getValue() {
			return processor.getProperty(key);
		}

		/**
		 * Get the raw value of the property key
		 *
		 * @return a raw value
		 */
		public String getRawValue() {
			return processor.getProperties()
				.getProperty(key);
		}

		@Override
		public int compareTo(PropertyKey o) {
			int n = key.compareTo(o.key);
			if (n != 0)
				return n;
			return Integer.compare(floor, o.floor);
		}

		/**
		 * Find visible property keys. "Visible" in this context means that
		 * among the {@code PropertyKey} objects with the same key, only the one
		 * with the lowest floor number is included in the result.
		 *
		 * @param keys
		 * @return only unique keys which are visible (lowest floor value)
		 */
		public static List findVisible(Collection keys) {
			List l = new ArrayList<>(keys);
			Collections.sort(l);
			String rover = null;
			Iterator it = l.iterator();
			while (it.hasNext()) {
				PropertyKey candidate = it.next();
				if (!candidate.key.equals(rover)) {
					rover = candidate.key;
				} else
					it.remove();
			}
			return l;
		}
	}

	/**
	 * Return a list of sorted PropertyKey that match the predicate and includes
	 * the inheritance chain. The intention is to capture the processor that
	 * defines a key.
	 *
	 * @param predicate the predicate to filter the key
	 * @return new modifiable sorted list of PropertyKey
	 */
	@SuppressWarnings("resource")
	public List getPropertyKeys(Predicate predicate) {
		List keys = new ArrayList<>();
		Processor rover = this;
		int level = 0;
		while (rover != null) {
			Processor localRover = rover;
			int localLevel = level;
			rover.stream(false) // local only
				.filter(predicate)
				.map(k -> new PropertyKey(localRover, k, localLevel))
				.forEach(keys::add);
			rover = rover.getParent();
			level++;
		}
		Collections.sort(keys);
		return keys;

	}

	/**
	 * Return the merge property keys
	 */
	public List getMergePropertyKeys(String stem) {
		String prefix = stem + ".";
		return getPropertyKeys(k -> k.equals(stem) || k.startsWith(prefix));
	}

	private String getWildcardProperty(String deflt, String separator, boolean inherit, Instruction ins) {
		// Handle a wildcard key, make sure they're sorted
		// for consistency
		String result = stream(inherit).filter(ins::matches)
			.sorted()
			.map(k -> getLiteralProperty(k, null, this, inherit))
			.filter(v -> (v != null) && !v.isEmpty())
			.collect(Strings.joining(separator, "", "", deflt));
		return result;
	}

	private String getLiteralProperty(String key, String deflt, Processor source, boolean inherit) {
		String value = null;
		// Use the key as is first, if found ok

		for (Processor proc = source; proc != null; proc = proc.getParent()) {
			Object raw = proc.getProperties()
				.get(key);
			if (raw != null) {
				if (raw instanceof String string) {
					value = string;
				} else if (isPedantic()) {
					warning("Key '%s' has a non-String value: %s:%s", key, raw.getClass()
						.getName(), raw);
				}
				source = proc;
				break;
			}

			if (!inherit) {
				break;
			}
			Collection keyFilter = proc.filter;
			if ((keyFilter != null) && (keyFilter.contains(key))) {
				break;
			}
		}
		//
		// Check if we can find a replacement through the
		// replacer, which takes profiles into account
		if (value == null) {
			value = getReplacer().getMacro(key, null);
		}

		if (value != null)
			return getReplacer().process(value, source);
		else if (deflt != null)
			return getReplacer().process(deflt, this);
		else
			return null;
	}

	/**
	 * Helper to load a properties file from disk.
	 *
	 * @param file
	 * @throws IOException
	 */
	public Properties loadProperties(File file) throws IOException {
		updateModified(file.lastModified(), "Properties file: " + file);
		UTF8Properties p = loadProperties0(file);
		return p;
	}

	/**
	 * Load Properties from disk. The default encoding is ISO-8859-1 but
	 * nowadays all files are encoded with UTF-8. So we try to load it first as
	 * UTF-8 and if this fails we fail back to ISO-8859-1
	 *
	 * @throws IOException
	 */
	UTF8Properties loadProperties0(File file) throws IOException {
		try {
			UTF8Properties p = new UTF8Properties();
			p.load(file, this, Constants.OSGI_SYNTAX_HEADERS);
			return p.replaceHere(file.getParentFile());
		} catch (Exception e) {
			error("Error during loading properties file: %s, error: %s", file, e);
			return new UTF8Properties();
		}
	}

	/**
	 * Replace a string in all the values of the map. This can be used to
	 * preassign variables that change. I.e. the base directory ${.} for a
	 * loaded properties
	 */
	public static Properties replaceAll(Properties p, String pattern, String replacement) {
		Pattern regex = Pattern.compile(pattern);
		UTF8Properties result = MapStream.of(p)
			.mapValue(value -> regex.matcher((String) value)
				.replaceAll(replacement))
			.collect(MapStream.toMap((u, v) -> v, UTF8Properties::new));
		return result;
	}

	/**
	 * Print a standard Map based OSGi header.
	 *
	 * @param exports map { name => Map { attribute|directive => value } }
	 * @return the clauses
	 * @throws IOException
	 */
	public static String printClauses(Map> exports) throws IOException {
		return printClauses(exports, false);
	}

	public static String printClauses(Map> exports, @SuppressWarnings("unused")
	boolean checkMultipleVersions) throws IOException {
		StringBuilder sb = new StringBuilder();
		String del = "";
		for (Entry> entry : exports.entrySet()) {
			String name = entry.getKey()
				.toString();
			Map clause = entry.getValue();

			// We allow names to be duplicated in the input
			// by ending them with '~'. This is necessary to use
			// the package names as keys. However, we remove these
			// suffixes in the output so that you can set multiple
			// exports with different attributes.
			String outname = removeDuplicateMarker(name);
			sb.append(del);
			sb.append(outname);
			printClause(clause, sb);
			del = ",";
		}
		return sb.toString();
	}

	public static void printClause(Map map, StringBuilder sb) throws IOException {
		if (map instanceof Attrs attrs) {
			for (Entry entry : attrs.entrySet()) {
				String key = entry.getKey();
				// Skip directives we do not recognize
				if (!AttributeClasses.MANIFEST.test(key))
					continue;

				sb.append(";");
				attrs.append(sb, entry);
			}
		} else {
			for (Entry entry : map.entrySet()) {
				String key = entry.getKey()
					.toString();
				// Skip directives we do not recognize
				if (!AttributeClasses.MANIFEST.test(key))
					continue;

				sb.append(";");
				sb.append(key);
				sb.append("=");
				String value = ((String) entry.getValue()).trim();
				quote(sb, value);
			}
		}
	}

	/**
	 * @param sb
	 * @param value
	 * @throws IOException
	 */
	public static boolean quote(Appendable sb, String value) throws IOException {
		return OSGiHeader.quote(sb, value);
	}

	public Macro getReplacer() {
		if (replacer == null)
			return replacer = new Macro(this, getMacroDomains());
		return replacer;
	}

	/**
	 * This should be overridden by subclasses to add extra macro command
	 * domains on the search list.
	 */
	protected Object[] getMacroDomains() {
		return new Object[] {};
	}

	/**
	 * Return the properties but expand all macros. This always returns a new
	 * Properties object that can be used in any way.
	 */
	public Properties getFlattenedProperties() {
		return getReplacer().getFlattenedProperties();

	}

	/**
	 * Return the properties but expand all macros. This always returns a new
	 * Properties object that can be used in any way.
	 */
	public Properties getFlattenedProperties(boolean ignoreInstructions) {
		return getReplacer().getFlattenedProperties(ignoreInstructions);
	}

	/**
	 * Return all inherited property keys. The keys are sorted for consistent
	 * ordering.
	 */
	public Set getPropertyKeys(boolean inherit) {
		Set result;
		if (getParent() == null || !inherit) {
			result = new TreeSet<>();
		} else {
			result = getParent().getPropertyKeys(inherit);
			if (filter != null) {
				result.removeAll(filter);
			}
		}
		for (Object o : getRawProperties().keySet()) {
			result.add(o.toString());
		}
		return result;
	}

	public boolean updateModified(long time, String reason) {
		if (time > lastModified) {
			lastModified = time;
			return true;
		}
		return false;
	}

	public long lastModified() {
		return lastModified;
	}

	/**
	 * Add or overwrite a new property.
	 *
	 * @param key
	 * @param value
	 */
	public void setProperty(String key, String value) {
		getProperties().put(normalizeKey(key), value);
	}

	/**
	 * Read a manifest but return a properties object.
	 *
	 * @param in
	 * @throws IOException
	 */
	public static Properties getManifestAsProperties(InputStream in) throws IOException {
		Properties p = new UTF8Properties();
		Manifest manifest = new Manifest(in);
		for (Object object : manifest.getMainAttributes()
			.keySet()) {
			Attributes.Name key = (Attributes.Name) object;
			String value = manifest.getMainAttributes()
				.getValue(key);
			p.put(key.toString(), value);
		}
		return p;
	}

	public File getPropertiesFile() {
		return propertiesFile;
	}

	/**
	 * Marks if the given Properties File really must exist.
	 */
	public void setFileMustExist(boolean mustexist) {
		fileMustExist = mustexist;
	}

	public boolean mustFileExist() {
		return fileMustExist;
	}

	static public String read(InputStream in) throws Exception {
		return IO.collect(in, UTF_8);
	}

	/**
	 * Join a list.
	 */
	public static String join(Collection list) {
		return join(list, ",");
	}

	public static String join(Collection list, String delimeter) {
		if (list == null || list.isEmpty())
			return "";
		StringBuilder sb = new StringBuilder();
		String del = "";
		for (Object item : list) {
			sb.append(del);
			sb.append(item);
			del = delimeter;
		}
		return sb.toString();
	}

	public static String join(Collection... lists) {
		return join(",", lists);
	}

	public static String join(String delimeter, Collection... lists) {
		if (lists == null || lists.length == 0)
			return "";
		StringBuilder sb = new StringBuilder();
		String del = "";
		for (Collection list : lists) {
			for (Object item : list) {
				sb.append(del);
				sb.append(item);
				del = delimeter;
			}
		}
		return sb.toString();
	}

	public static String join(Object[] list, String delimeter) {
		if (list == null || list.length == 0)
			return "";
		StringBuilder sb = new StringBuilder();
		String del = "";
		for (Object item : list) {
			sb.append(del);
			sb.append(item);
			del = delimeter;
		}
		return sb.toString();
	}

	public static  String join(T[] list) {
		return join(list, ",");
	}

	public static void split(String s, Collection collection) {
		Strings.splitAsStream(s)
			.forEachOrdered(collection::add);
	}

	public static Collection split(String s) {
		return Strings.split(s);
	}

	public static Collection split(String s, String splitter) {
		return Strings.split(splitter, s);
	}

	public static String merge(String... strings) {
		ArrayList result = new ArrayList<>();
		for (String s : strings) {
			if (s != null)
				split(s, result);
		}
		return join(result);
	}

	/**
	 * Make the file short if it is inside our base directory, otherwise long.
	 *
	 * @param file
	 */
	public String normalize(String file) {
		file = IO.normalizePath(file);
		String path = IO.absolutePath(base);
		int len = path.length();
		if (file.startsWith(path) && file.charAt(len) == '/') {
			return file.substring(len + 1);
		}
		return file;
	}

	public String normalize(File file) {
		return normalize(file.getAbsolutePath());
	}

	public static String removeDuplicateMarker(String key) {
		int i = key.length() - 1;
		while ((i >= 0) && (key.charAt(i) == DUPLICATE_MARKER)) {
			--i;
		}
		return key.substring(0, i + 1);
	}

	public static boolean isDuplicate(String key) {
		return key.indexOf(DUPLICATE_MARKER, key.length() - 1) >= 0;
	}

	public static class CL extends ActivelyClosingClassLoader {
		static {
			ClassLoader.registerAsParallelCapable();
		}

		public CL(Processor p) {
			super(p, p.getClass()
				.getClassLoader());
		}

		@Override
		@Deprecated
		public URL[] getURLs() {
			return new URL[0];
		}

		@Override
		public void add(File file) {
			super.add(file);
		}

	}

	protected CL getLoader() {
		return pluginLoader.get();
	}

	private CloseableMemoize newPluginLoader() {
		return CloseableMemoize.closeableSupplier(() -> {
			CL pluginLoader = new CL(this);
			if (IO.isWindows() && isInteractive()) {
				pluginLoader.autopurge(TimeUnit.SECONDS.toNanos(5L));
			}
			return pluginLoader;
		});
	}

	/*
	 * Check if this is a valid project.
	 */
	public boolean exists() {
		return base != null && base.isDirectory() && propertiesFile != null && propertiesFile.isFile();
	}

	@Override
	public boolean isOk() {
		return isFailOk() || getErrors().isEmpty();
	}

	public boolean check(String... pattern) throws IOException {
		Set missed = Create.set();
		List errors = getErrors();
		List warnings = getWarnings();

		if (pattern != null) {
			for (String p : pattern) {
				boolean match = false;
				Pattern pat = Pattern.compile(p);
				for (Iterator i = errors.iterator(); i.hasNext();) {
					String next = i.next();
					if (pat.matcher(next)
						.find()) {
						i.remove();
						match = true;
						reporter.remove(next);
					}
				}
				for (Iterator i = warnings.iterator(); i.hasNext();) {
					String next = i.next();
					if (pat.matcher(next)
						.find()) {
						i.remove();
						match = true;
						reporter.remove(next);
					}
				}
				if (!match)
					missed.add(p);

			}
		}
		if (missed.isEmpty() && errors.isEmpty() && warnings.isEmpty())
			return true;

		if (!missed.isEmpty())
			System.err.println("Missed the following patterns in the warnings or errors: " + missed);

		report(System.err);
		return false;
	}

	protected void report(Appendable out) throws IOException {
		List errors = getErrors();
		List warnings = getWarnings();

		if (errors.size() > 0) {
			out.append(String.format("-----------------%nErrors%n"));
			for (int i = 0; i < errors.size(); i++) {
				out.append(String.format("%03d: %s%n", i, errors.get(i)));
			}
		}
		if (warnings.size() > 0) {
			out.append(String.format("-----------------%nWarnings%n"));
			for (int i = 0; i < warnings.size(); i++) {
				out.append(String.format("%03d: %s%n", i, warnings.get(i)));
			}
		}
	}

	public boolean isPerfect() {
		return getErrors().isEmpty() && getWarnings().isEmpty();
	}

	public void setForceLocal(Collection local) {
		filter = local;
	}

	/**
	 * Answer if the name is a missing plugin's command name. If a bnd file
	 * contains the command name of a plugin, and that plugin is not available,
	 * then an error is reported during manifest calculation. This allows the
	 * plugin to fail to load when it is not needed. We first get the plugins to
	 * ensure it is properly initialized.
	 *
	 * @param name
	 */
	public boolean isMissingPlugin(String name) {
		return getPlugins().isMissingPlugin(name);
	}

	/**
	 * Append two strings to for a path in a ZIP or JAR file. It is guaranteed
	 * to return a string that does not start, nor ends with a '/', while it is
	 * properly separated with slashes. Double slashes are properly removed.
	 *
	 * 
	 * "/" + "abc/def/" becomes "abc/def"
	 * @param prefix @param suffix @return
	 */
	public static String appendPath(String... parts) {
		StringBuilder sb = new StringBuilder(parts.length * 16);
		boolean lastSlash = true;
		for (String part : parts) {
			final int partlen = part.length();
			if (partlen == 0) {
				continue;
			}
			if (!lastSlash) {
				sb.append('/');
				lastSlash = true;
			}
			for (int i = 0; i < partlen; i++) {
				char c = part.charAt(i);
				if (lastSlash) {
					if (c != '/') {
						sb.append(c);
						lastSlash = false;
					}
				} else {
					sb.append(c);
					if (c == '/') {
						lastSlash = true;
					}
				}
			}
		}
		if (lastSlash) {
			int sblen = sb.length();
			if (sblen > 0) {
				sb.setLength(sblen - 1);
			}
		}

		return sb.toString();
	}

	/**
	 * Parse the a=b strings and return a map of them.
	 *
	 * @param attrs
	 * @param clazz
	 */
	public static Attrs doAttrbutes(Object[] attrs, Clazz clazz, Macro macro) {
		Attrs map = new Attrs();

		if (attrs == null || attrs.length == 0)
			return map;

		for (Object a : attrs) {
			String attr = (String) a;
			int n = attr.indexOf('=');
			if (n > 0) {
				map.put(attr.substring(0, n), macro.process(attr.substring(n + 1)));
			} else
				throw new IllegalArgumentException(formatArrays(
					"Invalid attribute on package-info.java in %s , %s. Must be = ", clazz, attr));
		}
		return map;
	}

	/**
	 * This method is the same as String.format but it makes sure that any
	 * arrays are transformed to strings.
	 *
	 * @param string
	 * @param parms
	 */
	public static String formatArrays(String string, Object... parms) {
		return Strings.format(string, parms);
	}

	/**
	 * Check if the object is an array and turn it into a string if it is,
	 * otherwise unchanged.
	 *
	 * @param object the object to make printable
	 * @return a string if it was an array or the original object
	 */
	public static Object makePrintable(Object object) {
		if (object == null)
			return null;

		if (object.getClass()
			.isArray()) {
			return Arrays.toString(makePrintableArray(object));
		}
		return object;
	}

	private static Object[] makePrintableArray(Object array) {
		final int length = Array.getLength(array);
		Object[] output = new Object[length];
		for (int i = 0; i < length; i++) {
			output[i] = makePrintable(Array.get(array, i));
		}
		return output;
	}

	public static String append(String... strings) {
		List result = Create.list();
		for (String s : strings) {
			result.addAll(split(s));
		}
		return join(result);
	}

	public synchronized Class getClass(String type, File jar) throws Exception {
		CL cl = getLoader();
		cl.add(jar);
		return cl.loadClass(type);
	}

	private static final Pattern DURATION_P = Pattern
		.compile("\\s*(\\d+)\\s*(NANOSECONDS|MICROSECONDS|MILLISECONDS|SECONDS|MINUTES|HOURS|DAYS)?");

	public static long getDuration(String tm, long dflt) {
		if (tm == null)
			return dflt;

		Matcher m = DURATION_P.matcher(tm.toUpperCase(Locale.ROOT));
		if (m.matches()) {
			long duration = Long.parseLong(m.group(1));
			String u = m.group(2);
			TimeUnit unit = (u != null) ? TimeUnit.valueOf(u) : TimeUnit.MILLISECONDS;
			return unit.toMillis(duration);
		}
		return dflt;
	}

	/**
	 * Generate a random string, which is guaranteed to be a valid Java
	 * identifier (first character is an ASCII letter, subsequent characters are
	 * ASCII letters or numbers). Takes an optional parameter for the length of
	 * string to generate; default is 8 characters.
	 */
	public String _random(String[] args) {
		int numchars = 8;
		if (args.length > 1) {
			try {
				numchars = Integer.parseInt(args[1]);
			} catch (NumberFormatException e) {
				throw new IllegalArgumentException("Invalid character count parameter in ${random} macro.");
			}
		}

		Random random = Processor.random.get();

		char[] letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
		char[] alphanums = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();

		char[] array = new char[numchars];
		for (int i = 0; i < numchars; i++) {
			char c;
			if (i == 0)
				c = letters[random.nextInt(letters.length)];
			else
				c = alphanums[random.nextInt(alphanums.length)];
			array[i] = c;
		}

		return new String(array);
	}

	/**
	 * 

* Generates a Capability string, in the format specified by the OSGi * Provide-Capability header, representing the current native platform * according to OSGi RFC 188. For example on Windows7 running on an x86_64 * processor it should generate the following: *

* *
	 * osgi.native;osgi.native.osname:List<String>="Windows7,Windows
	 * 7,Win32";osgi.native.osversion:Version=6.1.0;osgi.native.processor:List&
	 * lt;String>="x86-64,amd64,em64t,x86_64"
	 * 
* * @param args The array of properties. For example: the macro invocation of * "${native_capability;osversion=3.2.4;osname=Linux}" results in * an args array of * [native_capability, osversion=3.2.4, osname=Linux] */ public String _native_capability(String... args) throws Exception { return OSInformation.getNativeCapabilityClause(this, args); } /** * Set the current command thread. This must be balanced with the * {@link #endHandleErrors(Processor)} method. The method returns the * previous command owner or null. The command owner will receive all * warnings and error reports. */ protected Processor beginHandleErrors(String message) { logger.debug("begin {}", message); Processor previous = current.get(); current.set(this); return previous; } /** * End a command. Will restore the previous command owner. * * @param previous */ protected void endHandleErrors(Processor previous) { logger.debug("end"); current.set(previous); } public static Executor getExecutor() { return executors.get() .getExecutor(); } public static ScheduledExecutorService getScheduledExecutor() { return executors.get() .getScheduledExecutor(); } public static PromiseFactory getPromiseFactory() { return executors.get() .getPromiseFactory(); } /** * These plugins are added to the total list of plugins. The separation is * necessary because the list of plugins is refreshed now and then so we * need to be able to add them at any moment in time. * * @param plugin */ public void addBasicPlugin(Object plugin) { basicPlugins.add(plugin); pluginsContainer.ifPresent(pc -> pc.add(plugin)); } public void removeBasicPlugin(Object plugin) { basicPlugins.remove(plugin); pluginsContainer.ifPresent(pc -> pc.remove(plugin)); } public List getIncluded() { return included; } /** * Overrides for the Domain class */ @Override public String get(String key) { return getProperty(key); } @Override public String get(String key, String deflt) { return getProperty(key, deflt); } @Override public void set(String key, String value) { setProperty(key, value); } Stream stream() { return stream(true); } private Stream stream(boolean inherit) { return StreamSupport.stream(iterable(inherit, Objects::nonNull).spliterator(), false); } @Override public Iterator iterator() { return iterable(true, Objects::nonNull).iterator(); } @Override public Spliterator spliterator() { return iterable(true, Objects::nonNull).spliterator(); } private Iterable iterable(boolean inherit, Predicate keyFilter) { Set first = getRawProperties().keySet(); Iterable second; if (getParent() == null || !inherit) { second = Collections.emptyList(); } else { second = getParent().iterable(inherit, (filter == null) ? keyFilter : keyFilter.and(key -> !filter.contains(key))); } Iterable iterable = Iterables.distinct(first, second, o -> (o instanceof String string) ? string : null, keyFilter); return iterable; } public Set keySet() { return getPropertyKeys(true); } /** * Printout of the status of this processor for toString() */ @Override public String toString() { try { StringBuilder sb = new StringBuilder(); report(sb); return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } } /** * Utiltity to replace an extension * * @param s * @param extension * @param newExtension */ public String replaceExtension(String s, String extension, String newExtension) { if (s.endsWith(extension)) s = s.substring(0, s.length() - extension.length()); return s + newExtension; } static class SetLocationImpl extends Location implements SetLocation { public SetLocationImpl(String s) { this.message = s; } @Override public SetLocation file(String file) { this.file = (file != null) ? IO.normalizePath(file) : null; return this; } @Override public SetLocation header(String header) { this.header = header; return this; } @Override public SetLocation context(String context) { this.context = context; return this; } @Override public SetLocation method(String methodName) { this.methodName = methodName; return this; } @Override public SetLocation line(int n) { this.line = n; return this; } @Override public SetLocation reference(String reference) { this.reference = reference; return this; } @Override public SetLocation details(Object details) { this.details = details; return this; } @Override public Location location() { return this; } @Override public SetLocation length(int length) { this.length = length; return this; } } public SetLocation setLocation(String header, String clause, SetLocation setLocation) { try { FileLine info = getHeader(header, clause); if (info != null) { info.set(setLocation); } else { setLocation.header(header) .context(clause); } } catch (Exception e) { exception(e, "unexpected exception in setLocation"); } return setLocation; } @Override public Location getLocation(String msg) { return reporter.getLocation(msg); } /** * Get a header relative to this processor, taking its parents and includes * into account. * * @param header * @throws IOException */ public FileLine getHeader(String header) throws Exception { return getHeader( Pattern.compile("^[ \t]*".concat(Pattern.quote(header)), Pattern.MULTILINE | Pattern.CASE_INSENSITIVE)); } public static Pattern toFullHeaderPattern(String header) { StringBuilder sb = new StringBuilder(); sb.append("^[ \t]*(") .append(header) .append(")(\\.[^\\s:=]*)?[ \t]*[ \t:=][ \t]*"); sb.append("[^\\\\\n\r]*(\\\\\n[^\\\\\n\r]*)*"); try { return Pattern.compile(sb.toString(), Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); } catch (Exception e) { return Pattern.compile("^[ \t]*".concat(Pattern.quote(header)), Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); } } public FileLine getHeader(Pattern header) throws Exception { return getHeader(header, null); } public FileLine getHeader(String header, String clause) throws Exception { return getHeader(toFullHeaderPattern(header), clause == null ? null : Pattern.compile(clause, Pattern.LITERAL)); } public FileLine getHeader(Pattern header, Pattern clause) throws Exception { FileLine fl = getHeader0(header, clause); if (fl != null) return fl; @SuppressWarnings("resource") Processor rover = this; while (rover.getPropertiesFile() == null) if (rover.getParent() == null) { return new FileLine(new File("ANONYMOUS"), 0, 0); } else rover = rover.getParent(); return new FileLine(rover.getPropertiesFile(), 0, 0); } private FileLine getHeader0(Pattern header, Pattern clause) throws Exception { FileLine fl; File f = getPropertiesFile(); if (f != null) { // Find in "our" local file fl = findHeader(f, header, clause); if (fl != null) return fl; // Get the includes (actually should parse the header // to see if they overwrite or only provide defaults? for (Iterator iter = new ArrayDeque<>(getIncluded()).descendingIterator(); iter.hasNext();) { File file = iter.next(); fl = findHeader(file, header); if (fl != null) { return fl; } } } // Ok, not on this level ... if (getParent() != null) { fl = getParent().getHeader(header, clause); if (fl != null) return fl; } // Ok, report the error on the sub file // Sometimes we do not have a file ... if (f == null && getParent() != null) f = getParent().getPropertiesFile(); if (f == null) return null; return new FileLine(f, 0, 0); } public static FileLine findHeader(File f, String header) throws IOException { return findHeader(f, Pattern.compile("^[ \t]*".concat(Pattern.quote(header)), Pattern.MULTILINE | Pattern.CASE_INSENSITIVE)); } public static FileLine findHeader(File f, Pattern header) throws IOException { return findHeader(f, header, null); } public static FileLine findHeader(File f, Pattern header, Pattern clause) throws IOException { if (f.isFile()) { String s = IO.collect(f); Matcher matcher = header.matcher(s); while (matcher.find()) { FileLine fl = new FileLine(); fl.file = f; fl.start = matcher.start(); fl.end = matcher.end(); fl.length = fl.end - fl.start; fl.line = getLine(s, fl.start); if (clause != null) { Matcher mclause = clause.matcher(s); mclause.region(fl.start, fl.end); if (mclause.find()) { fl.start = mclause.start(); fl.end = mclause.end(); } else // // If no clause matches, maybe // we have merged headers // continue; } return fl; } } return null; } public static int getLine(String s, int index) { int n = 0; while (--index > 0) { char c = s.charAt(index); if (c == '\n') { n++; } } return n; } /** * This method is about compatibility. New behavior can be conditionally * introduced by calling this method and passing what version this behavior * was introduced. This allows users of bnd to set the -upto instructions to * the version that they want to be compatible with. If this instruction is * not set, we assume the latest version. */ Version upto = null; public boolean since(Version introduced) { if (upto == null) { String uptov = getProperty(UPTO); if (uptov == null) { upto = Version.HIGHEST; return true; } if (!Version.VERSION.matcher(uptov) .matches()) { error("The %s given version is not a version: %s", UPTO, uptov); upto = Version.HIGHEST; return true; } upto = new Version(uptov); } return upto.compareTo(introduced) >= 0; } /** * Report the details of this processor. Should in general be overridden * * @param table * @throws Exception */ public void report(Map table) throws Exception { table.put("Included Files", getIncluded()); table.put("Base", getBase()); table.put("Properties", getRawProperties().entrySet()); } /** * Simplified way to check booleans */ public boolean is(String propertyName) { return isTrue(getProperty(propertyName)); } /** * Return merged properties. The parameters provide a list of property names * which are concatenated in the output, separated by a comma. Not only are * those property names looked for, also all property names that have that * constant as a prefix, a '.', and then whatever (.*). The result is either * null if nothing was found or a list of properties */ public String mergeProperties(String key) { return mergeProperties(key, ","); } public String mergeLocalProperties(String key) { return getProperty(makeWildcard(key), null, ",", false); } public String mergeProperties(String key, String separator) { return getProperty(makeWildcard(key), null, separator, true); } private String makeWildcard(String key) { return key + ".*"; } /** * Get a Parameters from merged properties */ public Parameters getMergedParameters(String key) { return new Parameters(mergeProperties(key), this); } /** * Add an element to an array, creating a new one if necessary */ public T[] concat(Class type, T[] prefix, T suffix) { @SuppressWarnings("unchecked") T[] result = (T[]) Array.newInstance(type, (prefix != null ? prefix.length : 0) + 1); if (result.length > 1) { System.arraycopy(prefix, 0, result, 0, result.length - 1); } result[result.length - 1] = suffix; return result; } /** * Try to get a Jar from a file name/path or a url, or in last resort from * the classpath name part of their files. * * @param name URL or filename relative to the base * @param from Message identifying the caller for errors * @return null or a Jar with the contents for the name */ public Jar getJarFromName(String name, String from) { File file = new File(name); if (!file.isAbsolute()) file = new File(getBase(), name); if (file.exists()) try { Jar jar = new Jar(file); addClose(jar); return jar; } catch (Exception e) { error("Exception in parsing jar file for %s: %s %s", from, name, e); } // It is not a file ... try { // Lets try a URL URL url = new URL(name); try (Resource resource = Resource.fromURL(url, getPlugin(HttpClient.class))) { Jar jar = Jar.fromResource(fileName(url.getPath()), resource); if (jar.lastModified() <= 0L) { // We assume the worst :-( jar.updateModified(System.currentTimeMillis(), "use current time"); } addClose(jar); return jar; } } catch (IOException ee) { // ignore } catch (Exception ee) { throw Exceptions.duck(ee); } return null; } private String fileName(String path) { int n = path.lastIndexOf('/'); if (n > 0) return path.substring(n + 1); return path; } /** * Return the name of the properties file */ public String _thisfile(String[] args) { if (propertiesFile == null) { error("${thisfile} executed on a processor without a properties file"); return null; } return IO.absolutePath(propertiesFile); } static final String _frangeHelp = "${frange;[;true|false]}"; /** * Return a range expression for a filter from a version. By default this is * based on consumer compatibility. You can specify a third argument (true) * to get provider compatibility. * *
	 *  ${frange;1.2.3} ->
	 * (&(version>=1.2.3)(!(version>=2.0.0)) ${frange;1.2.3, true} ->
	 * (&(version>=1.2.3)(!(version>=1.3.0)) ${frange;[1.2.3,2.3.4)} ->
	 * (&(version>=1.2.3)(!(version>=2.3.4))
	 * 
*/ public String _frange(String[] args) { if (args.length < 2 || args.length > 3) { error("Invalid filter range, 2 or 3 args" + _frangeHelp); return null; } String v = args[1]; boolean isProvider = args.length == 3 && isTrue(args[2]); VersionRange vr; if (Verifier.isVersion(v)) { Version l = new Version(v); Version h = isProvider ? l.bumpMinor() : l.bumpMajor(); vr = new VersionRange(true, l, h, false); } else if (Verifier.isVersionRange(v)) { vr = new VersionRange(v); } else { error("The _frange parameter %s is neither a version nor a version range", v); return null; } return vr.toFilter(); } public String _findfile(String args[]) { File f = getFile(args[1]); List files = new ArrayList<>(); tree(files, f, "", new Instruction(args[2])); return join(files); } void tree(List list, File current, String path, Instruction instr) { if (path.length() > 0) path = path.concat("/"); for (String sub : IO.list(current)) { File f = new File(current, sub); if (f.isFile()) { if (instr.matches(sub) ^ instr.isNegated()) list.add(path + sub); } else tree(list, f, path + sub, instr); } } /** * Return an instance of an interface where each method is mapped to an * instruction available from this Processor. See {@link SyntaxAnnotation} * for how to annotate this interface. */ public T getInstructions(Class type) { return Syntax.getInstructions(this, type); } /** * Return if this is an interactive environment like Eclipse or runs in * batch mode. If interactive, things can get refreshed. */ public boolean isInteractive() { if (getParent() != null) { return getParent().isInteractive(); } return false; } @Override public Parameters getParameters(String key, boolean allowDuplicates) { return new Parameters(get(key), this, allowDuplicates); } public String system(boolean allowFail, String command, String input) throws IOException, InterruptedException { List args; if (IO.isWindows()) { args = Lists.of("cmd", "/c", Command.windowsQuote(command)); } else { args = new QuotedTokenizer(command, " \t", false, true).stream() .filter(token -> !token.isEmpty()) .collect(toList()); } Process process = new ProcessBuilder(args).directory(getBase()) .start(); try (OutputStream stdin = process.getOutputStream()) { if (input != null) { IO.store(input, stdin, UTF_8); } } String out = IO.collect(process.getInputStream(), UTF_8); String err = IO.collect(process.getErrorStream(), UTF_8); int exitValue = process.waitFor(); if (exitValue == 0) { return out.trim(); } if (allowFail) { warning("System command %s failed with exit code %d (allowed)", command, exitValue); } else { error("System command %s failed with exit code %d: %s%n---%n%s", command, exitValue, out, err); } return null; } public String system(String command, String input) throws IOException, InterruptedException { boolean allowFail = false; command = command.trim(); if (command.startsWith("-")) { command = command.substring(1); allowFail = true; } return system(allowFail, command, input); } public String getJavaExecutable(String java) { String path = getProperty(requireNonNull(java)); if ((path == null) || path.equals(java)) { return IO.getJavaExecutablePath(java); } return path; } /** * Return a parameters that contains the merged properties of the given key * and that is decorated by the merged properties of the key + '+', * optionally including literals, and decorated by the merged properties of * the key + '++', always including literals. * * @param key The key of the property */ public Parameters decorated(String key, boolean literalsIncluded) { Parameters parameters = getMergedParameters(key); Instructions decorator = new Instructions(mergeProperties(key + "+")); decorator.decorate(parameters, literalsIncluded); decorator = new Instructions(mergeProperties(key + "++")); decorator.decorate(parameters, true); return parameters; } public Parameters decorated(String key) { return decorated(key, false); } public synchronized String getProfile() { if (profile == null) { profile = "cycle"; profile = getProperty(Constants.PROFILE); } return profile; } /** * A checksum based on the values of the properties * * @return A checksum based on the values of the properties */ public String getChecksum() { try (Processor p = new Processor(this)) { p.setProperty(Constants.TSTAMP, "0"); Properties flattenedProperties = p.getFlattenedProperties(); Digester digester = SHA1.getDigester(); @SuppressWarnings({ "unchecked", "rawtypes" }) Set keySet = new TreeSet<>((Set) flattenedProperties.keySet()); keySet.forEach(k -> { try { byte[] bytes = k.getBytes(StandardCharsets.UTF_8); digester.write(bytes); String s = flattenedProperties.getProperty(k); if (s == null) return; bytes = s.getBytes(StandardCharsets.UTF_8); digester.write(bytes); } catch (Exception e) { throw Exceptions.duck(e); } }); String checksum = digester.digest() .asHex(); return checksum; } catch (Exception e) { throw Exceptions.duck(e); } } /** * Return a list of all files that provide the properties for this * Processor. This includes its own properties file, all included files, and * the same for its ancestor Processor. *

* The order of the list is parent?.getSelfAndAncestors(), includes, * properties file * * @return a list of files that this processor depends on */ public List getSelfAndAncestors() { List l = new ArrayList<>(); return getSelfAndAncestors(l); } private List getSelfAndAncestors(List l) { if (getParent() != null) getParent().getSelfAndAncestors(l); l.addAll(getIncluded()); File f = getPropertiesFile(); if (f != null) l.add(f); return l; } /** * Set the properties file but do **not** load the properties. * * @param source the properties file */ public void setPropertiesFile(File source) { this.propertiesFile = source; } /** * Answer true if any of the property keys is set as a property * * @param keys list of property keys * @return true if any of the property values is set */ public boolean isPropertySet(Set keys) { for (String key : keys) { if (getProperty(key) != null) return true; } return false; } class Bracket { final List atEnds = new ArrayList<>(); final Map, Object> data = new HashMap<>(); } /** * Can be called by Processors to bracket an operation. A bracketed * operation allows the called methods to register a Runnable for execution * at the end of the bracket. Brackets can be nested to any depth. * * @param call the Callable to execute inside the bracket * @throws Exception thrown by the callable */ protected T bracketed(Callable call) throws Exception { Bracket old = bracket.get(); bracket.set(new Bracket()); try { return call.call(); } finally { bracket.get().atEnds.forEach(this::runit); bracket.set(old); } } /** * Can be called by Processors to bracket an operation. A bracketed * operation allows the called methods to register a Runnable for execution * at the end of the bracket. Brackets can be nested to any depth. * * @param runnable the runnable to execute inside the bracket * @throws Exception thrown by the runnable */ protected void bracketed(RunnableWithException runnable) throws Exception { bracketed(() -> { runnable.run(); return null; }); } /** * This method is intended to coalesce multiple values. Typical use case is * if you have an error that can happen multiple times over a bracket but * you want to report it once. To keep plugins stateless, they should not * store data in a bracket nor do they have a callback mechanism at the end * of a bracket. *

* This method provides a unique type for the coalescing, this is best a * class inside a method for uniqueness. The work method, takes an instance * of the type and can do some work, for example, a name that should be * reported at the end as a list instead for each occurrence. The factory is * used to create the instance when the type is used for the first time in * the bracket. * *

	 * void dosomething(Processor p, String name) {
	 * 	class Foo extends AutoCloseable {
	 * 		final Set names = new TreeSet<>();
	 *
	 * 		public void close() {
	 * 			p.error("names too long: %s", names);
	 * 		}
	 * 	}
	 * 	if (name.size() > 10) {
	 * 		p.atEnd(Foo.class, foo -> foo.names.add(name), Foo::new);
	 * 	}
	 * }
	 * 
* * @param the type of the worker * @param type the worker type * @param work the work to do * @param factory the factory */ @SuppressWarnings({ "rawtypes", "unchecked" }) public void atEndOfBracket(Class type, Consumer work, Supplier factory) { Bracket b = bracket.get(); if (b == null) { runit(() -> { X x = factory.get(); work.accept(x); x.close(); }); } else { X data = (X) b.data.computeIfAbsent(type, t -> { X newData = factory.get(); b.atEnds.add(newData::close); return newData; }); work.accept(data); } } private void runit(RunnableWithException r) { try { r.run(); } catch (Exception e) { exception(e, "failed to run a runnable at the end of a bracket: %s", e.getMessage()); } } /** * Copy the settings of another processor */ public void getSettings(Processor p) { this.trace = p.isTrace(); this.pedantic = p.isPedantic(); this.exceptions = p.isExceptions(); } public boolean isExceptions() { return this.exceptions; } public void setExceptions(boolean exceptions) { this.exceptions = exceptions; } public void setTrace(boolean x) { trace = x; } public boolean isTrace() { Processor p = current(); return p.trace; } @Override public boolean isPedantic() { return this.pedantic; } public void setPedantic(boolean pedantic) { this.pedantic = pedantic; } /** * Enum used in getMacroReferences() to filter the properties by reason. */ public enum MacroReference { /** * Property is neither a COMMAND nor EXISTS. */ UNKNOWN, /** * Exists as a property */ EXISTS, /** * Is a built in command */ COMMAND, /** * return all property keys */ ALL } /** * Find all the macro references in the properties defined in this processor * or its ancestors. A reference can exist as property, be a command, or * unknown. If no {@link MacroReference}'s are given, all references are * returned. * * @param what specifies requested reference type * @return the set of property keys that match what */ public Set getMacroReferences(MacroReference... what) { Set propertyKeys = getPropertyKeys(true); Set result = new LinkedHashSet<>(); class EMacro extends Macro { boolean exists = false; boolean unknown = false; boolean command = false; boolean all = false; public EMacro() { super(Processor.this, getMacroDomains()); for (MacroReference w : what) { switch (w) { case UNKNOWN -> unknown = true; case EXISTS -> exists = true; case COMMAND -> command = true; default -> all = true; } } all |= exists == unknown && unknown == command; } @Override protected String replace(String invocation, List args, Link link, char begin, char end) { if (args != null && !args.isEmpty()) { String key = args.remove(0); reference(key, args); for (String arg : args) { process(arg, link); } } return ""; } private void reference(String key, List args) { if (all) { result.add(key); } else { boolean x = propertyKeys.contains(key); if (x) { if (exists) result.add(key); } else { BiFunction function = getFunction(key); if (function == null) { if (unknown) result.add(key); } else { switch (key) { case "def", "template", "foreach" -> { if (args.size() > 1) { reference(args.get(0), Collections.emptyList()); } } } if (command) result.add(key); } } } } } EMacro macro = new EMacro(); for (String key : propertyKeys) { String unexpandedProperty = getUnexpandedProperty(key); macro.process(unexpandedProperty); } return result; } }