diff options
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.java | 205 |
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 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import com.google.common.base.Splitter; | ||
9 | import com.google.common.cache.Cache; | ||
10 | import com.google.common.cache.CacheBuilder; | ||
11 | import org.apache.log4j.Logger; | ||
12 | import org.eclipse.emf.common.notify.Notification; | ||
13 | import org.eclipse.emf.common.notify.impl.AdapterImpl; | ||
14 | import org.eclipse.emf.common.util.URI; | ||
15 | import org.eclipse.emf.ecore.resource.Resource; | ||
16 | import org.eclipse.emf.ecore.resource.ResourceSet; | ||
17 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
18 | import org.eclipse.xtext.naming.QualifiedName; | ||
19 | import tools.refinery.language.library.RefineryLibrary; | ||
20 | |||
21 | import java.io.File; | ||
22 | import java.nio.file.Path; | ||
23 | import java.util.*; | ||
24 | |||
25 | public 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 | } | ||