diff --git a/CLAUDE.md b/CLAUDE.md index 0f24856b..6050ce1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ that file before editing anything under `diylc/`. | Path | Contents | | --- | --- | -| `diylc/` | The application. Maven reactor with `diylc-core`, `diylc-library`, `diylc-swing`. This is where nearly all work happens. | +| `diylc/` | The application. Maven reactor with `diylc-core`, `diylc-library`, `diylc-swing`, and the build-time `diylc-processor`. This is where nearly all work happens. | | `diylc-regression-data/` | Sample `.diy` projects plus reference PNG and netlist outputs used by the regression suite, and the historical reports. | | `diylc-server-api/` | PHP backend for cloud and collaboration features. Runs in an isolated environment on diy-fever.com and cannot be tested locally — do not modify unless explicitly asked. | | `snap/` | Snap packaging metadata. | diff --git a/diylc/.claude/skills/add-component/SKILL.md b/diylc/.claude/skills/add-component/SKILL.md index af3e6172..d34831d8 100644 --- a/diylc/.claude/skills/add-component/SKILL.md +++ b/diylc/.claude/skills/add-component/SKILL.md @@ -6,7 +6,8 @@ description: Add a new component type to the DIYLC library, or substantially mod # Adding a component to DIYLC Components live in `diylc-library/src/main/java/org/diylc/components/`. They are discovered at -runtime by Reflections scanning `org.diylc` — **there is no registry to update**. A class becomes a +runtime from a class index that `diylc-processor` writes at compile time — **there is no registry +to update**. A class becomes a component by carrying `@ComponentDescriptor` and implementing `IDIYComponent` (in practice, by extending one of the abstract bases). diff --git a/diylc/CLAUDE.md b/diylc/CLAUDE.md index 86d9807e..61c0b2bb 100644 --- a/diylc/CLAUDE.md +++ b/diylc/CLAUDE.md @@ -15,14 +15,15 @@ if it compiles and works. ## Modules -Maven reactor rooted at `diylc/pom.xml`, version `6.4.0`, three modules with a strict one-way -dependency chain: +Maven reactor rooted at `diylc/pom.xml`, version `6.4.0`, three application modules with a strict +one-way dependency chain, plus a build-time annotation processor: | Module | Contains | Depends on | | --- | --- | --- | | `diylc-core` | Domain model, `Presenter`, rendering pipeline, messaging, serialization, netlist engine, annotations | — | | `diylc-library` | Concrete component classes, transformers, datasheets, legacy file parsers, component-aware analyzers | `diylc-core` | | `diylc-swing` | Swing shell, plugins, dialogs, editors, `DIYLCStarter` | `diylc-core`, `diylc-library` | +| `diylc-processor` | `ServiceIndexProcessor`, run by `javac` over the three modules above; never on the runtime classpath | — | **`diylc-core` must never reference a concrete component type.** It knows components only through `IDIYComponent` and the annotations. If core logic seems to need knowledge of a specific component, @@ -30,8 +31,11 @@ that knowledge belongs in `diylc-library` (an analyzer, a transformer, a parser) through a new interface in core that the component implements — look at `org.diylc.core.gerber.IGerberComponentSimple` for the established shape of that escape hatch. -Component and plugin classes are discovered at runtime by Reflections scanning the `org.diylc` -package (`ComponentProcessor`). There is no registry to update when adding a component. +Components, netlist analyzers and parsers, legacy file parsers and auto-creators are discovered +through a class index built at compile time: every concrete class implementing a type marked +`@Indexed` (`IDIYComponent`, `INetlistAnalyzer`, ...) is listed under `META-INF/services/` by +`diylc-processor`, and `ReflectionUtils.getIndexedClasses()` reads that back at startup. Nothing +scans the classpath, and there is no registry to update when adding a component. ## Build and verify diff --git a/diylc/diylc-core/pom.xml b/diylc/diylc-core/pom.xml index 035e087e..442b5f99 100644 --- a/diylc/diylc-core/pom.xml +++ b/diylc/diylc-core/pom.xml @@ -19,6 +19,12 @@ + + + org.diylc + diylc-processor + + log4j @@ -52,11 +58,6 @@ jettison 1.5.4 - - org.reflections - reflections - 0.10.2 - com.fasterxml.jackson.core jackson-databind diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/INetlistAnalyzer.java b/diylc/diylc-core/src/main/java/org/diylc/common/INetlistAnalyzer.java index 60c44afa..63b7f506 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/INetlistAnalyzer.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/INetlistAnalyzer.java @@ -24,11 +24,13 @@ package org.diylc.common; import java.util.List; import java.util.Set; +import org.diylc.core.annotations.Indexed; import org.diylc.netlist.Netlist; import org.diylc.netlist.Node; import org.diylc.netlist.Summary; import org.diylc.netlist.TreeException; +@Indexed public interface INetlistAnalyzer { /** diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java b/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java index 5804bce0..50a7a1ea 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/IDIYComponent.java @@ -29,6 +29,7 @@ import java.util.UUID; import org.diylc.core.annotations.ComponentDescriptor; import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.Indexed; /** * Interface for component instance. Implementation classes of this interface will be instantiated @@ -47,6 +48,7 @@ import org.diylc.core.annotations.EditableProperty; * * @param type of component values, e.g. Resistance for resistors or String for transistors. */ +@Indexed public interface IDIYComponent extends Serializable, Cloneable { public static final int CHASSIS = 1; diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/annotations/IAutoCreator.java b/diylc/diylc-core/src/main/java/org/diylc/core/annotations/IAutoCreator.java index 49b2a4bf..d206309d 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/annotations/IAutoCreator.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/annotations/IAutoCreator.java @@ -30,6 +30,7 @@ import org.diylc.core.IDIYComponent; * * @author Branislav Stojkovic */ +@Indexed public interface IAutoCreator { /** diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/annotations/Indexed.java b/diylc/diylc-core/src/main/java/org/diylc/core/annotations/Indexed.java new file mode 100644 index 00000000..57762cc0 --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/core/annotations/Indexed.java @@ -0,0 +1,44 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc.core.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.diylc.utils.ReflectionUtils; + +/** + * Marks a type whose concrete implementations are discovered at runtime. The + * {@code diylc-processor} annotation processor lists them under {@code META-INF/services} when + * the implementing module is compiled, and {@link ReflectionUtils#getIndexedClasses(Class)} reads + * that list back, so nothing has to scan the classpath. An implementation is picked up just by + * implementing the type; there is no registry to update. + * + * @author Branislav Stojkovic + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface Indexed { + +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/netlist/IGuitarDiagramAnalyzer.java b/diylc/diylc-core/src/main/java/org/diylc/netlist/IGuitarDiagramAnalyzer.java index 6d1f8971..56287018 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/netlist/IGuitarDiagramAnalyzer.java +++ b/diylc/diylc-core/src/main/java/org/diylc/netlist/IGuitarDiagramAnalyzer.java @@ -2,6 +2,9 @@ package org.diylc.netlist; import java.util.List; +import org.diylc.core.annotations.Indexed; + +@Indexed public interface IGuitarDiagramAnalyzer { List collectNotes(Netlist netlist); diff --git a/diylc/diylc-core/src/main/java/org/diylc/netlist/INetlistParser.java b/diylc/diylc-core/src/main/java/org/diylc/netlist/INetlistParser.java index 83b1dfba..6e3d4307 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/netlist/INetlistParser.java +++ b/diylc/diylc-core/src/main/java/org/diylc/netlist/INetlistParser.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import org.diylc.core.IDIYComponent; +import org.diylc.core.annotations.Indexed; /* * @@ -41,6 +42,7 @@ import org.diylc.core.IDIYComponent; * . * */ +@Indexed public interface INetlistParser { String getName(); diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentIcon.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentIcon.java new file mode 100644 index 00000000..fac83c76 --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentIcon.java @@ -0,0 +1,90 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc.presenter; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; + +import javax.swing.Icon; +import org.apache.log4j.Logger; + +import org.diylc.core.IDIYComponent; + +/** + * Toolbox icon of a component type. Drawing it means instantiating the component, so it is + * deferred until the icon is first painted rather than done for every type at startup. + * + * @author Branislav Stojkovic + */ +class ComponentIcon implements Icon { + + private static final Logger LOG = Logger.getLogger(ComponentIcon.class); + + private final Class> componentClass; + private BufferedImage image; + + ComponentIcon(Class> componentClass) { + this.componentClass = componentClass; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + if (image == null) { + image = drawImage(); + } + g.drawImage(image, x, y, null); + } + + @Override + public int getIconWidth() { + return Presenter.ICON_SIZE; + } + + @Override + public int getIconHeight() { + return Presenter.ICON_SIZE; + } + + private BufferedImage drawImage() { + BufferedImage image = + new BufferedImage(Presenter.ICON_SIZE, Presenter.ICON_SIZE, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + try { + IDIYComponent componentInstance = componentClass.getDeclaredConstructor().newInstance(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON); + componentInstance.drawIcon(g2d, Presenter.ICON_SIZE, Presenter.ICON_SIZE); + } catch (Exception e) { + LOG.error("Error drawing component icon for " + componentClass.getName(), e); + } finally { + g2d.dispose(); + } + return image; + } +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java index 663d16bb..921c196f 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentProcessor.java @@ -21,19 +21,13 @@ */ package org.diylc.presenter; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.*; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import javax.swing.Icon; -import javax.swing.ImageIcon; import org.apache.log4j.Logger; import org.diylc.common.ComponentType; @@ -53,11 +47,7 @@ import org.diylc.core.annotations.EditableProperty; import org.diylc.core.annotations.KeywordPolicy; import org.diylc.lang.LangUtil; -import org.reflections.Reflections; -import org.reflections.util.ConfigurationBuilder; -import org.reflections.util.FilterBuilder; - -import static org.reflections.scanners.Scanners.TypesAnnotated; +import org.diylc.utils.ReflectionUtils; /** * Utility class with component processing methods. @@ -138,26 +128,7 @@ public class ComponentProcessor { } else { // default return null; } - icon = null; - // Draw component icon. - try { - IDIYComponent componentInstance = (IDIYComponent) clazz.getDeclaredConstructor().newInstance(); - Image image = - new BufferedImage(Presenter.ICON_SIZE, Presenter.ICON_SIZE, java.awt.image.BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = (Graphics2D) image.getGraphics(); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BICUBIC); - g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, - RenderingHints.VALUE_FRACTIONALMETRICS_ON); - // g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - // RenderingHints.VALUE_STROKE_PURE); - componentInstance.drawIcon(g2d, Presenter.ICON_SIZE, Presenter.ICON_SIZE); - icon = new ImageIcon(image); - } catch (Exception e) { - LOG.error("Error drawing component icon", e); - } + icon = new ComponentIcon(clazz); ComponentType componentType = new ComponentType(name, description, creationMethod, category, namePrefix, author, icon, clazz, zOrder, flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet, @@ -254,64 +225,57 @@ public class ComponentProcessor { if (componentTypes == null) { LOG.info("Loading component types."); componentTypes = new HashMap>(); - Reflections reflections = new Reflections( - new ConfigurationBuilder() - .forPackage("org.diylc") - .filterInputsBy(new FilterBuilder().includePackage("org.diylc.components")) - .setScanners(TypesAnnotated)); - Set> componentTypeClasses = null; try { - componentTypeClasses = reflections.getTypesAnnotatedWith(ComponentDescriptor.class, false); List> componentSnapshots = LOG_COMPONENT_TYPE_DETAILS ? new ArrayList<>() : null; - for (Class clazz : componentTypeClasses) { - if (!Modifier.isAbstract(clazz.getModifiers()) && IDIYComponent.class.isAssignableFrom(clazz)) { - ComponentType componentType = - ComponentProcessor.getInstance().extractComponentTypeFrom((Class>) clazz); - if (componentType == null) - continue; + for (Class clazz : ReflectionUtils.getIndexedClasses(IDIYComponent.class)) { + ComponentType componentType = + ComponentProcessor.getInstance().extractComponentTypeFrom((Class>) clazz); + if (componentType == null) { + continue; + } - // just to store in the cache - ComponentProcessor.getInstance().extractProperties(clazz); + // just to store in the cache + ComponentProcessor.getInstance().extractProperties(clazz); - // types created only programmatically (e.g. CompositeComponent) don't belong in the - // palette, but still need to go through extractProperties() above so the cache is warm - if (clazz.getAnnotation(ComponentDescriptor.class).hiddenInPalette()) - continue; + // types created only programmatically (e.g. CompositeComponent) don't belong in the + // palette, but still need to go through extractProperties() above so the cache is warm + if (clazz.getAnnotation(ComponentDescriptor.class).hiddenInPalette()) { + continue; + } - List nestedList; - if (componentTypes.containsKey(componentType.getCategory())) { - nestedList = componentTypes.get(componentType.getCategory()); - } else { - nestedList = new ArrayList(); - componentTypes.put(componentType.getCategory(), nestedList); - } - nestedList.add(componentType); + List nestedList; + if (componentTypes.containsKey(componentType.getCategory())) { + nestedList = componentTypes.get(componentType.getCategory()); + } else { + nestedList = new ArrayList(); + componentTypes.put(componentType.getCategory(), nestedList); + } + nestedList.add(componentType); - if (LOG_COMPONENT_TYPE_DETAILS) { - try { - Map snapshot = new LinkedHashMap<>(); - snapshot.put("name", componentType.getName()); - snapshot.put("description", componentType.getDescription()); - snapshot.put("creationMethod", componentType.getCreationMethod()); - snapshot.put("category", componentType.getCategory()); - List props = propertyCache.get(clazz.getName()); - snapshot.put("properties", props != null ? props.stream() - .map(p -> { - Map prop = new LinkedHashMap<>(); - prop.put("name", p.getName()); - prop.put("type", formatPropertyType(p.getType())); - List possibleValues = getPossibleValues(p.getType()); - if (possibleValues != null) { - prop.put("possibleValues", possibleValues); - } - return prop; - }) - .collect(Collectors.toList()) : Collections.emptyList()); - componentSnapshots.add(snapshot); - } catch (Exception e) { - LOG.warn("Failed to log component type details for " + clazz.getName(), e); - } + if (LOG_COMPONENT_TYPE_DETAILS) { + try { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("name", componentType.getName()); + snapshot.put("description", componentType.getDescription()); + snapshot.put("creationMethod", componentType.getCreationMethod()); + snapshot.put("category", componentType.getCategory()); + List props = propertyCache.get(clazz.getName()); + snapshot.put("properties", props != null ? props.stream() + .map(p -> { + Map prop = new LinkedHashMap<>(); + prop.put("name", p.getName()); + prop.put("type", formatPropertyType(p.getType())); + List possibleValues = getPossibleValues(p.getType()); + if (possibleValues != null) { + prop.put("possibleValues", possibleValues); + } + return prop; + }) + .collect(Collectors.toList()) : Collections.emptyList()); + componentSnapshots.add(snapshot); + } catch (Exception e) { + LOG.warn("Failed to log component type details for " + clazz.getName(), e); } } } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java index ef618dc9..0f043a81 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java @@ -31,7 +31,6 @@ import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.lang.reflect.Modifier; import java.util.*; import java.util.function.Function; import java.util.function.Supplier; @@ -3133,16 +3132,8 @@ public class Presenter implements IPlugInPort { @Override public List getNetlistAnalyzers() { - Set> classes; try { - classes = Utils.getClasses("org.diylc.netlist"); - List result = new ArrayList(); - - for (Class clazz : classes) { - if (!Modifier.isAbstract(clazz.getModifiers()) && INetlistAnalyzer.class.isAssignableFrom(clazz)) { - result.add((INetlistAnalyzer) clazz.getDeclaredConstructor().newInstance()); - } - } + List result = ReflectionUtils.getIndexedInstances(INetlistAnalyzer.class); result.sort(new Comparator() { diff --git a/diylc/diylc-core/src/main/java/org/diylc/serialization/IOldFileParser.java b/diylc/diylc-core/src/main/java/org/diylc/serialization/IOldFileParser.java index 1a80a911..494e67ca 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/serialization/IOldFileParser.java +++ b/diylc/diylc-core/src/main/java/org/diylc/serialization/IOldFileParser.java @@ -26,7 +26,9 @@ import java.util.List; import org.w3c.dom.Element; import org.diylc.core.Project; +import org.diylc.core.annotations.Indexed; +@Indexed public interface IOldFileParser { boolean canParse(String version); diff --git a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java index 704a0b91..ac107343 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java +++ b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java @@ -30,7 +30,6 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -41,7 +40,6 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.log4j.Logger; -import org.diylc.appframework.miscutils.Utils; import org.diylc.appframework.simplemq.MessageDispatcher; import org.diylc.appframework.update.VersionNumber; import org.w3c.dom.Document; @@ -59,6 +57,7 @@ import org.diylc.core.IDIYComponent; import org.diylc.core.Project; import org.diylc.presenter.Presenter; import org.diylc.test.DIYTest; +import org.diylc.utils.ReflectionUtils; public class ProjectFileManager { @@ -144,17 +143,10 @@ public class ProjectFileManager { public List getParsers() { if (parsers == null) { - parsers = new ArrayList(); - Set> componentTypeClasses = null; try { - componentTypeClasses = Utils.getClasses("org.diylc.serialization"); - for (Class clazz : componentTypeClasses) { - if (!Modifier.isAbstract(clazz.getModifiers()) && IOldFileParser.class.isAssignableFrom(clazz)) { - IOldFileParser instance = (IOldFileParser) clazz.getDeclaredConstructor().newInstance(); - parsers.add(instance); - } - } + parsers = ReflectionUtils.getIndexedInstances(IOldFileParser.class); } catch (Exception e) { + parsers = new ArrayList(); LOG.error("Could not find old version parsers", e); } } diff --git a/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java b/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java index 1dfac784..3e4797f1 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java +++ b/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java @@ -21,23 +21,30 @@ */ package org.diylc.utils; -import java.lang.reflect.Modifier; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Enumeration; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; -import org.diylc.appframework.miscutils.Utils; import org.diylc.core.annotations.IAutoCreator; +import org.diylc.core.annotations.Indexed; import org.diylc.netlist.IGuitarDiagramAnalyzer; import org.diylc.netlist.INetlistParser; -import org.diylc.presenter.Presenter; public class ReflectionUtils { - private static final Logger LOG = Logger.getLogger(Presenter.class); + private static final Logger LOG = Logger.getLogger(ReflectionUtils.class); + + private static final String SERVICES_DIR = "META-INF/services/"; /** * {@link List} of {@link IAutoCreator} objects that are capable of creating more components @@ -45,19 +52,66 @@ public class ReflectionUtils { */ private static List autoCreators; - public static List getAutoCreators() { - if (autoCreators == null) { - autoCreators = new ArrayList(); - Set> classes = null; - try { - classes = Utils.getClasses("org.diylc.components.autocreate"); - for (Class clazz : classes) { - if (IAutoCreator.class.isAssignableFrom(clazz)) { - autoCreators.add((IAutoCreator) clazz.getDeclaredConstructor().newInstance()); - LOG.debug("Loaded auto-creator: " + clazz.getName()); + /** + * Returns the concrete implementations of an {@link Indexed} type, as listed at build time by + * {@code diylc-processor}. Classes are loaded but not initialized. + */ + public static List> getIndexedClasses(Class type) { + ClassLoader classLoader = ReflectionUtils.class.getClassLoader(); + // each module jar carries its own index file for the same type + Set classNames = new LinkedHashSet(); + try { + Enumeration indexFiles = classLoader.getResources(SERVICES_DIR + type.getName()); + while (indexFiles.hasMoreElements()) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(indexFiles.nextElement().openStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + classNames.add(line); + } } } + } + } catch (IOException e) { + LOG.error("Could not read the class index of " + type.getName(), e); + } + + List> result = new ArrayList>(classNames.size()); + for (String className : classNames) { + try { + Class clazz = Class.forName(className, false, classLoader); + if (type.isAssignableFrom(clazz)) { + result.add(clazz.asSubclass(type)); + } else { + LOG.warn("Class index of " + type.getName() + " lists unrelated class " + className); + } + } catch (ClassNotFoundException e) { + LOG.warn("Class index of " + type.getName() + " lists missing class " + className); + } + } + return result; + } + + /** + * Instantiates every class returned by {@link #getIndexedClasses(Class)} through its no-arg + * constructor. + */ + public static List getIndexedInstances(Class type) throws ReflectiveOperationException { + List result = new ArrayList(); + for (Class clazz : getIndexedClasses(type)) { + result.add(clazz.getDeclaredConstructor().newInstance()); + } + return result; + } + + public static List getAutoCreators() { + if (autoCreators == null) { + try { + autoCreators = getIndexedInstances(IAutoCreator.class); } catch (Exception e) { + autoCreators = new ArrayList(); LOG.error("Error loading auto-creator types", e); } } @@ -65,16 +119,8 @@ public class ReflectionUtils { } public static List getNetlistParserDefinitions() { - Set> classes; try { - classes = Utils.getClasses("org.diylc.netlist"); - List result = new ArrayList(); - - for (Class clazz : classes) { - if (!Modifier.isAbstract(clazz.getModifiers()) && INetlistParser.class.isAssignableFrom(clazz)) { - result.add((INetlistParser) clazz.getDeclaredConstructor().newInstance()); - } - } + List result = getIndexedInstances(INetlistParser.class); Collections.sort(result, new Comparator() { @@ -92,16 +138,7 @@ public class ReflectionUtils { } public static IGuitarDiagramAnalyzer getGuitarDiagramAnalyzer() throws Exception { - Set> classes; - classes = Utils.getClasses("org.diylc.netlist"); - List result = new ArrayList<>(); - - for (Class clazz : classes) { - if (!Modifier.isAbstract(clazz.getModifiers()) && IGuitarDiagramAnalyzer.class.isAssignableFrom(clazz)) { - result.add((IGuitarDiagramAnalyzer) clazz.getDeclaredConstructor().newInstance()); - } - } - + List result = getIndexedInstances(IGuitarDiagramAnalyzer.class); if (result.isEmpty()) { return null; diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index 35253af5..a8f77d4e 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -611,7 +611,11 @@ IMPROVEMENT - Improve performance of 'Project Explorer' pane + Improved performance of application initialization + + + IMPROVEMENT + Improved performance of 'Project Explorer' pane BUG_FIX diff --git a/diylc/diylc-library/pom.xml b/diylc/diylc-library/pom.xml index 0e513cb1..8c802441 100644 --- a/diylc/diylc-library/pom.xml +++ b/diylc/diylc-library/pom.xml @@ -20,6 +20,10 @@ + + org.diylc + diylc-processor + org.diylc diylc-core diff --git a/diylc/diylc-processor/pom.xml b/diylc/diylc-processor/pom.xml new file mode 100644 index 00000000..ee1af64a --- /dev/null +++ b/diylc/diylc-processor/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + org.diylc + diylc-parent + 6.8.0 + + + diylc-processor + DIYLC Annotation Processor + Build-time indexer that lists the implementations of @Indexed types under META-INF/services + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + none + + + + + diff --git a/diylc/diylc-processor/src/main/java/org/diylc/processor/ServiceIndexProcessor.java b/diylc/diylc-processor/src/main/java/org/diylc/processor/ServiceIndexProcessor.java new file mode 100644 index 00000000..0c97effe --- /dev/null +++ b/diylc/diylc-processor/src/main/java/org/diylc/processor/ServiceIndexProcessor.java @@ -0,0 +1,192 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc.processor; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +/** + * Lists every concrete class of the module being compiled under + * {@code META-INF/services/} for each of its supertypes that carries + * {@code org.diylc.core.annotations.Indexed}. The application reads these files at startup instead + * of scanning the classpath. + * + * @author Branislav Stojkovic + */ +@SupportedAnnotationTypes("*") +public class ServiceIndexProcessor extends AbstractProcessor { + + private static final String INDEXED_ANNOTATION = "org.diylc.core.annotations.Indexed"; + private static final String SERVICES_DIR = "META-INF/services/"; + + private final Map> index = new TreeMap>(); + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (roundEnv.processingOver()) { + writeIndex(); + } else { + for (Element element : roundEnv.getRootElements()) { + collect(element); + } + } + return false; + } + + private void collect(Element element) { + if (!(element instanceof TypeElement)) { + return; + } + TypeElement type = (TypeElement) element; + if (isInstantiable(type)) { + String className = processingEnv.getElementUtils().getBinaryName(type).toString(); + for (String service : findIndexedSupertypes(type)) { + index.computeIfAbsent(service, k -> new TreeSet()).add(className); + } + } + for (Element enclosed : type.getEnclosedElements()) { + if (enclosed.getModifiers().contains(Modifier.STATIC)) { + collect(enclosed); + } + } + } + + private boolean isInstantiable(TypeElement type) { + return type.getKind() == ElementKind.CLASS && !type.getModifiers().contains(Modifier.ABSTRACT); + } + + private Set findIndexedSupertypes(TypeElement type) { + Set result = new HashSet(); + Set visited = new HashSet(); + Deque pending = new ArrayDeque(); + pending.add(type.asType()); + while (!pending.isEmpty()) { + for (TypeMirror supertype : processingEnv.getTypeUtils().directSupertypes(pending.poll())) { + if (supertype.getKind() != TypeKind.DECLARED) { + continue; + } + TypeElement supertypeElement = (TypeElement) ((DeclaredType) supertype).asElement(); + String name = processingEnv.getElementUtils().getBinaryName(supertypeElement).toString(); + if (!visited.add(name)) { + continue; + } + if (isIndexed(supertypeElement)) { + result.add(name); + } + pending.add(supertype); + } + } + return result; + } + + private boolean isIndexed(TypeElement type) { + for (AnnotationMirror annotation : type.getAnnotationMirrors()) { + TypeElement annotationType = (TypeElement) annotation.getAnnotationType().asElement(); + if (annotationType.getQualifiedName().contentEquals(INDEXED_ANNOTATION)) { + return true; + } + } + return false; + } + + private void writeIndex() { + for (Map.Entry> entry : index.entrySet()) { + String path = SERVICES_DIR + entry.getKey(); + Set classNames = new TreeSet(entry.getValue()); + // an incremental build (e.g. in the IDE) compiles only the changed classes, so carry over + // the entries from the previous build that still qualify + classNames.addAll(readPreviousEntries(path, entry.getKey())); + try { + FileObject file = + processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", path); + try (Writer writer = + new OutputStreamWriter(file.openOutputStream(), StandardCharsets.UTF_8)) { + for (String className : classNames) { + writer.write(className); + writer.write('\n'); + } + } + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + "Could not write " + path + ": " + e.getMessage()); + } + } + } + + private Set readPreviousEntries(String path, String service) { + Set result = new TreeSet(); + try { + FileObject file = + processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", path); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(file.openInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && stillQualifies(line, service)) { + result.add(line); + } + } + } + } catch (IOException e) { + // no previous build output + } + return result; + } + + private boolean stillQualifies(String className, String service) { + TypeElement type = processingEnv.getElementUtils().getTypeElement(className.replace('$', '.')); + return type != null && isInstantiable(type) && findIndexedSupertypes(type).contains(service); + } +} diff --git a/diylc/diylc-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/diylc/diylc-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000..b55093a8 --- /dev/null +++ b/diylc/diylc-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.diylc.processor.ServiceIndexProcessor diff --git a/diylc/diylc-swing/pom.xml b/diylc/diylc-swing/pom.xml index 1de2c3e3..4d41233b 100644 --- a/diylc/diylc-swing/pom.xml +++ b/diylc/diylc-swing/pom.xml @@ -20,6 +20,10 @@ + + org.diylc + diylc-processor + org.diylc diylc-core diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java index bccc4934..07db6810 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/toolbox/ComponentTabbedPane.java @@ -90,6 +90,8 @@ class ComponentTabbedPane extends JTabbedPane { private List blocks; private final Map categoryPanels = new HashMap(); + private final Map> unpopulatedPanels = + new HashMap>(); private boolean refreshing = false; public ComponentTabbedPane(IPlugInPort plugInPort, ISwingUI swingUI) { @@ -111,6 +113,7 @@ class ComponentTabbedPane extends JTabbedPane { if (lastSelectedTab >= 0 && lastSelectedTab < getTabCount()) { setSelectedIndex(lastSelectedTab); } + populateTab(getSelectedComponent()); addChangeListener(new ChangeListener() { @@ -119,6 +122,7 @@ class ComponentTabbedPane extends JTabbedPane { if (refreshing) { return; } + populateTab(getSelectedComponent()); ConfigurationManager.getInstance().writeValue(LAST_SELECTED_TAB, getSelectedIndex()); ComponentTabbedPane.this.plugInPort.setNewComponentTypeSlot(null, null, null, false); // Refresh recent components if needed @@ -157,6 +161,11 @@ class ComponentTabbedPane extends JTabbedPane { } addTab(category, panel); } + // the tabbed pane takes its height from the tallest tab, so keep one category populated + // even while a sparse tab like Favorites is selected + if (getTabCount() > 3) { + populateTab(getComponentAt(3)); + } if (selectedTitle != null) { for (int i = 0; i < getTabCount(); i++) { @@ -175,6 +184,7 @@ class ComponentTabbedPane extends JTabbedPane { } else if (getTabCount() > 0) { setSelectedIndex(0); } + populateTab(getSelectedComponent()); } @SuppressWarnings("unchecked") @@ -238,11 +248,21 @@ class ComponentTabbedPane extends JTabbedPane { JPanel panel = new JPanel(new BorderLayout()); panel.setOpaque(false); - panel.add(createComponentPanel(componentTypes), BorderLayout.CENTER); + // buttons are added by populateTab() when the tab is first shown, as creating a button draws + // its component icon + unpopulatedPanels.put(panel, componentTypes); return panel; } + private void populateTab(Component panel) { + List componentTypes = unpopulatedPanels.remove(panel); + if (componentTypes != null) { + ((JPanel) panel).add(createComponentPanel(componentTypes), BorderLayout.CENTER); + panel.revalidate(); + } + } + public Container getRecentToolbar() { if (recentToolbar == null) { recentToolbar = new Container(); diff --git a/diylc/pom.xml b/diylc/pom.xml index 3c52592a..37fd8dbd 100644 --- a/diylc/pom.xml +++ b/diylc/pom.xml @@ -13,6 +13,7 @@ A free, open-source, cross-platform tool for drawing circuit layouts + diylc-processor diylc-core diylc-library diylc-swing @@ -31,6 +32,13 @@ + + org.diylc + diylc-processor + ${project.version} + provided + true + org.diylc diylc-core @@ -94,7 +102,20 @@ ${maven.compiler.source} ${maven.compiler.target} ${project.build.sourceEncoding} + + + org.diylc.processor.ServiceIndexProcessor + + + + + default-testCompile + + none + + + org.apache.maven.plugins