Skip to content

Commit

Permalink
Enable preload of annotation metadata (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
edreed authored Feb 7, 2024
1 parent f213e8c commit f23cfd0
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 77 deletions.
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The table below lists the published versions of the library and the WPILib relea

| Version | Git Tag/Branch | Required WPILib Version |
|-------------------|----------------|-------------------------|
| 2024.1.1-SNAPSHOT | main | 2024.1.1 |
| 2024.1.2-SNAPSHOT | main | 2024.1.1 |
| 2023.4.1-SNAPSHOT | main | 2023.4.2 |
| 2023.4.0 | v2023.4.0 | 2023.4.2 |
| 2023.2.0 | v2023.2.0 | 2023.2.1 |
Expand Down Expand Up @@ -47,6 +47,59 @@ dependencies {
}
```

> **NOTE:** If you want the latest build from `main`, use `'com.nrg948:nrgcommon:2023.4.1-SNAPSHOT'`. There may be breaking changes and it certainly will not be as stable, so use with caution.
> **NOTE:** If you want the latest build from `main`, use `'com.nrg948:nrgcommon:2024.1.2-SNAPSHOT'`. There may be breaking changes and it certainly will not be as stable, so use with caution.
On the next build, the library will be downloaded from GitHub packages and installed in the Gradle build cache.

## Optimizing library load time

The library uses the [`Reflections`](https://github.com/ronmamo/reflections) library to scan the Java classes for annotations. This can be a time consuming process on the original RoboRio due to its somewhat slow flash storage. To reduce load time, you can add a custom build step in your `build.gradle` to generate the annotation metadata at build time.

To generate the annotation metadata at build time, add the following build dependencies before the `plugins` section in `build.gradle`.

```gradle
buildscript {
dependencies {
// Add the Reflections library and dependencies to the classpath so we
// can generate its metadata during the build.
classpath 'org.reflections:reflections:0.10.2'
classpath 'org.dom4j:dom4j:2.1.3'
}
}
```

Then, add the following custom task toward the end of the `build.gradle` file.

```gradle
// Generates metadata consumed by the NRG Common Library to find annotations at runtime.
task generateReflectionsMetadata {
dependsOn compileJava
doLast {
// Create a class loader for the main project files.
Set<File> projectDirs = project.sourceSets.main.output.classesDirs.files
URL[] projectUrls = projectDirs.collect { it.toURI().toURL() }.toArray(new URL[0])
ClassLoader projectLoader = new URLClassLoader(projectUrls, (java.lang.ClassLoader)null)
// Create a class loader for the project runtime dependencies.
Set<File> classpathFiles = project.configurations.runtimeClasspath.files
URL[] classpathUrls = classpathFiles.collect { it.toURI().toURL() }.toArray(new URL[0])
ClassLoader classpathLoader = new URLClassLoader(classpathUrls, (java.lang.ClassLoader)null)
// Generate the metadata for the Reflections library.
new org.reflections.Reflections(
new org.reflections.util.ConfigurationBuilder()
.forPackage("frc.robot", projectLoader)
.forPackage("com.nrg948", classpathLoader)
.setScanners(
org.reflections.scanners.Scanners.FieldsAnnotated,
org.reflections.scanners.Scanners.MethodsAnnotated,
org.reflections.scanners.Scanners.SubTypes,
org.reflections.scanners.Scanners.TypesAnnotated))
.save("${project.sourceSets.main.output.classesDirs.asPath}/META-INF/reflections/${project.archivesBaseName}-reflections.xml")
}
}
// Generate the NRG Common Library metadata when the robot code is built.
jar.dependsOn generateReflectionsMetadata
```
3 changes: 2 additions & 1 deletion nrgcommon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ plugins {
}

group = 'com.nrg948'
version = '2024.1.1' + (Boolean.valueOf(System.getProperty("release")) ? "" : "-SNAPSHOT")
version = '2024.1.2' + (Boolean.valueOf(System.getProperty("release")) ? "" : "-SNAPSHOT")

sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
Expand All @@ -56,6 +56,7 @@ dependencies {
implementation "edu.wpi.first.wpiutil:wpiutil-java:2024.1.1"
implementation 'org.reflections:reflections:0.10.2'
implementation 'edu.wpi.first.wpilibNewCommands:wpilibNewCommands-java:2024.1.1'
implementation 'org.dom4j:dom4j:2.1.3'
}

java {
Expand Down
49 changes: 49 additions & 0 deletions nrgcommon/src/main/java/com/nrg948/Common.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
MIT License
Copyright (c) 2024 Newport Robotics Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package com.nrg948;

import com.nrg948.annotations.Annotations;
import com.nrg948.preferences.RobotPreferences;

/** A class to initialize the NRG Common Library. */
public final class Common {
/* Disallow instantiation */
private Common() {
}

/**
* Initializes the NRG Common library.
*
* This method must be called in the <code>Robot.initRobot()</code> method
* before the <code>RobotContainer</code> is created.
*
* @param pkgs The packages to scan for annotations implemented by the NRG
* Common Library.
*/
public static void init(String... pkgs) {
Annotations.init(pkgs);
RobotPreferences.init();
}
}
146 changes: 146 additions & 0 deletions nrgcommon/src/main/java/com/nrg948/annotations/Annotations.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
MIT License
Copyright (c) 2024 Newport Robotics Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package com.nrg948.annotations;

import static org.reflections.scanners.Scanners.FieldsAnnotated;
import static org.reflections.scanners.Scanners.MethodsAnnotated;
import static org.reflections.scanners.Scanners.SubTypes;
import static org.reflections.scanners.Scanners.TypesAnnotated;

import java.net.JarURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.reflections.Reflections;
import org.reflections.Store;
import org.reflections.serializers.Serializer;
import org.reflections.serializers.XmlSerializer;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.QueryFunction;

/**
* A class providing access to types annotated by the NRG Common Library
* annotations.
*/
public final class Annotations {
private static Reflections reflections;

/* Disallow instantiation */
private Annotations() {
}

/**
* Initializes the annotation metadata for the NRG Common Library.
*
* @param pkgs The packages to scan for annotations implemented by the NRG
* Common Library.
*/
public static void init(String... pkgs) {
reflections = loadFromMetadata().orElseGet(() -> scanPackages(pkgs));
}

/**
* Creates and initializes a {@link Reflections} instance from metadata, if
* present.
*
* Reflections metadata is stored in the META-INF/reflections directory in the
* program's JAR file.
*
* @return An optional {@link Reflections} instance initialized from metadata.
* If no metadata is present, {@link Optional#empty()} is returned.
*/
private static Optional<Reflections> loadFromMetadata() {
Optional<Reflections> reflections = Optional.empty();
Serializer xmlSerializer = new XmlSerializer();
ClassLoader loader = ClassLoader.getSystemClassLoader();

try {
Enumeration<URL> resources = loader.getResources("META-INF/reflections");

while (resources.hasMoreElements()) {
URL url = resources.nextElement();
JarURLConnection connection = (JarURLConnection) url.openConnection();

try (JarFile jar = connection.getJarFile()) {
Enumeration<JarEntry> entries = jar.entries();

while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();

if (entry.getName().endsWith("-reflections.xml")) {
if (reflections.isEmpty()) {
reflections = Optional.of(new Reflections());
}

reflections.get().collect(jar.getInputStream(entry), xmlSerializer);
}
}
}
}
} catch (Exception e) {
System.err.println("WARNING: Failed to load Reflections metadata: " + e.getMessage());
reflections = null;
}

return reflections;
}

/**
* Scans the specified packages for annotations implemented by the NRG Common
* Library.
*
* @param pkgs The packages to scan for annotations implemented by the NRG
* Common Library.
*
* @return The annotation metadata for the specified packages.
*/
private static Reflections scanPackages(String... pkgs) {
String[] allPkgs = Arrays.copyOf(pkgs, pkgs.length + 1);

allPkgs[allPkgs.length - 1] = "com.nrg948";

return new Reflections(
new ConfigurationBuilder()
.forPackages(allPkgs)
.setScanners(FieldsAnnotated, MethodsAnnotated, SubTypes, TypesAnnotated));
}

/**
* Returns the set of elements annotated with the specified annotation.
*
* @param <T> The type of element.
* @param query The query function.
*
* @return The set of elements annotated with the specified annotation.
*/
public static <T> Set<T> get(QueryFunction<Store, T> query) {
return reflections.get(query);
}
}
13 changes: 4 additions & 9 deletions nrgcommon/src/main/java/com/nrg948/autonomous/Autonomous.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.stream.Stream;

import org.javatuples.LabelValue;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import com.nrg948.annotations.Annotations;

import edu.wpi.first.wpilibj.smartdashboard.SendableChooser;
import edu.wpi.first.wpilibj2.command.Command;
Expand Down Expand Up @@ -108,27 +107,23 @@ default int compareTo(CommandFactory<T> obj) {
* @return A {@link SendableChooser} object containing the autonomous commands.
*/
public static <T> SendableChooser<Command> getChooser(T container, String... pkgs) {
Reflections reflections = new Reflections(
new ConfigurationBuilder()
.forPackages(pkgs)
.addScanners(SubTypes, MethodsAnnotated));
SendableChooser<Command> chooser = new SendableChooser<>();

Stream<CommandFactory<T>> commandClasses = reflections
Stream<CommandFactory<T>> commandClasses = Annotations
.get(SubTypes
.of(Command.class)
.asClass()
.filter(withAnnotation(AutonomousCommand.class)))
.stream()
.map(Autonomous::<T>toCommandFactory);
Stream<CommandFactory<T>> commandMethods = reflections
Stream<CommandFactory<T>> commandMethods = Annotations
.get(MethodsAnnotated
.with(AutonomousCommandMethod.class)
.as(Method.class)
.filter(withStatic()))
.stream()
.map(Autonomous::<T>toCommandFactory);
Stream<CommandFactory<T>> commandGenerators = reflections
Stream<CommandFactory<T>> commandGenerators = Annotations
.get(MethodsAnnotated
.with(AutonomousCommandGenerator.class)
.as(Method.class)
Expand Down
50 changes: 50 additions & 0 deletions nrgcommon/src/main/java/com/nrg948/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
MIT License
Copyright (c) 2024 Newport Robotics Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
* The NRG Common Library provides a set of classes, annotations and utilities
* used by FIRST Robotics Competition Team 948 - Newport Robotics Group (NRG948).
*
* <p>
* To initialize the libray, you must call the {@link Common#init(String...)}
* method passing the name of the robot package. In the Command-based Robot,
* this must be done in the <code>Robot.initRobot()</code> method before the
* <code>RobotContainer</code> is created.<br>
*
* <pre>
* <code>
* {@literal @}Override
* public void robotInit() {
* // Initialize the NRG Common Library before creating the RobotContainer so that
* // it is initialized and ready for use by the subsystems.
* Common.init("frc.robot");
*
* // Instantiate our RobotContainer. This will perform all our button bindings,
* // and put our autonomous chooser on the dashboard.
* m_robotContainer = new RobotContainer();
* }
* </code>
* </pre>
*/
package com.nrg948;
Loading

0 comments on commit f23cfd0

Please sign in to comment.