Skip to content

Commit 63351d8

Browse files
committed
Continued with AbstractPythonMappingPlugin, integrated Python version check, added new property for codeLocation, still required proper integration
1 parent 5462193 commit 63351d8

File tree

6 files changed

+121
-155
lines changed

6 files changed

+121
-155
lines changed

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/plugins/AbstractPythonMappingPlugin.java

Lines changed: 108 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,55 @@
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
*
2441
* @author jejkal
2542
*/
43+
@Component
2644
public abstract class AbstractPythonMappingPlugin implements IMappingPlugin {
2745

2846
private final Logger LOGGER = LoggerFactory.getLogger(AbstractPythonMappingPlugin.class);
47+
48+
/**
49+
* Application properties autowired at instantiation time.
50+
*/
51+
@Autowired
52+
private final ApplicationProperties applicationProperties;
2953
/**
3054
* The plugin name.
3155
*/
@@ -108,27 +132,28 @@ public AbstractPythonMappingPlugin(String pluginName, String repositoryUrl) {
108132

109133
/**
110134
* 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
135+
* plugin to gather all information required for starting a Python process
136+
* executing the mapping script. The returned array must contain at least
113137
* the following information:
114138
*
115139
* <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
140+
* with the working dir received as argument, where all checked-out code is
117141
* 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>
142+
* mappingFile, inputFile, and outputFile to the script execution. Depending
143+
* on the script implementation, the number and kind of required arguments
144+
* may differ.</li> </ul>
121145
*
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
146+
* Example: In standalone mode, a script is called via `plugin_wrapper.py
147+
* sem -m mappingFile -i inputFile -o outputFile -debug`. In that case, the
124148
* resulting array should look as follows: [workingDir +
125149
* "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.
150+
* inputFile.toString(), "-o", outputFile.toString(), "-debug"].
151+
*
152+
* The Python call itself will be added according to the Venv used for
153+
* plugin execution and must not be included.
129154
*
130155
* @param workingDir The working directory, i.e., where the plugin code was
131-
* checked out into.
156+
* checked-out into.
132157
* @param mappingFile The file which contains the mapping rules registered
133158
* at the mapping-service and used by the script.
134159
* @param inputFile The file which was uploaded by the user, i.e., the
@@ -165,23 +190,33 @@ public String uri() {
165190
@Override
166191
public void setup() {
167192
LOGGER.trace("Setting up mapping plugin {} {}", name(), version());
168-
193+
169194
//testing minimal Python version
170-
171-
172-
173-
195+
if (minPython != null) {
196+
if (!hasMinimalPythonVersion(minPython)) {
197+
throw new PluginInitializationFailedException("Minimal Python version '" + minPython + "' required by plugin not met.");
198+
}
199+
}
200+
201+
//checkout and install plugin
174202
try {
175-
LOGGER.info("Cloning git repository {}, Tag {}", repositoryUrl, tag);
176-
dir = FileUtil.cloneGitRepository(repositoryUrl, tag);
203+
LOGGER.info("Cloning git repository {}, tag {}", repositoryUrl, tag);
204+
dir = FileUtil.cloneGitRepository(repositoryUrl, tag, Paths.get(applicationProperties.getCodeLocation().toURI()).toAbsolutePath().toString());
177205
// Install Python dependencies
178206
MappingPluginState venvState = PythonRunnerUtil.runPythonScript("-m", "venv", "--system-site-packages", dir + "/" + pluginVenv);
179207
if (MappingPluginState.SUCCESS().getState().equals(venvState.getState())) {
180208
LOGGER.info("Venv for plugin installed successfully. Installing packages.");
181-
ShellRunnerUtil.run(dir + "/" + venvInterpreter, "-m", "pip", "install", "-r", dir + "/" + "requirements.dist.txt");
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+
}
182215
} else {
183-
throw new PluginInitializationFailedException("Venv installation was not successful. Status: " + venvState.getState());
216+
throw new PluginInitializationFailedException("Venv installation has failed. Status: " + venvState.getState());
184217
}
218+
} catch (URISyntaxException e) {
219+
throw new PluginInitializationFailedException("Invalid codeLocation configured in application.properties.", e);
185220
} catch (MappingPluginException e) {
186221
throw new PluginInitializationFailedException("Unexpected error during plugin setup.", e);
187222
}
@@ -190,16 +225,64 @@ public void setup() {
190225
@Override
191226
public MappingPluginState mapFile(Path mappingFile, Path inputFile, Path outputFile) throws MappingPluginException {
192227
long startTime = System.currentTimeMillis();
193-
LOGGER.trace("Run SEM-Mapping-Tool on '{}' with mapping '{}' -> '{}'", mappingFile, inputFile, outputFile);
228+
LOGGER.trace("Run mapping plugin {} {} on '{}' with mapping '{}' -> '{}'", name(), version(), mappingFile, inputFile, outputFile);
194229
String[] commandArray = getCommandArray(dir, mappingFile, inputFile, outputFile);
195230
List<String> command = new LinkedList<>();
196231
command.add(dir + "/" + venvInterpreter);
197232
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());
199233
MappingPluginState result = ShellRunnerUtil.run(command.toArray(String[]::new));
200234
long endTime = System.currentTimeMillis();
201235
long totalTime = endTime - startTime;
202236
LOGGER.info("Execution time of mapFile: {} milliseconds", totalTime);
203237
return result;
204238
}
239+
240+
/**
241+
* This method checks if the local Python installation version is larger or
242+
* equal the provided version number. The version should be provided as
243+
* semantic version number, i.e., 3.13.2
244+
*
245+
* The method will return TRUE if the minimal requirements are met and false
246+
* otherwise. False is also returned if obtaining/parsing the local python
247+
* version fails. for any reason.
248+
*
249+
* @param versionString The semantic version string to compare the local
250+
* Python version against.
251+
*
252+
* @return True if versionString is smaller or equal the local Python
253+
* version, false otherwise.
254+
*/
255+
private boolean hasMinimalPythonVersion(String versionString) {
256+
boolean result = false;
257+
try {
258+
LOGGER.trace("Checking for minimal Python version {}.", versionString);
259+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
260+
List<String> command = new LinkedList<>();
261+
command.add(dir + "/" + venvInterpreter);
262+
command.addAll(Arrays.asList("--version"));
263+
MappingPluginState state = ShellRunnerUtil.run(bout, System.err, command.toArray(String[]::new));
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/impl/GemmaPlugin.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
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.*;
2119
import org.slf4j.Logger;
2220
import org.slf4j.LoggerFactory;
2321
import org.springframework.util.MimeType;

src/main/java/edu/kit/datamanager/mappingservice/util/FileUtil.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
import org.apache.tika.mime.MimeType;
4343
import org.apache.tika.mime.MimeTypeException;
4444
import org.apache.tika.mime.MimeTypes;
45-
import org.eclipse.jgit.errors.RepositoryNotFoundException;
4645

4746
/**
4847
* Various utility methods for file handling.
@@ -282,20 +281,10 @@ private static String guessFileExtension(String filename, byte[] fewKilobytesOfF
282281
}
283282

284283
/**
285-
* This method clones a git repository into the 'lib' folder.
286-
*
287-
* @param repositoryUrl the url of the repository to clone
288-
* @param branch the branch to clone
289-
* @return the path to the cloned repository
290-
*/
291-
public static Path cloneGitRepository(String repositoryUrl, String branch) {
292-
String target = "lib/" + repositoryUrl.trim().replace("https://", "").replace("http://", "").replace(".git", "") + "_" + branch;
293-
return cloneGitRepository(repositoryUrl, branch, target);
294-
}
295-
296-
/**
297-
* This method clones a git repository into the 'lib' folder. If the folder
298-
* already exists, a pull is performed.
284+
* This method clones a git repository into the provided target folder. If
285+
* the folder already exists, a pull is performed, otherwise it is created
286+
* before. Typically, the target folder should be takes from property
287+
* 'mapping-service.codeLocation' obtained from ApplicationProperties.
299288
*
300289
* @param repositoryUrl the url of the repository to clone
301290
* @param branch the branch to clone

0 commit comments

Comments
 (0)