Skip to content

Commit 1d6192a

Browse files
committed
Add package export command in samm-cli
1 parent 4fd8cd1 commit 1d6192a

File tree

10 files changed

+181
-19
lines changed

10 files changed

+181
-19
lines changed

core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ public AspectModel load( final AspectModelUrn urn ) {
157157
return loadUrns( List.of( urn ) );
158158
}
159159

160+
/**
161+
* Loads all contents of a complete namespace into an Aspect Model
162+
*
163+
* @param urn the namespace URN
164+
* @return the Aspect Model
165+
*/
166+
public AspectModel loadNamespace( final AspectModelUrn urn ) {
167+
if ( !urn.getName().isEmpty() ) {
168+
throw new AspectLoadingException( "URN does not denote a namespace" );
169+
}
170+
return loadAspectModelFiles( loadContentsForNamespace( urn ).toList() );
171+
}
172+
160173
/**
161174
* Load an Aspect Model by transitively resolving a set of given input URNs
162175
*
@@ -234,8 +247,12 @@ public AspectModel loadNamespacePackage( final byte[] binaryContent, final URI l
234247

235248
/**
236249
* Load a namespace package from an input stream with a given location. The location is not resolved or loaded from, but is only attached
237-
* to the files loaded from the input stream to indicate their original source, e.g., file system location or URL of the
238-
* namespace package.
250+
* to the files loaded from the input stream to indicate their original source, i.e., a file system location or URL of the
251+
* namespace package. For example, if the namespace package is located at {@code file:/some/path/package.zip} or
252+
* {@code https://example.com/package.zip}, the files in the package will have a location URI such as
253+
* {@code jar:file:/some/path/package.zip!/com.example.namespace/1.0.0/AspectModel.ttl} or
254+
* {@code jar:https://example.com/package.zip!/com.example.namespace/1.0.0/AspectModel.ttl}, respectively, as described in
255+
* the JavaDoc for {@link java.net.JarURLConnection}.
239256
*
240257
* @param location the source location
241258
* @param inputStream the input stream to load the ZIP content from
@@ -432,15 +449,18 @@ public AspectModel emptyModel() {
432449
* @return the Aspect Model
433450
*/
434451
public AspectModel loadAspectModelFiles( final Collection<AspectModelFile> inputFiles ) {
452+
final Collection<AspectModelFile> migratedInputs = inputFiles.stream()
453+
.map( this::migrate )
454+
.toList();
435455
final Model mergedModel = ModelFactory.createDefaultModel();
436-
for ( final AspectModelFile file : inputFiles ) {
456+
for ( final AspectModelFile file : migratedInputs ) {
437457
RdfUtil.mergeModel( mergedModel, file.sourceModel() );
438458
}
439459
mergedModel.add( MetaModelFile.metaModelDefinitions() );
440460

441461
final List<ModelElement> elements = new ArrayList<>();
442462
final List<AspectModelFile> files = new ArrayList<>();
443-
for ( final AspectModelFile file : inputFiles ) {
463+
for ( final AspectModelFile file : migratedInputs ) {
444464
final DefaultAspectModelFile aspectModelFile = new DefaultAspectModelFile( file.sourceModel(), file.headerComment(),
445465
file.sourceLocation() );
446466
files.add( aspectModelFile );

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,14 @@ private String findModelsRootInZip() {
159159
return "";
160160
}
161161

162+
/**
163+
* Constructs the "jar:" URL as described in {@link java.net.JarURLConnection}, as a URI.
164+
*
165+
* @param filePath the relative path inside the file
166+
* @return the full jar URL
167+
*/
162168
private URI constructLocationForFile( final String filePath ) {
163-
return URI.create( "jar:%s!%s".formatted( location, filePath ) );
169+
return URI.create( "jar:%s!/%s".formatted( location, filePath ) );
164170
}
165171

166172
private List<AspectModelFile> loadZipContent() {

core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/versionupdate/MetaModelVersionMigrator.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ private VersionNumber getUsedMetaModelVersion( final Model model ) {
8282
.map( Resource::getURI )
8383
.filter( uri -> uri.startsWith( sammUrnStart ) )
8484
.flatMap( uri -> AspectModelUrn.from( uri ).toJavaStream() )
85-
.filter( urn -> (urn.getElementType().equals( ElementType.META_MODEL ) || urn.getElementType()
86-
.equals( ElementType.CHARACTERISTIC )) )
85+
.filter( urn -> ( urn.getElementType().equals( ElementType.META_MODEL ) || urn.getElementType()
86+
.equals( ElementType.CHARACTERISTIC ) ) )
8787
.map( AspectModelUrn::getVersion )
8888
.map( VersionNumber::parse )
8989
.collect( Collectors.toSet() );
@@ -105,7 +105,6 @@ private VersionNumber getUsedMetaModelVersion( final Model model ) {
105105
* @param modelFile the source model file
106106
* @return the resulting {@link AspectModelFile} that corresponds to the input Aspect model file, but with the new meta model version
107107
*/
108-
// public AspectModelFile updateMetaModelVersion( final AspectModelFile modelFile ) {
109108
@Override
110109
public AspectModelFile apply( final AspectModelFile modelFile ) {
111110
// Before any semantic migration, perform the mechanical translation of legacy BAMM models

core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/NamespacePackageTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ void testLoadAspectModelFromZipArchiveFile() {
6464
assertThat( aspectModel ).files().anySatisfy( aspectModelFile -> {
6565
Assertions.assertThat( aspectsNames ).contains( aspectModelFile.aspect().getName() );
6666
} );
67+
68+
assertThat( aspectModel ).files().allMatch( file ->
69+
file.sourceLocation().orElseThrow().toString().matches(
70+
"^jar:file:[^!]*namespaces\\.zip!/org\\.eclipse\\.esmf\\.examples/\\d\\.\\d\\.\\d/" + file.filename().orElseThrow() ) );
6771
}
6872

6973
@Test

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ can also be seen in the help text of the individual subcommands.
5757

5858
=== List of commands
5959

60+
{empty} +
61+
6062
[TIP]
6163
====
6264
In place of `<model>` in the command descriptions below, you can provide either one of:
@@ -240,14 +242,21 @@ of model resolution] for more information.
240242
run `samm aas <aas file> list` to list them. | `samm aas AssetAdminShell.aasx to aspect -s 1 -s 2`
241243
.1+| [[aas-list]] aas <aas file> list | Retrieve a list of submodel templates contained within the provided
242244
Asset Administration Shell (AAS) file. | `samm aas AssetAdminShell.aasx list`
243-
.4+| [[package-list]] package <namespace package> import | Imports a Namespace Package (file or URL) into a given
245+
.4+| [[package-import]] package <namespace package> import | Imports a Namespace Package (file or URL) into a given
244246
xref:models-directory-structure[models directory] | `samm package MyPackage.zip import --models-root c:\models`
245247
| _--dry-run_ : Don't write changes to the file system, but print a report of changes
246248
that would be performed. |
247249
| _--details_ : When used with `--dry-run`, include details about model content
248250
changes in the report . |
249251
| _--force_ : When a new file is to be created but it already exists in the file system,
250252
the operation will be cancelled, unless `--force` is used. |
253+
.2+| [[package-export]] package <model or namespace URN> export | Exports an Aspect Model with its dependencies
254+
or a complete namespace to a Namespace Package (.zip) | `samm package urn:samm:org.eclipse.example.myns:1.0.0 --output package.zip` +
255+
{empty} +
256+
`samm package AspectModel.ttl export --output package.zip`
257+
| _--output, -o_ : output file path (default: stdout); as ZIP is a binary format, it is
258+
strongly recommended to output the result to a file by using the -o option or the
259+
console redirection operator '>')|
251260
|===
252261

253262
[[configuration-of-model-resolution]]

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,21 @@ public static boolean appliesToInput( final String input ) {
6868

6969
@Override
7070
public AspectModel loadAspectModel() {
71-
return applyAspectModelLoader( aspectModelLoader -> aspectModelLoader.load( urn ) );
71+
// Does the URN denote a namespace?
72+
return urn.getName().isEmpty()
73+
? applyAspectModelLoader( aspectModelLoader -> aspectModelLoader.loadNamespace( urn ) )
74+
: applyAspectModelLoader( aspectModelLoader -> aspectModelLoader.load( urn ) );
7275
}
7376

7477
@Override
7578
public URI inputUri() {
7679
return URI.create( urn.toString() );
7780
}
7881

82+
public AspectModelUrn urn() {
83+
return urn;
84+
}
85+
7986
@Override
8087
public AspectModelFile loadAspectModelFile() {
8188
final AspectModel aspectModel = loadAspectModel();

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

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

101-
@SuppressWarnings( "UseOfSystemOutOrSystemErr" )
102101
@Override
103102
public void run() {
104103
setDetails( details );

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
description = "Import and export Namespace Packages",
2828
subcommands = {
2929
CommandLine.HelpCommand.class,
30-
PackageImportCommand.class
30+
PackageImportCommand.class,
31+
PackageExportCommand.class
3132
},
3233
headerHeading = "@|bold Usage|@:%n%n",
3334
descriptionHeading = "%n@|bold Description|@:%n%n",
@@ -42,7 +43,7 @@ public class PackageCommand extends AbstractCommand {
4243

4344
@CommandLine.Parameters(
4445
paramLabel = "INPUT",
45-
description = "Input Namespace Package file or URL",
46+
description = "Input Namespace Package file, URL or Aspect Model URN",
4647
arity = "1",
4748
index = "0"
4849
)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 java.io.IOException;
17+
import java.io.OutputStream;
18+
19+
import org.eclipse.esmf.AbstractCommand;
20+
import org.eclipse.esmf.InputHandler;
21+
import org.eclipse.esmf.LoggingMixin;
22+
import org.eclipse.esmf.ResolverConfigurationMixin;
23+
import org.eclipse.esmf.aspectmodel.resolver.NamespacePackage;
24+
import org.eclipse.esmf.exception.CommandException;
25+
26+
import picocli.CommandLine;
27+
28+
/**
29+
* Command to export a Namespace Package from a given Namespace or an Aspect Model with its transitive dependencies
30+
*/
31+
@CommandLine.Command(
32+
name = PackageExportCommand.COMMAND_NAME,
33+
description = "Export a Namespace or an Aspect Model with its dependencies into a Namespace Package",
34+
subcommands = {
35+
CommandLine.HelpCommand.class
36+
},
37+
headerHeading = "@|bold Usage|@:%n%n",
38+
descriptionHeading = "%n@|bold Description|@:%n%n",
39+
parameterListHeading = "%n@|bold Parameters|@:%n",
40+
optionListHeading = "%n@|bold Options|@:%n"
41+
)
42+
public class PackageExportCommand extends AbstractCommand {
43+
public static final String COMMAND_NAME = "export";
44+
45+
@CommandLine.ParentCommand
46+
public PackageCommand parentCommand;
47+
48+
@CommandLine.Mixin
49+
private LoggingMixin loggingMixin;
50+
51+
@CommandLine.Mixin
52+
private ResolverConfigurationMixin resolverConfiguration;
53+
54+
@CommandLine.Option(
55+
names = { "--output", "-o" },
56+
description = "Output file path (default: stdout; as ZIP is a binary format, it is strongly recommended to output the result to "
57+
+ "a file by using the -o option or the console redirection operator '>')" )
58+
private String outputFilePath = "-";
59+
60+
@CommandLine.Option(
61+
names = { "--details" },
62+
description = "Print detailed reports on errors" )
63+
private boolean details = false;
64+
65+
@Override
66+
public void run() {
67+
setDetails( details );
68+
setResolverConfig( resolverConfiguration );
69+
70+
final String input = parentCommand.getInput();
71+
final InputHandler inputHandler = getInputHandler( input );
72+
final NamespacePackage namespacePackage = new NamespacePackage( inputHandler.loadAspectModel() );
73+
74+
try ( final OutputStream out = getStreamForFile( outputFilePath ) ) {
75+
out.write( namespacePackage.serialize() );
76+
} catch ( final IOException exception ) {
77+
throw new CommandException( "Could not write to output file" );
78+
}
79+
}
80+
}

tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.eclipse.esmf.test.InvalidTestAspect;
4242
import org.eclipse.esmf.test.TestAspect;
4343
import org.eclipse.esmf.test.TestModel;
44+
import org.eclipse.esmf.test.TestSharedAspect;
4445
import org.eclipse.esmf.test.TestSharedModel;
4546

4647
import org.apache.commons.io.FileUtils;
@@ -603,8 +604,7 @@ void testAspectToJavaWithDefaultPackageName() {
603604
void testAspectToJavaWithCustomPackageName() {
604605
final File outputDir = outputDirectory.toFile();
605606
final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", defaultInputFile, "to", "java",
606-
"--output-directory",
607-
outputDir.getAbsolutePath(), "--package-name", "com.example.foo" );
607+
"--output-directory", outputDir.getAbsolutePath(), "--package-name", "com.example.foo" );
608608
assertThat( result.stdout() ).isEmpty();
609609
assertThat( result.stderr() ).isEmpty();
610610

@@ -618,8 +618,7 @@ void testAspectToJavaWithCustomPackageName() {
618618
void testAspectToJavaWithoutJacksonAnnotations() {
619619
final File outputDir = outputDirectory.toFile();
620620
final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", defaultInputFile, "to", "java",
621-
"--output-directory",
622-
outputDir.getAbsolutePath(), "--no-jackson" );
621+
"--output-directory", outputDir.getAbsolutePath(), "--no-jackson" );
623622
assertThat( result.stdout() ).isEmpty();
624623
assertThat( result.stderr() ).isEmpty();
625624

@@ -658,8 +657,8 @@ void testAspectToJavaWithCustomFileHeader() {
658657
System.getProperty( "user.dir" ) + "/../../core/esmf-aspect-model-java-generator/templates/test-macro-lib.vm" );
659658

660659
final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", defaultInputFile, "to", "java",
661-
"--output-directory",
662-
outputDir.getAbsolutePath(), "--execute-library-macros", "--template-library-file", templateLibraryFile.getAbsolutePath() );
660+
"--output-directory", outputDir.getAbsolutePath(), "--execute-library-macros", "--template-library-file",
661+
templateLibraryFile.getAbsolutePath() );
663662
assertThat( result.stdout() ).isEmpty();
664663
assertThat( result.stderr() ).isEmpty();
665664

@@ -1530,6 +1529,44 @@ void testPackageImport() {
15301529
assertThat( result2.stderr() ).contains( "already exists" );
15311530
}
15321531

1532+
@Test
1533+
void testPackageExportForNamespace() {
1534+
final TestModel testModel = TestSharedAspect.ASPECT_WITH_COLLECTION_ENTITY;
1535+
final String namespaceUrn = testModel.getUrn().getNamespaceIdentifier();
1536+
final String modelsRoot = inputFile( testModel ).getParentFile().getParentFile().getParentFile()
1537+
.getAbsolutePath();
1538+
final Path outputFile = outputDirectory.resolve( "package.zip" );
1539+
final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "package",
1540+
namespaceUrn, "export", "--models-root", modelsRoot, "--output", outputFile.toFile().getAbsolutePath() );
1541+
assertThat( result.stdout() ).isEmpty();
1542+
assertThat( result.stderr() ).isEmpty();
1543+
assertThat( outputFile ).exists();
1544+
assertThat( contentType( outputFile.toFile() ) ).isEqualTo( MediaType.application( "zip" ) );
1545+
}
1546+
1547+
@Test
1548+
void testPackageExportForAspectFromFile() {
1549+
final Path outputFile = outputDirectory.resolve( "package.zip" );
1550+
final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "package",
1551+
inputFile( testModel ).getAbsolutePath(), "export", "--output", outputFile.toFile().getAbsolutePath() );
1552+
assertThat( result.stdout() ).isEmpty();
1553+
assertThat( result.stderr() ).isEmpty();
1554+
assertThat( outputFile ).exists();
1555+
assertThat( contentType( outputFile.toFile() ) ).isEqualTo( MediaType.application( "zip" ) );
1556+
}
1557+
1558+
@Test
1559+
void testPackageExportForAspectFromUrn() {
1560+
final String modelsRoot = inputFile( testModel ).getParentFile().getParentFile().getParentFile().getAbsolutePath();
1561+
final Path outputFile = outputDirectory.resolve( "package.zip" );
1562+
final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "package",
1563+
testModel.getUrn().toString(), "export", "--models-root", modelsRoot, "--output", outputFile.toFile().getAbsolutePath() );
1564+
assertThat( result.stdout() ).isEmpty();
1565+
assertThat( result.stderr() ).isEmpty();
1566+
assertThat( outputFile ).exists();
1567+
assertThat( contentType( outputFile.toFile() ) ).isEqualTo( MediaType.application( "zip" ) );
1568+
}
1569+
15331570
/**
15341571
* Returns the File object for a test namespace package file
15351572
*/

0 commit comments

Comments
 (0)