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
*
24
41
* @author jejkal
25
42
*/
43
+ @ Component
26
44
public abstract class AbstractPythonMappingPlugin implements IMappingPlugin {
27
45
28
46
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 ;
29
53
/**
30
54
* The plugin name.
31
55
*/
@@ -108,27 +132,28 @@ public AbstractPythonMappingPlugin(String pluginName, String repositoryUrl) {
108
132
109
133
/**
110
134
* 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
113
137
* the following information:
114
138
*
115
139
* <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
117
141
* 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>
121
145
*
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
124
148
* resulting array should look as follows: [workingDir +
125
149
* "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.
129
154
*
130
155
* @param workingDir The working directory, i.e., where the plugin code was
131
- * checked out into.
156
+ * checked- out into.
132
157
* @param mappingFile The file which contains the mapping rules registered
133
158
* at the mapping-service and used by the script.
134
159
* @param inputFile The file which was uploaded by the user, i.e., the
@@ -165,23 +190,33 @@ public String uri() {
165
190
@ Override
166
191
public void setup () {
167
192
LOGGER .trace ("Setting up mapping plugin {} {}" , name (), version ());
168
-
193
+
169
194
//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
174
202
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 () );
177
205
// Install Python dependencies
178
206
MappingPluginState venvState = PythonRunnerUtil .runPythonScript ("-m" , "venv" , "--system-site-packages" , dir + "/" + pluginVenv );
179
207
if (MappingPluginState .SUCCESS ().getState ().equals (venvState .getState ())) {
180
208
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
+ }
182
215
} else {
183
- throw new PluginInitializationFailedException ("Venv installation was not successful . Status: " + venvState .getState ());
216
+ throw new PluginInitializationFailedException ("Venv installation has failed . Status: " + venvState .getState ());
184
217
}
218
+ } catch (URISyntaxException e ) {
219
+ throw new PluginInitializationFailedException ("Invalid codeLocation configured in application.properties." , e );
185
220
} catch (MappingPluginException e ) {
186
221
throw new PluginInitializationFailedException ("Unexpected error during plugin setup." , e );
187
222
}
@@ -190,16 +225,64 @@ public void setup() {
190
225
@ Override
191
226
public MappingPluginState mapFile (Path mappingFile , Path inputFile , Path outputFile ) throws MappingPluginException {
192
227
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 );
194
229
String [] commandArray = getCommandArray (dir , mappingFile , inputFile , outputFile );
195
230
List <String > command = new LinkedList <>();
196
231
command .add (dir + "/" + venvInterpreter );
197
232
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
233
MappingPluginState result = ShellRunnerUtil .run (command .toArray (String []::new ));
200
234
long endTime = System .currentTimeMillis ();
201
235
long totalTime = endTime - startTime ;
202
236
LOGGER .info ("Execution time of mapFile: {} milliseconds" , totalTime );
203
237
return result ;
204
238
}
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
+ }
205
288
}
0 commit comments