aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java')
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java205
1 files changed, 205 insertions, 0 deletions
diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java
new file mode 100644
index 00000000..d7a5304f
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java
@@ -0,0 +1,205 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.scoping.imports;
7
8import com.google.common.base.Splitter;
9import com.google.common.cache.Cache;
10import com.google.common.cache.CacheBuilder;
11import org.apache.log4j.Logger;
12import org.eclipse.emf.common.notify.Notification;
13import org.eclipse.emf.common.notify.impl.AdapterImpl;
14import org.eclipse.emf.common.util.URI;
15import org.eclipse.emf.ecore.resource.Resource;
16import org.eclipse.emf.ecore.resource.ResourceSet;
17import org.eclipse.emf.ecore.util.EcoreUtil;
18import org.eclipse.xtext.naming.QualifiedName;
19import tools.refinery.language.library.RefineryLibrary;
20
21import java.io.File;
22import java.nio.file.Path;
23import java.util.*;
24
25public class ImportAdapter extends AdapterImpl {
26 private static final Logger LOG = Logger.getLogger(ImportAdapter.class);
27 private static final List<RefineryLibrary> DEFAULT_LIBRARIES;
28 private static final List<Path> DEFAULT_PATHS;
29
30 static {
31 var serviceLoader = ServiceLoader.load(RefineryLibrary.class);
32 var defaultLibraries = new ArrayList<RefineryLibrary>();
33 for (var service : serviceLoader) {
34 defaultLibraries.add(service);
35 }
36 DEFAULT_LIBRARIES = List.copyOf(defaultLibraries);
37 var pathEnv = System.getenv("REFINERY_LIBRARY_PATH");
38 if (pathEnv == null) {
39 DEFAULT_PATHS = List.of();
40 } else {
41 DEFAULT_PATHS = Splitter.on(File.pathSeparatorChar)
42 .splitToStream(pathEnv)
43 .map(pathString -> Path.of(pathString).toAbsolutePath().normalize())
44 .toList();
45 }
46 }
47
48 private final List<RefineryLibrary> libraries;
49 private final List<Path> libraryPaths;
50 private final Cache<QualifiedName, QualifiedName> failedResolutions =
51 CacheBuilder.newBuilder().maximumSize(100).build();
52 private final Map<QualifiedName, URI> qualifiedNameToUriMap = new LinkedHashMap<>();
53 private final Map<URI, QualifiedName> uriToQualifiedNameMap = new LinkedHashMap<>();
54
55 private ImportAdapter(ResourceSet resourceSet) {
56 libraries = new ArrayList<>(DEFAULT_LIBRARIES);
57 libraryPaths = new ArrayList<>(DEFAULT_PATHS);
58 for (var resource : resourceSet.getResources()) {
59 resourceAdded(resource);
60 }
61 }
62
63 @Override
64 public boolean isAdapterForType(Object type) {
65 return type == ImportAdapter.class;
66 }
67
68 public List<RefineryLibrary> getLibraries() {
69 return libraries;
70 }
71
72 public List<Path> getLibraryPaths() {
73 return libraryPaths;
74 }
75
76 public URI resolveQualifiedName(QualifiedName qualifiedName) {
77 var uri = getResolvedUri(qualifiedName);
78 if (uri != null) {
79 return uri;
80 }
81 if (isFailed(qualifiedName)) {
82 return null;
83 }
84 for (var library : libraries) {
85 var result = library.resolveQualifiedName(qualifiedName, libraryPaths);
86 if (result.isPresent()) {
87 uri = result.get();
88 markAsResolved(qualifiedName, uri);
89 return uri;
90 }
91 }
92 markAsUnresolved(qualifiedName);
93 return null;
94 }
95
96 private URI getResolvedUri(QualifiedName qualifiedName) {
97 return qualifiedNameToUriMap.get(qualifiedName);
98 }
99
100 private boolean isFailed(QualifiedName qualifiedName) {
101 return failedResolutions.getIfPresent(qualifiedName) != null;
102 }
103
104 private void markAsResolved(QualifiedName qualifiedName, URI uri) {
105 if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) {
106 throw new IllegalArgumentException("Already resolved " + qualifiedName);
107 }
108 // We don't need to signal an error here, because modules with multiple qualified names will lead to
109 // validation errors later.
110 uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName);
111 failedResolutions.invalidate(qualifiedName);
112 }
113
114 private void markAsUnresolved(QualifiedName qualifiedName) {
115 failedResolutions.put(qualifiedName, qualifiedName);
116 }
117
118 public QualifiedName getQualifiedName(URI uri) {
119 return uriToQualifiedNameMap.get(uri);
120 }
121
122 @Override
123 public void notifyChanged(Notification msg) {
124 switch (msg.getEventType()) {
125 case Notification.ADD -> {
126 if (msg.getNewValue() instanceof Resource resource) {
127 resourceAdded(resource);
128 }
129 }
130 case Notification.ADD_MANY -> {
131 if (msg.getNewValue() instanceof List<?> list) {
132 manyResourcesAdded(list);
133 }
134 }
135 case Notification.REMOVE -> {
136 if (msg.getOldValue() instanceof Resource resource) {
137 resourceRemoved(resource);
138 }
139 }
140 case Notification.REMOVE_MANY -> {
141 if (msg.getOldValue() instanceof List<?> list) {
142 manyResourcesRemoved(list);
143 }
144 }
145 default -> {
146 // Nothing to update.
147 }
148 }
149 }
150
151 private void manyResourcesAdded(List<?> list) {
152 for (var element : list) {
153 if (element instanceof Resource resource) {
154 resourceAdded(resource);
155 }
156 }
157 }
158
159 private void manyResourcesRemoved(List<?> list) {
160 for (var element : list) {
161 if (element instanceof Resource resource) {
162 resourceRemoved(resource);
163 }
164 }
165 }
166
167 private void resourceAdded(Resource resource) {
168 var uri = resource.getURI();
169 for (var library : libraries) {
170 var result = library.getQualifiedName(uri, libraryPaths);
171 if (result.isPresent()) {
172 var qualifiedName = result.get();
173 var previousQualifiedName = uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName);
174 if (previousQualifiedName == null) {
175 if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) {
176 throw new IllegalArgumentException("Duplicate resource for" + qualifiedName);
177 }
178 } else if (!previousQualifiedName.equals(qualifiedName)) {
179 LOG.warn("Expected %s to have qualified name %s, got %s instead".formatted(
180 uri, previousQualifiedName, qualifiedName));
181 }
182 }
183 }
184 }
185
186 private void resourceRemoved(Resource resource) {
187 var qualifiedName = uriToQualifiedNameMap.remove(resource.getURI());
188 if (qualifiedName != null) {
189 qualifiedNameToUriMap.remove(qualifiedName);
190 }
191 }
192
193 public static ImportAdapter getOrInstall(ResourceSet resourceSet) {
194 var adapter = getAdapter(resourceSet);
195 if (adapter == null) {
196 adapter = new ImportAdapter(resourceSet);
197 resourceSet.eAdapters().add(adapter);
198 }
199 return adapter;
200 }
201
202 private static ImportAdapter getAdapter(ResourceSet resourceSet) {
203 return (ImportAdapter) EcoreUtil.getAdapter(resourceSet.eAdapters(), ImportAdapter.class);
204 }
205}