Skip to content

Commit 4fd8cd1

Browse files
committed
Fix execution of custom resolvers that output many lines
1 parent 993db23 commit 4fd8cd1

File tree

21 files changed

+248
-190
lines changed

21 files changed

+248
-190
lines changed

core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/edit/WriteResult.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* Represents the result of the operation of writing changes in the {@link AspectChangeManager} to the file system.
2020
*/
21-
sealed public interface WriteResult {
21+
public sealed interface WriteResult {
2222
interface Visitor<T> {
2323
T visitSuccess( Success success );
2424

core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/CommandExecutor.java

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,42 @@
1313

1414
package org.eclipse.esmf.aspectmodel.resolver;
1515

16-
import java.io.IOException;
16+
import java.io.File;
1717
import java.io.InputStream;
18+
import java.util.Arrays;
19+
import java.util.List;
20+
import java.util.Optional;
1821
import java.util.Scanner;
1922
import java.util.StringTokenizer;
2023

21-
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ModelResolutionException;
24+
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ProcessExecutionException;
25+
import org.eclipse.esmf.aspectmodel.resolver.process.BinaryLauncher;
26+
import org.eclipse.esmf.aspectmodel.resolver.process.ExecutableJarLauncher;
27+
import org.eclipse.esmf.aspectmodel.resolver.process.ProcessLauncher;
2228

2329
/**
2430
* Executes an external resolver via the underlying OS command and returns the stdout from the command as result.
2531
*/
2632
public class CommandExecutor {
27-
public static String executeCommand( String command ) {
28-
// convenience: if just the name of the jar is given, expand to the proper java invocation command
29-
if ( isJarInvocation( command ) ) {
30-
command = String.format( "%s -jar %s", ProcessHandle.current().info().command().orElse( "java" ), command );
31-
}
33+
public static String executeCommand( final String command ) {
34+
final List<String> parts = Arrays.asList( command.split( " " ) );
35+
final String executableOrJar = parts.get( 0 );
36+
final ProcessLauncher processLauncher = executableOrJar.toLowerCase().endsWith( ".jar" )
37+
? new ExecutableJarLauncher( new File( executableOrJar ) )
38+
: new BinaryLauncher( new File( executableOrJar ) );
39+
final List<String> arguments = parts.size() == 1
40+
? List.of()
41+
: parts.subList( 1, parts.size() );
42+
final ProcessLauncher.ExecutionContext context = new ProcessLauncher.ExecutionContext(
43+
arguments, Optional.empty(), new File( System.getProperty( "user.dir" ) ) );
44+
final ProcessLauncher.ExecutionResult result = processLauncher.apply( context );
3245

33-
try {
34-
final Process p = Runtime.getRuntime().exec( command );
35-
final int result = p.waitFor();
36-
if ( result != 0 ) {
37-
throw new ModelResolutionException( getOutputFrom( p.getErrorStream() ) );
38-
}
39-
return getOutputFrom( p.getInputStream() );
40-
} catch ( final IOException | InterruptedException exception ) {
41-
throw new ModelResolutionException( "The attempt to execute external resolver failed with the error:", exception );
46+
if ( result.exitStatus() == 0 ) {
47+
return result.stdout();
4248
}
49+
50+
throw new ProcessExecutionException( "Execution of '" + executableOrJar + "' failed (status " + result.exitStatus() + "). "
51+
+ "Error output: " + result.stderr() );
4352
}
4453

4554
private static boolean isJarInvocation( final String command ) {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH
3+
*
4+
* See the AUTHORS file(s) distributed with this work for additional
5+
* information regarding authorship.
6+
*
7+
* This Source Code Form is subject to the terms of the Mozilla Public
8+
* License, v. 2.0. If a copy of the MPL was not distributed with this
9+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
10+
*
11+
* SPDX-License-Identifier: MPL-2.0
12+
*/
13+
14+
package org.eclipse.esmf.aspectmodel.resolver.exceptions;
15+
16+
public class ProcessExecutionException extends RuntimeException {
17+
public ProcessExecutionException( final String message ) {
18+
super( message );
19+
}
20+
21+
public ProcessExecutionException( final Exception exception ) {
22+
super( exception );
23+
}
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH
3+
*
4+
* See the AUTHORS file(s) distributed with this work for additional
5+
* information regarding authorship.
6+
*
7+
* This Source Code Form is subject to the terms of the Mozilla Public
8+
* License, v. 2.0. If a copy of the MPL was not distributed with this
9+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
10+
*
11+
* SPDX-License-Identifier: MPL-2.0
12+
*/
13+
14+
package org.eclipse.esmf.aspectmodel.resolver.process;
15+
16+
import java.io.File;
17+
import java.util.List;
18+
19+
/**
20+
* A {@link ProcessLauncher} that executes a native binary.
21+
*/
22+
public class BinaryLauncher extends OsProcessLauncher {
23+
public BinaryLauncher( final File binary ) {
24+
super( List.of( binary.getAbsolutePath() ) );
25+
}
26+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH
3+
*
4+
* See the AUTHORS file(s) distributed with this work for additional
5+
* information regarding authorship.
6+
*
7+
* This Source Code Form is subject to the terms of the Mozilla Public
8+
* License, v. 2.0. If a copy of the MPL was not distributed with this
9+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
10+
*
11+
* SPDX-License-Identifier: MPL-2.0
12+
*/
13+
14+
package org.eclipse.esmf.aspectmodel.resolver.process;
15+
16+
import java.io.File;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
20+
/**
21+
* A {@link ProcessLauncher} that executes an executable jar.
22+
*/
23+
public class ExecutableJarLauncher extends OsProcessLauncher {
24+
public ExecutableJarLauncher( final File executableJar ) {
25+
this( executableJar, List.of() );
26+
}
27+
28+
public ExecutableJarLauncher( final File executableJar, final List<String> jvmArguments ) {
29+
super( buildCommand( executableJar, jvmArguments ) );
30+
}
31+
32+
private static List<String> buildCommand( final File executableJar, final List<String> jvmArguments ) {
33+
final List<String> commandWithArguments = new ArrayList<>();
34+
commandWithArguments.add( ProcessHandle.current().info().command().orElse( "java" ) );
35+
commandWithArguments.addAll( jvmArguments );
36+
commandWithArguments.add( "-jar" );
37+
commandWithArguments.add( executableJar.getAbsolutePath() );
38+
return commandWithArguments;
39+
}
40+
}

tools/samm-cli/src/test/java/org/eclipse/esmf/OsProcessLauncher.java renamed to core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/process/OsProcessLauncher.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH
2+
* Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH
33
*
44
* See the AUTHORS file(s) distributed with this work for additional
55
* information regarding authorship.
@@ -11,9 +11,7 @@
1111
* SPDX-License-Identifier: MPL-2.0
1212
*/
1313

14-
package org.eclipse.esmf;
15-
16-
import static org.junit.jupiter.api.Assertions.fail;
14+
package org.eclipse.esmf.aspectmodel.resolver.process;
1715

1816
import java.io.ByteArrayInputStream;
1917
import java.io.ByteArrayOutputStream;
@@ -30,6 +28,8 @@
3028
import java.util.concurrent.Future;
3129
import java.util.stream.Collectors;
3230

31+
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ProcessExecutionException;
32+
3333
import org.apache.commons.io.IOUtils;
3434
import org.slf4j.Logger;
3535
import org.slf4j.LoggerFactory;
@@ -82,16 +82,15 @@ public ExecutionResult apply( final ExecutionContext context ) {
8282
stdoutRaw = stdoutFuture.get().toByteArray();
8383
stderrRaw = stderrFuture.get().toByteArray();
8484
} catch ( final ExecutionException | InterruptedException exception ) {
85-
throw new RuntimeException( exception );
85+
throw new ProcessExecutionException( exception );
8686
}
8787

8888
return new ExecutionResult( process.exitValue(), new String( stdoutRaw, StandardCharsets.UTF_8 ),
8989
new String( stderrRaw, StandardCharsets.UTF_8 ),
9090
stdoutRaw, stderrRaw );
9191
} catch ( final IOException | InterruptedException exception ) {
92-
fail( exception );
92+
throw new ProcessExecutionException( exception );
9393
}
94-
return null;
9594
}
9695

9796
/**
Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH
2+
* Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH
33
*
44
* See the AUTHORS file(s) distributed with this work for additional
55
* information regarding authorship.
@@ -11,16 +11,16 @@
1111
* SPDX-License-Identifier: MPL-2.0
1212
*/
1313

14-
package org.eclipse.esmf;
15-
16-
import static org.junit.jupiter.api.Assertions.fail;
14+
package org.eclipse.esmf.aspectmodel.resolver.process;
1715

1816
import java.io.File;
1917
import java.util.Arrays;
2018
import java.util.List;
2119
import java.util.Optional;
2220
import java.util.function.Function;
2321

22+
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ProcessExecutionException;
23+
2424
/**
2525
* This class abstracts running a "process", i.e. running a program by providing its arguments, optional stdin and its working directory,
2626
* and representing the output using exit status and stdout and stderr streams.
@@ -36,20 +36,21 @@ public ExecutionResult apply( final String... arguments ) {
3636
public ExecutionResult runAndExpectSuccess( final String... arguments ) {
3737
final ExecutionResult result = apply( arguments );
3838
if ( result.exitStatus() != 0 ) {
39-
System.out.printf( "Execution failed (status %d):%n", result.exitStatus() );
40-
System.out.println( "stdout:" );
41-
System.out.println( result.stdout() );
42-
System.out.println();
43-
System.out.println( "stderr:" );
44-
System.out.println( result.stderr() );
45-
fail();
39+
throw new ProcessExecutionException(
40+
"Execution failed (status " + result.exitStatus + "):\n"
41+
+ "stdout:"
42+
+ result.stdout()
43+
+ "\n"
44+
+ "stderr:"
45+
+ result.stderr()
46+
);
4647
}
4748
return result;
4849
}
4950

50-
public static record ExecutionContext( List<String> arguments, Optional<byte[]> stdin, File workingDirectory ) {
51+
public record ExecutionContext( List<String> arguments, Optional<byte[]> stdin, File workingDirectory ) {
5152
}
5253

53-
public static record ExecutionResult( int exitStatus, String stdout, String stderr, byte[] stdoutRaw, byte[] stderrRaw ) {
54+
public record ExecutionResult( int exitStatus, String stdout, String stderr, byte[] stdoutRaw, byte[] stderrRaw ) {
5455
}
5556
}

core/esmf-aspect-model-java-generator/src/test/java/org/eclipse/esmf/aspectmodel/java/AspectModelJavaGeneratorTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.util.List;
2929
import java.util.Optional;
3030
import java.util.Set;
31-
3231
import javax.xml.datatype.Duration;
3332
import javax.xml.datatype.XMLGregorianCalendar;
3433

tools/samm-cli/src/main/java/org/eclipse/esmf/AbstractInputHandler.java

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
package org.eclipse.esmf;
1515

1616
import java.nio.file.Path;
17-
import java.util.ArrayList;
1817
import java.util.List;
18+
import java.util.Optional;
1919
import java.util.function.Function;
2020
import java.util.stream.Collectors;
21+
import java.util.stream.Stream;
2122

2223
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
2324
import org.eclipse.esmf.aspectmodel.resolver.ExternalResolverStrategy;
@@ -68,35 +69,43 @@ public AbstractInputHandler( final String input, final ResolverConfigurationMixi
6869
protected abstract String expectedAspectName();
6970

7071
protected List<ResolutionStrategy> configuredStrategies() {
71-
final List<ResolutionStrategy> strategies = new ArrayList<>();
7272
if ( resolverConfig == null ) {
73-
return strategies;
73+
return List.of();
7474
}
75-
final List<String> modelsRoots = resolverConfig.modelsRoots == null
76-
? List.of()
77-
: resolverConfig.modelsRoots;
78-
for ( final String modelsRoot : modelsRoots ) {
79-
strategies.add( new FileSystemStrategy( new StructuredModelsRoot( Path.of( modelsRoot ) ) ) );
80-
}
81-
if ( resolverConfig.commandLine != null && !resolverConfig.commandLine.isBlank() ) {
82-
strategies.add( new ExternalResolverStrategy( resolverConfig.commandLine ) );
83-
}
84-
if ( resolverConfig.gitHubResolutionOptions != null && resolverConfig.gitHubResolutionOptions.gitHubName != null ) {
85-
final String[] parts = resolverConfig.gitHubResolutionOptions.gitHubName.split( "/" );
86-
final String owner = parts[0];
87-
final String repositoryName = parts[1];
88-
final GithubRepository.Ref branchOrTag = resolverConfig.gitHubResolutionOptions.gitHubTag != null
89-
? new GithubRepository.Tag( resolverConfig.gitHubResolutionOptions.gitHubTag )
90-
: new GithubRepository.Branch( resolverConfig.gitHubResolutionOptions.gitHubBranch );
91-
final GithubRepository repository = new GithubRepository( owner, repositoryName, branchOrTag );
92-
final GithubModelSourceConfig modelSourceConfig = GithubModelSourceConfigBuilder.builder()
93-
.repository( repository )
94-
.directory( resolverConfig.gitHubResolutionOptions.gitHubDirectory )
95-
.token( resolverConfig.gitHubResolutionOptions.gitHubToken )
96-
.build();
97-
strategies.add( new GitHubStrategy( modelSourceConfig ) );
75+
return Stream.of(
76+
Optional.ofNullable( resolverConfig.modelsRoots ).orElse( List.of() )
77+
.stream()
78+
.map( modelsRoot -> new FileSystemStrategy( new StructuredModelsRoot( Path.of( modelsRoot ) ) ) ),
79+
Optional.ofNullable( resolverConfig.commandLine )
80+
.orElse( List.of() )
81+
.stream()
82+
.map( ExternalResolverStrategy::new ),
83+
Optional.ofNullable( resolverConfig.gitHubResolverOptions ).orElse( List.of() )
84+
.stream()
85+
.map( options -> buildGithubModelSourceConfig( options, resolverConfig.gitHubToken ) )
86+
.flatMap( Optional::stream )
87+
.map( GitHubStrategy::new ) )
88+
.<ResolutionStrategy> flatMap( Function.identity() )
89+
.toList();
90+
}
91+
92+
private Optional<GithubModelSourceConfig> buildGithubModelSourceConfig(
93+
final ResolverConfigurationMixin.GitHubResolverOptions options, final String gitHubToken ) {
94+
if ( options.gitHubName == null ) {
95+
return Optional.empty();
9896
}
99-
return strategies;
97+
final String[] parts = options.gitHubName.split( "/" );
98+
final String owner = parts[0];
99+
final String repositoryName = parts[1];
100+
final GithubRepository.Ref branchOrTag = options.gitHubTag != null
101+
? new GithubRepository.Tag( options.gitHubTag )
102+
: new GithubRepository.Branch( options.gitHubBranch );
103+
final GithubRepository repository = new GithubRepository( owner, repositoryName, branchOrTag );
104+
return Optional.of( GithubModelSourceConfigBuilder.builder()
105+
.repository( repository )
106+
.directory( options.gitHubDirectory )
107+
.token( gitHubToken )
108+
.build() );
100109
}
101110

102111
@Override

tools/samm-cli/src/main/java/org/eclipse/esmf/AspectModelUrnInputHandler.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ private AspectModelUrn urnFromInput( final String input ) {
4646
protected List<ResolutionStrategy> resolutionStrategies() {
4747
final boolean noName = urn.getName().isEmpty();
4848
final boolean noResolverConfig = resolverConfig == null;
49-
final boolean noGitHubResolver =
50-
noResolverConfig || resolverConfig.gitHubResolutionOptions == null || resolverConfig.gitHubResolutionOptions.gitHubName == null;
49+
final boolean noGitHubResolver = noResolverConfig || resolverConfig.gitHubResolverOptions == null
50+
|| resolverConfig.gitHubResolverOptions.isEmpty();
5151
final boolean noLocalModelRoots = noResolverConfig || resolverConfig.modelsRoots == null || resolverConfig.modelsRoots.isEmpty();
52-
if ( !noName && noGitHubResolver && noLocalModelRoots ) {
53-
throw new CommandException( "When resolving a URN, at least one models root directory or GitHub repository must be set" );
52+
final boolean noCustomResolver = noResolverConfig || resolverConfig.commandLine == null || resolverConfig.commandLine.isEmpty();
53+
if ( !noName && noGitHubResolver && noLocalModelRoots && noCustomResolver ) {
54+
throw new CommandException(
55+
"When resolving a URN, at least one models root directory, GitHub repository or custom resolver must be set" );
5456
}
5557
return configuredStrategies();
5658
}

0 commit comments

Comments
 (0)