Skip to content

Commit deff172

Browse files
committed
Enable resolution from private GitHub repositories
Fixes #665
1 parent a7cf8f2 commit deff172

File tree

10 files changed

+126
-54
lines changed

10 files changed

+126
-54
lines changed

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import java.net.http.HttpRequest;
2323
import java.net.http.HttpResponse;
2424
import java.time.Duration;
25+
import java.util.Map;
2526
import java.util.Optional;
27+
import java.util.stream.Stream;
2628

2729
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ModelResolutionException;
2830

@@ -48,9 +50,10 @@ public Download() {
4850
* Download the file and return the contents as byte array
4951
*
5052
* @param fileUrl the URL
53+
* @param headers list of additional headers to set
5154
* @return the file contents
5255
*/
53-
public byte[] downloadFile( final URL fileUrl ) {
56+
public byte[] downloadFile( final URL fileUrl, final Map<String, String> headers ) {
5457
try {
5558
final HttpClient.Builder clientBuilder = HttpClient.newBuilder()
5659
.version( HttpClient.Version.HTTP_1_1 )
@@ -59,14 +62,31 @@ public byte[] downloadFile( final URL fileUrl ) {
5962
Optional.ofNullable( proxyConfig.proxy() ).ifPresent( clientBuilder::proxy );
6063
Optional.ofNullable( proxyConfig.authenticator() ).ifPresent( clientBuilder::authenticator );
6164
final HttpClient client = clientBuilder.build();
62-
final HttpRequest request = HttpRequest.newBuilder().uri( fileUrl.toURI() ).build();
65+
final String[] headersArray = headers.entrySet().stream()
66+
.flatMap( entry -> Stream.of( entry.getKey(), entry.getValue() ) )
67+
.toList()
68+
.toArray( new String[0] );
69+
final HttpRequest request = HttpRequest.newBuilder()
70+
.uri( fileUrl.toURI() )
71+
.headers( headersArray )
72+
.build();
6373
final HttpResponse<byte[]> response = client.send( request, HttpResponse.BodyHandlers.ofByteArray() );
6474
return response.body();
6575
} catch ( final InterruptedException | URISyntaxException | IOException exception ) {
6676
throw new ModelResolutionException( "Could not retrieve " + fileUrl, exception );
6777
}
6878
}
6979

80+
/**
81+
* Download the file and return the contents as byte array
82+
*
83+
* @param fileUrl the URL
84+
* @return the file contents
85+
*/
86+
public byte[] downloadFile( final URL fileUrl ) {
87+
return downloadFile( fileUrl, Map.of() );
88+
}
89+
7090
/**
7191
* Download the file and write it to the file system
7292
*
@@ -75,8 +95,12 @@ public byte[] downloadFile( final URL fileUrl ) {
7595
* @return the file written
7696
*/
7797
public File downloadFile( final URL fileUrl, final File outputFile ) {
98+
return downloadFile( fileUrl, Map.of(), outputFile );
99+
}
100+
101+
public File downloadFile( final URL fileUrl, final Map<String, String> headers, final File outputFile ) {
78102
try ( final FileOutputStream outputStream = new FileOutputStream( outputFile ) ) {
79-
final byte[] fileContent = downloadFile( fileUrl );
103+
final byte[] fileContent = downloadFile( fileUrl, headers );
80104
outputStream.write( fileContent );
81105
} catch ( final IOException exception ) {
82106
throw new ModelResolutionException( "Could not write file " + outputFile, exception );

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public record GithubRepository(
2525
Ref branchOrTag
2626
) {
2727
public GithubRepository( final String owner, final String repository, final Ref branchOrTag ) {
28-
this( "github.com", owner, repository, branchOrTag );
28+
this( "api.github.com", owner, repository, branchOrTag );
2929
}
3030

3131
public sealed interface Ref {
@@ -49,8 +49,9 @@ public String refType() {
4949
}
5050

5151
public URL zipLocation() {
52-
final String url = "https://%s/%s/%s/archive/refs/%s/%s.zip".formatted(
53-
host(), owner(), repository(), branchOrTag().refType(), branchOrTag().name() );
52+
// See https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#download-a-repository-archive-zip
53+
// General URL structure: https://api.github.com/repos/OWNER/REPO/zipball/REF
54+
final String url = "https://%s/repos/%s/%s/zipball/%s".formatted( host(), owner(), repository(), branchOrTag().name() );
5455
try {
5556
return new URL( url );
5657
} catch ( final MalformedURLException exception ) {

core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GitHubModelSource.java

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import java.time.ZoneId;
2222
import java.time.ZonedDateTime;
2323
import java.time.format.DateTimeFormatter;
24+
import java.util.HashMap;
2425
import java.util.List;
26+
import java.util.Map;
2527
import java.util.Optional;
2628
import java.util.stream.Stream;
2729
import java.util.zip.ZipFile;
@@ -31,7 +33,7 @@
3133
import org.eclipse.esmf.aspectmodel.resolver.Download;
3234
import org.eclipse.esmf.aspectmodel.resolver.GithubRepository;
3335
import org.eclipse.esmf.aspectmodel.resolver.ModelSource;
34-
import org.eclipse.esmf.aspectmodel.resolver.ProxyConfig;
36+
import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFile;
3537
import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn;
3638

3739
import com.google.common.collect.Streams;
@@ -44,51 +46,53 @@
4446
*/
4547
public class GitHubModelSource implements ModelSource {
4648
private static final Logger LOG = LoggerFactory.getLogger( GitHubModelSource.class );
47-
private final ProxyConfig proxyConfig;
4849
File repositoryZipFile = null;
4950
private List<AspectModelFile> files = null;
50-
protected final GithubRepository repository;
51-
protected final String directory;
51+
GithubModelSourceConfig config;
5252

53-
/**
54-
* Constructor.
55-
*
56-
* @param repository the repository this model sources refers to
57-
* @param directory the relative directory inside the repository
58-
* @param proxyConfig the proxy configuration
59-
*/
60-
public GitHubModelSource( final GithubRepository repository, final String directory, final ProxyConfig proxyConfig ) {
61-
this.repository = repository;
62-
this.directory = Optional.ofNullable( directory ).map( d ->
63-
d.endsWith( "/" ) ? d.substring( 0, d.length() - 1 ) : d ).orElse( "" );
64-
this.proxyConfig = proxyConfig;
53+
public GitHubModelSource( final GithubModelSourceConfig config ) {
54+
this.config = config;
6555
}
6656

6757
/**
68-
* Constructor. Proxy settings are automatically detected.
58+
* Convenience constructor for public repositories. Proxy settings are automatically detected, no authentication is used.
6959
*
7060
* @param repository the repository this model sources refers to
7161
* @param directory the relative directory inside the repository
7262
*/
7363
public GitHubModelSource( final GithubRepository repository, final String directory ) {
74-
this( repository, directory, ProxyConfig.detectProxySettings() );
64+
this( GithubModelSourceConfigBuilder.builder()
65+
.repository( repository )
66+
.directory( directory )
67+
.build() );
7568
}
7669

7770
private String sourceUrl( final String filename ) {
78-
return "https://%s/%s/%s/blob/%s/%s".formatted( repository.host(), repository.owner(), repository.repository(),
79-
repository.branchOrTag().name(), filename );
71+
return "https://%s/%s/%s/blob/%s/%s".formatted(
72+
config.repository().host().equals( "api.github.com" ) ? "github.com" : config.repository().host(),
73+
config.repository().owner(),
74+
config.repository().repository(),
75+
config.repository().branchOrTag().name(),
76+
filename );
8077
}
8178

8279
private void init() {
8380
try {
8481
final Path tempDirectory = Files.createTempDirectory( "esmf" );
8582
final File outputZipFile = tempDirectory.resolve( ZonedDateTime.now( ZoneId.systemDefault() )
8683
.format( DateTimeFormatter.ofPattern( "uuuu-MM-dd.HH.mm.ss" ) ) + ".zip" ).toFile();
87-
repositoryZipFile = new Download( proxyConfig ).downloadFile( repository.zipLocation(), outputZipFile );
84+
final Map<String, String> headers = new HashMap<>();
85+
headers.put( "Accept", "application/vnd.github+json" );
86+
headers.put( "X-GitHub-Api-Version", "2022-11-28" );
87+
if ( config.token() != null ) {
88+
headers.put( "Authorization", "Bearer " + config.token() );
89+
}
90+
repositoryZipFile = new Download( config.proxyConfig() )
91+
.downloadFile( config.repository().zipLocation(), headers, outputZipFile );
8892
loadFilesFromZip();
8993
final boolean packageIsDeleted = outputZipFile.delete() && tempDirectory.toFile().delete();
9094
if ( packageIsDeleted ) {
91-
LOG.debug( String.format( "Temporary package file %s was deleted", outputZipFile.getName() ) );
95+
LOG.debug( "Temporary package file {} was deleted", outputZipFile.getName() );
9296
}
9397
} catch ( final IOException exception ) {
9498
throw new GitHubResolverException( exception );
@@ -102,21 +106,23 @@ void loadFilesFromZip() {
102106
try ( final ZipFile zipFile = new ZipFile( repositoryZipFile ) ) {
103107
LOG.debug( "Loading Aspect Model files from {}", repositoryZipFile );
104108
files = Streams.stream( zipFile.entries().asIterator() ).flatMap( zipEntry -> {
105-
final String pathPrefix = repository.repository() + "-" + repository.branchOrTag().name() + "/" + directory;
106-
if ( !zipEntry.getName().startsWith( pathPrefix ) || !zipEntry.getName().endsWith( ".ttl" ) ) {
109+
final String path = zipEntry.getName().substring( zipEntry.getName().indexOf( '/' ) + 1 );
110+
if ( !zipEntry.getName().endsWith( ".ttl" ) ) {
107111
return Stream.empty();
108112
}
109-
final int offset = pathPrefix.endsWith( "/" ) ? 0 : 1;
110-
final String path = zipEntry.getName().substring( pathPrefix.length() + offset );
111-
// Path should now look like org.eclipse.esmf.example/1.0.0/File.ttl
112113
final String[] parts = path.split( "/" );
113-
if ( parts.length != 3 || AspectModelUrn.from( "urn:samm:" + parts[0] + ":" + parts[1] ).isFailure() ) {
114+
if ( parts.length < 3 ||
115+
AspectModelUrn.from( "urn:samm:" + parts[parts.length - 3] + ":" + parts[parts.length - 2] ).isFailure() ) {
114116
LOG.debug( "Tried to load file {} but the path contains no valid URN structure", zipEntry.getName() );
115117
return Stream.<AspectModelFile> empty();
116118
}
117-
final String relativeFilePath = zipEntry.getName().substring( zipEntry.getName().indexOf( "/" ) + 1 );
118-
return Try.of( () -> zipFile.getInputStream( zipEntry ) ).toJavaStream().map( inputStream ->
119-
AspectModelFileLoader.load( inputStream, Optional.of( URI.create( sourceUrl( relativeFilePath ) ) ) ) );
119+
final URI uri = URI.create( sourceUrl( path ) );
120+
final Try<RawAspectModelFile> file = Try.of( () -> zipFile.getInputStream( zipEntry ) )
121+
.map( inputStream -> AspectModelFileLoader.load( inputStream, Optional.of( uri ) ) );
122+
if ( file.isFailure() ) {
123+
LOG.debug( "Tried to load {}, but it failed", uri );
124+
}
125+
return file.toJavaStream();
120126
} ).toList();
121127
} catch ( final IOException exception ) {
122128
throw new GitHubResolverException( exception );

core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GitHubStrategy.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import org.eclipse.esmf.aspectmodel.AspectModelFile;
1717
import org.eclipse.esmf.aspectmodel.resolver.GithubRepository;
18-
import org.eclipse.esmf.aspectmodel.resolver.ProxyConfig;
1918
import org.eclipse.esmf.aspectmodel.resolver.ResolutionStrategy;
2019
import org.eclipse.esmf.aspectmodel.resolver.ResolutionStrategySupport;
2120
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ModelResolutionException;
@@ -33,16 +32,14 @@ public class GitHubStrategy extends GitHubModelSource implements ResolutionStrat
3332
/**
3433
* Constructor.
3534
*
36-
* @param repository the GitHub repository
37-
* @param directory the relative directory inside the repository
38-
* @param proxyConfig the proxy configuration
35+
* @param sourceConfig the configuration for the model source
3936
*/
40-
public GitHubStrategy( final GithubRepository repository, final String directory, final ProxyConfig proxyConfig ) {
41-
super( repository, directory, proxyConfig );
37+
public GitHubStrategy( final GithubModelSourceConfig sourceConfig ) {
38+
super( sourceConfig );
4239
}
4340

4441
/**
45-
* Constructor. Proxy settings are automatically detected.
42+
* Convenience constructor. Proxy settings are automatically detected, no authentication is used.
4643
*
4744
* @param repository the GitHub repository
4845
* @param directory the relative directory inside the repository
@@ -60,8 +57,11 @@ public AspectModelFile apply( final AspectModelUrn aspectModelUrn, final Resolut
6057
} )
6158
.filter( file -> resolutionStrategySupport.containsDefinition( file, aspectModelUrn ) )
6259
.findFirst()
63-
.orElseThrow( () -> new ModelResolutionException( "No model file containing " + aspectModelUrn
64-
+ " could be found in GitHub repository: " + repository.owner() + "/" + repository.repository()
65-
+ " in branch/tag " + repository.branchOrTag().name() ) );
60+
.orElseThrow( () -> new ModelResolutionException(
61+
"No model file containing %s could be found in GitHub repository: %s/%s in branch/tag %s".formatted(
62+
aspectModelUrn,
63+
config.repository().owner(),
64+
config.repository().repository(),
65+
config.repository().branchOrTag().name() ) ) );
6666
}
6767
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2024 Bosch Software Innovations GmbH. All rights reserved.
3+
*/
4+
5+
package org.eclipse.esmf.aspectmodel.resolver.github;
6+
7+
import java.util.Optional;
8+
9+
import org.eclipse.esmf.aspectmodel.resolver.GithubRepository;
10+
import org.eclipse.esmf.aspectmodel.resolver.ProxyConfig;
11+
12+
import io.soabase.recordbuilder.core.RecordBuilder;
13+
14+
@RecordBuilder
15+
public record GithubModelSourceConfig(
16+
GithubRepository repository,
17+
String directory,
18+
ProxyConfig proxyConfig,
19+
String token
20+
) {
21+
public GithubModelSourceConfig {
22+
directory = Optional.ofNullable( directory ).map( d -> d.endsWith( "/" ) ? d.substring( 0, d.length() - 1 ) : d ).orElse( "" );
23+
proxyConfig = Optional.ofNullable( proxyConfig ).orElse( ProxyConfig.detectProxySettings() );
24+
}
25+
}

documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ model element definitions:
181181
* The `EitherStrategy` can be used to chain two or more different `ResolutionStrategy`&#8203;s.
182182
* The `ExternalResolverStrategy` delegates resolution to an external command such as a script; it is used in
183183
the implementation of the `--custom-resolver` option of the xref:tooling-guide:samm-cli.adoc[samm-cli].
184+
* The `GitHubStrategy` resolves model elements from repositories hosted on GitHub.
184185

185186
In addition, custom resolution strategies can be provided by implementing the `ResolutionStrategy` interface.
186187

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,11 @@ to load. You can configure where such lookup should be done:
261261
language/script language, including complex logic if necessary.
262262

263263
* Using the `--github` switch, you can configure a location in a remote GitHub repository as models
264-
root, e.g., `eclipse-esmf/esmf-sdk`. Optionally, you can also provide `--github-directory`
265-
to set the remote directory and `--github-branch` or `--github-tag` to set the branch name or tag,
266-
respectively.
264+
root, e.g., `eclipse-esmf/esmf-sdk`. Optionally, you can also provide `--github-directory` to set
265+
the remote directory and `--github-branch` or `--github-tag` to set the branch name or tag,
266+
respectively. If the repository is private, you can also pass `--github-token` and set a
267+
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token[fine-grained
268+
access token]; the token needs at least "Contents" repository read permissions.
267269

268270
NOTE: When using an Aspect Model URN as input to a command (such as
269271
`urn:samm:org.eclipse.esmf.example:1.0.0#Aspect`), you must also provide at least one of the

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ModelResolutionException;
2828
import org.eclipse.esmf.aspectmodel.resolver.fs.StructuredModelsRoot;
2929
import org.eclipse.esmf.aspectmodel.resolver.github.GitHubStrategy;
30+
import org.eclipse.esmf.aspectmodel.resolver.github.GithubModelSourceConfig;
31+
import org.eclipse.esmf.aspectmodel.resolver.github.GithubModelSourceConfigBuilder;
3032
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
3133
import org.eclipse.esmf.aspectmodel.validation.services.AspectModelValidator;
3234
import org.eclipse.esmf.aspectmodel.validation.services.DetailedViolationFormatter;
@@ -88,7 +90,12 @@ protected List<ResolutionStrategy> configuredStrategies() {
8890
? new GithubRepository.Tag( resolverConfig.gitHubResolutionOptions.gitHubTag )
8991
: new GithubRepository.Branch( resolverConfig.gitHubResolutionOptions.gitHubBranch );
9092
final GithubRepository repository = new GithubRepository( owner, repositoryName, branchOrTag );
91-
strategies.add( new GitHubStrategy( repository, resolverConfig.gitHubResolutionOptions.gitHubDirectory ) );
93+
final GithubModelSourceConfig modelSourceConfig = GithubModelSourceConfigBuilder.builder()
94+
.repository( repository )
95+
.directory( resolverConfig.gitHubResolutionOptions.gitHubDirectory )
96+
.token( resolverConfig.gitHubResolutionOptions.gitHubToken )
97+
.build();
98+
strategies.add( new GitHubStrategy( modelSourceConfig ) );
9299
}
93100
return strategies;
94101
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,22 @@ public static class GitHubResolutionOptions {
4444

4545
@CommandLine.Option(
4646
names = { "--github-directory", "-ghd" },
47-
description = "Set the GitHub directory (default: ${DEFAULT-VALUE}" )
47+
description = "Set the GitHub directory (default: ${DEFAULT-VALUE})" )
4848
public String gitHubDirectory = "/";
4949

5050
@CommandLine.Option(
5151
names = { "--github-branch", "-ghb" },
52-
description = "Set the GitHub branch (default: ${DEFAULT-VALUE}" )
52+
description = "Set the GitHub branch (default: ${DEFAULT-VALUE})" )
5353
public String gitHubBranch = "main";
5454

5555
@CommandLine.Option(
5656
names = { "--github-tag", "-ght" },
57-
description = "Set the GitHub tag (default: ${DEFAULT-VALUE}" )
57+
description = "Set the GitHub tag" )
5858
public String gitHubTag = null;
59+
60+
@CommandLine.Option(
61+
names = { "--github-token", "-token" },
62+
description = "Set the GitHub token" )
63+
public String gitHubToken = null;
5964
}
6065
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ public static void main( final String[] argv ) {
169169
for ( final String arg : argv ) {
170170
if ( arg.equals( "--disable-color" ) || arg.equals( "-D" ) ) {
171171
disableColor = true;
172+
break;
172173
}
173174
}
174175

0 commit comments

Comments
 (0)