1
1
/*
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.
4
14
*/
5
15
package edu .kit .datamanager .mappingservice .plugins ;
6
16
17
+ import edu .kit .datamanager .mappingservice .configuration .ApplicationProperties ;
7
18
import edu .kit .datamanager .mappingservice .exception .PluginInitializationFailedException ;
8
19
import edu .kit .datamanager .mappingservice .util .FileUtil ;
9
20
import edu .kit .datamanager .mappingservice .util .PythonRunnerUtil ;
10
21
import edu .kit .datamanager .mappingservice .util .ShellRunnerUtil ;
22
+ import java .io .ByteArrayOutputStream ;
11
23
import java .io .IOException ;
12
24
import java .io .InputStream ;
25
+ import java .net .URISyntaxException ;
13
26
import java .net .URL ;
14
27
import java .nio .file .Path ;
28
+ import java .nio .file .Paths ;
15
29
import java .util .Arrays ;
16
30
import java .util .LinkedList ;
17
31
import java .util .List ;
18
32
import java .util .Properties ;
33
+ import org .apache .maven .artifact .versioning .ComparableVersion ;
19
34
import org .slf4j .Logger ;
20
35
import org .slf4j .LoggerFactory ;
36
+ import org .springframework .beans .factory .annotation .Autowired ;
37
+ import org .springframework .stereotype .Component ;
21
38
22
39
/**
23
40
*
26
43
public abstract class AbstractPythonMappingPlugin implements IMappingPlugin {
27
44
28
45
private final Logger LOGGER = LoggerFactory .getLogger (AbstractPythonMappingPlugin .class );
46
+
29
47
/**
30
48
* The plugin name.
31
49
*/
@@ -108,27 +126,28 @@ public AbstractPythonMappingPlugin(String pluginName, String repositoryUrl) {
108
126
109
127
/**
110
128
* 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
113
131
* the following information:
114
132
*
115
133
* <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
117
135
* 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>
121
139
*
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
124
142
* resulting array should look as follows: [workingDir +
125
143
* "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.
129
148
*
130
149
* @param workingDir The working directory, i.e., where the plugin code was
131
- * checked out into.
150
+ * checked- out into.
132
151
* @param mappingFile The file which contains the mapping rules registered
133
152
* at the mapping-service and used by the script.
134
153
* @param inputFile The file which was uploaded by the user, i.e., the
@@ -163,25 +182,44 @@ public String uri() {
163
182
}
164
183
165
184
@ Override
166
- public void setup () {
185
+ public void setup (ApplicationProperties applicationProperties ) {
167
186
LOGGER .trace ("Setting up mapping plugin {} {}" , name (), version ());
168
-
187
+
169
188
//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
174
196
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 ());
177
202
// Install Python dependencies
178
203
MappingPluginState venvState = PythonRunnerUtil .runPythonScript ("-m" , "venv" , "--system-site-packages" , dir + "/" + pluginVenv );
179
204
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
+ }
182
218
} else {
183
- throw new PluginInitializationFailedException ("Venv installation was not successful . Status: " + venvState .getState ());
219
+ throw new PluginInitializationFailedException ("Venv installation has failed . Status: " + venvState .getState ());
184
220
}
221
+ } catch (URISyntaxException e ) {
222
+ throw new PluginInitializationFailedException ("Invalid codeLocation configured in application.properties." , e );
185
223
} catch (MappingPluginException e ) {
186
224
throw new PluginInitializationFailedException ("Unexpected error during plugin setup." , e );
187
225
}
@@ -190,16 +228,61 @@ public void setup() {
190
228
@ Override
191
229
public MappingPluginState mapFile (Path mappingFile , Path inputFile , Path outputFile ) throws MappingPluginException {
192
230
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 );
194
232
String [] commandArray = getCommandArray (dir , mappingFile , inputFile , outputFile );
195
233
List <String > command = new LinkedList <>();
196
234
command .add (dir + "/" + venvInterpreter );
197
235
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());
199
236
MappingPluginState result = ShellRunnerUtil .run (command .toArray (String []::new ));
200
237
long endTime = System .currentTimeMillis ();
201
238
long totalTime = endTime - startTime ;
202
239
LOGGER .info ("Execution time of mapFile: {} milliseconds" , totalTime );
203
240
return result ;
204
241
}
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
+ }
205
288
}
0 commit comments