From e5e9d3a6efb07b483c281d3dba43cbc382ac9477 Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Wed, 3 Apr 2024 14:49:20 +0200 Subject: [PATCH 1/5] Minor update of some utils --- Signed-off-by: Peter Kriens Signed-off-by: Peter Kriens --- aQute.libg/src/aQute/lib/io/IO.java | 71 ++++++++----------- .../src/aQute/lib/io/NullAppendable.java | 18 +++++ aQute.libg/src/aQute/lib/io/Other.java | 51 +++++++++++++ aQute.libg/src/aQute/lib/io/Windows.java | 10 +++ aQute.libg/src/aQute/libg/re/Catalog.java | 8 +++ aQute.libg/src/aQute/libg/re/RE.java | 8 +++ aQute.libg/test/aQute/lib/io/IOTest.java | 11 +++ 7 files changed, 136 insertions(+), 41 deletions(-) create mode 100644 aQute.libg/src/aQute/lib/io/NullAppendable.java diff --git a/aQute.libg/src/aQute/lib/io/IO.java b/aQute.libg/src/aQute/lib/io/IO.java index d1e87e200d..d6609d0797 100644 --- a/aQute.libg/src/aQute/lib/io/IO.java +++ b/aQute.libg/src/aQute/lib/io/IO.java @@ -123,6 +123,16 @@ interface OS { * @return a safe file name */ String toSafeFileName(String name); + + /** + * Return a file from a base. The file must use forward slash but may + * start on windows with c:/... or /c:/ to indicate a drive. + * + * @param base the base to resolve the file from + * @param file the path + * @return a file + */ + File getFile(File base, String file); } final static OS os = File.separatorChar == '\\' ? new Windows() : new Other(); @@ -840,47 +850,7 @@ public static File getFile(String file) { } public static File getFile(File base, String file) { - StringRover rover = new StringRover(file); - if (rover.startsWith("~/")) { - rover.increment(2); - if (!rover.startsWith("~/")) { - return getFile(home, rover.substring(0)); - } - } - if (rover.startsWith("~")) { - return getFile(home.getParentFile(), rover.substring(1)); - } - - File f = new File(rover.substring(0)); - if (f.isAbsolute()) { - return f; - } - - if (base == null) { - base = work; - } - - for (f = base.getAbsoluteFile(); !rover.isEmpty();) { - int n = rover.indexOf('/'); - if (n < 0) { - n = rover.length(); - } - if ((n == 0) || ((n == 1) && (rover.charAt(0) == '.'))) { - // case "" or "." - } else if ((n == 2) && (rover.charAt(0) == '.') && (rover.charAt(1) == '.')) { - // case ".." - File parent = f.getParentFile(); - if (parent != null) { - f = parent; - } - } else { - String segment = rover.substring(0, n); - f = new File(f, segment); - } - rover.increment(n + 1); - } - - return f.getAbsoluteFile(); + return os.getFile(base, file); } /** @@ -1794,4 +1764,23 @@ public static String getJavaExecutablePath(String name) { return name; } + /** + * Create a new unique file name in the given folder + * + * @param folder the folder to create a File in + * @param stem the name stem, "untitled-" if null + * @return a file in the folder that does not exist + */ + public static File unique(File folder, String stem) { + if (stem == null) + stem = "untitled-"; + int n = 0; + while (true) { + File f = new File(folder, stem + n); + if (!f.exists()) + return f; + n++; + } + } + } diff --git a/aQute.libg/src/aQute/lib/io/NullAppendable.java b/aQute.libg/src/aQute/lib/io/NullAppendable.java new file mode 100644 index 0000000000..bae05d6c44 --- /dev/null +++ b/aQute.libg/src/aQute/lib/io/NullAppendable.java @@ -0,0 +1,18 @@ +package aQute.lib.io; +public class NullAppendable implements Appendable { + + @Override + public Appendable append(CharSequence csq) { + return this; + } + + @Override + public Appendable append(CharSequence csq, int start, int end) { + return this; + } + + @Override + public Appendable append(char c) { + return this; + } +} \ No newline at end of file diff --git a/aQute.libg/src/aQute/lib/io/Other.java b/aQute.libg/src/aQute/lib/io/Other.java index d0a4fac09d..a6b913c049 100644 --- a/aQute.libg/src/aQute/lib/io/Other.java +++ b/aQute.libg/src/aQute/lib/io/Other.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import aQute.lib.io.IO.OS; +import aQute.lib.stringrover.StringRover; class Other implements OS { @@ -46,4 +47,54 @@ public String toSafeFileName(String string) { } return sb.toString(); } + + @Override + public File getFile(File base, String file) { + return getFile0(base, file); + } + + static File getFile0(File base, String path) { + StringRover rover = new StringRover(path); + if (rover.startsWith("~/")) { + rover.increment(2); + if (!rover.startsWith("~/")) { + return getFile0(IO.home, rover.substring(0)); + } + } + if (rover.startsWith("~")) { + return getFile0(IO.home.getParentFile(), rover.substring(1)); + } + + File f = new File(rover.substring(0)); + if (f.isAbsolute()) { + return f; + } + + if (base == null) { + base = IO.work; + } + + for (f = base.getAbsoluteFile(); !rover.isEmpty();) { + int n = rover.indexOf('/'); + if (n < 0) { + n = rover.length(); + } + if ((n == 0) || ((n == 1) && (rover.charAt(0) == '.'))) { + // case "" or "." + } else if ((n == 2) && (rover.charAt(0) == '.') && (rover.charAt(1) == '.')) { + // case ".." + File parent = f.getParentFile(); + if (parent != null) { + f = parent; + } + } else { + String segment = rover.substring(0, n); + f = new File(f, segment); + } + rover.increment(n + 1); + } + + return f.getAbsoluteFile(); + } + } diff --git a/aQute.libg/src/aQute/lib/io/Windows.java b/aQute.libg/src/aQute/lib/io/Windows.java index c74c480f7f..da4eb9088e 100644 --- a/aQute.libg/src/aQute/lib/io/Windows.java +++ b/aQute.libg/src/aQute/lib/io/Windows.java @@ -12,6 +12,7 @@ class Windows implements OS { final static Pattern WINDOWS_BAD_FILE_NAME_P = Pattern.compile( "(?:(:?.*[\u0000-\u001F<>:\"|/\\\\?*].*)|\\.\\.|CON|PRN|AUX|NUL|COM\\d|COM¹|COM²|COM³|LPT\\d|LPT¹|LPT²|LPT³)(?:\\.\\w+)?", Pattern.CASE_INSENSITIVE); + final static Pattern DRIVE_P = Pattern.compile("/?(?[a-z]:)", Pattern.CASE_INSENSITIVE); @Override public File getBasedFile(File base, String subPath) throws IOException { @@ -72,4 +73,13 @@ public String toSafeFileName(String string) { return sb.toString(); } + @Override + public File getFile(File base, String file) { + file = file.replace('\\', '/'); + Matcher m = DRIVE_P.matcher(file); + if (m.lookingAt()) { + base = new File(m.group("drive")); + } + return Other.getFile0(base, file); + } } diff --git a/aQute.libg/src/aQute/libg/re/Catalog.java b/aQute.libg/src/aQute/libg/re/Catalog.java index c36e50705d..09eb6c036b 100644 --- a/aQute.libg/src/aQute/libg/re/Catalog.java +++ b/aQute.libg/src/aQute/libg/re/Catalog.java @@ -1189,6 +1189,14 @@ public Optional group(int group) { return Optional.of(new MatchGroupImplIndex(group, value)); } + @Override + public String presentGroup(String groupName) { + String group = matcher.group(groupName); + if (group == null) + throw new IllegalArgumentException("no such group " + groupName); + return group; + } + } return Optional.of(new MatchImpl()); } else diff --git a/aQute.libg/src/aQute/libg/re/RE.java b/aQute.libg/src/aQute/libg/re/RE.java index 11c1052cef..9bfe56abc3 100644 --- a/aQute.libg/src/aQute/libg/re/RE.java +++ b/aQute.libg/src/aQute/libg/re/RE.java @@ -323,6 +323,14 @@ default boolean check(RE expected) { String tryMatch(RE match); Optional group(int group); + + /** + * This gets the value of a group but throws an exception of the group + * is not there. + * + * @param groupName the name of the group + */ + String presentGroup(String groupName); } /** diff --git a/aQute.libg/test/aQute/lib/io/IOTest.java b/aQute.libg/test/aQute/lib/io/IOTest.java index 6ee1e8c15f..c2a16dc6c8 100644 --- a/aQute.libg/test/aQute/lib/io/IOTest.java +++ b/aQute.libg/test/aQute/lib/io/IOTest.java @@ -89,6 +89,17 @@ public void testSafeFileNameWindows() { assertEquals("COM2_", IO.toSafeFileName("COM2")); } + @Test + @EnabledOnOs(WINDOWS) + public void testGetFileOnWindows() { + assertThat(IO.getFile("f:/abc")).isEqualTo(new File("f:\\abc")); + assertThat(IO.getFile("/f:/abc")).isEqualTo(new File("f:\\abc")); + + File f = new File("f:"); + assertThat(IO.getFile(f, "abc")).isEqualTo(new File("f:\\abc")); + } + + @Test public void testFilesetCopy(@InjectTemporaryDirectory File destDir) throws Exception { From 5266270e16c02d298ad7c09a480cd247df721c52 Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Wed, 3 Apr 2024 15:05:55 +0200 Subject: [PATCH 2/5] Supporting of .pmvn and .pobr files in the ext directory --- Signed-off-by: Peter Kriens Signed-off-by: Peter Kriens --- .../src/aQute/bnd/build/MagicBnd.java | 170 ++++++++++++++++++ .../src/aQute/bnd/build/Workspace.java | 98 ++++++---- .../src/aQute/bnd/osgi/Processor.java | 45 ++++- 3 files changed, 272 insertions(+), 41 deletions(-) create mode 100644 biz.aQute.bndlib/src/aQute/bnd/build/MagicBnd.java diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/MagicBnd.java b/biz.aQute.bndlib/src/aQute/bnd/build/MagicBnd.java new file mode 100644 index 0000000000..ec594e6309 --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/build/MagicBnd.java @@ -0,0 +1,170 @@ +package aQute.bnd.build; + +import static aQute.libg.re.Catalog.g; +import static aQute.libg.re.Catalog.lit; +import static aQute.libg.re.Catalog.re; +import static aQute.libg.re.Catalog.term; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Collectors; + +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamReader; + +import aQute.bnd.result.Result; +import aQute.lib.io.IO; +import aQute.lib.strings.Strings; +import aQute.libg.re.Catalog; +import aQute.libg.re.RE; +import aQute.libg.re.RE.Match; + +/** + * This is used in included files in a bnd file. It allows another file to be + * mapped to the properties. This can be used to setup a plugin that is + * parameterized by that file. + */ +public class MagicBnd { + final static RE KEY_P = re("[\\w\\d_\\.]+"); + final static RE VALUE_P = Catalog.setAll; + final static RE REPO_ATTR_P = term(lit("#"), g("key", KEY_P), lit("="), g("value", VALUE_P), Catalog.setWs); + + public static Result map(Workspace workspace, File file) { + String parts[] = Strings.extension(file.getName()); + if (parts == null) { + return Result.ok(null); + } + + String ext = parts[1]; + return switch (ext) { + case "pmvn" -> convertMaven(workspace, file); + case "pobr" -> convertOBR(workspace, file); + default -> Result.ok(null); + }; + } + + /* + * Convert an OBR XML file to bnd plugin + */ + private static Result convertOBR(Workspace workspace, File file) { + StringBuilder sb = new StringBuilder(); + String parts[] = Strings.extension(file.getName()); + + String name = getName(file, file.getName()); + + Map attrs = new LinkedHashMap<>(); + attrs.put("name", name); + attrs.put("locations", file.toURI() + .toString()); + + sb.append("aQute.bnd.repository.osgi.OSGiRepository"); + + for (Map.Entry e : attrs.entrySet()) { + sb.append(";") + .append(e.getKey()) + .append("='") + .append(e.getValue()) + .append("'"); + } + + Properties p = new Properties(); + p.put("-plugin.ext." + file.getName(), sb.toString()); + return Result.ok(p); + } + + private static String getName(File file, String defaultName) throws FactoryConfigurationError { + XMLInputFactory factory = XMLInputFactory.newInstance(); + try (InputStream inputStream = new FileInputStream(file);) { + XMLStreamReader reader = factory.createXMLStreamReader(inputStream); + + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + return reader.getAttributeValue(null, "name"); + } + } + } catch (Exception e1) { + // ignore + } + return defaultName; + } + + /* + * Convert an Maven GAV file into a MavenBndRepository plugin setup + */ + + private static Result convertMaven(Workspace ws, File file) { + StringBuilder sb = new StringBuilder(); + String parts[] = Strings.extension(file.getName()); + + Map attrs = new LinkedHashMap<>(); + attrs.put("name", file.getName()); + boolean repo = false; + List release = new ArrayList<>(); + List snapshot = new ArrayList<>(); + try (BufferedReader br = IO.reader(file)) { + String line; + while ((line = br.readLine()) != null) { + Optional matches = REPO_ATTR_P.matches(line); + if (matches.isPresent()) { + Match m = matches.get(); + String key = m.presentGroup("key"); + String value = m.presentGroup("value"); + switch (key) { + case "releaseUrl", "releaseUrls" -> release.add(value); + case "snapshotUrl", "snapshotUrls" -> snapshot.add(value); + case "repo" -> { + release.add(value); + snapshot.add("value"); + } + case "index" -> { + } + default -> { + attrs.put(key, value); + } + } + } else + break; + } + } catch (Exception e) { + return Result.err("reading file %s to convert to bnd properties failed: %s", file, e.getMessage()); + } + sb.append("aQute.bnd.repository.maven.provider.MavenBndRepository") + .append(";index=") + .append(file.getAbsolutePath() + .replace('\\', '/')); + + for (Map.Entry e : attrs.entrySet()) { + sb.append(";") + .append(e.getKey()) + .append("='") + .append(e.getValue()) + .append("'"); + } + + if (release.isEmpty()) + release.add("https://repo.maven.apache.org/maven2/"); + sb.append(";releaseUrl=") + .append(release.stream() + .collect(Collectors.joining())) + .append("'"); + sb.append(";snapshotUrl=") + .append(snapshot.stream() + .collect(Collectors.joining())) + .append("'"); + + Properties p = new Properties(); + p.put("-plugin.ext." + file.getName(), sb.toString()); + return Result.ok(p); + } +} diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java b/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java index b7647f3070..c26a138ea9 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java @@ -168,7 +168,7 @@ public void close() { } } - private final static Map> cache = newHashMap(); + private final static Map> cache = newHashMap(); private static final Memoize defaults; static { defaults = Memoize.supplier(() -> { @@ -185,31 +185,31 @@ public void close() { return new Processor(props, false); }); } - final Map commands = newMap(); - final Maven maven; - private final AtomicBoolean offline = new AtomicBoolean(); - Settings settings = new Settings( + final Map commands = newMap(); + final Maven maven; + private final AtomicBoolean offline = new AtomicBoolean(); + Settings settings = new Settings( Home.getUserHomeBnd("settings.json")); - WorkspaceRepository workspaceRepo = new WorkspaceRepository(this); - static String overallDriver = "unset"; - static Parameters overallGestalt = new Parameters(); + WorkspaceRepository workspaceRepo = new WorkspaceRepository(this); + static String overallDriver = "unset"; + static Parameters overallGestalt = new Parameters(); /** * Signal a BndListener plugin. We ran an infinite bug loop :-( */ - final ThreadLocal signalBusy = new ThreadLocal<>(); - ResourceRepositoryImpl resourceRepositoryImpl; - private String driver; - private final WorkspaceLayout layout; - final Set trail = Collections + final ThreadLocal signalBusy = new ThreadLocal<>(); + ResourceRepositoryImpl resourceRepositoryImpl; + private String driver; + private final WorkspaceLayout layout; + final Set trail = Collections .newSetFromMap(new ConcurrentHashMap()); - private volatile WorkspaceData data = new WorkspaceData(); - private File buildDir; - private final ProjectTracker projects = new ProjectTracker(this); + private volatile WorkspaceData data = new WorkspaceData(); + private File buildDir; + private final ProjectTracker projects = new ProjectTracker(this); private final WorkspaceLock workspaceLock = new WorkspaceLock(true); private static final long WORKSPACE_LOCK_DEFAULT_TIMEOUTMS = 120_000L; - final WorkspaceNotifier notifier = new WorkspaceNotifier(this); + final WorkspaceNotifier notifier = new WorkspaceNotifier(this); - public static boolean remoteWorkspaces = false; + public static boolean remoteWorkspaces = false; /** * This static method finds the workspace and creates a project (or returns @@ -490,21 +490,14 @@ public void forceRefreshProjects() { projects.forceRefresh(); } + final static Set INCLUDE_EXTS = Set.of("bnd", "mf", "pmvn", "pobr"); + @Override public void propertiesChanged() { try { writeLocked(() -> { refreshData(); - File extDir = new File(getBuildDir(), EXT); - for (File extension : IO.listFiles(extDir, (dir, name) -> name.endsWith(".bnd"))) { - String extensionName = extension.getName(); - extensionName = extensionName.substring(0, extensionName.length() - ".bnd".length()); - try { - doIncludeFile(extension, false, getProperties(), "ext." + extensionName); - } catch (Exception e) { - exception(e, "PropertiesChanged: %s", e); - } - } + doExtDir(); super.propertiesChanged(); if (doExtend(this)) { super.propertiesChanged(); @@ -512,11 +505,28 @@ public void propertiesChanged() { forceInitialization(); return null; }); - } catch (Exception e) { + } catch ( + + Exception e) { throw Exceptions.duck(e); } } + void doExtDir() { + File extDir = new File(getBuildDir(), EXT); + for (File extension : IO.listFiles(extDir)) { + String parts[] = Strings.extension(extension.getName()); + if (parts == null || !INCLUDE_EXTS.contains(parts[1])) + continue; + + try { + doIncludeFile(extension, false, getProperties(), "ext." + extension.getName()); + } catch (Exception e) { + exception(e, "PropertiesChanged: %s", e); + } + } + } + private void forceInitialization() { if (notifier.mute) return; @@ -1533,6 +1543,7 @@ public void writeLocked(Runnable runnable) throws Exception { return null; }); } + public T writeLocked(Callable callable) throws Exception { return workspaceLock.locked(workspaceLock.writeLock(), WORKSPACE_LOCK_DEFAULT_TIMEOUTMS, callable, () -> false); } @@ -1555,13 +1566,13 @@ public T writeLocked(Callable callable) throws Exception { * obtain the lock. * @throws Exception If the callable or function throws an exception. */ - public T writeLocked(Callable underWrite, FunctionWithException underRead, - BooleanSupplier canceled, long timeoutInMs) throws Exception { + public T writeLocked(Callable underWrite, FunctionWithException underRead, BooleanSupplier canceled, + long timeoutInMs) throws Exception { return workspaceLock.writeReadLocked(timeoutInMs, underWrite, underRead, canceled); } - public T writeLocked(Callable underWrite, FunctionWithException underRead, - BooleanSupplier canceled) throws Exception { + public T writeLocked(Callable underWrite, FunctionWithException underRead, BooleanSupplier canceled) + throws Exception { return workspaceLock.writeReadLocked(WORKSPACE_LOCK_DEFAULT_TIMEOUTMS, underWrite, underRead, canceled); } @@ -1977,4 +1988,25 @@ public Result getExpandedInCache(String urn, File file) throws IOException return Result.err("Failed to expand %s into %s: %s", file, cache, e); } } + + /** + * Add "mvn" files. They are mapped to a MavenBndRepository. The .mvn file + * can in the the first lines define repositories to add with #repo= If no repositories are specified, maven central is used + */ + @Override + protected Properties magicBnd(File file) throws IOException { + Result result = MagicBnd.map(this, file); + if (result.isOk()) { + if (result.unwrap() == null) + return super.magicBnd(file); + else + return result.unwrap(); + } else { + error("failed to convert %s to properties in an include: %s", file, result.error() + .get()); + return super.magicBnd(file); + } + } + } diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java index e1cb7ec05b..e36ee75b1b 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java @@ -11,6 +11,7 @@ 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; @@ -670,6 +671,12 @@ 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); getProperties0().putAll(properties); @@ -795,13 +802,7 @@ public void doIncludeFile(File file, boolean overwrite, Properties target, Strin return; } updateModified(file.lastModified(), file.toString()); - Properties sub; - if (Strings.endsWithIgnoreCase(file.getName(), ".mf")) { - try (InputStream in = IO.stream(file)) { - sub = getManifestAsProperties(in); - } - } else - sub = loadProperties(file); + Properties sub = magicBnd(file); doIncludes(file.getParentFile(), sub); // take care regarding overwriting properties @@ -819,6 +820,27 @@ public void doIncludeFile(File file, boolean overwrite, Properties target, Strin } } + /** + * 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); @@ -906,6 +928,12 @@ public void setProperties(File propertiesFile, File base) { } } + 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); @@ -2061,7 +2089,8 @@ public static int getLine(String s, int index) { * not set, we assume the latest version. */ - Version upto = null; + Version upto = null; + public boolean since(Version introduced) { if (upto == null) { String uptov = getProperty(UPTO); From 4a8a86984f0076d83bb398e52e060c56fe2f6aab Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Wed, 3 Apr 2024 15:06:20 +0200 Subject: [PATCH 3/5] Workspace Fragment Template support --- Signed-off-by: Peter Kriens Signed-off-by: Peter Kriens --- biz.aQute.bndlib/bnd.bnd | 1 + .../wstemplates/FragmentTemplateEngine.java | 411 ++++++++++++++++++ .../src/aQute/bnd/wstemplates/TemplateID.java | 82 ++++ .../aQute/bnd/wstemplates/package-info.java | 4 + .../wstemplates/TemplateFragmentsTest.java | 185 ++++++++ 5 files changed, 683 insertions(+) create mode 100644 biz.aQute.bndlib/src/aQute/bnd/wstemplates/FragmentTemplateEngine.java create mode 100644 biz.aQute.bndlib/src/aQute/bnd/wstemplates/TemplateID.java create mode 100644 biz.aQute.bndlib/src/aQute/bnd/wstemplates/package-info.java create mode 100644 biz.aQute.bndlib/test/aQute/bnd/wstemplates/TemplateFragmentsTest.java diff --git a/biz.aQute.bndlib/bnd.bnd b/biz.aQute.bndlib/bnd.bnd index 617c5e284a..6b17085a30 100644 --- a/biz.aQute.bndlib/bnd.bnd +++ b/biz.aQute.bndlib/bnd.bnd @@ -36,6 +36,7 @@ Export-Package: \ aQute.bnd.util.home;-noimport:=true,\ aQute.bnd.util.repository;-noimport:=true,\ aQute.bnd.version;-noimport:=true,\ + aQute.bnd.wstemplates;-noimport:=true,\ aQute.lib.deployer;-noimport:=true,\ aQute.service.reporter;-noimport:=true diff --git a/biz.aQute.bndlib/src/aQute/bnd/wstemplates/FragmentTemplateEngine.java b/biz.aQute.bndlib/src/aQute/bnd/wstemplates/FragmentTemplateEngine.java new file mode 100644 index 0000000000..958cc22643 --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/wstemplates/FragmentTemplateEngine.java @@ -0,0 +1,411 @@ +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.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"; + + 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); + } + } + + 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) { + 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) { + Jar jar = getFiles(template.id() + .uri()); + closeables.add(jar); + + String prefix = fixup(template.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); + } + +} diff --git a/biz.aQute.bndlib/src/aQute/bnd/wstemplates/TemplateID.java b/biz.aQute.bndlib/src/aQute/bnd/wstemplates/TemplateID.java new file mode 100644 index 0000000000..525b93cf0d --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/wstemplates/TemplateID.java @@ -0,0 +1,82 @@ +package aQute.bnd.wstemplates; + +import static aQute.libg.re.Catalog.cc; +import static aQute.libg.re.Catalog.g; +import static aQute.libg.re.Catalog.lit; +import static aQute.libg.re.Catalog.opt; +import static aQute.libg.re.Catalog.set; +import static aQute.libg.re.Catalog.some; + +import java.net.URI; +import java.util.Map; + +import aQute.libg.re.RE; + +/** + * The ID of a template. This is either an org/repo/path#ref pattern or a uri to + * a zip file. + */ +public record TemplateID(String organisation, String repository, String path, String ref, String other) + implements Comparable { + + final static RE SEGMENT_P = some(cc("\\d\\w_.-")); + final static RE REF_P = some(cc("\\d\\w_.-/")); + final static RE PATH_P = opt( // + g("org", SEGMENT_P), opt(lit("/"), g("repo", SEGMENT_P), // + g("path", set(lit("/"), SEGMENT_P)), opt(lit("/"))), + opt(lit("#"), g("branch", REF_P)) // + ); + final static URI ROOT = URI.create("https://github.com/bndtools/workspace#master"); + + @Override + public int compareTo(TemplateID o) { + if (other != null) + return other.compareTo(o.other); + int n = organisation.compareTo(o.organisation); + if (n != 0) + return n; + n = repository.compareTo(o.repository); + if (n != 0) + return n; + n = path.compareTo(o.path); + if (n != 0) + return n; + n = ref.compareTo(o.ref); + return n; + } + + /** + * Return the URI. + */ + public URI uri() { + String uri = this.other; + if (uri == null) { + uri = "https://github.com/" + organisation + "/" + repository + "/archive/" + ref + ".zip"; + } + return URI.create(uri); + } + + /** + * Parse the id into a Template ID. The default is + * `bndtools/workspace#master`. The missing fields are taken from this + * default. If the id does not match the pattern, it is assumed to be a URI. + * + * @param id id or uri + * @return a TemplateId + */ + public static TemplateID from(String id) { + return PATH_P.matches(id) + .map(match -> { + Map vs = match.getGroupValues(); + String org = vs.getOrDefault("org", "bndtools"); + String repo = vs.getOrDefault("repo", "workspace"); + String path = vs.getOrDefault("path", ""); + if (!path.isEmpty()) + path = path.substring(1); + String branch = vs.getOrDefault("branch", "master"); + return new TemplateID(org, repo, path, branch, null); + }) + .orElse(new TemplateID(null, null, "", null, id)); + } + +} diff --git a/biz.aQute.bndlib/src/aQute/bnd/wstemplates/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/wstemplates/package-info.java new file mode 100644 index 0000000000..4e420ea2a2 --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/wstemplates/package-info.java @@ -0,0 +1,4 @@ +@Version("1.0.0") +package aQute.bnd.wstemplates; + +import org.osgi.annotation.versioning.Version; diff --git a/biz.aQute.bndlib/test/aQute/bnd/wstemplates/TemplateFragmentsTest.java b/biz.aQute.bndlib/test/aQute/bnd/wstemplates/TemplateFragmentsTest.java new file mode 100644 index 0000000000..0454190d4d --- /dev/null +++ b/biz.aQute.bndlib/test/aQute/bnd/wstemplates/TemplateFragmentsTest.java @@ -0,0 +1,185 @@ +package aQute.bnd.wstemplates; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import aQute.bnd.build.Workspace; +import aQute.bnd.http.HttpClient; +import aQute.bnd.osgi.Builder; +import aQute.bnd.result.Result; +import aQute.bnd.wstemplates.FragmentTemplateEngine.TemplateInfo; +import aQute.bnd.wstemplates.FragmentTemplateEngine.TemplateUpdater; +import aQute.bnd.wstemplates.FragmentTemplateEngine.Update; +import aQute.lib.io.IO; + +class TemplateFragmentsTest { + @TempDir + static File wsDir; + @TempDir + static File zip; + + @Test + void testId() { + TemplateID defaultId = TemplateID.from(""); + assertThat(defaultId.organisation()).isEqualTo("bndtools"); + assertThat(defaultId.repository()).isEqualTo("workspace"); + assertThat(defaultId.path()).isEqualTo(""); + assertThat(defaultId.ref()).isEqualTo("master"); + + TemplateID withOrg = TemplateID.from("acme"); + assertThat(withOrg.organisation()).isEqualTo("acme"); + assertThat(withOrg.repository()).isEqualTo("workspace"); + assertThat(withOrg.path()).isEqualTo(""); + assertThat(withOrg.ref()).isEqualTo("master"); + + TemplateID withOrgRef = TemplateID.from("acme#main"); + assertThat(withOrgRef.organisation()).isEqualTo("acme"); + assertThat(withOrgRef.repository()).isEqualTo("workspace"); + assertThat(withOrgRef.path()).isEqualTo(""); + assertThat(withOrgRef.ref()).isEqualTo("main"); + + TemplateID withOrgRepo = TemplateID.from("acme/template"); + assertThat(withOrgRepo.organisation()).isEqualTo("acme"); + assertThat(withOrgRepo.repository()).isEqualTo("template"); + assertThat(withOrgRepo.path()).isEqualTo(""); + assertThat(withOrgRepo.ref()).isEqualTo("master"); + + TemplateID withOrgRepoRef = TemplateID.from("acme/template#foo/bar"); + assertThat(withOrgRepoRef.organisation()).isEqualTo("acme"); + assertThat(withOrgRepoRef.repository()).isEqualTo("template"); + assertThat(withOrgRepoRef.path()).isEqualTo(""); + assertThat(withOrgRepoRef.ref()).isEqualTo("foo/bar"); + + TemplateID withOrgRepoPath = TemplateID.from("acme/template/foo/bar"); + assertThat(withOrgRepoPath.organisation()).isEqualTo("acme"); + assertThat(withOrgRepoPath.repository()).isEqualTo("template"); + assertThat(withOrgRepoPath.path()).isEqualTo("foo/bar"); + assertThat(withOrgRepoPath.ref()).isEqualTo("master"); + + TemplateID withOrgRepoPathRef = TemplateID.from("acme/template/foo/bar/y#feature/bar/x"); + assertThat(withOrgRepoPathRef.organisation()).isEqualTo("acme"); + assertThat(withOrgRepoPathRef.repository()).isEqualTo("template"); + assertThat(withOrgRepoPathRef.path()).isEqualTo("foo/bar/y"); + assertThat(withOrgRepoPathRef.ref()).isEqualTo("feature/bar/x"); + + TemplateID withOrgRepoPathxRef = TemplateID.from("acme/template/foo/bar/y/#feature/bar/x"); + assertThat(withOrgRepoPathxRef.organisation()).isEqualTo("acme"); + assertThat(withOrgRepoPathxRef.repository()).isEqualTo("template"); + assertThat(withOrgRepoPathxRef.path()).isEqualTo("foo/bar/y"); + assertThat(withOrgRepoPathxRef.ref()).isEqualTo("feature/bar/x"); + + TemplateID other = TemplateID.from("file://z.zip"); + assertThat(other.other()).isEqualTo("file://z.zip"); + + } + + @Test + void testRemote() throws Exception { + try (Workspace w = getworkSpace()) { + FragmentTemplateEngine tfs = new FragmentTemplateEngine(w); + File a = makeJar("a.zip", """ + -includeresource prefix/cnf/build.bnd;literal="# a\\n" + """); + + // use an archived repository + Result> result = tfs.read("-workspace-templates " + a.toURI() + ";name=a;description=A," + + "bndtools/workspace-templates/gradle#567648ff425693b27b191bd38ace7c9c10539c2d;name=b;description=B"); + + assertThat(result.isOk()).describedAs(result.toString()) + .isTrue(); + + List infos = result.unwrap(); + assertThat(infos).hasSize(2); + assertThat(infos.remove(0) + .name()).isEqualTo("a"); + + TemplateUpdater updater = tfs.updater(wsDir, infos); + updater.commit(); + + assertThat(IO.getFile(wsDir, "cnf/build.bnd")).isFile(); + assertThat(new File(wsDir, "gradle/wrapper")).isDirectory(); + if (!IO.isWindows()) { + assertThat(new File(wsDir, "gradlew")).isFile() + .isExecutable(); + } + assertThat(new File(wsDir, "gradle.properties")).isFile(); + assertThat(new File(wsDir, "readme.md").isFile()).isFalse(); + } + + } + + @Test + void test() throws Exception { + try (Workspace w = getworkSpace()) { + FragmentTemplateEngine tfs = new FragmentTemplateEngine(w); + File a = makeJar("a.zip", """ + -includeresource workspace-master/cnf/build.bnd;literal="# a\\n" + """); + File b = makeJar("b.zip", """ + -includeresource workspace-master/cnf/build.bnd;literal="# b\\n" + """); + + Result> result = tfs.read( + "-workspace-templates " + a.toURI() + ";name=a;description=A," + b.toURI() + ";name=b;description=B"); + + assertThat(result.isOk()).describedAs(result.toString()) + .isTrue(); + + List infos = result.unwrap(); + assertThat(infos).hasSize(2); + + TemplateUpdater updater = tfs.updater(wsDir, infos); + updater.commit(); + + assertThat(IO.getFile(wsDir, "cnf/build.bnd")).isFile(); + assertThat(IO.collect(IO.getFile(wsDir, "cnf/build.bnd"))).isEqualTo("# a\n# b\n"); + + File build = IO.getFile(wsDir, "cnf/build.bnd"); + + Map> updaters = updater.updaters(); + assertThat(updaters).containsKey(build); + assertThat(updaters.get(build)).hasSize(2); + + updater.commit(); + + assertThat(build).isFile(); + } + + } + + private static Workspace getworkSpace() throws Exception { + File f = IO.getFile(wsDir, "cnf/build.bnd"); + f.getParentFile() + .mkdirs(); + IO.store("#test\n", f); + Workspace workspace = Workspace.getWorkspace(wsDir); + workspace.addBasicPlugin(new HttpClient()); + return workspace; + } + + static File makeJar(String name, String spec) throws Exception { + File props = new File(zip, "props"); + File jar = new File(zip, name); + IO.store(spec, props); + try (Builder b = new Builder()) { + b.setProperties(props); + b.build(); + assertThat(b.check()); + b.getJar() + .removePrefix("META-INF"); + b.getJar() + .setManifest((Manifest) null); + b.getJar() + .write(jar); + } + return jar; + } + +} From d8f49e8f9861720bc2b9af15cf09d9f7d669f021 Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Wed, 3 Apr 2024 15:07:04 +0200 Subject: [PATCH 4/5] New Workspace Wizard and after creation synchronization --- Signed-off-by: Peter Kriens Signed-off-by: Peter Kriens --- bndtools.core/_plugin.xml | 25 +- bndtools.core/bnd.bnd | 8 +- .../src/bndtools/central/Central.java | 70 +- .../src/bndtools/central/Starter.java | 15 + .../sync/SynchronizeWorkspaceWithEclipse.java | 4 +- .../bndtools/perspective/BndPerspective.java | 1 + bndtools.core/src/bndtools/util/ui/UI.java | 616 ++++++++++++++++++ .../bndtools/wizards/newworkspace/Model.java | 165 +++++ .../newworkspace/NewWorkspaceWizard.java | 261 ++++++++ .../TemplateDefinitionDialog.java | 66 ++ .../test/bndtools/util/ui/UITest.java | 220 +++++++ 11 files changed, 1436 insertions(+), 15 deletions(-) create mode 100644 bndtools.core/src/bndtools/central/Starter.java create mode 100644 bndtools.core/src/bndtools/util/ui/UI.java create mode 100644 bndtools.core/src/bndtools/wizards/newworkspace/Model.java create mode 100644 bndtools.core/src/bndtools/wizards/newworkspace/NewWorkspaceWizard.java create mode 100644 bndtools.core/src/bndtools/wizards/newworkspace/TemplateDefinitionDialog.java create mode 100644 bndtools.core/test/bndtools/util/ui/UITest.java diff --git a/bndtools.core/_plugin.xml b/bndtools.core/_plugin.xml index 04756e7ad9..6ec239377e 100644 --- a/bndtools.core/_plugin.xml +++ b/bndtools.core/_plugin.xml @@ -55,6 +55,10 @@ + + + @@ -143,14 +147,29 @@ preferredPerspectives="bndtools.perspective" icon="icons/bricks.png" name="Bnd OSGi Project" project="true"> - + + Creates a new bnd workspace. You will be able to select template fragments + that will define the new workspace. At finish, you can switch to the new + workspace. + + + + icon="icons/bndtools-logo-16x16.png" name="Bnd OSGi Workspace (Deprecated)"> + + Old style workspace creation, will be deprecated in a coming release. + + eclipseWorkspaceRepository = Memoize .supplier(EclipseWorkspaceRepository::new); + private final static AtomicBoolean syncing = new AtomicBoolean(); private static Auxiliary auxiliary; @@ -96,7 +107,6 @@ public class Central implements IStartupParticipant { private final BundleContext bundleContext; private final Map javaProjectToModel = new HashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); - private RepositoryListenerPluginTracker repoListenerTracker; private final InternalPluginTracker internalPlugins; @@ -135,7 +145,6 @@ public Central() { @Override public void start() { instance = this; - repoListenerTracker = new RepositoryListenerPluginTracker(bundleContext); repoListenerTracker.open(); internalPlugins.open(); @@ -416,9 +425,58 @@ private static File getWorkspaceDirectory() throws CoreException { .getParentFile(); } + String path = Platform.getInstanceLocation() + .getURL() + .getPath(); + + if (IO.isWindows() && path.startsWith("/")) + path = path.substring(1); + + File folder = new File(path); + File build = IO.getFile(folder, "cnf/build.bnd"); + if (build.isFile()) { + if (syncing.getAndSet(true) == false) { + Job job = Job.create("sync ws", mon -> { + WorkspaceSynchronizer wss = new WorkspaceSynchronizer(); + wss.synchronize(false, mon, () -> { + syncing.set(false); + }); + setBndtoolsPerspective(); + final IIntroManager introManager = PlatformUI.getWorkbench() + .getIntroManager(); + IIntroPart part = introManager.getIntro(); + introManager.closeIntro(part); + }); + job.schedule(); + } + return folder; + } + return null; } + public static void setBndtoolsPerspective() { + Display.getDefault() + .syncExec(() -> { + IWorkbenchWindow window = PlatformUI.getWorkbench() + .getActiveWorkbenchWindow(); + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + IPerspectiveRegistry reg = PlatformUI.getWorkbench() + .getPerspectiveRegistry(); + // Replace "your.perspective.id" with the + // actual ID + // of + // the perspective you want to switch to + IPerspectiveDescriptor bndtools = reg.findPerspectiveWithId("bndtools.perspective"); + if (bndtools != null) + page.setPerspective(bndtools); + + return; + } + }); + } + /** * Determine if the given directory is a workspace. * @@ -700,8 +758,8 @@ public static IResource toResource(File file) { * @throws Exception If the callable throws an exception. */ public static V bndCall(BiFunctionWithException, BooleanSupplier, V> lockMethod, - FunctionWithException, V> callable, - IProgressMonitor monitorOrNull) throws Exception { + FunctionWithException, V> callable, IProgressMonitor monitorOrNull) + throws Exception { IProgressMonitor monitor = monitorOrNull == null ? new NullProgressMonitor() : monitorOrNull; Task task = new Task() { @Override @@ -728,14 +786,14 @@ public void abort() { try { Callable with = () -> TaskManager.with(task, () -> callable.apply((name, runnable) -> after.add(() -> { monitor.subTask(name); - try { + try { runnable.run(); } catch (Exception e) { if (!(e instanceof OperationCanceledException)) { status.add(new Status(IStatus.ERROR, runnable.getClass(), "Unexpected exception in bndCall after action: " + name, e)); - } } + } }))); return lockMethod.apply(with, monitor::isCanceled); } finally { diff --git a/bndtools.core/src/bndtools/central/Starter.java b/bndtools.core/src/bndtools/central/Starter.java new file mode 100644 index 0000000000..638336ac1a --- /dev/null +++ b/bndtools.core/src/bndtools/central/Starter.java @@ -0,0 +1,15 @@ +package bndtools.central; + +import org.eclipse.ui.IStartup; + +public class Starter implements IStartup { + + @Override + public void earlyStartup() { + try { + Central.getWorkspace(); + } catch (Exception e) { + } + } + +} diff --git a/bndtools.core/src/bndtools/central/sync/SynchronizeWorkspaceWithEclipse.java b/bndtools.core/src/bndtools/central/sync/SynchronizeWorkspaceWithEclipse.java index 089fb34545..653301ac21 100644 --- a/bndtools.core/src/bndtools/central/sync/SynchronizeWorkspaceWithEclipse.java +++ b/bndtools.core/src/bndtools/central/sync/SynchronizeWorkspaceWithEclipse.java @@ -42,7 +42,7 @@ * with the file system. Any deltas are processed by creating or deleting the * project. */ -@Component(enabled = false) +@Component(enabled = true) public class SynchronizeWorkspaceWithEclipse { static IWorkspace eclipse = ResourcesPlugin.getWorkspace(); final static IWorkspaceRoot root = eclipse.getRoot(); @@ -86,8 +86,6 @@ private void sync(Collection doNotUse) { return; } - // No need to turn off autobuild as the lock will take care of it - Job sync = Job.create("sync workspace", (IProgressMonitor monitor) -> { Map projects = Stream.of(root.getProjects()) diff --git a/bndtools.core/src/bndtools/perspective/BndPerspective.java b/bndtools.core/src/bndtools/perspective/BndPerspective.java index e902eccb32..ac06352bf2 100644 --- a/bndtools.core/src/bndtools/perspective/BndPerspective.java +++ b/bndtools.core/src/bndtools/perspective/BndPerspective.java @@ -74,6 +74,7 @@ public void createInitialLayout(IPageLayout layout) { // new actions - Java project creation wizard layout.addNewWizardShortcut(PartConstants.WIZARD_ID_NEWPROJECT); layout.addNewWizardShortcut(PartConstants.WIZARD_ID_NEWWORKSPACE); + layout.addNewWizardShortcut(PartConstants.WIZARD_ID_NEWWORKSPACE + "Deprecated"); layout.addNewWizardShortcut(PartConstants.WIZARD_ID_NEWBNDRUN); layout.addNewWizardShortcut(PartConstants.WIZARD_ID_NEWBND); layout.addNewWizardShortcut(PartConstants.WIZARD_ID_NEWBLUEPRINT_XML); diff --git a/bndtools.core/src/bndtools/util/ui/UI.java b/bndtools.core/src/bndtools/util/ui/UI.java new file mode 100644 index 0000000000..1830902481 --- /dev/null +++ b/bndtools.core/src/bndtools/util/ui/UI.java @@ -0,0 +1,616 @@ +package bndtools.util.ui; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import aQute.bnd.exceptions.Exceptions; +import aQute.lib.io.IO; + +/** + * A Utility for MVC like programming in Java. + *

+ * The model is a DTO class with fields and methods. The idea is that you can + * change these variables and then they are automatically updating the Widgets. + * The model can contain fields and methods. This class uses a method in + * preference of a field. A get method is applicable if the it takes the + * identical return type as the field and has no parameters. A set method is if + * it takes 1 parameter with the exact type of the field. A field, however, is + * mandatory because that is how the fields are discovered. A field must be + * non-final, non-static, non-synthetic and non-transient. + *

+ * The only requirement is that you modify them in a {@link #read(Supplier)} or + * {@link #write(Runnable)} block. These methods ensure that any updates are + * handled thread safe and properly synchronized. + *

+ * A UI is created as follows: + * + *

+ * final M model = new M();
+ * final UI ui = new UI<>(model);
+ * 
+ *

+ * To other side of the model is the _world_. These are widgets or methods + * updating some information on the GUI. These are bound through a Target + * interface. The mandatory method {@link Target#set(Object)} sets the value + * from the model to the world. The optional {@link Target#subscribe(Consumer)} + * method can be used to let the world update the model from a subscription + * model like addXXXListeners in SWT. There are convenient methods in this class + * to transform common widgets to Target. + * + *

+ * ui.u("name", model.name)
+ * 	.bind(UI.checkbox(myCheckbox));
+ * 
+ *

+ * However, a Target is also a functional interface. This makes it possible + * to just use a lambda: + * + *

+ * ui.u("name", model.name)
+ * 	.bind(this::setTitle);
+ * 
+ *

+ * The updating of the world is delayed and changes are coalesced. On the world + * side, there is a guarantee that only changes are updated. If the subscription + * sets a value than that value is is assumed to be the world's value. I.e. if + * the model tries to set that same value back, the world will not be updated. + *

+ * Values in the model must be simple type. Changes are detected with the normal + * equals and hashCode. null is properly handled everywhere. + *

+ * If the model requires some calculation before the world is updated, it can + * implement Runnable. This runnable is called inside the lock to do for example + * validation. + * + * @param model type + */ +public class UI implements AutoCloseable { + final static Logger log = LoggerFactory.getLogger(UI.class); + final static Lookup lookup = MethodHandles.lookup(); + final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + final Map access = new HashMap<>(); + final List> updaters = new ArrayList<>(); + final Class modelType; + final List updates = new CopyOnWriteArrayList<>(); + final M model; + + class Guarded { + int version = 100; + CountDownLatch updated = null; + } + + final Guarded lock = new Guarded(); + + /* + * The Access class maps to a single field in the model. It methods the + * MethodHandles to access the field or methods and it has a map of bindings + * and their last updated value. + */ + class Access implements AutoCloseable { + + final MethodHandle get; + final MethodHandle set; + final List> bindings = new ArrayList<>(); + final Class type; + final String name; + + /* + * A Binding connects the access class to n worlds that depend on the + * the same value of the model. It keeps a last value and it maintains + * the subscription. + */ + class Binding implements AutoCloseable { + final Target target; + Object lastValue; + AutoCloseable subscription; + + Binding(Target target) { + this.target = target; + subscription = target.subscribe(value -> { + lastValue = value; + toModel(value); + }); + + } + + @SuppressWarnings("unchecked") + void update(Object value) { + if (!Objects.equals(value, lastValue)) { + lastValue = value; + target.set((T) value); + } + } + + @Override + public void close() { + IO.close(subscription); + } + } + + Access(Field field) { + this.name = field.getName(); + this.type = field.getType(); + MethodHandle get = null; + MethodHandle set = null; + field.setAccessible(true); + try { + Method m = modelType.getDeclaredMethod(name); + m.setAccessible(true); + get = lookup.unreflect(m); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException e) { + try { + get = lookup.unreflectGetter(field); + } catch (IllegalAccessException e1) {} + } + try { + Method m = modelType.getDeclaredMethod(name, type); + m.setAccessible(true); + set = lookup.unreflect(m); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException e) { + try { + set = lookup.unreflectSetter(field); + } catch (IllegalAccessException e1) {} + } + assert get != null && set != null; + this.set = set; + this.get = get; + } + + Object fromModel() { + try { + return get.invoke(model); + } catch (Throwable e) { + throw Exceptions.duck(e); + } + } + + void toModel(Object newer) { + try { + set.invoke(model, newer); + trigger(); + } catch (Throwable e) { + throw Exceptions.duck(e); + } + } + + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + void toWorld() { + Object value = fromModel(); + for (Binding binding : bindings) { + binding.update(value); + } + } + + void add(Target target) { + bindings.add(new Binding<>(target)); + } + + @Override + public void close() throws Exception { + bindings.forEach(IO::close); + } + + // test methods + + @SuppressWarnings("resource") + Object last(int i) { + return bindings.get(i).lastValue; + } + + @SuppressWarnings("resource") + Target target(int i) { + return bindings.get(i).target; + } + } + + /** + * An interface that should be implemented by parties that want to get + * updated and can be subscribed to. It is for this UI class the abstraction + * of the world. + *

+ * Although the interface has two methods, the subscribe is default + * implemented as a noop. This makes this interface easy to use as a + * Functional interface and Consumer like lambdas map well to it. + * + * @param the type of the target + */ + public interface Target { + /** + * Set the model value into the world. + * + * @param value the value + */ + void set(T value); + + /** + * Subscribe to changes in the world. + * + * @param subscription the callback to call when the world changes + * @return a closeable that will remove the subscription + */ + default AutoCloseable subscribe(Consumer subscription) { + return () -> {}; + } + + /** + * Subscribe to changes in the world. + * + * @param subscription the callback to call when the world changes + * @return a closeable that will remove the subscription + */ + default AutoCloseable subscribe(Runnable subscription) { + return subscribe(x -> subscription.run()); + } + + /** + * Sometimes the target takes a different type than the model. This + * method will create a mediator that maps the value back and forth. + * + * @param the other type + * @param down the downstream towards the world + * @param up upstream towards the model + * @return another target + */ + default Target map(Function down, Function up) { + Target THIS = this; + return new Target<>() { + + @Override + public void set(U value) { + THIS.set(down.apply(value)); + } + + @Override + public AutoCloseable subscribe(Consumer subscription) { + AutoCloseable subscribed = THIS.subscribe(v -> { + U apply = up.apply(v); + subscription.accept(apply); + }); + return subscribed; + } + }; + } + + } + + /** + * External interface to bind targets to the model. + * + * @param the type + */ + public interface Binder { + Binder bind(Target target); + } + + /** + * Constructor. + * + * @param model the model to use + */ + @SuppressWarnings("unchecked") + public UI(M model) { + this((Class) model.getClass(), model); + } + + /** + * Specify a type to use + * + * @param modelType the model type + * @param model the model + */ + UI(Class modelType, M model) { + this.modelType = modelType; + this.model = model; + + for (Field field : modelType.getDeclaredFields()) { + int mods = field.getModifiers(); + if (Modifier.isStatic(mods) || Modifier.isTransient(mods) || Modifier.isPrivate(mods) + || (field.getModifiers() & 0x00001000) != 0) + continue; + + access.put(field.getName(), new Access(field)); + } + } + + /** + * Create a binder for a given model field. + * + * @param the type of the field + * @param name the name of the field + * @param guard guard to ensure the model field's type matches the targets. + * The value is discarded. + * @return a binder + */ + public Binder u(String name, T guard) { + assert name != null; + Access access = this.access.get(name); + assert access != null : name + " is not a field in the model " + modelType.getSimpleName(); + + return new Binder<>() { + @Override + public Binder bind(Target target) { + access.add(target); + return this; + } + }; + } + + /** + * Bind the given target and return a binder for subsequent targets to bind. + * + * @param the model field's type + * @param name the name of the field + * @param guard guard to ensure the model field's type matches the targets. + * The value is discarded. + * @param target the target to bind + * @return a Binder + */ + public Binder u(String name, T guard, Target target) { + return u(name, guard).bind(target); + } + + /** + * Return a target for a Text widget. This will use + * {@link Text#setText(String)} for {@link Target#set(Object)} and it will + * subscribe to modifications with + * {@link Text#addModifyListener(ModifyListener)} + * + * @param widget the text widget + * @return a target + */ + public static Target text(Text widget) { + return new Target() { + String last; + + @Override + public void set(String value) { + if (!Objects.equals(widget.getText(), value)) { + System.out.println("setting " + widget + " " + value); + last = value; + widget.setText(value); + } + } + + @Override + public AutoCloseable subscribe(Consumer subscription) { + ModifyListener listener = e -> { + String value = widget.getText(); + if (!Objects.equals(last, value)) { + last = value; + System.out.println("event " + widget + " " + widget.getText()); + subscription.accept(widget.getText()); + } + }; + widget.addModifyListener(listener); + return () -> widget.removeModifyListener(listener); + } + }; + } + + /** + * Return a target for a checkbox button. The {@link Target#set(Object)} + * maps to {@link Button#setSelection(boolean)} and the subscription is + * handled via {@link Button#addSelectionListener(SelectionListener)}. + * + * @param widget the widget to map + * @return a target that can set and subscribe the button selection + */ + public static Target checkbox(Button widget) { + return new Target() { + + @Override + public void set(Boolean value) { + widget.setSelection(value); + } + + @Override + public AutoCloseable subscribe(Consumer subscription) { + SelectionListener listener = onSelect(e -> subscription.accept(widget.getSelection())); + widget.addSelectionListener(listener); + return () -> widget.removeSelectionListener(listener); + } + }; + } + + /** + * Map the selection of a CheckboxTableViewer to a Target. It uses + * {@link CheckboxTableViewer#setCheckedElements(Object[])} and the + * subscription is handled via the + * {@link CheckboxTableViewer#addSelectionChangedListener(ISelectionChangedListener)} + * + * @param widget the CheckboxTableViewer + * @return a new Target + */ + public static Target widget(CheckboxTableViewer widget) { + return new Target<>() { + + @Override + public void set(Object[] value) { + widget.setCheckedElements(value); + } + + @Override + public AutoCloseable subscribe(Consumer subscription) { + ISelectionChangedListener listener = se -> { + subscription.accept(widget.getCheckedElements()); + }; + widget.addSelectionChangedListener(listener); + return () -> widget.removeSelectionChangedListener(listener); + } + }; + } + + /** + * Create a selection listener with a lambda for the selection and the + * default selection + * + * @param listener the listener + * @param defaultListener the listener to default + * @return a proper listener + */ + public static SelectionListener onSelect(Consumer listener, + Consumer defaultListener) { + return new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + listener.accept(e); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + defaultListener.accept(e); + } + }; + } + + /** + * Create a selection listener with the same lambda for the selection and + * the default selection + * + * @param listener the listener + * @return a proper listener + */ + public static SelectionListener onSelect(Consumer listener) { + return new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + listener.accept(e); + } + }; + } + + @Override + public void close() throws Exception { + + } + + /** + * Updates to the model should be handled in here. The runnable should be + * very short since it runs in a lock that is acquired in the display + * thread. + * + * @param r the runnable to execute that updates the model + * @return a CountDownLatch that will unlatch when the write has updated the + * world. + */ + public CountDownLatch write(Runnable r) { + synchronized (lock) { + r.run(); + return trigger(); + } + } + + /** + * A read in the same lock as the write but without the world update. The + * supplier's value is returned. + * + * @param the type the return + * @param r the supplier + * @return the value returned from the supllier + */ + public X read(Supplier r) { + synchronized (lock) { + return r.get(); + } + } + + /** + * Trigger a world update. This will delay 50 ms to coalesce additional + * updates. If during the update of the world (which is done without holding + * a lock) there is another change, the update will be repeated. + * + * @return a {@link CountDownLatch} that will be unlatched when the state of + * the model at this moment is represented in the world. + */ + public CountDownLatch trigger() { + synchronized (lock) { + lock.version++; + if (lock.updated == null) { + lock.updated = new CountDownLatch(1); + scheduler.schedule(() -> { + while (true) { + int current; + synchronized (lock) { + current = lock.version; + } + + try { + dispatch(); + } catch (Exception e) { + log.error("failed to update model to world {}", e, e); + } + + synchronized (lock) { + if (current == lock.version) { + lock.updated.countDown(); + lock.updated = null; + return; + } + } + } + }, 50, TimeUnit.MILLISECONDS); + } + return lock.updated; + } + } + + /** + * This method is Eclispe SWT specific. It dispatches the updates on the UI + * thread when there are no events present. + */ + void dispatch() { + Display display = Display.getDefault(); + display.asyncExec(() -> { + while (!display.isDisposed()) { + if (!display.readAndDispatch()) { + update(); + return; + } + } + }); + } + + /** + * Copy the model to the world. + */ + public void update() { + if (model instanceof Runnable r) { + r.run(); + } + + for (Access access : this.access.values()) { + access.toWorld(); + } + } +} diff --git a/bndtools.core/src/bndtools/wizards/newworkspace/Model.java b/bndtools.core/src/bndtools/wizards/newworkspace/Model.java new file mode 100644 index 0000000000..4ab95e4090 --- /dev/null +++ b/bndtools.core/src/bndtools/wizards/newworkspace/Model.java @@ -0,0 +1,165 @@ +package bndtools.wizards.newworkspace; + +import java.io.File; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PlatformUI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import aQute.bnd.build.Workspace; +import aQute.bnd.wstemplates.FragmentTemplateEngine; +import aQute.bnd.wstemplates.FragmentTemplateEngine.TemplateInfo; +import aQute.bnd.wstemplates.FragmentTemplateEngine.TemplateUpdater; +import aQute.lib.io.IO; + +public class Model implements Runnable { + static final Logger log = LoggerFactory.getLogger(Model.class); + static final IWorkspace ECLIPSE_WORKSPACE = ResourcesPlugin.getWorkspace(); + static final IWorkspaceRoot ROOT = ECLIPSE_WORKSPACE.getRoot(); + static final IPath ROOT_LOCATION = ROOT.getLocation(); + static final URI TEMPLATE_HOME = URI.create("https:://github.com/bndtools/workspace"); + + final static File current = ROOT_LOCATION.toFile(); + + enum NewWorkspaceType { + newbnd, + derive, + classic + } + + File location = getUniqueWorkspaceName(); + boolean clean = false; + boolean updateWorkspace = false; + boolean switchWorkspace = true; + List templates = new ArrayList<>(); + List selectedTemplates = new ArrayList<>(); + Progress validatedUrl = Progress.init; + String urlValidationError; + String error; + String valid; + NewWorkspaceType choice = NewWorkspaceType.newbnd; + + enum Progress { + init, + start, + finished, + error + } + + boolean isValid() { + String valid; + if (location.isFile()) { + valid = "the location " + location + " is not a directory"; + } else if (location.equals(current) && !updateWorkspace) { + valid = "selected the current workspace, select another directory"; + } else if (!clean && !updateWorkspace && !getDataFiles().isEmpty()) { + valid = "the target location contains files, set delete files to delete them"; + } else { + valid = null; + } + this.valid = valid; + return valid != null; + } + + List getDataFiles() { + if (!location.isDirectory()) + return Collections.emptyList(); + + return Stream.of(location) + .filter(f -> { + if (f.getName() + .equals(".metadata")) + return false; + + return true; + }) + .toList(); + } + + boolean execute(TemplateUpdater updater) { + Display display = PlatformUI.getWorkbench() + .getDisplay(); + + Job job = Job.create("create workspace", mon -> { + try { + if (clean) { + getDataFiles().forEach(IO::delete); + } + if (!updateWorkspace) { + location.mkdirs(); + File b = IO.getFile(location, "cnf/build.bnd"); + b.getParentFile() + .mkdirs(); + + IO.store("", b); + } + updater.commit(); + + if (updateWorkspace) { + IResource workspaceRoot = ResourcesPlugin.getWorkspace() + .getRoot(); + workspaceRoot.refreshLocal(IResource.DEPTH_INFINITE, null); + } else if (switchWorkspace) { + display.asyncExec(() -> { + System.setProperty("osgi.instance.area", location.getAbsolutePath()); + System.setProperty("osgi.instance.area.default", location.getAbsolutePath()); + + PlatformUI.getWorkbench() + .restart(); + }); + } + } catch (Exception e) { + log.error("creating new workspace {}", e, e); + } + }); + job.schedule(); + return true; + } + + void updateWorkspace(boolean useEclipse) { + if (useEclipse != updateWorkspace) { + updateWorkspace = useEclipse; + if (useEclipse) { + location = current; + } else { + location = getUniqueWorkspaceName(); + } + } + } + + static File getUniqueWorkspaceName() { + return IO.unique(IO.getFile("~/workspace"), null); + } + + public void location(File file) { + location = file; + } + + public void clean(boolean selection) { + clean = selection; + } + + void selectedTemplates(List list) { + selectedTemplates = list; + } + + @Override + public void run() { + isValid(); + } + + void init(FragmentTemplateEngine templateFragments, Workspace workspace) {} + +} diff --git a/bndtools.core/src/bndtools/wizards/newworkspace/NewWorkspaceWizard.java b/bndtools.core/src/bndtools/wizards/newworkspace/NewWorkspaceWizard.java new file mode 100644 index 0000000000..fa89131c13 --- /dev/null +++ b/bndtools.core/src/bndtools/wizards/newworkspace/NewWorkspaceWizard.java @@ -0,0 +1,261 @@ +package bndtools.wizards.newworkspace; + +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableLayout; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.window.Window; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IImportWizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import aQute.bnd.build.Workspace; +import aQute.bnd.exceptions.Exceptions; +import aQute.bnd.header.Parameters; +import aQute.bnd.result.Result; +import aQute.bnd.wstemplates.FragmentTemplateEngine; +import aQute.bnd.wstemplates.FragmentTemplateEngine.TemplateInfo; +import aQute.bnd.wstemplates.FragmentTemplateEngine.TemplateUpdater; +import bndtools.central.Central; +import bndtools.util.ui.UI; + +/** + * Create a new Workspace Wizard. + */ +public class NewWorkspaceWizard extends Wizard implements IImportWizard, INewWizard { + static final String DEFAULT_INDEX = "https://raw.githubusercontent.com/bndtools/workspace-templates/master/index.bnd"; + static final Logger log = LoggerFactory.getLogger(NewWorkspaceWizard.class); + + final Model model = new Model(); + final UI ui = new UI<>(model); + final NewWorkspaceWizardPage page = new NewWorkspaceWizardPage(); + final FragmentTemplateEngine templates; + + public NewWorkspaceWizard() throws Exception { + setWindowTitle("Create New bnd Workspace"); + Workspace workspace = Central.getWorkspace(); + templates = new FragmentTemplateEngine(workspace); + try { + Job job = Job.create("load index", mon -> { + try { + templates.read(new URL(DEFAULT_INDEX)) + .unwrap() + .forEach(templates::add); + Parameters p = workspace.getMergedParameters("-workspace-template"); + templates.read(p) + .forEach(templates::add); + ui.write(() -> model.templates = templates.getAvailableTemplates()); + } catch (Exception e) { + log.error("failed to read default index {}", e, e); + } + }); + job.schedule(); + } catch (Throwable e) { + log.error("initialization {}", e, e); + throw Exceptions.duck(e); + } + } + + @Override + public void addPages() { + addPage(page); + } + + @Override + public boolean performFinish() { + if (model.valid == null) { + ui.write(() -> { + TemplateUpdater updater = templates.updater(model.location, model.selectedTemplates); + model.execute(updater); + }); + return true; + } else + return false; + } + + class NewWorkspaceWizardPage extends WizardPage { + NewWorkspaceWizardPage() { + super("New Workspace"); + setTitle("Create New Workspace"); + setDescription("Specify the workspace details."); + } + + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NONE); + setControl(container); + container.setLayout(new GridLayout(8, false)); + + Button useEclipseWorkspace = new Button(container, SWT.CHECK); + useEclipseWorkspace.setText("Update current Eclipse workspace"); + useEclipseWorkspace.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 8, 1)); + + Label locationLabel = new Label(container, SWT.NONE); + locationLabel.setText("Location"); + locationLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 8, 1)); + + Text location = new Text(container, SWT.BORDER); + location.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 6, 1)); + + Button browseButton = new Button(container, SWT.PUSH); + browseButton.setText("Browse..."); + browseButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1)); + + Button clean = new Button(container, SWT.CHECK); + clean.setText("Clean the directory"); + clean.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 8, 1)); + + Button switchWorkspace = new Button(container, SWT.CHECK); + switchWorkspace.setText("Switch to new workspace after finish"); + switchWorkspace.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 8, 1)); + + CheckboxTableViewer selectedTemplates = CheckboxTableViewer.newCheckList(container, + SWT.BORDER | SWT.FULL_SELECTION); + selectedTemplates.setContentProvider(ArrayContentProvider.getInstance()); + Table table = selectedTemplates.getTable(); + table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 6, 10)); + TableLayout tableLayout = new TableLayout(); + table.setLayout(tableLayout); + table.setHeaderVisible(true); + + TableViewerColumn nameColumn = new TableViewerColumn(selectedTemplates, SWT.NONE); + nameColumn.getColumn() + .setText("Name"); + nameColumn.setLabelProvider(new ColumnLabelProvider() { + + @Override + public String getText(Object element) { + if (element instanceof TemplateInfo) { + return ((TemplateInfo) element).name(); + } + return super.getText(element); + } + }); + + TableViewerColumn descriptionColumn = new TableViewerColumn(selectedTemplates, SWT.NONE); + descriptionColumn.getColumn() + .setText("Description"); + descriptionColumn.setLabelProvider(new ColumnLabelProvider() { + + @Override + public String getText(Object element) { + if (element instanceof TemplateInfo) { + return ((TemplateInfo) element).description(); + } + return super.getText(element); + } + }); + tableLayout.addColumnData(new ColumnWeightData(1, 80, false)); + tableLayout.addColumnData(new ColumnWeightData(10, 200, true)); + + Button addButton = new Button(container, SWT.PUSH); + addButton.setText("+"); + addButton.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1)); + + ui.u("location", model.location, UI.text(location) + .map(File::getAbsolutePath, File::new)); + ui.u("clean", model.clean, UI.checkbox(clean)); + ui.u("updateWorkspace", model.updateWorkspace, UI.checkbox(useEclipseWorkspace)) + .bind(v -> location.setEnabled(!v)) + .bind(v -> browseButton.setEnabled(!v)) + .bind(v -> switchWorkspace.setEnabled(!v)) + .bind(v -> clean.setEnabled(!v)) + .bind(v -> setTitle(v ? "Update Workspace" : "Create New Workspace")) + .bind(v -> setWindowTitle(v ? "Update Workspace" : "Create New Workspace")); + + ui.u("valid", model.valid, this::setErrorMessage); + ui.u("error", model.error, this::setErrorMessage); + ui.u("valid", model.valid, v -> setPageComplete(v == null)); + ui.u("switchWorkspace", model.switchWorkspace, UI.checkbox(switchWorkspace)); + ui.u("templates", model.templates, l -> selectedTemplates.setInput(l.toArray())); + ui.u("selectedTemplates", model.selectedTemplates, UI.widget(selectedTemplates) + .map(List::toArray, this::toTemplates)); + UI.checkbox(addButton) + .subscribe(this::addTemplate); + UI.checkbox(browseButton) + .subscribe(this::browseForLocation); + + ui.update(); + } + + List toTemplates(Object[] selection) { + return Stream.of(selection) + .map(o -> (TemplateInfo) o) + .toList(); + } + + void browseForLocation() { + DirectoryDialog dialog = new DirectoryDialog(getShell()); + dialog.setFilterPath(model.location.getAbsolutePath()); + String path = dialog.open(); + if (path != null) { + ui.write(() -> model.location(new File(path))); + } + } + + void addTemplate() { + TemplateDefinitionDialog dialog = new TemplateDefinitionDialog(getShell()); + if (dialog.open() == Window.OK) { + String selectedPath = dialog.getSelectedPath(); + if (!selectedPath.isBlank()) { + Job job = Job.create("read " + selectedPath, mon -> { + try { + URI uri = toURI(selectedPath); + Result> result = templates.read(uri.toURL()); + + if (result.isErr()) { + ui.write(() -> model.error = result.toString()); + } else { + result.unwrap() + .forEach(templates::add); + ui.write(() -> model.templates = templates.getAvailableTemplates()); + } + } catch (Exception e) { + ui.write(() -> model.error = "failed to add the index: " + e); + } + }); + job.schedule(); + } + } + + } + + URI toURI(String path) { + URI uri; + File f = new File(path); + if (f.isFile()) { + uri = f.toURI(); + } else { + uri = URI.create(path); + } + return uri; + } + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) {} + +} diff --git a/bndtools.core/src/bndtools/wizards/newworkspace/TemplateDefinitionDialog.java b/bndtools.core/src/bndtools/wizards/newworkspace/TemplateDefinitionDialog.java new file mode 100644 index 0000000000..a209807bb0 --- /dev/null +++ b/bndtools.core/src/bndtools/wizards/newworkspace/TemplateDefinitionDialog.java @@ -0,0 +1,66 @@ +package bndtools.wizards.newworkspace; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import bndtools.util.ui.UI; + +/** + * Asks for a url or file path + */ +class TemplateDefinitionDialog extends Dialog { + final UI ui = new UI<>(this); + String path; + + public TemplateDefinitionDialog(Shell parentShell) { + super(parentShell); + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Template Definitions"); + } + + @Override + protected Composite createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + GridLayout layout = new GridLayout(12, false); + container.setLayout(layout); + + Label label = new Label(container, SWT.NONE); + label.setText("Template definitions. You can enter a URL or a file path"); + label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 12, 1)); + + Text textField = new Text(container, SWT.BORDER); + GridData textFieldLayoutData = new GridData(SWT.FILL, SWT.CENTER, true, false, 11, 1); + textFieldLayoutData.minimumWidth = 200; + textField.setLayoutData(textFieldLayoutData); + + Button browseButton = new Button(container, SWT.PUSH); + browseButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + browseButton.setText("Browse..."); + ui.u("path", path, UI.text(textField)); + browseButton.addSelectionListener(UI.onSelect(x -> browseForFile())); + return container; + } + + private void browseForFile() { + FileDialog dialog = new FileDialog(getShell()); + String path = dialog.open(); + ui.write(() -> this.path = path); + } + + public String getSelectedPath() { + return path; + } + +} diff --git a/bndtools.core/test/bndtools/util/ui/UITest.java b/bndtools.core/test/bndtools/util/ui/UITest.java new file mode 100644 index 0000000000..786321a510 --- /dev/null +++ b/bndtools.core/test/bndtools/util/ui/UITest.java @@ -0,0 +1,220 @@ +package bndtools.util.ui; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import bndtools.util.ui.UI.Access; +import bndtools.util.ui.UI.Target; + +class UITest { + + @SuppressWarnings("rawtypes") + @Test + void test() throws Exception { + class M { + boolean vm; + } + class W { + int n = 100; + final List> subs = new ArrayList<>(); + boolean vw; + + void set(boolean v) { + vw = v; + subs.forEach(x -> x.accept(v)); + n++; + } + + AutoCloseable subscribe(Consumer sub) { + subs.add(sub); + return () -> subs.remove(sub); + } + } + M model = new M(); + W world_1 = new W(); + W world_2 = new W(); + + try (UI ui = new UI<>(model) { + public void dispatch() { + update(); + } + }) { + Target target_1 = new Target<>() { + + @Override + public void set(Boolean value) { + world_1.set(value); + } + + public AutoCloseable subscribe(Consumer subscription) { + return world_1.subscribe(subscription); + } + }; + Target target_2 = new Target<>() { + + @Override + public void set(Boolean value) { + world_2.set(value); + } + + public AutoCloseable subscribe(Consumer subscription) { + return world_2.subscribe(subscription); + } + }; + + ui.u("vm", model.vm) + .bind(target_1) + .bind(target_2); + + Access access = ui.access.get("vm"); + assertThat(access.last(0)).isNull(); + + System.out.println("write to model true, check world update"); + ui.write(() -> model.vm = true) + .await(); + assertThat(access.last(0)).isEqualTo(Boolean.TRUE); + assertThat(access.last(1)).isEqualTo(Boolean.TRUE); + assertThat(world_1.vw).isTrue(); + assertThat(world_1.n).isEqualTo(101); + assertThat(world_2.vw).isTrue(); + assertThat(world_2.n).isEqualTo(101); + + System.out.println("write to model false, check world update"); + ui.write(() -> model.vm = false) + .await(); + assertThat(access.last(0)).isEqualTo(Boolean.FALSE); + assertThat(access.last(1)).isEqualTo(Boolean.FALSE); + assertThat(world_1.vw).isFalse(); + assertThat(world_1.n).isEqualTo(102); + assertThat(world_2.vw).isFalse(); + assertThat(world_2.n).isEqualTo(102); + + System.out.println("write to model false, check no world"); + ui.write(() -> model.vm = false) + .await(); + assertThat(access.last(0)).isEqualTo(Boolean.FALSE); + assertThat(access.last(1)).isEqualTo(Boolean.FALSE); + assertThat(world_1.vw).isFalse(); + assertThat(world_1.n).isEqualTo(102); + assertThat(world_2.vw).isFalse(); + assertThat(world_2.n).isEqualTo(102); + assertThat(model.vm).isFalse(); + + System.out.println("write to world 1 true, check other world 2 update"); + assertThat(model.vm).isFalse(); + int v = ui.lock.version; + world_1.set(true); + assertThat(model.vm).isTrue(); + CountDownLatch cd = ui.lock.updated; + assertThat(cd).isNotNull(); + assertThat(world_1.vw).isTrue(); + assertThat(world_1.n).isEqualTo(103); + assertThat(world_2.vw).isFalse(); + assertThat(world_2.n).isEqualTo(102); + assertThat(access.last(0)).isEqualTo(Boolean.TRUE); + assertThat(access.last(1)).isEqualTo(Boolean.FALSE); + cd.await(); + + assertThat(world_1.vw).isTrue(); + assertThat(world_1.n).isEqualTo(103); + assertThat(world_2.vw).isTrue(); + assertThat(world_2.n).isEqualTo(103); + assertThat(access.last(0)).isEqualTo(Boolean.TRUE); + assertThat(access.last(1)).isEqualTo(Boolean.TRUE); + assertThat(model.vm).isTrue(); + + } + + } + + @SuppressWarnings("rawtypes") + @Test + void testMethodField() throws Exception { + class M { + int n = 100; + boolean vm; + + @SuppressWarnings("unused") + void vm(boolean value) { + vm = value; + n++; + } + } + M model = new M(); + + try (UI ui = new UI<>(model) { + public void dispatch() { + update(); + } + }) { + AtomicBoolean world = new AtomicBoolean(false); + ui.u("vm", model.vm) + .bind(world::set); + + Access access = ui.access.get("vm"); + access.toModel(true); + assertThat(model.n).isEqualTo(101); + } + + } + + @SuppressWarnings("rawtypes") + @Test + void testMapping() throws Exception { + class M { + boolean vm; + } + M model = new M(); + class W implements Target { + final List> subs = new ArrayList<>(); + String vw; + + @Override + public void set(String value) { + vw = value; + subs.forEach(c -> c.accept(value)); + } + + @Override + public AutoCloseable subscribe(Consumer subscription) { + subs.add(subscription); + return () -> subs.remove(subscription); + } + } + W world = new W(); + + try (UI ui = new UI<>(model) { + public void dispatch() { + update(); + } + }) { + + ui.u("vm", model.vm) + .bind(world.map(b -> Boolean.toString(b), Boolean::valueOf)); + + ui.write(() -> model.vm = true) + .await(); + assertThat(world.vw).isEqualTo("true"); + + ui.write(() -> model.vm = false) + .await(); + assertThat(world.vw).isEqualTo("false"); + + world.set("true"); + ui.lock.updated.await(); + assertThat(model.vm).isEqualTo(true); + + world.set("false"); + ui.lock.updated.await(); + assertThat(model.vm).isEqualTo(false); + } + + } +} From 7157ee69e0f78d54a3cf14d8db9468bf43a0202b Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Wed, 3 Apr 2024 15:08:08 +0200 Subject: [PATCH 5/5] Docs --- Signed-off-by: Peter Kriens Signed-off-by: Peter Kriens --- docs/_chapters/110-introduction.md | 2 +- docs/_chapters/150-build.md | 9 ++- docs/_chapters/620-template-fragments.md | 89 +++++++++++++++++++++++ docs/_instructions/workspace-templates.md | 8 ++ 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 docs/_chapters/620-template-fragments.md create mode 100644 docs/_instructions/workspace-templates.md diff --git a/docs/_chapters/110-introduction.md b/docs/_chapters/110-introduction.md index 45b520100c..b63c0c0f7e 100644 --- a/docs/_chapters/110-introduction.md +++ b/docs/_chapters/110-introduction.md @@ -7,7 +7,7 @@ OSGi is arguably one of the best specifications in the Java world. It is a no co For some, the previous paragraph may come as a surprise because OSGi has had its share of people complaining about it. Surprisingly, the biggest complaint is often the Class Not Found Exception, which is always a perfect sign that people try to push a round peg in a too small square hole, and with all their might. You only see those exceptions when you're not doing engineering but when you are hacking. If you run head on into the walls that OSGi installs and it is giving you a headache, then just look around and find the elegant and easy to use doors: services. -Though this is all true, I do not claim that OSGi is trivial to use; triviality has a way to clash with large system that must evolve over many years. The software profession has a brutal industry that lures us with the siren song of a 'few hours work' to devour us while trying to main gigantic hairballs. As Fred Brooks already said so many years ago in his seminal book 'The Mythical Man Month', there is no silver bullet. Even OSGi will require hard work to build evolvable systems. And though we cannot make building complex systems easy, bnd can at least make it easier (and considerably more fun). +Though this is all true, I do not claim that OSGi is trivial to use; triviality has a way to clash with large system that must evolve over many years. The software profession is a brutal industry that lures us with the siren song of a 'few hours work' to devour us while trying to make gigantic hairballs. As Fred Brooks already said so many years ago in his seminal book 'The Mythical Man Month', there is no silver bullet. Even OSGi will require hard work to build evolvable systems. And though we cannot make building complex systems easy, bnd can at least make it easier (and considerably more fun). ## A Bit of History diff --git a/docs/_chapters/150-build.md b/docs/_chapters/150-build.md index a6860bb34a..9e8731c7b4 100644 --- a/docs/_chapters/150-build.md +++ b/docs/_chapters/150-build.md @@ -15,7 +15,14 @@ A bndlib workspace is a _valid_ workspace when it contains a `cnf` file. If this However, the advised model is to use a directory with a `cnf/build.bnd` file. The purpose of the `cnf` directory is to provide a place for shared information. Though this includes bndlib setup information, it also can be used to define for example shared licensing, copyright, and vendor headers for your organization. -The `cnf` directory can have an `ext` directory, this directory contains any extensions to bnd. +The `cnf` directory can have an `ext` directory. Files in this directory are added to the properties +of the workspace. They can have the following extensions: + +* `.bnd` – Contain bnd properties +* `.pmvn` – An index file for a [Maven Bnd Repository](plugins/maven.html). The first lines can contain properties for this plugin in the format of `# key = value`, e.g. `# name = OSGi R8`. +* `.pobr` – An OSGi Repository file in XML. + +The `ext` directory is a convenient way to add add reusable components. See [template fragments](620-template-fragments.html] how they can be used to manage workspaces. When files change in this directory the workspace will be reloaded. To cache some intermediate files, bndlib will create a `cnf/cache/` directory, this file should not be under source control. E.g. in Git it should be defined in the `.gitignore` file. diff --git a/docs/_chapters/620-template-fragments.md b/docs/_chapters/620-template-fragments.md new file mode 100644 index 0000000000..766335bc89 --- /dev/null +++ b/docs/_chapters/620-template-fragments.md @@ -0,0 +1,89 @@ +--- +title: Templates for Workspaces +layout: default +--- + +A good workspace setup makes all the difference in the development cycle. Since a workspace can +contain many projects, creating new workspaces is relatively rare. However, a workspace tends +to consist of many aspects. There is the gradle setup, the OSGi release, maybe the maven +layout etc. Initially, we attempted to use a single GitHub repository as a template for new workspaces, but this approach quickly became complex. +Instead, we developed _fragment_ templates. Fragments model one aspect of a workspace and are maintained in Github +repositories. One repository can hold many fragments. + +For example, if someone created a [library][3] for bnd, to make it easy for the +user to setup his functionality, the author of the library had to maintain a full workspace +with gradle, etc. This put the burden on chasing the OSGi release, the gradle releases, on the +people that were willing to spread the gospel around OSGi/bndtools. + +The workspace is already prepared for this model of fragments. The [merged instructions][2] mean that +we can extend the properties and instructions from many different sources. However, the most important +feature here is the `cnf/ext` folder. Any `bnd` or special fragment file placed in this folder will automatically +be ready before the `build.bnd` file is read. For example, a fragment could contain the +index for OSGi R8. See [build](150-build.html). + +There is a single master index for all template fragments hosted on + + https://github.com/bndtools/workspace-templates/blob/master/index.bnd + +This index is open for any person or organization that maintains one or more fragment and +seeks an easy way to make them available to bndtools users. All that is needed is to host the fragment somewhere and provide +a pull request (PR) to add it to this index file. + +The format of the index file is like all Parameters. + +* `key` – The key is the identity of the fragment. It is either a URL or provides the following structure: + + id ::= [organization ['/' repository [ '/' path ] ['#' git-ref ]] + +The id is resolved against `bndtools/workspace-templates#master`. Therefore, in the `example` organization, +the `example` is a valid id that resolves to `example/workspace-templates#master`. Since the ID has a path, +it is possible to host multiple fragments in a single repository. + +The index has the following attributes for a clause: + +* `name` – The human readable name for the template fragment +* `description` – A human readable description for the template fragment +* `require` – A comma separated list of fragment ids. Do not forget to quote when multiple fragments are required +* `tag` – A comma separated list of tags, quotes are needed when there are multiple. + +For example: + + -workspace-templates \ + bndtools/workspace-templates/gradle; \ + name=gradle; \ + description="Setup gradle build for bnd workspace", \ + bndtools/workspace-templates/maven; \ + name=maven; \ + description="Use the maven directory layout for sources and binaries", \ + bndtools/workspace-templates/osgi; \ + name=osgi; \ + description="OSGi R8 with Felix distribution" + + +The id must resolve to a folder in the repository. By default, bnd will recursively copy the +content to the new workspace. However, if there is a file `tool.bnd` present it will use this to +guide the copying process. This bnd file looks like: + + -tool \ + .*;tool.bnd;readme.md;skip=true, \ + gradle.properties;macro=true;append=true,\ + gradlew;exec=true, \ + * + +The `-tool` instruction is a SELECT that is matched against the file paths (not names!) in the fragment template folder. +The attributes that can be set are: + +* `skip=true` – Skip the selected file, +* `append=true` – Allow duplicates, append +* `exec=true` – Set the execute bit on non-windows systems +* `preprocess=true` – Preprocess the file (this uses the tool file and the current workspace as domain. +* `delete=true` – Deletes an existing file in the target. Cannot be wildcarded + +It is possible to define the [`-workspace-templates`][4] instruction in the `build.bnd` file. +It must contain the same format as the index. This local index is merged with the main index. In the +Bndtools GUI, it is possible to add an additional index from a URL or file. + + +[2]: https://bnd.bndtools.org/releases/3.5.0/chapters/820-instructions.html#merged-instructions +[3]: https://bnd.bndtools.org/instructions/library.html +[4]: /instructions/workspace-templates.html \ No newline at end of file diff --git a/docs/_instructions/workspace-templates.md b/docs/_instructions/workspace-templates.md new file mode 100644 index 0000000000..c8016ea3b6 --- /dev/null +++ b/docs/_instructions/workspace-templates.md @@ -0,0 +1,8 @@ +--- +layout: default +class: Workspace +title: -workspace-templates +summary: Define workspace templates for a new workspace +--- + +See [workspace templates](/chapters/620-template-fragments.html) \ No newline at end of file