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 |
| --- | --- |
| `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. |
+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
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).
+8 -4
View File
@@ -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
+6 -5
View File
@@ -19,6 +19,12 @@
</properties>
<dependencies>
<!-- Internal module dependencies -->
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-processor</artifactId>
</dependency>
<!-- Common dependencies from Maven Central -->
<dependency>
<groupId>log4j</groupId>
@@ -52,11 +58,6 @@
<artifactId>jettison</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -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 {
/**
@@ -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 <T> type of component values, e.g. Resistance for resistors or String for transistors.
*/
@Indexed
public interface IDIYComponent<T> extends Serializable, Cloneable {
public static final int CHASSIS = 1;
@@ -30,6 +30,7 @@ import org.diylc.core.IDIYComponent;
*
* @author Branislav Stojkovic
*/
@Indexed
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 org.diylc.core.annotations.Indexed;
@Indexed
public interface IGuitarDiagramAnalyzer {
List<String> collectNotes(Netlist netlist);
@@ -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;
* <http://www.gnu.org/licenses/>.
*
*/
@Indexed
public interface INetlistParser {
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;
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,30 +225,24 @@ public class ComponentProcessor {
if (componentTypes == null) {
LOG.info("Loading component types.");
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 {
componentTypeClasses = reflections.getTypesAnnotatedWith(ComponentDescriptor.class, false);
List<Map<String, Object>> componentSnapshots = LOG_COMPONENT_TYPE_DETAILS ? new ArrayList<>() : null;
for (Class<?> clazz : componentTypeClasses) {
if (!Modifier.isAbstract(clazz.getModifiers()) && IDIYComponent.class.isAssignableFrom(clazz)) {
for (Class<?> clazz : ReflectionUtils.getIndexedClasses(IDIYComponent.class)) {
ComponentType componentType =
ComponentProcessor.getInstance().extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) clazz);
if (componentType == null)
if (componentType == null) {
continue;
}
// 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())
if (clazz.getAnnotation(ComponentDescriptor.class).hiddenInPalette()) {
continue;
}
List<ComponentType> nestedList;
if (componentTypes.containsKey(componentType.getCategory())) {
@@ -314,7 +279,6 @@ public class ComponentProcessor {
}
}
}
}
if (LOG_COMPONENT_TYPE_DETAILS && componentSnapshots != null && !componentSnapshots.isEmpty()) {
try {
@@ -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<INetlistAnalyzer> getNetlistAnalyzers() {
Set<Class<?>> classes;
try {
classes = Utils.getClasses("org.diylc.netlist");
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());
}
}
List<INetlistAnalyzer> result = ReflectionUtils.getIndexedInstances(INetlistAnalyzer.class);
result.sort(new Comparator<INetlistAnalyzer>() {
@@ -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);
@@ -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<IOldFileParser> getParsers() {
if (parsers == null) {
parsers = new ArrayList<IOldFileParser>();
Set<Class<?>> 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<IOldFileParser>();
LOG.error("Could not find old version parsers", e);
}
}
@@ -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<IAutoCreator> autoCreators;
/**
* 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 <T> List<Class<? extends T>> getIndexedClasses(Class<T> type) {
ClassLoader classLoader = ReflectionUtils.class.getClassLoader();
// each module jar carries its own index file for the same type
Set<String> classNames = new LinkedHashSet<String>();
try {
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) {
autoCreators = new ArrayList<IAutoCreator>();
Set<Class<?>> 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());
}
}
autoCreators = getIndexedInstances(IAutoCreator.class);
} catch (Exception e) {
autoCreators = new ArrayList<IAutoCreator>();
LOG.error("Error loading auto-creator types", e);
}
}
@@ -65,16 +119,8 @@ public class ReflectionUtils {
}
public static List<INetlistParser> getNetlistParserDefinitions() {
Set<Class<?>> classes;
try {
classes = Utils.getClasses("org.diylc.netlist");
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());
}
}
List<INetlistParser> result = getIndexedInstances(INetlistParser.class);
Collections.sort(result, new Comparator<INetlistParser>() {
@@ -92,16 +138,7 @@ public class ReflectionUtils {
}
public static IGuitarDiagramAnalyzer getGuitarDiagramAnalyzer() throws Exception {
Set<Class<?>> classes;
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());
}
}
List<IGuitarDiagramAnalyzer> result = getIndexedInstances(IGuitarDiagramAnalyzer.class);
if (result.isEmpty()) {
return null;
@@ -611,7 +611,11 @@
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<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>
<changeType>BUG_FIX</changeType>
+4
View File
@@ -20,6 +20,10 @@
<dependencies>
<!-- Internal module dependencies -->
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-processor</artifactId>
</dependency>
<dependency>
<groupId>org.diylc</groupId>
<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>
<!-- Internal module dependencies -->
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-processor</artifactId>
</dependency>
<dependency>
<groupId>org.diylc</groupId>
<artifactId>diylc-core</artifactId>
@@ -90,6 +90,8 @@ class ComponentTabbedPane extends JTabbedPane {
private List<String> blocks;
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;
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<ComponentType> 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();
+21
View File
@@ -13,6 +13,7 @@
<description>A free, open-source, cross-platform tool for drawing circuit layouts</description>
<modules>
<module>diylc-processor</module>
<module>diylc-core</module>
<module>diylc-library</module>
<module>diylc-swing</module>
@@ -31,6 +32,13 @@
<dependencyManagement>
<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>
<groupId>org.diylc</groupId>
<artifactId>diylc-core</artifactId>
@@ -94,7 +102,20 @@
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<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>
<executions>
<execution>
<!-- test fixtures must not end up in the index -->
<id>default-testCompile</id>
<configuration>
<proc>none</proc>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>