Skip to content

Commit ff389b9

Browse files
committed
Fix #18, externalized plugin library folder, changed PluginManager to use spring autowire instead of custom singleton, made PluginLoader using current classloader such that packaging the interface with plugins is not longer required, fixed some UI issues causing HTTP 404 and not loaded list of mappings, updated README, added Gradle task buildPluginJar to allow to maintain some plugins in the mapping-service and to extract their classes as separate JAR without effort
1 parent d9b6511 commit ff389b9

22 files changed

+330
-181
lines changed

README.md

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,44 +21,61 @@ Dependencies that are needed to build and are not being downloaded via gradle:
2121

2222
- OpenJDK 17
2323
- Python 3
24-
- pip
24+
- pip (runtime only)
2525

2626
`./gradlew -Pclean-release build`
2727

2828
### Python Location
2929

30-
Currently, mapping-service requires Python to be installed in order to build and to run. By default, the Python location is set to `/usr/bin/python3`. In case
31-
your Python installation is located elsewhere or you build the mapping-service under Windows, you can provide the Python location externally, i.e.:
30+
Currently, mapping-service requires Python to be installed in order to build and to run. At runtime, the Python location is configured in
31+
`application.properties`(see below). For building the mapping-service Python location is set to `/usr/bin/python3` by default. In case you want to build
32+
the mapping-service on a machine on which the Python installation is located elsewhere, e.g., under Windows, you can provide the Python location
33+
used at compile time externally, i.e.:
3234

3335
```
34-
`./gradlew -Pclean-release build -DpythonLocation=file:///C:/Python310/python.EXE`
36+
.\gradlew -Pclean-release "-DpythonLocation=file:///C:/Python310/python.exe" build
3537
```
3638

3739
## How to start
3840

3941
Before you can start the mapping-service, you first have to create an `application.properties` file in the source folder. As an example you may use `config/application.default.properties`
4042
and modify it according to your needs. Espacially the following properties (at the end of the file) are important:
41-
- `spring.datasource.url=jdbc:h2:file:e:/tmp/mapping-service/database`
43+
- `spring.datasource.url=jdbc:h2:file:/tmp/mapping-service/database`
4244
The path points to the location of the database in which your configured mappings are stored.
4345
- `mapping-service.pythonLocation=${pythonLocation:'file:///usr/bin/python3'}` \
4446
If no pythonLocation is provided externally (see above) the default `/usr/bin/python3` is used.
47+
- `mapping-service.pluginLocation=file:///tmp/mapping-service/plugins` \
48+
The local folder where available plugins are located.
4549
- `mapping-service.mappingsLocation:file:///tmp/mapping-service/` \
4650
Enter the location where you want to store your mappings. This folder will be created if it does not exist yet.
4751

