Skip to content

Commit 993db23

Browse files
committed
Add package and package import subcommands in samm-cli
1 parent 17e21a4 commit 993db23

File tree

10 files changed

+407
-92
lines changed

10 files changed

+407
-92
lines changed
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.edit;
15+
16+
import io.soabase.recordbuilder.core.RecordBuilder;
17+
18+
/**
19+
* Configures the file system writing operation in {@link AspectChangeManager}
20+
*
21+
* @param forceOverwrite determines whether existing files should be overwritten
22+
*/
23+
@RecordBuilder
24+
public record WriteConfig(
25+
boolean forceOverwrite
26+
) {}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.edit;
15+
16+
import java.util.List;
17+
18+
/**
19+
* Represents the result of the operation of writing changes in the {@link AspectChangeManager} to the file system.
20+
*/
21+
sealed public interface WriteResult {
22+
interface Visitor<T> {
23+
T visitSuccess( Success success );
24+
25+
T visitWriteFailure( WriteFailure failure );
26+
27+
T visitPreconditionsNotMet( PreconditionsNotMet preconditionsNotMet );
28+
}
29+
30+
<T> T accept( Visitor<T> visitor );
31+
32+
record PreconditionsNotMet(
33+
List<String> errorMessages,
34+
boolean canBeFixedByOverwriting
35+
) implements WriteResult {
36+
@Override
37+
public <T> T accept( final Visitor<T> visitor ) {
38+
return visitor.visitPreconditionsNotMet( this );
39+
}
40+
}
41+
42+
record WriteFailure(
43+
List<String> errorMessages
44+
) implements WriteResult {
45+
@Override
46+
public <T> T accept( final Visitor<T> visitor ) {
47+
return visitor.visitWriteFailure( this );
48+
}
49+
}
50+
51+
final class Success implements WriteResult {
52+
@Override
53+
public <T> T accept( final Visitor<T> visitor ) {
54+
return visitor.visitSuccess( this );
55+
}
56+
}
57+
}

documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,14 @@ of model resolution] for more information.
240240
run `samm aas <aas file> list` to list them. | `samm aas AssetAdminShell.aasx to aspect -s 1 -s 2`
241241
.1+| [[aas-list]] aas <aas file> list | Retrieve a list of submodel templates contained within the provided
242242
Asset Administration Shell (AAS) file. | `samm aas AssetAdminShell.aasx list`
243-
243+
.4+| [[package-list]] package <namespace package> import | Imports a Namespace Package (file or URL) into a given
244+
xref:models-directory-structure[models directory] | `samm package MyPackage.zip import --models-root c:\models`
245+
| _--dry-run_ : Don't write changes to the file system, but print a report of changes
246+
that would be performed. |
247+
| _--details_ : When used with `--dry-run`, include details about model content
248+
changes in the report . |
249+
| _--force_ : When a new file is to be created but it already exists in the file system,
250+
the operation will be cancelled, unless `--force` is used. |
244251
|===
245252

246253
[[configuration-of-model-resolution]]

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

Lines changed: 34 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,24 @@
1818
import java.io.FileOutputStream;
1919
import java.io.IOException;
2020
import java.io.OutputStream;
21-
import java.net.URI;
2221
import java.nio.file.Path;
23-
import java.nio.file.Paths;
24-
import java.util.ArrayList;
25-
import java.util.List;
2622
import java.util.Locale;
27-
import java.util.Optional;
2823
import java.util.Set;
2924
import java.util.function.Consumer;
25+
import java.util.stream.Collectors;
3026

