1 /*
2 * Copyright 2020-2022 Foreseeti AB <https://foreseeti.com>
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.mal_lang.langspec;
18
19 import static java.util.Objects.requireNonNull;
20
21 import jakarta.json.Json;
22 import jakarta.json.JsonObject;
23 import java.util.LinkedHashMap;
24 import java.util.List;
25 import java.util.Map;
26 import org.mal_lang.langspec.builders.AssetBuilder;
27
28 /**
29 * Immutable class representing an asset in a MAL language.
30 *
31 * @since 1.0.0
32 */
33 public final class Asset {
34 private final String name;
35 private final Meta meta;
36 private final Category category;
37 private final boolean isAbstract;
38 private Asset superAsset;
39 private final Map<String, Field> fields = new LinkedHashMap<>();
40 private final Map<String, Variable> variables = new LinkedHashMap<>();
41 private final Map<String, AttackStep> attackSteps = new LinkedHashMap<>();
42 private final byte[] svgIcon;
43 private final byte[] pngIcon;
44
45 private Asset(
46 String name,
47 Meta meta,
48 Category category,
49 boolean isAbstract,
50 byte[] svgIcon,
51 byte[] pngIcon) {
52 this.name = requireNonNull(name);
53 this.meta = requireNonNull(meta);
54 this.category = requireNonNull(category);
55 this.isAbstract = isAbstract;
56 this.svgIcon = svgIcon == null ? null : svgIcon.clone();
57 this.pngIcon = pngIcon == null ? null : pngIcon.clone();
58 category.addAsset(this);
59 }
60
61 /**
62 * Returns the name of this {@code Asset} object.
63 *
64 * @return the name of this {@code Asset} object
65 * @since 1.0.0
66 */
67 public String getName() {
68 return this.name;
69 }
70
71 /**
72 * Returns the meta info of this {@code Asset} object.
73 *
74 * @return the meta info of this {@code Asset} object
75 * @since 1.0.0
76 */
77 public Meta getMeta() {
78 return this.meta;
79 }
80
81 /**
82 * Returns the category of this {@code Asset} object.
83 *
84 * @return the category of this {@code Asset} object
85 * @since 1.0.0
86 */
87 public Category getCategory() {
88 return this.category;
89 }
90
91 /**
92 * Returns whether this {@code Asset} object is abstract.
93 *
94 * @return whether this {@code Asset} object is abstract
95 * @since 1.0.0
96 */
97 public boolean isAbstract() {
98 return this.isAbstract;
99 }
100
101 /**
102 * Returns whether this {@code Asset} object has a super asset.
103 *
104 * @return whether this {@code Asset} object has a super asset
105 * @since 1.0.0
106 */
107 public boolean hasSuperAsset() {
108 return this.superAsset != null;
109 }
110
111 /**
112 * Returns the super asset of this {@code Asset} object.
113 *
114 * @return the super asset of this {@code Asset} object
115 * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have a
116 * super asset
117 * @since 1.0.0
118 */
119 public Asset getSuperAsset() {
120 if (!this.hasSuperAsset()) {
121 throw new UnsupportedOperationException("Super asset not found");
122 }
123 return this.superAsset;
124 }
125
126 void setSuperAsset(Asset superAsset) {
127 this.superAsset = requireNonNull(superAsset);
128 }
129
130 /**
131 * Returns whether {@code name} is the name of a local field in this {@code Asset} object.
132 *
133 * @param name the name of the local field
134 * @return whether {@code name} is the name of a local field in this {@code Asset} object
135 * @throws java.lang.NullPointerException if {@code name} is {@code null}
136 * @since 1.0.0
137 */
138 public boolean hasLocalField(String name) {
139 return this.fields.containsKey(requireNonNull(name));
140 }
141
142 /**
143 * Returns the local field with the name {@code name} in this {@code Asset} object.
144 *
145 * @param name the name of the local field
146 * @return the local field with the name {@code name} in this {@code Asset} object
147 * @throws java.lang.NullPointerException if {@code name} is {@code null}
148 * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a local field in
149 * this {@code Asset} object
150 * @since 1.0.0
151 */
152 public Field getLocalField(String name) {
153 if (!this.hasLocalField(name)) {
154 throw new IllegalArgumentException(String.format("Local field \"%s\" not found", name));
155 }
156 return this.fields.get(name);
157 }
158
159 /**
160 * Returns a list of all local fields in this {@code Asset} object.
161 *
162 * @return a list of all local fields in this {@code Asset} object
163 * @since 1.0.0
164 */
165 public List<Field> getLocalFields() {
166 return List.copyOf(this.fields.values());
167 }
168
169 /**
170 * Returns whether {@code name} is the name of a field in this {@code Asset} object.
171 *
172 * @param name the name of the field
173 * @return whether {@code name} is the name of a field in this {@code Asset} object
174 * @throws java.lang.NullPointerException if {@code name} is {@code null}
175 * @since 1.0.0
176 */
177 public boolean hasField(String name) {
178 return this.hasLocalField(name) || this.hasSuperAsset() && this.getSuperAsset().hasField(name);
179 }
180
181 /**
182 * Returns the field with the name {@code name} in this {@code Asset} object.
183 *
184 * @param name the name of the field
185 * @return the field with the name {@code name} in this {@code Asset} object
186 * @throws java.lang.NullPointerException if {@code name} is {@code null}
187 * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a field in this
188 * {@code Asset} object
189 * @since 1.0.0
190 */
191 public Field getField(String name) {
192 if (!this.hasField(name)) {
193 throw new IllegalArgumentException(String.format("Field \"%s\" not found", name));
194 }
195 return this.hasLocalField(name)
196 ? this.getLocalField(name)
197 : this.getSuperAsset().getField(name);
198 }
199
200 /**
201 * Returns a list of all fields in this {@code Asset} object.
202 *
203 * @return a list of all fields in this {@code Asset} object
204 * @since 1.0.0
205 */
206 public List<Field> getFields() {
207 return List.copyOf(this.getFieldsMap().values());
208 }
209
210 void addField(Field field) {
211 requireNonNull(field);
212 this.fields.put(field.getName(), field);
213 }
214
215 private Map<String, Field> getFieldsMap() {
216 var fieldsMap =
217 this.hasSuperAsset()
218 ? this.getSuperAsset().getFieldsMap()
219 : new LinkedHashMap<String, Field>();
220 fieldsMap.putAll(this.fields);
221 return fieldsMap;
222 }
223
224 /**
225 * Returns whether {@code name} is the name of a local variable in this {@code Asset} object.
226 *
227 * @param name the name of the local variable
228 * @return whether {@code name} is the name of a local variable in this {@code Asset} object
229 * @throws java.lang.NullPointerException if {@code name} is {@code null}
230 * @since 1.0.0
231 */
232 public boolean hasLocalVariable(String name) {
233 return this.variables.containsKey(requireNonNull(name));
234 }
235
236 /**
237 * Returns the local variable with the name {@code name} in this {@code Asset} object.
238 *
239 * @param name the name of the local variable
240 * @return the local variable with the name {@code name} in this {@code Asset} object
241 * @throws java.lang.NullPointerException if {@code name} is {@code null}
242 * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a local variable
243 * in this {@code Asset} object
244 * @since 1.0.0
245 */
246 public Variable getLocalVariable(String name) {
247 if (!this.hasLocalVariable(name)) {
248 throw new IllegalArgumentException(String.format("Local variable \"%s\" not found", name));
249 }
250 return this.variables.get(name);
251 }
252
253 /**
254 * Returns a list of all local variables in this {@code Asset} object.
255 *
256 * @return a list of all local variables in this {@code Asset} object
257 * @since 1.0.0
258 */
259 public List<Variable> getLocalVariables() {
260 return List.copyOf(this.variables.values());
261 }
262
263 /**
264 * Returns whether {@code name} is the name of a variable in this {@code Asset} object.
265 *
266 * @param name the name of the variable
267 * @return whether {@code name} is the name of a variable in this {@code Asset} object
268 * @throws java.lang.NullPointerException if {@code name} is {@code null}
269 * @since 1.0.0
270 */
271 public boolean hasVariable(String name) {
272 return this.hasLocalVariable(name)
273 || this.hasSuperAsset() && this.getSuperAsset().hasVariable(name);
274 }
275
276 /**
277 * Returns the variable with the name {@code name} in this {@code Asset} object.
278 *
279 * @param name the name of the variable
280 * @return the variable with the name {@code name} in this {@code Asset} object
281 * @throws java.lang.NullPointerException if {@code name} is {@code null}
282 * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a variable in
283 * this {@code Asset} object
284 * @since 1.0.0
285 */
286 public Variable getVariable(String name) {
287 if (!this.hasVariable(name)) {
288 throw new IllegalArgumentException(String.format("Variable \"%s\" not found", name));
289 }
290 return this.hasLocalVariable(name)
291 ? this.getLocalVariable(name)
292 : this.getSuperAsset().getVariable(name);
293 }
294
295 /**
296 * Returns a list of all variables in this {@code Asset} object.
297 *
298 * @return a list of all variables in this {@code Asset} object
299 * @since 1.0.0
300 */
301 public List<Variable> getVariables() {
302 return List.copyOf(this.getVariablesMap().values());
303 }
304
305 private void addVariable(Variable variable) {
306 requireNonNull(variable);
307 this.variables.put(variable.getName(), variable);
308 }
309
310 private Map<String, Variable> getVariablesMap() {
311 var variablesMap =
312 this.hasSuperAsset()
313 ? this.getSuperAsset().getVariablesMap()
314 : new LinkedHashMap<String, Variable>();
315 variablesMap.putAll(this.variables);
316 return variablesMap;
317 }
318
319 /**
320 * Returns whether {@code name} is the name of a local attack step in this {@code Asset} object.
321 *
322 * @param name the name of the local attack step
323 * @return whether {@code name} is the name of a local attack step in this {@code Asset} object
324 * @throws java.lang.NullPointerException if {@code name} is {@code null}
325 * @since 1.0.0
326 */
327 public boolean hasLocalAttackStep(String name) {
328 return this.attackSteps.containsKey(requireNonNull(name));
329 }
330
331 /**
332 * Returns the local attack step with the name {@code name} in this {@code Asset} object.
333 *
334 * @param name the name of the local attack step
335 * @return the local attack step with the name {@code name} in this {@code Asset} object
336 * @throws java.lang.NullPointerException if {@code name} is {@code null}
337 * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a local attack
338 * step in this {@code Asset} object
339 * @since 1.0.0
340 */
341 public AttackStep getLocalAttackStep(String name) {
342 if (!this.hasLocalAttackStep(name)) {
343 throw new IllegalArgumentException(String.format("Local attack step \"%s\" not found", name));
344 }
345 return this.attackSteps.get(name);
346 }
347
348 /**
349 * Returns a list of all local attack steps in this {@code Asset} object.
350 *
351 * @return a list of all local attack steps in this {@code Asset} object
352 * @since 1.0.0
353 */
354 public List<AttackStep> getLocalAttackSteps() {
355 return List.copyOf(this.attackSteps.values());
356 }
357
358 /**
359 * Returns whether {@code name} is the name of an attack step in this {@code Asset} object.
360 *
361 * @param name the name of the attack step
362 * @return whether {@code name} is the name of an attack step in this {@code Asset} object
363 * @throws java.lang.NullPointerException if {@code name} is {@code null}
364 * @since 1.0.0
365 */
366 public boolean hasAttackStep(String name) {
367 return this.hasLocalAttackStep(name)
368 || this.hasSuperAsset() && this.getSuperAsset().hasAttackStep(name);
369 }
370
371 /**
372 * Returns the attack step with the name {@code name} in this {@code Asset} object.
373 *
374 * @param name the name of the attack step
375 * @return the attack step with the name {@code name} in this {@code Asset} object
376 * @throws java.lang.NullPointerException if {@code name} is {@code null}
377 * @throws java.lang.IllegalArgumentException if {@code name} is not the name of an attack step in
378 * this {@code Asset} object
379 * @since 1.0.0
380 */
381 public AttackStep getAttackStep(String name) {
382 if (!this.hasAttackStep(name)) {
383 throw new IllegalArgumentException(String.format("Attack step \"%s\" not found", name));
384 }
385 return this.hasLocalAttackStep(name)
386 ? this.getLocalAttackStep(name)
387 : this.getSuperAsset().getAttackStep(name);
388 }
389
390 /**
391 * Returns a list of all attack steps in this {@code Asset} object.
392 *
393 * @return a list of all attack steps in this {@code Asset} object
394 * @since 1.0.0
395 */
396 public List<AttackStep> getAttackSteps() {
397 return List.copyOf(this.getAttackStepsMap().values());
398 }
399
400 private void addAttackStep(AttackStep attackStep) {
401 requireNonNull(attackStep);
402 this.attackSteps.put(attackStep.getName(), attackStep);
403 }
404
405 private Map<String, AttackStep> getAttackStepsMap() {
406 var attackStepsMap =
407 this.hasSuperAsset()
408 ? this.getSuperAsset().getAttackStepsMap()
409 : new LinkedHashMap<String, AttackStep>();
410 attackStepsMap.putAll(this.attackSteps);
411 return attackStepsMap;
412 }
413
414 /**
415 * Returns whether this {@code Asset} object has a local SVG icon.
416 *
417 * @return whether this {@code Asset} object has a local SVG icon
418 * @since 1.0.0
419 */
420 public boolean hasLocalSvgIcon() {
421 return this.svgIcon != null;
422 }
423
424 /**
425 * Returns the local SVG icon of this {@code Asset} object.
426 *
427 * @return the local SVG icon of this {@code Asset} object
428 * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have a
429 * local SVG icon
430 * @since 1.0.0
431 */
432 public byte[] getLocalSvgIcon() {
433 if (!this.hasLocalSvgIcon()) {
434 throw new UnsupportedOperationException("Local SVG icon not found");
435 }
436 return this.svgIcon.clone();
437 }
438
439 /**
440 * Returns whether this {@code Asset} object has an SVG icon.
441 *
442 * @return whether this {@code Asset} object has an SVG icon
443 * @since 1.0.0
444 */
445 public boolean hasSvgIcon() {
446 return this.hasLocalSvgIcon() || this.hasSuperAsset() && this.getSuperAsset().hasSvgIcon();
447 }
448
449 /**
450 * Returns the SVG icon of this {@code Asset} object.
451 *
452 * @return the SVG icon of this {@code Asset} object
453 * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have an
454 * SVG icon
455 * @since 1.0.0
456 */
457 public byte[] getSvgIcon() {
458 if (!this.hasSvgIcon()) {
459 throw new UnsupportedOperationException("SVG icon not found");
460 }
461 return this.hasLocalSvgIcon() ? this.getLocalSvgIcon() : this.getSuperAsset().getSvgIcon();
462 }
463
464 /**
465 * Returns whether this {@code Asset} object has a local PNG icon.
466 *
467 * @return whether this {@code Asset} object has a local PNG icon
468 * @since 1.0.0
469 */
470 public boolean hasLocalPngIcon() {
471 return this.pngIcon != null;
472 }
473
474 /**
475 * Returns the local PNG icon of this {@code Asset} object.
476 *
477 * @return the local PNG icon of this {@code Asset} object
478 * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have a
479 * local PNG icon
480 * @since 1.0.0
481 */
482 public byte[] getLocalPngIcon() {
483 if (!this.hasLocalPngIcon()) {
484 throw new UnsupportedOperationException("Local PNG icon not found");
485 }
486 return this.pngIcon.clone();
487 }
488
489 /**
490 * Returns whether this {@code Asset} object has an PNG icon.
491 *
492 * @return whether this {@code Asset} object has an PNG icon
493 * @since 1.0.0
494 */
495 public boolean hasPngIcon() {
496 return this.hasLocalPngIcon() || this.hasSuperAsset() && this.getSuperAsset().hasPngIcon();
497 }
498
499 /**
500 * Returns the PNG icon of this {@code Asset} object.
501 *
502 * @return the PNG icon of this {@code Asset} object
503 * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have an
504 * PNG icon
505 * @since 1.0.0
506 */
507 public byte[] getPngIcon() {
508 if (!this.hasPngIcon()) {
509 throw new UnsupportedOperationException("PNG icon not found");
510 }
511 return this.hasLocalPngIcon() ? this.getLocalPngIcon() : this.getSuperAsset().getPngIcon();
512 }
513
514 /**
515 * Returns whether this {@code Asset} object is a sub type of {@code other}.
516 *
517 * @param other another {@code Asset} object
518 * @return whether this {@code Asset} object is a sub type of {@code other}
519 * @throws java.lang.NullPointerException if {@code other} is {@code null}
520 * @since 1.0.0
521 */
522 public boolean isSubTypeOf(Asset other) {
523 requireNonNull(other);
524 if (this == other) {
525 return true;
526 }
527 if (!this.hasSuperAsset()) {
528 return false;
529 }
530 return this.getSuperAsset().isSubTypeOf(other);
531 }
532
533 /**
534 * Returns the least upper bound of {@code asset1} and {@code asset2}, or {@code null} if {@code
535 * asset1} and {@code asset2} have no upper bound.
536 *
537 * @param asset1 an {@code Asset} object
538 * @param asset2 an {@code Asset} object
539 * @return the least upper bound of {@code asset1} and {@code asset2}, or {@code null} if {@code
540 * asset1} and {@code asset2} have no upper bound
541 * @throws java.lang.NullPointerException if {@code asset1} or {@code asset2} is {@code null}
542 * @since 1.0.0
543 */
544 public static Asset leastUpperBound(Asset asset1, Asset asset2) {
545 requireNonNull(asset1);
546 requireNonNull(asset2);
547 if (asset1.isSubTypeOf(asset2)) {
548 return asset2;
549 }
550 if (asset2.isSubTypeOf(asset1)) {
551 return asset1;
552 }
553 if (!asset1.hasSuperAsset() || !asset2.hasSuperAsset()) {
554 return null;
555 }
556 return Asset.leastUpperBound(asset1.getSuperAsset(), asset2.getSuperAsset());
557 }
558
559 JsonObject toJson() {
560 var jsonVariables = Json.createArrayBuilder();
561 for (var variable : this.variables.values()) {
562 jsonVariables.add(variable.toJson());
563 }
564
565 var jsonAttackSteps = Json.createArrayBuilder();
566 for (var attackStep : this.attackSteps.values()) {
567 jsonAttackSteps.add(attackStep.toJson());
568 }
569
570 var jsonAsset =
571 Json.createObjectBuilder()
572 .add("name", this.name)
573 .add("meta", this.meta.toJson())
574 .add("category", this.category.getName())
575 .add("isAbstract", this.isAbstract);
576 if (this.superAsset == null) {
577 jsonAsset.addNull("superAsset");
578 } else {
579 jsonAsset.add("superAsset", this.superAsset.getName());
580 }
581 return jsonAsset.add("variables", jsonVariables).add("attackSteps", jsonAttackSteps).build();
582 }
583
584 static Asset fromBuilder(AssetBuilder builder, Map<String, Category> categories) {
585 requireNonNull(builder);
586 requireNonNull(categories);
587 if (!categories.containsKey(builder.getCategory())) {
588 throw new IllegalArgumentException(
589 String.format("Category \"%s\" not found", builder.getCategory()));
590 }
591 var asset =
592 new Asset(
593 builder.getName(),
594 Meta.fromBuilder(builder.getMeta()),
595 categories.get(builder.getCategory()),
596 builder.isAbstract(),
597 builder.getSvgIcon(),
598 builder.getPngIcon());
599 for (var variableBuilder : builder.getVariables()) {
600 asset.addVariable(Variable.fromBuilder(variableBuilder, asset));
601 }
602 for (var attackStepBuilder : builder.getAttackSteps()) {
603 asset.addAttackStep(AttackStep.fromBuilder(attackStepBuilder, asset));
604 }
605 return asset;
606 }
607 }