Reflection (#1085)

* index instead of reflection

* Update update.xml
This commit is contained in:
Bane Stojkovic
2026-10-13 21:17:30 +02:00
committed by GitHub
parent 2397ea2021
commit 42d3d557c6
24 changed files with 560 additions and 149 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ that file before editing anything under `diylc/`.
| Path | Contents | | 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-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. | | `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. | | `snap/` | Snap packaging metadata. |
+2 -1
View File
@@ -6,7 +6,8 @@ description: Add a new component type to the DIYLC library, or substantially mod
# Adding a component to DIYLC # Adding a component to DIYLC
Components live in `diylc-library/src/main/java/org/diylc/components/`. They are discovered at 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 component by carrying `@ComponentDescriptor` and implementing `IDIYComponent` (in practice, by
extending one of the abstract bases). extending one of the abstract bases).
+8 -4
View File
@@ -15,14 +15,15 @@ if it compiles and works.
## Modules ## Modules
Maven reactor rooted at `diylc/pom.xml`, version `6.4.0`, three modules with a strict one-way Maven reactor rooted at `diylc/pom.xml`, version `6.4.0`, three application modules with a strict
dependency chain: one-way dependency chain, plus a build-time annotation processor:
| Module | Contains | Depends on | | Module | Contains | Depends on |
| --- | --- | --- | | --- | --- | --- |
| `diylc-core` | Domain model, `Presenter`, rendering pipeline, messaging, serialization, netlist engine, annotations | — | | `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-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-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 **`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, `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 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. `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` Components, netlist analyzers and parsers, legacy file parsers and auto-creators are discovered
package (`ComponentProcessor`). There is no registry to update when adding a component. 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 ## Build and verify
+6 -5
View File
@@ -19,6 +19,12 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- Internal module dependencies -->
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-processor</artifactId>
</dependency>
<!-- Common dependencies from Maven Central --> <!-- Common dependencies from Maven Central -->
<dependency> <dependency>
<groupId>log4j</groupId> <groupId>log4j</groupId>
@@ -52,11 +58,6 @@
<artifactId>jettison</artifactId> <artifactId>jettison</artifactId>
<version>1.5.4</version> <version>1.5.4</version>
</dependency> </dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
@@ -24,11 +24,13 @@ package org.diylc.common;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.diylc.core.annotations.Indexed;
import org.diylc.netlist.Netlist; import org.diylc.netlist.Netlist;
import org.diylc.netlist.Node; import org.diylc.netlist.Node;
import org.diylc.netlist.Summary; import org.diylc.netlist.Summary;
import org.diylc.netlist.TreeException; import org.diylc.netlist.TreeException;
@Indexed
public interface INetlistAnalyzer { public interface INetlistAnalyzer {
/** /**
@@ -29,6 +29,7 @@ import java.util.UUID;
import org.diylc.core.annotations.ComponentDescriptor; import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty; import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.Indexed;
/** /**
* Interface for component instance. Implementation classes of this interface will be instantiated * Interface for component instance. Implementation classes of this interface will be instantiated
@@ -47,6 +48,7 @@ import org.diylc.core.annotations.EditableProperty;
* *
* @param <T> type of component values, e.g. Resistance for resistors or String for transistors. * @param <T> type of component values, e.g. Resistance for resistors or String for transistors.
*/ */
@Indexed
public interface IDIYComponent<T> extends Serializable, Cloneable { public interface IDIYComponent<T> extends Serializable, Cloneable {
public static final int CHASSIS = 1; public static final int CHASSIS = 1;
@@ -30,6 +30,7 @@ import org.diylc.core.IDIYComponent;
* *
* @author Branislav Stojkovic * @author Branislav Stojkovic
*/ */
@Indexed
public interface IAutoCreator { public interface IAutoCreator {
/** /**
@@ -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 <http://www.gnu.org/licenses/>.
*/
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 {
}
@@ -2,6 +2,9 @@ package org.diylc.netlist;
import java.util.List; import java.util.List;
import org.diylc.core.annotations.Indexed;
@Indexed
public interface IGuitarDiagramAnalyzer { public interface IGuitarDiagramAnalyzer {
List<String> collectNotes(Netlist netlist); List<String> collectNotes(Netlist netlist);
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.annotations.Indexed;
/* /*
* *
@@ -41,6 +42,7 @@ import org.diylc.core.IDIYComponent;
* <http://www.gnu.org/licenses/>. * <http://www.gnu.org/licenses/>.
* *
*/ */
@Indexed
public interface INetlistParser { public interface INetlistParser {
String getName(); String getName();
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<? extends IDIYComponent<?>> componentClass;
private BufferedImage image;
ComponentIcon(Class<? extends IDIYComponent<?>> 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;
}
}
@@ -21,19 +21,13 @@
*/ */
package org.diylc.presenter; 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.Method;
import java.lang.reflect.Modifier;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.diylc.common.ComponentType; import org.diylc.common.ComponentType;
@@ -53,11 +47,7 @@ import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy; import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.lang.LangUtil; import org.diylc.lang.LangUtil;
import org.reflections.Reflections; import org.diylc.utils.ReflectionUtils;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import static org.reflections.scanners.Scanners.TypesAnnotated;
/** /**
* Utility class with component processing methods. * Utility class with component processing methods.
@@ -138,26 +128,7 @@ public class ComponentProcessor {
} else { // default } else { // default
return null; return null;
} }
icon = null; icon = new ComponentIcon(clazz);
// 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);
}
ComponentType componentType = ComponentType componentType =
new ComponentType(name, description, creationMethod, category, namePrefix, author, icon, clazz, zOrder, new ComponentType(name, description, creationMethod, category, namePrefix, author, icon, clazz, zOrder,
flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet, flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet,
@@ -254,64 +225,57 @@ public class ComponentProcessor {
if (componentTypes == null) { if (componentTypes == null) {
LOG.info("Loading component types."); LOG.info("Loading component types.");
componentTypes = new HashMap<String, List<ComponentType>>(); componentTypes = new HashMap<String, List<ComponentType>>();
Reflections reflections = new Reflections(
new ConfigurationBuilder()
.forPackage("org.diylc")
.filterInputsBy(new FilterBuilder().includePackage("org.diylc.components"))
.setScanners(TypesAnnotated));
Set<Class<?>> componentTypeClasses = null;
try { try {
componentTypeClasses = reflections.getTypesAnnotatedWith(ComponentDescriptor.class, false);
List<Map<String, Object>> componentSnapshots = LOG_COMPONENT_TYPE_DETAILS ? new ArrayList<>() : null; List<Map<String, Object>> componentSnapshots = LOG_COMPONENT_TYPE_DETAILS ? new ArrayList<>() : null;
for (Class<?> clazz : componentTypeClasses) { for (Class<?> clazz : ReflectionUtils.getIndexedClasses(IDIYComponent.class)) {
if (!Modifier.isAbstract(clazz.getModifiers()) && IDIYComponent.class.isAssignableFrom(clazz)) { ComponentType componentType =
ComponentType componentType = ComponentProcessor.getInstance().extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) clazz);
ComponentProcessor.getInstance().extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) clazz); if (componentType == null) {
if (componentType == null) continue;
continue; }
// just to store in the cache // just to store in the cache
ComponentProcessor.getInstance().extractProperties(clazz); ComponentProcessor.getInstance().extractProperties(clazz);
// types created only programmatically (e.g. CompositeComponent) don't belong in the // 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 // palette, but still need to go through extractProperties() above so the cache is warm
if (clazz.getAnnotation(ComponentDescriptor.class).hiddenInPalette()) if (clazz.getAnnotation(ComponentDescriptor.class).hiddenInPalette()) {
continue; continue;
}
List<ComponentType> nestedList; List<ComponentType> nestedList;
if (componentTypes.containsKey(componentType.getCategory())) { if (componentTypes.containsKey(componentType.getCategory())) {
nestedList = componentTypes.get(componentType.getCategory()); nestedList = componentTypes.get(componentType.getCategory());
} else { } else {
nestedList = new ArrayList<ComponentType>(); nestedList = new ArrayList<ComponentType>();
componentTypes.put(componentType.getCategory(), nestedList); componentTypes.put(componentType.getCategory(), nestedList);
} }
nestedList.add(componentType); nestedList.add(componentType);
if (LOG_COMPONENT_TYPE_DETAILS) { if (LOG_COMPONENT_TYPE_DETAILS) {
try { try {
Map<String, Object> snapshot = new LinkedHashMap<>(); Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("name", componentType.getName()); snapshot.put("name", componentType.getName());
snapshot.put("description", componentType.getDescription()); snapshot.put("description", componentType.getDescription());
snapshot.put("creationMethod", componentType.getCreationMethod()); snapshot.put("creationMethod", componentType.getCreationMethod());
snapshot.put("category", componentType.getCategory()); snapshot.put("category", componentType.getCategory());
List<PropertyWrapper> props = propertyCache.get(clazz.getName()); List<PropertyWrapper> props = propertyCache.get(clazz.getName());
snapshot.put("properties", props != null ? props.stream() snapshot.put("properties", props != null ? props.stream()
.map(p -> { .map(p -> {
Map<String, Object> prop = new LinkedHashMap<>(); Map<String, Object> prop = new LinkedHashMap<>();
prop.put("name", p.getName()); prop.put("name", p.getName());
prop.put("type", formatPropertyType(p.getType())); prop.put("type", formatPropertyType(p.getType()));
List<String> possibleValues = getPossibleValues(p.getType()); List<String> possibleValues = getPossibleValues(p.getType());
if (possibleValues != null) { if (possibleValues != null) {
prop.put("possibleValues", possibleValues); prop.put("possibleValues", possibleValues);
} }
return prop; return prop;
}) })
.collect(Collectors.toList()) : Collections.emptyList()); .collect(Collectors.toList()) : Collections.emptyList());
componentSnapshots.add(snapshot); componentSnapshots.add(snapshot);
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Failed to log component type details for " + clazz.getName(), e); LOG.warn("Failed to log component type details for " + clazz.getName(), e);
}
} }
} }
} }
@@ -31,7 +31,6 @@ import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -3133,16 +3132,8 @@ public class Presenter implements IPlugInPort {
@Override @Override
public List<INetlistAnalyzer> getNetlistAnalyzers() { public List<INetlistAnalyzer> getNetlistAnalyzers() {
Set<Class<?>> classes;
try { try {
classes = Utils.getClasses("org.diylc.netlist"); List<INetlistAnalyzer> result = ReflectionUtils.getIndexedInstances(INetlistAnalyzer.class);
List<INetlistAnalyzer> result = new ArrayList<INetlistAnalyzer>();
for (Class<?> clazz : classes) {
if (!Modifier.isAbstract(clazz.getModifiers()) && INetlistAnalyzer.class.isAssignableFrom(clazz)) {
result.add((INetlistAnalyzer) clazz.getDeclaredConstructor().newInstance());
}
}
result.sort(new Comparator<INetlistAnalyzer>() { result.sort(new Comparator<INetlistAnalyzer>() {
@@ -26,7 +26,9 @@ import java.util.List;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.diylc.core.Project; import org.diylc.core.Project;
import org.diylc.core.annotations.Indexed;
@Indexed
public interface IOldFileParser { public interface IOldFileParser {
boolean canParse(String version); boolean canParse(String version);
@@ -30,7 +30,6 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Reader; import java.io.Reader;
import java.io.Writer; import java.io.Writer;
import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@@ -41,7 +40,6 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.Utils;
import org.diylc.appframework.simplemq.MessageDispatcher; import org.diylc.appframework.simplemq.MessageDispatcher;
import org.diylc.appframework.update.VersionNumber; import org.diylc.appframework.update.VersionNumber;
import org.w3c.dom.Document; import org.w3c.dom.Document;
@@ -59,6 +57,7 @@ import org.diylc.core.IDIYComponent;
import org.diylc.core.Project; import org.diylc.core.Project;
import org.diylc.presenter.Presenter; import org.diylc.presenter.Presenter;
import org.diylc.test.DIYTest; import org.diylc.test.DIYTest;
import org.diylc.utils.ReflectionUtils;
public class ProjectFileManager { public class ProjectFileManager {
@@ -144,17 +143,10 @@ public class ProjectFileManager {
public List<IOldFileParser> getParsers() { public List<IOldFileParser> getParsers() {
if (parsers == null) { if (parsers == null) {
parsers = new ArrayList<IOldFileParser>();
Set<Class<?>> componentTypeClasses = null;
try { try {
componentTypeClasses = Utils.getClasses("org.diylc.serialization"); parsers = ReflectionUtils.getIndexedInstances(IOldFileParser.class);
for (Class<?> clazz : componentTypeClasses) {
if (!Modifier.isAbstract(clazz.getModifiers()) && IOldFileParser.class.isAssignableFrom(clazz)) {
IOldFileParser instance = (IOldFileParser) clazz.getDeclaredConstructor().newInstance();
parsers.add(instance);
}
}
} catch (Exception e) { } catch (Exception e) {
parsers = new ArrayList<IOldFileParser>();
LOG.error("Could not find old version parsers", e); LOG.error("Could not find old version parsers", e);
} }
} }
@@ -21,23 +21,30 @@
*/ */
package org.diylc.utils; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.Utils;
import org.diylc.core.annotations.IAutoCreator; import org.diylc.core.annotations.IAutoCreator;
import org.diylc.core.annotations.Indexed;
import org.diylc.netlist.IGuitarDiagramAnalyzer; import org.diylc.netlist.IGuitarDiagramAnalyzer;
import org.diylc.netlist.INetlistParser; import org.diylc.netlist.INetlistParser;
import org.diylc.presenter.Presenter;
public class ReflectionUtils { 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 * {@link List} of {@link IAutoCreator} objects that are capable of creating more components
@@ -45,19 +52,66 @@ public class ReflectionUtils {
*/ */
private static List<IAutoCreator> autoCreators; private static List<IAutoCreator> autoCreators;
public static List<IAutoCreator> getAutoCreators() { /**
if (autoCreators == null) { * Returns the concrete implementations of an {@link Indexed} type, as listed at build time by
autoCreators = new ArrayList<IAutoCreator>(); * {@code diylc-processor}. Classes are loaded but not initialized.
Set<Class<?>> classes = null; */
try { public static <T> List<Class<? extends T>> getIndexedClasses(Class<T> type) {
classes = Utils.getClasses("org.diylc.components.autocreate"); ClassLoader classLoader = ReflectionUtils.class.getClassLoader();
for (Class<?> clazz : classes) { // each module jar carries its own index file for the same type
if (IAutoCreator.class.isAssignableFrom(clazz)) { Set<String> classNames = new LinkedHashSet<String>();
autoCreators.add((IAutoCreator) clazz.getDeclaredConstructor().newInstance()); try {
LOG.debug("Loaded auto-creator: " + clazz.getName()); Enumeration<URL> 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<Class<? extends T>> result = new ArrayList<Class<? extends T>>(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 <T> List<T> getIndexedInstances(Class<T> type) throws ReflectiveOperationException {
List<T> result = new ArrayList<T>();
for (Class<? extends T> clazz : getIndexedClasses(type)) {
result.add(clazz.getDeclaredConstructor().newInstance());
}
return result;
}
public static List<IAutoCreator> getAutoCreators() {
if (autoCreators == null) {
try {
autoCreators = getIndexedInstances(IAutoCreator.class);
} catch (Exception e) { } catch (Exception e) {
autoCreators = new ArrayList<IAutoCreator>();
LOG.error("Error loading auto-creator types", e); LOG.error("Error loading auto-creator types", e);
} }
} }
@@ -65,16 +119,8 @@ public class ReflectionUtils {
} }
public static List<INetlistParser> getNetlistParserDefinitions() { public static List<INetlistParser> getNetlistParserDefinitions() {
Set<Class<?>> classes;
try { try {
classes = Utils.getClasses("org.diylc.netlist"); List<INetlistParser> result = getIndexedInstances(INetlistParser.class);
List<INetlistParser> result = new ArrayList<INetlistParser>();
for (Class<?> clazz : classes) {
if (!Modifier.isAbstract(clazz.getModifiers()) && INetlistParser.class.isAssignableFrom(clazz)) {
result.add((INetlistParser) clazz.getDeclaredConstructor().newInstance());
}
}
Collections.sort(result, new Comparator<INetlistParser>() { Collections.sort(result, new Comparator<INetlistParser>() {
@@ -92,16 +138,7 @@ public class ReflectionUtils {
} }
public static IGuitarDiagramAnalyzer getGuitarDiagramAnalyzer() throws Exception { public static IGuitarDiagramAnalyzer getGuitarDiagramAnalyzer() throws Exception {
Set<Class<?>> classes; List<IGuitarDiagramAnalyzer> result = getIndexedInstances(IGuitarDiagramAnalyzer.class);
classes = Utils.getClasses("org.diylc.netlist");
List<IGuitarDiagramAnalyzer> result = new ArrayList<>();
for (Class<?> clazz : classes) {
if (!Modifier.isAbstract(clazz.getModifiers()) && IGuitarDiagramAnalyzer.class.isAssignableFrom(clazz)) {
result.add((IGuitarDiagramAnalyzer) clazz.getDeclaredConstructor().newInstance());
}
}
if (result.isEmpty()) { if (result.isEmpty()) {
return null; return null;
@@ -611,7 +611,11 @@
</org.diylc.appframework.update.Change> </org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change> <org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType> <changeType>IMPROVEMENT</changeType>
<description>Improve performance of 'Project Explorer' pane</description> <description>Improved performance of application initialization</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved performance of 'Project Explorer' pane</description>
</org.diylc.appframework.update.Change> </org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change> <org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType> <changeType>BUG_FIX</changeType>
+4
View File
@@ -20,6 +20,10 @@
<dependencies> <dependencies>
<!-- Internal module dependencies --> <!-- Internal module dependencies -->
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-processor</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.diylc</groupId> <groupId>org.diylc</groupId>
<artifactId>diylc-core</artifactId> <artifactId>diylc-core</artifactId>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.8.0</version>
</parent>
<artifactId>diylc-processor</artifactId>
<name>DIYLC Annotation Processor</name>
<description>Build-time indexer that lists the implementations of @Indexed types under META-INF/services</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- the processor cannot run while it is itself being compiled -->
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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 <http://www.gnu.org/licenses/>.
*/
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/<type>} 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<String, Set<String>> index = new TreeMap<String, Set<String>>();
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> 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<String>()).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<String> findIndexedSupertypes(TypeElement type) {
Set<String> result = new HashSet<String>();
Set<String> visited = new HashSet<String>();
Deque<TypeMirror> pending = new ArrayDeque<TypeMirror>();
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<String, Set<String>> entry : index.entrySet()) {
String path = SERVICES_DIR + entry.getKey();
Set<String> classNames = new TreeSet<String>(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<String> readPreviousEntries(String path, String service) {
Set<String> result = new TreeSet<String>();
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);
}
}
@@ -0,0 +1 @@
org.diylc.processor.ServiceIndexProcessor
+4
View File
@@ -20,6 +20,10 @@
<dependencies> <dependencies>
<!-- Internal module dependencies --> <!-- Internal module dependencies -->
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-processor</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.diylc</groupId> <groupId>org.diylc</groupId>
<artifactId>diylc-core</artifactId> <artifactId>diylc-core</artifactId>
@@ -90,6 +90,8 @@ class ComponentTabbedPane extends JTabbedPane {
private List<String> blocks; private List<String> blocks;
private final Map<String, Component> categoryPanels = new HashMap<String, Component>(); private final Map<String, Component> categoryPanels = new HashMap<String, Component>();
private final Map<Component, List<ComponentType>> unpopulatedPanels =
new HashMap<Component, List<ComponentType>>();
private boolean refreshing = false; private boolean refreshing = false;
public ComponentTabbedPane(IPlugInPort plugInPort, ISwingUI swingUI) { public ComponentTabbedPane(IPlugInPort plugInPort, ISwingUI swingUI) {
@@ -111,6 +113,7 @@ class ComponentTabbedPane extends JTabbedPane {
if (lastSelectedTab >= 0 && lastSelectedTab < getTabCount()) { if (lastSelectedTab >= 0 && lastSelectedTab < getTabCount()) {
setSelectedIndex(lastSelectedTab); setSelectedIndex(lastSelectedTab);
} }
populateTab(getSelectedComponent());
addChangeListener(new ChangeListener() { addChangeListener(new ChangeListener() {
@@ -119,6 +122,7 @@ class ComponentTabbedPane extends JTabbedPane {
if (refreshing) { if (refreshing) {
return; return;
} }
populateTab(getSelectedComponent());
ConfigurationManager.getInstance().writeValue(LAST_SELECTED_TAB, getSelectedIndex()); ConfigurationManager.getInstance().writeValue(LAST_SELECTED_TAB, getSelectedIndex());
ComponentTabbedPane.this.plugInPort.setNewComponentTypeSlot(null, null, null, false); ComponentTabbedPane.this.plugInPort.setNewComponentTypeSlot(null, null, null, false);
// Refresh recent components if needed // Refresh recent components if needed
@@ -157,6 +161,11 @@ class ComponentTabbedPane extends JTabbedPane {
} }
addTab(category, panel); 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) { if (selectedTitle != null) {
for (int i = 0; i < getTabCount(); i++) { for (int i = 0; i < getTabCount(); i++) {
@@ -175,6 +184,7 @@ class ComponentTabbedPane extends JTabbedPane {
} else if (getTabCount() > 0) { } else if (getTabCount() > 0) {
setSelectedIndex(0); setSelectedIndex(0);
} }
populateTab(getSelectedComponent());
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -238,11 +248,21 @@ class ComponentTabbedPane extends JTabbedPane {
JPanel panel = new JPanel(new BorderLayout()); JPanel panel = new JPanel(new BorderLayout());
panel.setOpaque(false); 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; return panel;
} }
private void populateTab(Component panel) {
List<ComponentType> componentTypes = unpopulatedPanels.remove(panel);
if (componentTypes != null) {
((JPanel) panel).add(createComponentPanel(componentTypes), BorderLayout.CENTER);
panel.revalidate();
}
}
public Container getRecentToolbar() { public Container getRecentToolbar() {
if (recentToolbar == null) { if (recentToolbar == null) {
recentToolbar = new Container(); recentToolbar = new Container();
+21
View File
@@ -13,6 +13,7 @@
<description>A free, open-source, cross-platform tool for drawing circuit layouts</description> <description>A free, open-source, cross-platform tool for drawing circuit layouts</description>
<modules> <modules>
<module>diylc-processor</module>
<module>diylc-core</module> <module>diylc-core</module>
<module>diylc-library</module> <module>diylc-library</module>
<module>diylc-swing</module> <module>diylc-swing</module>
@@ -31,6 +32,13 @@
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!-- Internal module dependencies --> <!-- Internal module dependencies -->
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-processor</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>org.diylc</groupId> <groupId>org.diylc</groupId>
<artifactId>diylc-core</artifactId> <artifactId>diylc-core</artifactId>
@@ -94,7 +102,20 @@
<source>${maven.compiler.source}</source> <source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target> <target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding> <encoding>${project.build.sourceEncoding}</encoding>
<!-- writes the META-INF/services class index; see org.diylc.core.annotations.Indexed -->
<annotationProcessors>
<annotationProcessor>org.diylc.processor.ServiceIndexProcessor</annotationProcessor>
</annotationProcessors>
</configuration> </configuration>
<executions>
<execution>
<!-- test fixtures must not end up in the index -->
<id>default-testCompile</id>
<configuration>
<proc>none</proc>
</configuration>
</execution>
</executions>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>