3127
import org.eclipse.esmf.aspectmodel.edit.AspectChangeManager;
3228
import org.eclipse.esmf.aspectmodel.edit.AspectChangeManagerConfig;
3329
import org.eclipse.esmf.aspectmodel.edit.Change;
3430
import org.eclipse.esmf.aspectmodel.edit.ChangeReport;
3531
import org.eclipse.esmf.aspectmodel.edit.ChangeReportFormatter;
32+
import org.eclipse.esmf.aspectmodel.edit.WriteConfig;
33+
import org.eclipse.esmf.aspectmodel.edit.WriteConfigBuilder;
34+
import org.eclipse.esmf.aspectmodel.edit.WriteResult;
3635
import org.eclipse.esmf.aspectmodel.generator.LanguageCollector;
3736
import org.eclipse.esmf.aspectmodel.generator.diagram.AspectModelDiagramGenerator;
3837
import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfig;
3938
import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfigBuilder;
40-
import org.eclipse.esmf.aspectmodel.serializer.AspectSerializer;
4139
import org.eclipse.esmf.exception.CommandException;
4240
import org.eclipse.esmf.metamodel.Aspect;
4341
import org.eclipse.esmf.metamodel.AspectModel;
@@ -145,80 +143,50 @@ private String getParentDirectory( final String filePath ) {
145143
return new File( filePath ).getParent();
146144
}
147145

