Skip to content

Commit a55f03b

Browse files
authored
Merge branch 'development' into renovate/springdocversion
2 parents 8b572dd + 45d9a40 commit a55f03b

File tree

26 files changed

+185
-744
lines changed

26 files changed

+185
-744
lines changed

build.gradle

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,13 @@ dependencies {
8282
implementation 'com.google.guava:guava:33.4.8-jre'
8383
implementation 'commons-io:commons-io:2.19.0'
8484
implementation 'javax.validation:validation-api:2.0.1.Final'
85-
implementation 'edu.kit.datamanager:service-base:1.3.3'
85+
implementation ('edu.kit.datamanager:service-base:1.3.3'){
86+
//exclude dependency as spring boot includes
87+
//org.glassfish.jaxb:jaxb-core:4.0.5 which leads to a duplication conflict
88+
exclude group: "com.sun.xml.bind"
89+
}
8690
// apache
87-
implementation "org.apache.tika:tika-core:2.9.3"
91+
implementation "org.apache.tika:tika-core:3.1.0"
8892

8993
testImplementation platform('org.junit:junit-bom')
9094
testImplementation 'org.junit.jupiter:junit-jupiter'

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ public class ApplicationProperties {
5959
@Value("${mapping-service.mappingSchemasLocation}")
6060
private URL mappingsLocation;
6161

62+
/**
63+
* The absolute path where mapping plugin code is checked out into, i.e.,
64+
* for Python-based plugins.
65+
*/
66+
@LocalFolderURL
67+
@Value("${mapping-service.codeLocation}")
68+
private URL codeLocation;
69+
6270
/**
6371
* The absolute path where job data is stored.
6472
*/

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,11 @@ private void init(ApplicationProperties applicationProperties) {
498498
} catch (IOException e) {
499499
throw new MappingServiceException(String.format("Could not initialize job output directory '%s'.", applicationProperties.getJobOutputLocation()), e);
500500
}
501+
try {
502+
Files.createDirectories(new File(applicationProperties.getCodeLocation().getPath()).getAbsoluteFile().toPath());
503+
} catch (IOException e) {
504+
throw new MappingServiceException(String.format("Could not initialize code target directory '%s'.", applicationProperties.getCodeLocation()), e);
505+
}
501506
} else {
502507
throw new MappingServiceException("Cannot configure MappingService due to missing application.properties.");
503508
}

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

Lines changed: 110 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
/*
2-
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
3-
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
2+
* Copyright 2025 Karlsruhe Institute of Technology.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
414
*/
515
package edu.kit.datamanager.mappingservice.plugins;
616

17+
import edu.kit.datamanager.mappingservice.configuration.ApplicationProperties;
718
import edu.kit.datamanager.mappingservice.exception.PluginInitializationFailedException;
819
import edu.kit.datamanager.mappingservice.util.FileUtil;
920
import edu.kit.datamanager.mappingservice.util.PythonRunnerUtil;
1021
import edu.kit.datamanager.mappingservice.util.ShellRunnerUtil;
22+
import java.io.ByteArrayOutputStream;
1123
import java.io.IOException;
1224
import java.io.InputStream;
25+
import java.net.URISyntaxException;
1326
import java.net.URL;
1427
import java.nio.file.Path;
28+
import java.nio.file.Paths;
1529
import java.util.Arrays;
1630
import java.util.LinkedList;
1731
import java.util.List;
1832
import java.util.Properties;
33+
import org.apache.maven.artifact.versioning.ComparableVersion;
1934
import org.slf4j.Logger;
2035
import org.slf4j.LoggerFactory;
36+
import org.springframework.beans.factory.annotation.Autowired;
37+
import org.springframework.stereotype.Component;
2138

2239
/**
2340
*
@@ -26,6 +43,7 @@
2643
public abstract class AbstractPythonMappingPlugin implements IMappingPlugin {
2744

2845
private final Logger LOGGER = LoggerFactory.getLogger(AbstractPythonMappingPlugin.class);
46+
2947
/**
3048
* The plugin name.
3149
*/
@@ -108,27 +126,28 @@ public AbstractPythonMappingPlugin(String pluginName, String repositoryUrl) {
108126

109127
/**
110128
* Abstract method that is supposed to be implemented by each Python mapping
111-
* plugin the gather all information required for starting a Python process
112-
* executing the mapping script. The returned array should contain at least
129+
* plugin to gather all information required for starting a Python process
130+
* executing the mapping script. The returned array must contain at least
113131
* the following information:
114132
*
115133
* <ul> <li>The absolute path of the main script. It must start
116-
* with the working dir given as argument, where all checked out code is
134+
* with the working dir received as argument, where all checked-out code is
117135
* located.</li> <li>Script-specific parameters to provide
118-
* mappingFile, inputFile, and outputFile. Depending on the script
119-
* implementation the number of required arguments may differ.</li>
120-
* </ul>
136+
* mappingFile, inputFile, and outputFile to the script execution. Depending
137+
* on the script implementation, the number and kind of required arguments
138+
* may differ.</li> </ul>
121139
*
122-
* Example: In standalone mode, your script is called via `plugin_wrapper.py
123-
* sem -m mappingFile -i inputFile -o outputFile`. In that case, the
140+
* Example: In standalone mode, a script is called via `plugin_wrapper.py
141+
* sem -m mappingFile -i inputFile -o outputFile -debug`. In that case, the
124142
* resulting array should look as follows: [workingDir +
125143
* "plugin_wrapper.py", "sem", "-m", mappingFile.toString(), "-i",
126-
* inputFile.toString(), "-o", outputFile.toString()]. The Python call
127-
* itself will be added depending on the local installation and must not be
128-
* included.
144+
* inputFile.toString(), "-o", outputFile.toString(), "-debug"].
145+
*
146+
* The Python call itself will be added according to the Venv used for
147+
* plugin execution and must not be included.
129148
*
130149
* @param workingDir The working directory, i.e., where the plugin code was
131-
* checked out into.
150+
* checked-out into.
132151
* @param mappingFile The file which contains the mapping rules registered
133152
* at the mapping-service and used by the script.
134153
* @param inputFile The file which was uploaded by the user, i.e., the
@@ -163,25 +182,44 @@ public String uri() {
163182
}
164183

165184
@Override
166-
public void setup() {
185+
public void setup(ApplicationProperties applicationProperties) {
167186
LOGGER.trace("Setting up mapping plugin {} {}", name(), version());
168-
187+
169188
//testing minimal Python version
170-
171-
172-
173-
189+
if (minPython != null) {
190+
if (!hasMinimalPythonVersion(minPython)) {
191+
throw new PluginInitializationFailedException("Minimal Python version '" + minPython + "' required by plugin not met.");
192+
}
193+
}
194+
195+
//checkout and install plugin
174196
try {
175-
LOGGER.info("Cloning git repository {}, Tag {}", repositoryUrl, tag);
176-
dir = FileUtil.cloneGitRepository(repositoryUrl, tag);
197+
LOGGER.info("Cloning git repository {}, tag {}", repositoryUrl, tag);
198+
Path path = Paths.get(applicationProperties.getCodeLocation().toURI());
199+
path = path.resolve(repositoryUrl.trim().replace("https://", "").replace("http://", "").replace(".git", "") + "_" + version());
200+
LOGGER.info("Target path: {}", path);
201+
dir = FileUtil.cloneGitRepository(repositoryUrl, tag, path.toAbsolutePath().toString());
177202
// Install Python dependencies
178203
MappingPluginState venvState = PythonRunnerUtil.runPythonScript("-m", "venv", "--system-site-packages", dir + "/" + pluginVenv);
179204
if (MappingPluginState.SUCCESS().getState().equals(venvState.getState())) {
180-
LOGGER.info("Venv for plugin installed successfully. Installing packages.");
181-
ShellRunnerUtil.run(dir + "/" + venvInterpreter, "-m", "pip", "install", "-r", dir + "/" + "requirements.dist.txt");
205+
LOGGER.info("Venv for plugin installed successfully. Installing requirements.");
206+
207+
Path requirementsFile = Paths.get(dir + "/" + "requirements.dist.txt");
208+
if (requirementsFile.toFile().exists()) {
209+
MappingPluginState requirementsInstallState = ShellRunnerUtil.run(dir + "/" + venvInterpreter, "-m", "pip", "install", "-r", dir + "/" + "requirements.dist.txt");
210+
if (MappingPluginState.SUCCESS().getState().equals(requirementsInstallState.getState())) {
211+
LOGGER.info("Requirements for plugin installed successfully. Setup complete.");
212+
} else {
213+
throw new PluginInitializationFailedException("Failed to install plugin requirements. Status: " + venvState.getState());
214+
}
215+
} else {
216+
LOGGER.info("No requirements file found. Skipping dependency installation.");
217+
}
182218
} else {
183-
throw new PluginInitializationFailedException("Venv installation was not successful. Status: " + venvState.getState());
219+
throw new PluginInitializationFailedException("Venv installation has failed. Status: " + venvState.getState());
184220
}
221+
} catch (URISyntaxException e) {
222+
throw new PluginInitializationFailedException("Invalid codeLocation configured in application.properties.", e);
185223
} catch (MappingPluginException e) {
186224
throw new PluginInitializationFailedException("Unexpected error during plugin setup.", e);
187225
}
@@ -190,16 +228,61 @@ public void setup() {
190228
@Override
191229
public MappingPluginState mapFile(Path mappingFile, Path inputFile, Path outputFile) throws MappingPluginException {
192230
long startTime = System.currentTimeMillis();
193-
LOGGER.trace("Run SEM-Mapping-Tool on '{}' with mapping '{}' -> '{}'", mappingFile, inputFile, outputFile);
231+
LOGGER.trace("Run mapping plugin {} {} on '{}' with mapping '{}' -> '{}'", name(), version(), mappingFile, inputFile, outputFile);
194232
String[] commandArray = getCommandArray(dir, mappingFile, inputFile, outputFile);
195233
List<String> command = new LinkedList<>();
196234
command.add(dir + "/" + venvInterpreter);
197235
command.addAll(Arrays.asList(commandArray));
198-
//MappingPluginState result = ShellRunnerUtil.run(dir + "/" + venvInterpreter, dir + "/plugin_wrapper.py", "sem", "-m", mappingFile.toString(), "-i", inputFile.toString(), "-o", outputFile.toString());
199236
MappingPluginState result = ShellRunnerUtil.run(command.toArray(String[]::new));
200237
long endTime = System.currentTimeMillis();
201238
long totalTime = endTime - startTime;
202239
LOGGER.info("Execution time of mapFile: {} milliseconds", totalTime);
203240
return result;
204241
}
242+
243+
/**
244+
* This method checks if the local Python installation version is larger or
245+
* equal the provided version number. The version should be provided as
246+
* semantic version number, i.e., 3.13.2
247+
*
248+
* The method will return TRUE if the minimal requirements are met and false
249+
* otherwise. False is also returned if obtaining/parsing the local python
250+
* version fails. for any reason.
251+
*
252+
* @param versionString The semantic version string to compare the local
253+
* Python version against.
254+
*
255+
* @return True if versionString is smaller or equal the local Python
256+
* version, false otherwise.
257+
*/
258+
private boolean hasMinimalPythonVersion(String versionString) {
259+
boolean result = false;
260+
try {
261+
LOGGER.trace("Checking for minimal Python version {}.", versionString);
262+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
263+
MappingPluginState state = PythonRunnerUtil.runPythonScript("--version", bout, System.err);
264+
265+
if (!MappingPluginState.StateEnum.SUCCESS.equals(state.getState())) {
266+
LOGGER.error("Failed to obtain Python version. python --version returned with status {}.", state.getState());
267+
} else {
268+
269+
LOGGER.trace("Version command output: {}", bout.toString());
270+
271+
String[] split = bout.toString().split(" ");
272+
273+
if (split.length == 2) {
274+
String localPythonVersion = bout.toString().split(" ")[1].trim();
275+
LOGGER.trace("Obtained local Python version: {}", localPythonVersion);
276+
ComparableVersion localVersion = new ComparableVersion(localPythonVersion);
277+
ComparableVersion minimalVersion = new ComparableVersion(versionString);
278+
result = minimalVersion.compareTo(localVersion) <= 0;
279+
} else {
280+
LOGGER.info("Unexpected Python version output. Unable to check for minimal version.");
281+
}
282+
}
283+
} catch (MappingPluginException e) {
284+
LOGGER.error("Failed to obtain Python version.", e);
285+
}
286+
return result;
287+
}
205288
}

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

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@
1212
* See the License for the specific language governing permissions and
1313
* limitations under the License.
1414
*/
15-
1615
package edu.kit.datamanager.mappingservice.plugins;
1716

17+
import edu.kit.datamanager.mappingservice.configuration.ApplicationProperties;
1818
import org.springframework.util.MimeType;
1919

2020
import java.nio.file.Path;
2121

2222
/**
23-
* Interface for mapping plugins.
24-
* Every plugin which implements this interface and is placed in the plugins folder will be loaded and usable via the REST-API.
23+
* Interface for mapping plugins. Every plugin which implements this interface
24+
* and is placed in the plugins folder will be loaded and usable via the
25+
* REST-API.
2526
*
2627
* @author maximilianiKIT
2728
*/
@@ -42,15 +43,17 @@ public interface IMappingPlugin {
4243
String description();
4344

4445
/**
45-
* The version of the plugin which gets displayed in the UI and is part of the id.
46+
* The version of the plugin which gets displayed in the UI and is part of
47+
* the id.
4648
*
4749
* @return The version of the plugin.
4850
*/
4951
String version();
5052

5153
/**
52-
* A URI which refers to the plugin or the technology used by the plugin (e.g. a link to a GitHub repository).
53-
* This URI will be displayed in the UI.
54+
* A URI which refers to the plugin or the technology used by the plugin
55+
* (e.g. a link to a GitHub repository). This URI will be displayed in the
56+
* UI.
5457
*
5558
* @return The URI of the plugin.
5659
*/
@@ -71,8 +74,9 @@ public interface IMappingPlugin {
7174
MimeType[] outputTypes();
7275

7376
/**
74-
* The id of the plugin which is used to identify the plugin.
75-
* By default, the id is composed of the name and the version of the plugin (e.g. testPlugin_2.1.0).
77+
* The id of the plugin which is used to identify the plugin. By default,
78+
* the id is composed of the name and the version of the plugin (e.g.
79+
* testPlugin_2.1.0).
7680
*
7781
* @return The id of the plugin.
7882
*/
@@ -81,19 +85,22 @@ default String id() {
8185
}
8286

8387
/**
84-
* This method is called when the plugin is loaded.
85-
* It can be used to initialize the plugin and install dependencies.
88+
* This method is called when the plugin is loaded. It can be used to
89+
* initialize the plugin and install dependencies.
90+
*
91+
* @param applicationProperties Properties object containing all
92+
* mapping-service settings.
8693
*/
87-
void setup();
94+
void setup(ApplicationProperties applicationProperties);
8895

8996
/**
9097
* The method which is called to execute the plugin.
9198
*
92-
* @param inputFile The path to the output document.
93-
* @param outputFile The path to the output document.
99+
* @param inputFile The path to the output document.
100+
* @param outputFile The path to the output document.
94101
* @param mappingFile The path to the mapping schema.
95102
* @return The exit code of the plugin.
96-
*
103+
*
97104
* @throws MappingPluginException If the mapping execution fails.
98105
*/
99106
MappingPluginState mapFile(Path mappingFile, Path inputFile, Path outputFile) throws MappingPluginException;

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ public Map<String, IMappingPlugin> loadPlugins(File pluginDir, String[] packages
100100
cl = Thread.currentThread().getContextClassLoader();
101101
}
102102

103-
List<Class<IMappingPlugin>> plugClasses = extractClassesFromJARs(pluginJars, packagesToScan, cl);
104-
List<IMappingPlugin> IMappingPluginList = createPluggableObjects(plugClasses);
103+
List<Class<IMappingPlugin>> pluginClasses = extractClassesFromJARs(pluginJars, packagesToScan, cl);
104+
List<IMappingPlugin> IMappingPluginList = createPluggableObjects(pluginClasses);
105105

106106
for (IMappingPlugin i : IMappingPluginList) {
107107
try {
108-
i.setup();
108+
i.setup(applicationProperties);
109109
LOG.trace(" - Adding new plugin {}, v{} to available list", i.name(), i.version());
110110
result.put(i.id(), i);
111111
} catch (PluginInitializationFailedException re) {

src/main/java/edu/kit/datamanager/mappingservice/plugins/impl/GemmaPlugin.java

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,14 @@
1515
*/
1616
package edu.kit.datamanager.mappingservice.plugins.impl;
1717

18-
import edu.kit.datamanager.mappingservice.exception.PluginInitializationFailedException;
1918
import edu.kit.datamanager.mappingservice.plugins.*;
20-
import edu.kit.datamanager.mappingservice.util.*;
21-
import org.slf4j.Logger;
22-
import org.slf4j.LoggerFactory;
2319
import org.springframework.util.MimeType;
2420
import org.springframework.util.MimeTypeUtils;
2521
import java.nio.file.Path;
2622

2723
public class GemmaPlugin extends AbstractPythonMappingPlugin {
2824

29-
private final Logger LOGGER = LoggerFactory.getLogger(GemmaPlugin.class);
3025
private static final String GEMMA_REPOSITORY = "https://github.com/kit-data-manager/gemma.git";
31-
private static final String GEMMA_BRANCH = "master";
32-
private static Path gemmaDir;
33-
private boolean initialized = false;
3426

3527
public GemmaPlugin() {
3628
super("GEMMA", GEMMA_REPOSITORY);
@@ -60,17 +52,4 @@ public String[] getCommandArray(Path workingDir, Path mappingFile, Path inputFil
6052
outputFile.toString()
6153
};
6254
}
63-
64-
/* @Override
65-
public MappingPluginState mapFile(Path mappingFile, Path inputFile, Path outputFile) throws MappingPluginException {
66-
if (initialized) {
67-
LOGGER.trace("Running plugin '{}' v{} on '{}' with mapping '{}' -> '{}'", name(), version(), inputFile, mappingFile, outputFile);
68-
return PythonRunnerUtil.runPythonScript(gemmaDir + "/mapping_single.py", mappingFile.toString(), inputFile.toString(), outputFile.toString());
69-
} else {
70-
LOGGER.error("Plugin '" + name() + "' " + version() + " not initialized. Returning EXECUTION_ERROR.");
71-
MappingPluginState result = MappingPluginState.EXECUTION_ERROR();
72-
result.setDetails("Plugin not initialized, probably due to missing dependencies or external plugin repository.");
73-
return result;
74-
}
75-
}*/
7655
}

0 commit comments

Comments
 (0)