48-
You might want to add a plugin to make the service working. Normally, there should be a gemma-plugin-x.x.x.jar in the plugins folder.
49-
If not, you can find its source code [here](https://github.com/maximilianiKIT/gemma-plugin).
52+
In order to provide the mapping-service with mapping functionality, there are already some pre-compiled plugins available under in the `plugins` folder of this repository.
53+
Copy them to your configured `mapping-service.pluginLocation` to make them available to the mapping-service.
54+
The source code of the gemma-plugin can be found [here](https://github.com/maximilianiKIT/gemma-plugin). The plugin shows how to integrate Python mappings easily.
55+
56+
There is also the possibility to add new plugins directly at the source tree and create a pluggable Jar out of them. Therefor, check
57+
`src/main/java/edu/kit/datamanager/mappingservice/plugins/impl`. Just add your new plugin, e.g., based on the `TestPlugin` example.
58+
In order to make the plugin usable by the mapping service, you then have to build a plugin Jar out of it. In order to do that, just call:
59+
60+
```
61+
./gradlew buildPluginJar
62+
```
63+
64+
This task creates a file `default-plugins-<VERSION>` at `build/libs` which has to be copied to `mapping-service.pluginLocation` to make it available.
5065

5166
After doing this, the mapping-service is ready for the first start. This can be achieved by executing:
5267

53-
`./gradlew bootRun`
68+
`java -jar build/lib/mapping-service-<VERSION>.jar`
5469

55-
### Python Location
70+
This assumes, that the command is called from the source folder and that your `application.properties` is located in the same folder.
71+
Otherwise, you may use:
5672

57-
Similar to configuring the Python location for the build process, you may also do the same for running the mapping-service via:
73+
`java -jar build/lib/mapping-service-<VERSION>.jar --spring.config.location=/tmp/application.properties`
5874

59-
```
60-
.\gradlew -DpythonLocation=file:///C:/Python310/python.EXE bootRun
61-
```
75+
Ideally, for production use, you place everything (`mapping-service-<VERSION>.jar`, `application.properties`, `mapping-service.pluginLocation`, `mapping-service.mappingsLocation`,
76+
and `spring.datasource.url`) in a separate folder from where you then call the mapping-service via:
77+
78+
`java -jar mapping-service-<VERSION>.jar`
6279

6380
## License
6481

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,10 @@ bootJar {
140140
release {
141141
tagTemplate = 'v${version}'
142142
}
143+
144+
task buildPluginJar(type: Jar) {
145+
description = 'Bundeling only plugin classes'
146+
archiveFileName.set("default-plugins-${version}.jar")
147+
from sourceSets.main.output
148+
include '**/plugins/impl/*.class'
149+
}

config/application.default.properties

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ server.port=8095
66
# The properties max-file-size and max-request-size define the maximum size of files
77
# transferred to and from the repository. Setting them to -1 removes all limits.
88
server.compression.enabled=false
9+
10+
# Max sizes of requests and uploaded files. This value may has to be increased for
11+
# bigger mapping inputs, e.g., while extracting information for a zipped dataset.
912
spring.servlet.multipart.max-file-size=100MB
1013
spring.servlet.multipart.max-request-size=100MB
1114
# Logging settings
@@ -32,8 +35,9 @@ spring.jpa.hibernate.ddl-auto=update
3235
##################################################
3336
# Mapping-Service specific settings
3437
##################################################
35-
# Absolute path to the local python interpreter
38+
# Absolute path to the local python interpreter.
3639
mapping-service.pythonLocation=${pythonLocation:'file:///usr/bin/python3'}
37-
38-
# Absolute path to the local gemma mappings folder
40+
# Absolute path to the folder where all plugins are located.
41+
mapping-service.pluginLocation=file:///${user.dir}/plugins
42+
# Absolute path to the local gemma mappings folder.
3943
mapping-service.mappingSchemasLocation=file:///tmp/mapping-service/mappingSchemas

src/main/java/edu/kit/datamanager/mappingservice/MappingServiceApplication.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ public ApplicationProperties applicationProperties() {
2424
return new ApplicationProperties();
2525
}
2626

27+
@Bean
28+
public PluginManager pluginManager(){
29+
return new PluginManager(applicationProperties());
30+
}
31+
2732
public static void main(String[] args) {
2833
SpringApplication.run(MappingServiceApplication.class, args);
2934

30-
PluginManager.soleInstance().getListOfAvailableValidators().forEach((value) -> LOG.info("Found validator: " + value));
31-
PythonRunnerUtil.printPythonVersion();
35+
//pluginManager().getListOfAvailableValidators().forEach((value) -> LOG.info("Found validator: " + value));
36+
//PythonRunnerUtil.printPythonVersion();
3237

3338
System.out.println("Mapping service is running! Access it at http://localhost:8095");
3439
}

src/main/java/edu/kit/datamanager/mappingservice/configuration/ApplicationProperties.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import edu.kit.datamanager.annotations.ExecutableFileURL;
1919
import edu.kit.datamanager.annotations.LocalFolderURL;
20-
import edu.kit.datamanager.configuration.GenericPluginProperties;
2120
import lombok.Data;
2221
import lombok.EqualsAndHashCode;
2322
import org.springframework.beans.factory.annotation.Value;
@@ -28,8 +27,8 @@
2827
import java.net.URL;
2928

3029
/**
31-
* This class is used to configure the application.
32-
* It reads the values from the application.properties file.
30+
* This class is used to configure the application. It reads the values from the
31+
* application.properties file.
3332
*
3433
* @author maximilianiKIT
3534
*/
@@ -39,13 +38,21 @@
3938
@Validated
4039
@EqualsAndHashCode
4140
public class ApplicationProperties {
41+
4242
/**
4343
* The absolute path to the python interpreter.
4444
*/
4545
@ExecutableFileURL
4646
@Value("${mapping-service.pythonLocation}")
4747
private URL pythonLocation;
4848

49+
/**
50+
* The absolute path where the plugins are stored.
51+
*/
52+
@LocalFolderURL
53+
@Value("${mapping-service.pluginLocation}")
54+
private URL pluginLocation;
55+
4956
/**
5057
* The absolute path where the mappings are stored.
5158
*/

src/main/java/edu/kit/datamanager/mappingservice/impl/MappingService.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ public class MappingService {
5757
*/
5858
@Autowired
5959
private IMappingRecordDao mappingRepo;
60+
61+
/**
62+
* The Plugin Manager.
63+
*/
64+
@Autowired
65+
private PluginManager pluginManager;
6066
/**
6167
* Path to directory holding all mapping files.
6268
*/
@@ -176,7 +182,7 @@ public Optional<Path> executeMapping(URI contentUrl, String mappingId) throws Ma
176182
Path resultFile;
177183
resultFile = FileUtil.createTempFile(mappingId + "_" + srcFile.hashCode(), ".result");
178184
LOGGER.trace("Temporary output file available at {}. Performing mapping.", resultFile);
179-
MappingPluginState result = PluginManager.soleInstance().mapFile(mappingRecord.getMappingType(), mappingFile, srcFile, resultFile);
185+
MappingPluginState result = pluginManager.mapFile(mappingRecord.getMappingType(), mappingFile, srcFile, resultFile);
180186
LOGGER.trace("Mapping returned with result {}. Returning result file.", result);
181187
returnValue = Optional.of(resultFile);
182188
// remove downloaded file

src/main/java/edu/kit/datamanager/mappingservice/plugins/PluginLoader.java

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,29 @@ public static void unload() {
6060
* the input.
6161
*/
6262
public static Map<String, IMappingPlugin> loadPlugins(File plugDir) throws IOException, MappingPluginException {
63-
if (plugDir == null || plugDir.getAbsolutePath().isBlank()) {
64-
throw new MappingPluginException(MappingPluginState.INVALID_INPUT, "Empty input!");
65-
}
66-
File[] plugJars = plugDir.listFiles(new JARFileFilter());
67-
if (plugJars == null || plugJars.length < 1) {
68-
throw new MappingPluginException(MappingPluginState.NOT_FOUND, "No plugins found.");
69-
}
70-
cl = new URLClassLoader(PluginLoader.fileArrayToURLArray(plugJars));
71-
72-
List<Class<IMappingPlugin>> plugClasses = PluginLoader.extractClassesFromJARs(plugJars, cl);
73-
74-
List<IMappingPlugin> IMappingPluginList = PluginLoader.createPluggableObjects(plugClasses);
7563
Map<String, IMappingPlugin> result = new HashMap<>();
76-
for (IMappingPlugin i : IMappingPluginList) {
77-
i.setup();
78-
result.put(i.id(), i);
64+
if (plugDir == null || plugDir.getAbsolutePath().isBlank()) {
65+
LOG.warn("Plugin folder " + plugDir + " is null. Unable to load plugins.");
66+
} else {
67+
File[] plugJars = plugDir.listFiles(new JARFileFilter());
68+
if (plugJars == null || plugJars.length < 1) {
69+
LOG.warn("Plugin folder " + plugDir + " is empty. Unable to load plugins.");
70+
} else {
71+
cl = new URLClassLoader(PluginLoader.fileArrayToURLArray(plugJars), Thread.currentThread().getContextClassLoader());
72+
73+
List<Class<IMappingPlugin>> plugClasses = PluginLoader.extractClassesFromJARs(plugJars, cl);
74+
List<IMappingPlugin> IMappingPluginList = PluginLoader.createPluggableObjects(plugClasses);
75+
76+
for (IMappingPlugin i : IMappingPluginList) {
77+
i.setup();
78+
result.put(i.id(), i);
79+
}
80+
}
7981
}
80-
8182
return result;
8283
}
8384

8485
private static URL[] fileArrayToURLArray(File[] files) throws MalformedURLException {
85-
8686
URL[] urls = new URL[files.length];
8787
for (int i = 0; i < files.length; i++) {
8888
urls[i] = files[i].toURI().toURL();
@@ -91,27 +91,30 @@ private static URL[] fileArrayToURLArray(File[] files) throws MalformedURLExcept
9191
}
9292

9393
private static List<Class<IMappingPlugin>> extractClassesFromJARs(File[] jars, ClassLoader cl) throws IOException, MappingPluginException {
94-
94+
LOG.trace("Extracting classes from plugin JARs.");
9595
List<Class<IMappingPlugin>> classes = new ArrayList<>();
9696
for (File jar : jars) {
97+
LOG.trace("Processing file {}.", jar.getAbsolutePath());
9798
classes.addAll(PluginLoader.extractClassesFromJAR(jar, cl));
9899
}
99100
return classes;
100101
}
101102

102103
private static List<Class<IMappingPlugin>> extractClassesFromJAR(File jar, ClassLoader cl) throws IOException, MappingPluginException {
103-
104+
LOG.trace("Extracting plugin classes from file {}.", jar.getAbsolutePath());
104105
List<Class<IMappingPlugin>> classes = new ArrayList<>();
105106
try (JarInputStream jaris = new JarInputStream(new FileInputStream(jar))) {
106107
JarEntry ent;
107108
while ((ent = jaris.getNextJarEntry()) != null) {
108109
if (ent.getName().toLowerCase().endsWith(".class")) {
109110
try {
110111
Class<?> cls = cl.loadClass(ent.getName().substring(0, ent.getName().length() - 6).replace('/', '.'));
112+
LOG.trace("Checking {}.", cls);
111113
if (PluginLoader.isPluggableClass(cls)) {
114+
LOG.trace("Plugin class found.");
112115
classes.add((Class<IMappingPlugin>) cls);
113116
}
114-
} catch (ClassNotFoundException e) {
117+
} catch (ClassNotFoundException | NoClassDefFoundError e) {
115118
LOG.info("Can't load Class " + ent.getName());
116119
throw new MappingPluginException(MappingPluginState.UNKNOWN_ERROR, "Can't load Class " + ent.getName(), e);
117120
}
@@ -122,18 +125,22 @@ private static List<Class<IMappingPlugin>> extractClassesFromJAR(File jar, Class
122125
}
123126

124127
private static boolean isPluggableClass(Class<?> cls) {
125-
126128
for (Class<?> i : cls.getInterfaces()) {
129+
LOG.trace("Checking {} against {}.", i, IMappingPlugin.class);
130+
LOG.trace("ASSIGN {}", IMappingPlugin.class.isAssignableFrom(cls));
127131
if (i.equals(IMappingPlugin.class)) {
132+
LOG.trace("IMappingPlugin interface found.");
128133
return true;
129134
}
130135
}
131136
return false;
132137
}
133138

134139
private static List<IMappingPlugin> createPluggableObjects(List<Class<IMappingPlugin>> pluggable) throws MappingPluginException {
140+
LOG.trace("Instantiating plugins from list: {}", pluggable);
135141
List<IMappingPlugin> plugs = new ArrayList<>(pluggable.size());
136142
for (Class<IMappingPlugin> plug : pluggable) {
143+
LOG.trace("Instantiating plugin from class {}.", plug);
137144
try {
138145
plugs.add(plug.getDeclaredConstructor().newInstance());
139146
} catch (InstantiationException | NoSuchMethodException | InvocationTargetException e) {

0 commit comments

Comments
 (0)