148-
protected Optional<AspectChangeManager> performRefactoring( final AspectModel aspectModel, final Change change,
149-
final AspectChangeManagerConfig config, final boolean dryRun ) {
146+
protected void performRefactoring( final AspectModel aspectModel, final Change change,
147+
final AspectChangeManagerConfig config, final boolean dryRun, final boolean forceOverwrite ) {
150148
final AspectChangeManager changeContext = new AspectChangeManager( config, aspectModel );
151149
final ChangeReport changeReport = changeContext.applyChange( change );
152150
if ( dryRun ) {
153151
System.out.println( "Changes to be performed" );
154152
System.out.println( "=======================" );
155153
System.out.println( ChangeReportFormatter.INSTANCE.apply( changeReport, config ) );
156-
return Optional.empty();
154+
return;
157155
}
158-
return Optional.of( changeContext );
159-
}
160-
161-
protected void performFileSystemWrite( final AspectChangeManager changeContext ) {
162-
changeContext.removedFiles()
163-
.map( fileToRemove -> Paths.get( fileToRemove.sourceLocation().orElseThrow() ).toFile() )
164-
.filter( file -> !file.delete() )
165-
.forEach( file -> {
166-
throw new CommandException( "Could not delete file: " + file );
167-
} );
168-
changeContext.createdFiles().forEach( fileToCreate -> {
169-
final File file = Paths.get( fileToCreate.sourceLocation().orElseThrow() ).toFile();
170-
file.getParentFile().mkdirs();
171-
AspectSerializer.INSTANCE.write( fileToCreate );
172-
} );
173-
changeContext.modifiedFiles().forEach( AspectSerializer.INSTANCE::write );
174-
}
175156

176-
protected void checkFilesystemConsistency( final AspectChangeManager changeContext, final boolean force ) {
177-
final List<String> messages = new ArrayList<>();
178-
changeContext.removedFiles().map( AspectSerializer.INSTANCE::aspectModelFileUrl ).forEach( url -> {
179-
if ( !url.getProtocol().equals( "file" ) ) {
180-
messages.add( "File should be removed, but it is not identified by a file: URL: " + url );
181-
}
182-
final File file = new File( URI.create( url.toString() ) );
183-
if ( !file.exists() ) {
184-
messages.add( "File should be removed, but it does not exist: " + file );
157+
final WriteConfig writeConfig = WriteConfigBuilder.builder().forceOverwrite( forceOverwrite ).build();
158+
final WriteResult writeResult = changeContext.writeChangesToDisk( writeConfig );
159+
System.err.print( writeResult.accept( new WriteResult.Visitor<String>() {
160+
@Override
161+
public String visitSuccess( final WriteResult.Success success ) {
162+
return "";
185163
}
186-
} );
187164

188-
changeContext.createdFiles().map( AspectSerializer.INSTANCE::aspectModelFileUrl ).forEach( url -> {
189-
if ( !url.getProtocol().equals( "file" ) ) {
190-
messages.add( "New file should be written, but it is not identified by a file: URL: " + url );
191-
}
192-
final File file = new File( URI.create( url.toString() ) );
193-
if ( file.exists() && !force ) {
194-
messages.add(
195-
"New file should be written, but it already exists: " + file + ". Use the --force flag to force overwriting." );
165+
@Override
166+
public String visitWriteFailure( final WriteResult.WriteFailure failure ) {
167+
return "Writing failed:\n"
168+
+ failure.errorMessages().stream()
169+
.map( message -> "- " + message )
170+
.collect( Collectors.joining( "\n" ) ) + "\n";
196171
}
197-
if ( file.exists() && force && !file.canWrite() ) {
198-
messages.add( "New file should be written, but it is not writable:" + file );
199-
}
200-
} );
201172

202-
changeContext.modifiedFiles().map( AspectSerializer.INSTANCE::aspectModelFileUrl ).forEach( url -> {
203-
if ( !url.getProtocol().equals( "file" ) ) {
204-
messages.add( "File should be modified, but it is not identified by a file: URL: " + url );
205-
}
206-
final File file = new File( URI.create( url.toString() ) );
207-
if ( !file.exists() ) {
208-
messages.add( "File should be modified, but it does not exist: " + file );
209-
}
210-
if ( !file.canWrite() ) {
211-
messages.add( "File should be modified, but it is not writable: " + file );
173+
@Override
174+
public String visitPreconditionsNotMet( final WriteResult.PreconditionsNotMet preconditionsNotMet ) {
175+
final StringBuilder builder = new StringBuilder();
176+
builder.append( "Encountered problems, cancelling writing:\n" );
177+
preconditionsNotMet.errorMessages().stream()
178+
.map( message -> "- " + message + "\n" )
179+
.forEach( builder::append );
180+
if ( preconditionsNotMet.canBeFixedByOverwriting() ) {
181+
builder.append( "Add --force to force overwriting existing files.\n" );
182+
}
183+
return builder.toString();
212184
}
213-
if ( !file.isFile() ) {
214-
messages.add( "File should be modified, but it is not a regular file: " + file );
215-
}
216-
} );
185+
} ) );
217186

218-
if ( !messages.isEmpty() ) {
219-
System.out.println( "Encountered problems, canceling writing." );
220-
messages.forEach( message -> System.out.println( "- " + message ) );
221-
System.exit( 1 );
187+
if ( writeResult instanceof WriteResult.Success ) {
188+
return;
222189
}
190+
System.exit( 1 );
223191
}
224192
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.eclipse.esmf.aspect.to.AspectToSvgCommand;
3535
import org.eclipse.esmf.exception.CommandException;
3636
import org.eclipse.esmf.exception.SubCommandException;
37+
import org.eclipse.esmf.namespacepackage.PackageCommand;
3738
import org.eclipse.esmf.substitution.IsWindows;
3839

3940
import org.fusesource.jansi.AnsiConsole;
@@ -100,6 +101,7 @@ public SammCli() {
100101
final CommandLine initialCommandLine = new CommandLine( this )
101102
.addSubcommand( new AspectCommand() )
102103
.addSubcommand( new AasCommand() )
104+
.addSubcommand( new PackageCommand() )
103105
.setCaseInsensitiveEnumValuesAllowed( true )
104106
.setExecutionStrategy( LoggingMixin::executionStrategy );
105107
initialCommandLine.getHelpSectionMap().put( SECTION_KEY_COMMAND_LIST, new CustomCommandListRenderer() );

tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/edit/AspectEditMoveCommand.java

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,7 @@ private void moveElementToNewFile( final File inputFile ) {
170170
final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder()
171171
.detailedChangeReport( details )
172172
.build();
173-
performRefactoring( aspectModel, move, config, dryRun ).ifPresent( changeContext -> {
174-
// Check & write changes to file system
175-
checkFilesystemConsistency( changeContext, force );
176-
performFileSystemWrite( changeContext );
177-
} );
173+
performRefactoring( aspectModel, move, config, dryRun, force );
178174
}
179175

180176
/**
@@ -200,11 +196,7 @@ private void moveElementToOtherNamespaceNewFile( final AspectModelUrn targetName
200196
final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder()
201197
.detailedChangeReport( details )
202198
.build();
203-
performRefactoring( aspectModel, move, config, dryRun ).ifPresent( changeContext -> {
204-
// Check & write changes to file system
205-
checkFilesystemConsistency( changeContext, force );
206-
performFileSystemWrite( changeContext );
207-
} );
199+
performRefactoring( aspectModel, move, config, dryRun, force );
208200
}
209201

210202
/**
@@ -227,11 +219,7 @@ private void moveElementToExistingFile( final File targetFileRelativeToInput ) {
227219
final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder()
228220
.detailedChangeReport( details )
229221
.build();
230-
performRefactoring( aspectModel, move, config, dryRun ).ifPresent( changeContext -> {
231-
// Check & write changes to file system
232-
checkFilesystemConsistency( changeContext, force );
233-
performFileSystemWrite( changeContext );
234-
} );
222+
performRefactoring( aspectModel, move, config, dryRun, force );
235223
}
236224

237225
/**
@@ -255,11 +243,7 @@ private void moveElementToOtherNamespaceExistingFile( final AspectModelUrn targe
255243
final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder()
256244
.detailedChangeReport( details )
257245
.build();
258-
performRefactoring( aspectModel, move, config, dryRun ).ifPresent( changeContext -> {
259-
// Check & write changes to file system
260-
checkFilesystemConsistency( changeContext, force );
261-
performFileSystemWrite( changeContext );
262-
} );
246+
performRefactoring( aspectModel, move, config, dryRun, force );
263247
}
264248

265249
private AspectModelFile determineTargetAspectModelFile( final AspectModel aspectModel, final Namespace targetNamespace ) {

tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/edit/AspectEditNewVersionCommand.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ static class VersionPart {
9898
)
9999
private boolean force;
100100

101+
@SuppressWarnings( "UseOfSystemOutOrSystemErr" )
101102
@Override
102103
public void run() {
103104
setDetails( details );
@@ -142,10 +143,6 @@ public void run() {
142143
final AspectChangeManagerConfig config = AspectChangeManagerConfigBuilder.builder()
143144
.detailedChangeReport( details )
144145
.build();
145-
performRefactoring( aspectModel, copy, config, dryRun ).ifPresent( changeContext -> {
146-
// Check & write changes to file system
147-
checkFilesystemConsistency( changeContext, force );
148-
performFileSystemWrite( changeContext );
149-
} );
146+
performRefactoring( aspectModel, copy, config, dryRun, force );
150147
}
151148
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.namespacepackage;
15+
16+
import org.eclipse.esmf.AbstractCommand;
17+
import org.eclipse.esmf.LoggingMixin;
18+
import org.eclipse.esmf.exception.SubCommandException;
19+
20+
import picocli.CommandLine;
21+
22+
/**
23+
* Top-level command for working with Namespace Packages
24+
*/
25+
@CommandLine.Command(
26+
name = PackageCommand.COMMAND_NAME,
27+
description = "Import and export Namespace Packages",
28+
subcommands = {
29+
CommandLine.HelpCommand.class,
30+
PackageImportCommand.class
31+
},
32+
headerHeading = "@|bold Usage|@:%n%n",
33+
descriptionHeading = "%n@|bold Description|@:%n%n",
34+
parameterListHeading = "%n@|bold Parameters|@:%n",
35+
optionListHeading = "%n@|bold Options|@:%n"
36+
)
37+
public class PackageCommand extends AbstractCommand {
38+
public static final String COMMAND_NAME = "package";
39+
40+
@CommandLine.Mixin
41+
private LoggingMixin loggingMixin;
42+
43+
@CommandLine.Parameters(
44+
paramLabel = "INPUT",
45+
description = "Input Namespace Package file or URL",
46+
arity = "1",
47+
index = "0"
48+
)
49+
private String input;
50+
51+
@Override
52+
public void run() {
53+
throw new SubCommandException( COMMAND_NAME );
54+
}
55+
56+
@SuppressWarnings( { "LombokGetterMayBeUsed", "RedundantSuppression" } )
57+
public String getInput() {
58+
return input;
59+
}
60+
}

0 commit comments

Comments
 (0)