diff --git a/.gitattributes b/.gitattributes index 4cab1f4d2..7898b9e36 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto + +*.emfatic text +*.treeview text diff --git a/.gitignore b/.gitignore index 582412849..4c7b782dd 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ hs_err_pid* # default result file name, should never be commited result/ result.zip + +# macOS +*.DS_Store diff --git a/README.md b/README.md index cf0e1ce0d..0423a8904 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ In the following, a list of all supported languages with their supported languag | [Scala](https://www.scala-lang.org) | 2.13.8 | scala | beta | Scalameta | | [Scheme](http://www.scheme-reports.org) | ? | scheme | unknown | JavaCC | | [Swift](https://www.swift.org) | 5.4 | swift | beta | ANTLR 4 | -| [EMF Metamodel](https://www.eclipse.org/modeling/emf/) | 2.25.0 | emf | alpha | EMF | +| [EMF Metamodel](https://www.eclipse.org/modeling/emf/) | 2.25.0 | emf | beta | EMF | +| [EMF Model](https://www.eclipse.org/modeling/emf/) | 2.25.0 | emf-model | alpha | EMF | | Text (naive) | - | text | legacy | CoreNLP | ## Download and Installation diff --git a/cli/pom.xml b/cli/pom.xml index 60fd9621b..55826915d 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -97,6 +97,11 @@ emf-metamodel ${revision} + + de.jplag + emf-model + ${revision} + org.kohsuke.metainf-services diff --git a/cli/src/test/java/de/jplag/cli/LanguageTest.java b/cli/src/test/java/de/jplag/cli/LanguageTest.java index 7ed7b9608..f6cb4913d 100644 --- a/cli/src/test/java/de/jplag/cli/LanguageTest.java +++ b/cli/src/test/java/de/jplag/cli/LanguageTest.java @@ -28,7 +28,7 @@ void testInvalidLanguage() throws Exception { @Test void testLoading() { var languages = LanguageLoader.getAllAvailableLanguages(); - assertEquals(15, languages.size(), "Loaded Languages: " + languages.keySet()); + assertEquals(16, languages.size(), "Loaded Languages: " + languages.keySet()); } @Test diff --git a/core/src/main/java/de/jplag/SubmissionSetBuilder.java b/core/src/main/java/de/jplag/SubmissionSetBuilder.java index a9cb61b22..4d93c0d44 100644 --- a/core/src/main/java/de/jplag/SubmissionSetBuilder.java +++ b/core/src/main/java/de/jplag/SubmissionSetBuilder.java @@ -81,6 +81,13 @@ public SubmissionSet buildSubmissionSet() throws ExitException { // Merge everything in a submission set. List submissions = new ArrayList<>(foundSubmissions.values()); + + // Some languages expect a certain order, which is ensured here: + if (options.language().expectsSubmissionOrder()) { + List rootFiles = foundSubmissions.values().stream().map(it -> it.getRoot()).toList(); + rootFiles = options.language().customizeSubmissionOrder(rootFiles); + submissions = new ArrayList<>(rootFiles.stream().map(foundSubmissions::get).toList()); + } return new SubmissionSet(submissions, baseCodeSubmission.orElse(null), options); } diff --git a/coverage-report/pom.xml b/coverage-report/pom.xml index 21b94cc95..646bf87c4 100644 --- a/coverage-report/pom.xml +++ b/coverage-report/pom.xml @@ -101,6 +101,16 @@ emf-metamodel ${revision} + + de.jplag + emf-metamodel-dynamic + ${revision} + + + de.jplag + emf-model + ${revision} + diff --git a/language-api/src/main/java/de/jplag/Language.java b/language-api/src/main/java/de/jplag/Language.java index 892e30a8d..e6c42664c 100644 --- a/language-api/src/main/java/de/jplag/Language.java +++ b/language-api/src/main/java/de/jplag/Language.java @@ -66,4 +66,21 @@ default boolean useViewFiles() { default String viewFileSuffix() { return ""; } + + /** + * Specifies if the submission order is relevant for this language. + * @return defaults to false. + */ + default boolean expectsSubmissionOrder() { + return false; + } + + /** + * Re-orders the provided submission according the requirements of the language. + * @param submissions is the list of submissions. + * @return the re-ordered list. + */ + default List customizeSubmissionOrder(List submissions) { + return submissions; + } } diff --git a/language-api/src/main/java/de/jplag/Token.java b/language-api/src/main/java/de/jplag/Token.java index 29a91610b..b279b886d 100644 --- a/language-api/src/main/java/de/jplag/Token.java +++ b/language-api/src/main/java/de/jplag/Token.java @@ -46,6 +46,16 @@ public Token(TokenType type, File file, int line, int column, int length) { this.length = length; } + /** + * Creates a token with column and length information. + * @param type is the token type. + * @param file is the name of the source code file. + * @param trace is the tracing information of the token, meaning line, column, and length. + */ + public Token(TokenType type, File file, TokenTrace trace) { + this(type, file, trace.line(), trace.column(), trace.length()); + } + /** * Creates a token with column, length and semantic information. * @param type is the token type. diff --git a/language-api/src/main/java/de/jplag/TokenTrace.java b/language-api/src/main/java/de/jplag/TokenTrace.java new file mode 100644 index 000000000..6fd9808f8 --- /dev/null +++ b/language-api/src/main/java/de/jplag/TokenTrace.java @@ -0,0 +1,17 @@ +package de.jplag; + +/** + * Tracing information to locate the corresponding code section of a token. + * @param line is the line index in the source code where the token resides. Index is 1-based. + * @param column is the column index, meaning where the token starts in the line. Index is 1-based. + * @param length is the length of the token in the source code. + */ +public record TokenTrace(int line, int column, int length) { + + /** + * Creates a empty trace with line, column, and length set to {@link Token#NO_VALUE NO_VALUE}. + */ + public TokenTrace() { + this(Token.NO_VALUE, Token.NO_VALUE, Token.NO_VALUE); + } +} diff --git a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java index e337baf3a..cf358cbfb 100644 --- a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java +++ b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java @@ -5,6 +5,7 @@ import org.eclipse.emf.ecore.EObject; +import de.jplag.TokenTrace; import de.jplag.TokenType; import de.jplag.emf.MetamodelToken; @@ -15,7 +16,7 @@ public class DynamicMetamodelToken extends MetamodelToken { public DynamicMetamodelToken(TokenType type, File file, EObject eObject) { - super(type, file, NO_VALUE, NO_VALUE, NO_VALUE, Optional.of(eObject)); + super(type, file, new TokenTrace(), Optional.of(eObject)); } public DynamicMetamodelToken(TokenType type, File file) { diff --git a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenType.java b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenType.java index 712dd9e86..27bcebf60 100644 --- a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenType.java +++ b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenType.java @@ -10,6 +10,7 @@ public DynamicMetamodelTokenType(EObject eObject) { this(eObject.eClass()); } + @Override public String getDescription() { return eClass.getName(); } diff --git a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java index 152461735..9f0635d64 100644 --- a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java +++ b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java @@ -1,25 +1,33 @@ package de.jplag.emf.dynamic; -import org.kohsuke.MetaInfServices; - import de.jplag.emf.dynamic.parser.DynamicEcoreParser; +import de.jplag.emf.parser.EcoreParser; /** * Language for EMF metamodels from the Eclipse Modeling Framework (EMF). This language is based on a dynamically * created token set instead of a hand-picked one. * @author Timur Saglam */ -@MetaInfServices(de.jplag.Language.class) -public class Language extends de.jplag.emf.Language { +public class Language extends de.jplag.emf.Language { // currently not included in the CLI private static final String NAME = "EMF metamodels (dynamically created token set)"; private static final String IDENTIFIER = "emf-dynamic"; private static final int DEFAULT_MIN_TOKEN_MATCH = 10; + /** + * Creates an EMF language instance with a dynamic token parser. + */ public Language() { super(new DynamicEcoreParser()); } + /** + * Creates an EMF language instance with a custom token parser. + */ + public Language(EcoreParser parser) { + super(parser); + } + @Override public String getName() { return NAME; diff --git a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java index c0477eb4f..2b355ac0f 100644 --- a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java +++ b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java @@ -1,10 +1,12 @@ package de.jplag.emf.dynamic.parser; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import de.jplag.TokenType; import de.jplag.emf.MetamodelToken; import de.jplag.emf.dynamic.DynamicMetamodelToken; -import de.jplag.emf.dynamic.DynamicMetamodelTokenType; +import de.jplag.emf.normalization.ModelSorter; import de.jplag.emf.parser.EcoreParser; import de.jplag.emf.util.AbstractMetamodelVisitor; @@ -14,12 +16,24 @@ */ public class DynamicEcoreParser extends EcoreParser { + private final DynamicElementTokenizer tokenizer; + + public DynamicEcoreParser() { + tokenizer = new DynamicElementTokenizer(); + } + @Override protected AbstractMetamodelVisitor createMetamodelVisitor() { - return new DynamicMetamodelTokenGenerator(this); + return new DynamicMetamodelTokenGenerator(this, tokenizer); } - public void addToken(DynamicMetamodelTokenType type, EObject source) { + @Override + protected void normalizeOrder(Resource modelResource) { + ModelSorter.sort(modelResource, tokenizer); + } + + @Override + public void addToken(TokenType type, EObject source) { MetamodelToken token = new DynamicMetamodelToken(type, currentFile, source); MetamodelToken metadataEnrichedToken = treeView.convertToMetadataEnrichedToken(token); tokens.add(metadataEnrichedToken); diff --git a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicElementTokenizer.java b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicElementTokenizer.java new file mode 100644 index 000000000..346771411 --- /dev/null +++ b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicElementTokenizer.java @@ -0,0 +1,38 @@ +package de.jplag.emf.dynamic.parser; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; + +import de.jplag.TokenType; +import de.jplag.emf.dynamic.DynamicMetamodelTokenType; +import de.jplag.emf.parser.ModelingElementTokenizer; + +/** + * Tokenizes any {@link EObject} via its {@link EClass}. Tracks all known tokens. + */ +public class DynamicElementTokenizer implements ModelingElementTokenizer { + + private final Set knownTokenTypes; + + /** + * Creates the tokenizer, initially with an empty token set. + */ + public DynamicElementTokenizer() { + knownTokenTypes = new HashSet<>(); + } + + @Override + public TokenType element2Token(EObject modelElement) { + DynamicMetamodelTokenType token = new DynamicMetamodelTokenType(modelElement); + knownTokenTypes.add(token); + return token; + } + + @Override + public Set allTokenTypes() { + return Set.copyOf(knownTokenTypes); + } +} diff --git a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java index 718a18457..d77f17e1d 100644 --- a/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java +++ b/languages/emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java @@ -2,7 +2,6 @@ import org.eclipse.emf.ecore.EObject; -import de.jplag.emf.dynamic.DynamicMetamodelTokenType; import de.jplag.emf.util.AbstractMetamodelVisitor; /** @@ -11,19 +10,19 @@ */ public class DynamicMetamodelTokenGenerator extends AbstractMetamodelVisitor { private final DynamicEcoreParser parser; + private final DynamicElementTokenizer tokenizer; /** * Creates the visitor. * @param parser is the parser which receives the generated tokens. */ - public DynamicMetamodelTokenGenerator(DynamicEcoreParser parser) { - super(false); + public DynamicMetamodelTokenGenerator(DynamicEcoreParser parser, DynamicElementTokenizer tokenizer) { this.parser = parser; + this.tokenizer = tokenizer; } @Override protected void visitEObject(EObject eObject) { - var tokenType = new DynamicMetamodelTokenType(eObject); - parser.addToken(tokenType, eObject); + parser.addToken(tokenizer.element2Token(eObject), eObject); } } diff --git a/languages/emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java b/languages/emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java index a5cc19562..2861a6edd 100644 --- a/languages/emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java +++ b/languages/emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +43,7 @@ public void setUp() { } @Test + @DisplayName("Test tokens generated from example metamodels") void testBookstoreMetamodels() throws ParsingException { List testFiles = Arrays.stream(TEST_SUBJECTS).map(path -> new File(BASE_PATH.toFile(), path)).toList(); List result = language.parse(new HashSet<>(testFiles)); diff --git a/languages/emf-metamodel/pom.xml b/languages/emf-metamodel/pom.xml index 0e5f502b1..8b328c38d 100644 --- a/languages/emf-metamodel/pom.xml +++ b/languages/emf-metamodel/pom.xml @@ -12,7 +12,7 @@ org.eclipse.emfatic org.eclipse.emfatic.core - 1.0.0 + ${emfatic.version} org.eclipse.emf diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java index 528fb94f1..0bc6544cf 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java @@ -6,6 +6,7 @@ import org.eclipse.emf.ecore.EObject; import de.jplag.Token; +import de.jplag.TokenTrace; import de.jplag.TokenType; /** @@ -22,8 +23,8 @@ public class MetamodelToken extends Token { * @param file is the source model file. * @param eObject is the corresponding eObject in the model from which this token was extracted. */ - public MetamodelToken(MetamodelTokenType type, File file, EObject eObject) { - this(type, file, NO_VALUE, NO_VALUE, NO_VALUE, Optional.of(eObject)); + public MetamodelToken(TokenType type, File file, EObject eObject) { + this(type, file, new TokenTrace(), Optional.of(eObject)); } /** @@ -32,20 +33,18 @@ public MetamodelToken(MetamodelTokenType type, File file, EObject eObject) { * @param file is the source model file. */ public MetamodelToken(TokenType type, File file) { - this(type, file, NO_VALUE, NO_VALUE, NO_VALUE, Optional.empty()); + this(type, file, new TokenTrace(), Optional.empty()); } /** * Creates a token with column and length information. * @param type is the token type. * @param file is the source code file. - * @param line is the line index in the source code where the token resides. Cannot be smaller than 1. - * @param column is the column index, meaning where the token starts in the line. - * @param length is the length of the token in the source code. + * @param trace is the tracing information of the token, meaning line, column, and length. * @param eObject is the corresponding eObject in the model from which this token was extracted */ - public MetamodelToken(TokenType type, File file, int line, int column, int length, Optional eObject) { - super(type, file, line, column, length); + public MetamodelToken(TokenType type, File file, TokenTrace trace, Optional eObject) { + super(type, file, trace); this.eObject = eObject; } diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenType.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenType.java index a12596ea4..c90c34571 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenType.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenType.java @@ -16,15 +16,15 @@ public enum MetamodelTokenType implements TokenType { ENUM_END(ENUM), ENUM_LITERAL("EEnumLiteral"), OPERATION("EOperation"), - OPERATION_END(OPERATION), REFERENCE("EReference"), + REFERENCE_MULT("EReference (multi-valued)"), ATTRIBUTE("EAttribute"), PARAMETER("EParameter"), INTERFACE("EInterface"), INTERFACE_END(INTERFACE), - SUPER_TYPE("ESuperType"), ID_ATTRIBUTE("EAttribute (ID)"), CONTAINMENT("EReference (Containment)"), + CONTAINMENT_MULT("EReference (Containment, multi-valued)"), ABSTRACT_CLASS("EAbstractClass"), ABSTRACT_CLASS_END(ABSTRACT_CLASS), RETURN_TYPE("EClassifier (Return Type"), @@ -37,6 +37,7 @@ public enum MetamodelTokenType implements TokenType { private final String description; private final boolean isEndToken; + @Override public String getDescription() { return description; } diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/ContainmentOrderNormalizer.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/ContainmentOrderNormalizer.java new file mode 100644 index 000000000..f8dc57934 --- /dev/null +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/ContainmentOrderNormalizer.java @@ -0,0 +1,132 @@ +package de.jplag.emf.normalization; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.emf.ecore.EObject; + +import de.jplag.TokenType; +import de.jplag.emf.parser.ModelingElementTokenizer; + +/** + * Comparator for normalizing the order in a model tree by sorting the elements of containment references according to + * their token type and then according to the distributions of token types in their subtrees. + */ +public class ContainmentOrderNormalizer implements Comparator { + + private final List modelElementsToSort; + private final Map> paths; + private final ModelingElementTokenizer tokenizer; + private final TokenVectorGenerator tokenVectorGenerator; + + /** + * Creates the normalizing comparator. + * @param modelElementsToSort are all model elements to sort with the comparator (required for normalization process). + */ + public ContainmentOrderNormalizer(List modelElementsToSort, ModelingElementTokenizer tokenizer) { + this.modelElementsToSort = modelElementsToSort; + this.tokenizer = tokenizer; + paths = new HashMap<>(); + tokenVectorGenerator = new TokenVectorGenerator(tokenizer); + } + + @Override + public int compare(EObject first, EObject second) { + TokenType firstType = tokenizer.element2Token(first); + TokenType secondType = tokenizer.element2Token(second); + + // 0. comparison if token types are absent for one or more elements. + if (firstType == null && secondType == null) { + return 0; + } else if (firstType == null) { + return -1; + } else if (secondType == null) { + return 1; + } + + // 1. comparison by token type + int comparisonByType = firstType.toString().compareTo(secondType.toString()); + if (comparisonByType != 0) { + return comparisonByType; + } + + // 2. compare by position of the nearest neighbor path of the token distribution vectors of the elements subtrees. + List path = paths.computeIfAbsent(firstType, this::calculatePath); + return path.indexOf(first) - path.indexOf(second); + } + + private List calculatePath(List elements, EObject start, double[][] distances) { + List path = new ArrayList<>(); + Set remaining = new HashSet<>(elements); + EObject current = start; + remaining.remove(current); + path.add(current); + while (!remaining.isEmpty()) { + double shortestDistance = Double.MAX_VALUE; + EObject next = null; + for (EObject potentialNext : remaining) { + double distance = distances[elements.indexOf(current)][elements.indexOf(potentialNext)]; + if (distance < shortestDistance) { + shortestDistance = distance; + next = potentialNext; + } else if (distance == shortestDistance && modelElementsToSort.indexOf(potentialNext) < modelElementsToSort.indexOf(next)) { + next = potentialNext; // Sort according to original order if equal + } + } + current = next; + remaining.remove(current); + path.add(current); + } + return path; + } + + private List calculatePath(TokenType type) { + List elements = modelElementsToSort.stream().filter(it -> type.equals(tokenizer.element2Token(it))).toList(); + + // Generate token type distributions for the subtrees of the elements to sort: + Map> subtreeVectors = new HashMap<>(); + elements.forEach(it -> subtreeVectors.put(it, tokenVectorGenerator.generateOccurenceVector(it.eAllContents()))); + + // Calculate distance matrix: + double[][] distances = new double[elements.size()][elements.size()]; + for (int from = 0; from < distances.length; from++) { + for (int to = 0; to < distances.length; to++) { + distances[from][to] = euclideanDistance(subtreeVectors.get(elements.get(from)), subtreeVectors.get(elements.get(to))); + } + } + + // Start with element that has the most tokens in the subtree: + var max = Collections.max(elements, (first, second) -> Integer.compare(countSubtreeTokens(first), countSubtreeTokens(second))); + return calculatePath(elements, max, distances); + } + + private int countSubtreeTokens(EObject modelElement) { + int count = 0; + Iterator iterator = modelElement.eAllContents(); + while (iterator.hasNext()) { + if (tokenizer.element2Token(iterator.next()) != null) { + count++; + } + } + return count; + } + + private static double euclideanDistance(List first, List second) { + if (first.size() != second.size()) { + throw new IllegalArgumentException("Lists must have the same size"); + } + double sum = 0; + for (int i = 0; i < first.size(); i++) { + double diff = first.get(i) - second.get(i); + sum += diff * diff; + } + return Math.sqrt(sum); + } +} diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/ModelSorter.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/ModelSorter.java new file mode 100644 index 000000000..0594f77c1 --- /dev/null +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/ModelSorter.java @@ -0,0 +1,53 @@ +package de.jplag.emf.normalization; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.resource.Resource; + +import de.jplag.emf.parser.ModelingElementTokenizer; +import de.jplag.emf.util.AbstractMetamodelVisitor; + +/** + * Utility class that sorts all containment references of an EMF model or metamodel. + */ +public class ModelSorter extends AbstractMetamodelVisitor { + + private final ModelingElementTokenizer tokenizer; + + /** + * Creates a model sorter. + */ + private ModelSorter(ModelingElementTokenizer tokenizer) { + this.tokenizer = tokenizer; // private constructor to hide visitor functionality. + } + + /** + * Sorts the given model or metamodel. + * @param modelResource is the resource of the model or metamodel. + * @param tokenizer provides the tokenization rules for the sorting. + */ + public static void sort(Resource modelResource, ModelingElementTokenizer tokenizer) { + modelResource.getContents().forEach(new ModelSorter(tokenizer)::visit); + } + + @Override + protected void visitEObject(EObject eObject) { + for (EReference reference : eObject.eClass().getEAllContainments()) { + if (reference.isMany()) { + Object containment = eObject.eGet(reference); + if (containment instanceof List) { + @SuppressWarnings("unchecked") // There is no cleaner way + List containmentList = (List) containment; + List sortedContent = new ArrayList<>(containmentList); + sortedContent.sort(new ContainmentOrderNormalizer(sortedContent, tokenizer)); + containmentList.clear(); + containmentList.addAll(sortedContent); + } + } + } + } + +} diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/TokenVectorGenerator.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/TokenVectorGenerator.java new file mode 100644 index 000000000..3eb4ce5ea --- /dev/null +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/normalization/TokenVectorGenerator.java @@ -0,0 +1,58 @@ +package de.jplag.emf.normalization; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.emf.ecore.EObject; + +import de.jplag.TokenType; +import de.jplag.emf.MetamodelTokenType; +import de.jplag.emf.parser.ModelingElementTokenizer; + +/** + * Utility class for the generation of token occurrence histograms for model subtrees. + */ +public class TokenVectorGenerator { + + private final ModelingElementTokenizer tokenizer; + + public TokenVectorGenerator(ModelingElementTokenizer tokenizer) { + this.tokenizer = tokenizer; + } + + /** + * Generate a token occurrence vector for a subtree of a model. + * @param modelElements is a visitor for the subtree. + * @return a list, where each entry represents the number of tokens in the subtree. The order is determined by + * {@link MetamodelTokenType}. + */ + public List generateOccurenceVector(Iterator modelElements) { + Map tokenTypeHistogram = new HashMap<>(); + + while (modelElements.hasNext()) { + tokenizer.element2OptionalToken(modelElements.next()).ifPresent(it -> tokenTypeHistogram.merge(it, 1, Integer::sum)); + } + List occurenceVector = new ArrayList<>(); + for (TokenType type : tokenizer.allTokenTypes()) { + occurenceVector.add(tokenTypeHistogram.getOrDefault(type, 0)); + } + return normalize(occurenceVector); + } + + public static List normalize(List vector) { + double magnitude = Math.sqrt(vector.stream().mapToInt(it -> it * it).sum()); + if (magnitude == 0) { + return Collections.nCopies(vector.size(), 0.0); + } + List normalizedVector = new ArrayList<>(); + for (int element : vector) { + double normalizedValue = element / magnitude; + normalizedVector.add(normalizedValue); + } + return normalizedVector; + } +} diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java index 2f740eba3..c92b20a7d 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java @@ -11,21 +11,22 @@ import de.jplag.AbstractParser; import de.jplag.ParsingException; import de.jplag.Token; +import de.jplag.TokenType; import de.jplag.emf.Language; import de.jplag.emf.MetamodelToken; -import de.jplag.emf.MetamodelTokenType; +import de.jplag.emf.normalization.ModelSorter; import de.jplag.emf.util.AbstractMetamodelVisitor; +import de.jplag.emf.util.AbstractModelView; import de.jplag.emf.util.EMFUtil; import de.jplag.emf.util.EmfaticModelView; /** * Parser for EMF metamodels. - * @author Timur Saglam */ public class EcoreParser extends AbstractParser { protected List tokens; protected File currentFile; - protected EmfaticModelView treeView; + protected AbstractModelView treeView; protected AbstractMetamodelVisitor visitor; /** @@ -58,16 +59,41 @@ protected void parseModelFile(File file) throws ParsingException { if (model == null) { throw new ParsingException(file, "failed to load model"); } else { - treeView = new EmfaticModelView(file, model); + normalizeOrder(model); + treeView = createView(file, model); + visitor = createMetamodelVisitor(); for (EObject root : model.getContents()) { - visitor = createMetamodelVisitor(); visitor.visit(root); } tokens.add(Token.fileEnd(currentFile)); - treeView.writeToFile(Language.VIEW_FILE_SUFFIX); + treeView.writeToFile(getCorrespondingViewFileSuffix()); } } + /** + * @return the correct view file suffix for the model view. Can be overriden in subclasses for alternative views. + */ + protected String getCorrespondingViewFileSuffix() { + return Language.VIEW_FILE_SUFFIX; + } + + /** + * Creates a model view. Can be overriden in subclasses for alternative views. + * @param file is the path for the view file to be created. + * @param modelResource is the resource containing the metamodel. + * @return the view implementation. + */ + protected AbstractModelView createView(File file, Resource modelResource) { + return new EmfaticModelView(file, modelResource); + } + + /** + * Extension point for subclasses to employ different normalization. + */ + protected void normalizeOrder(Resource modelResource) { + ModelSorter.sort(modelResource, new MetamodelElementTokenizer()); + } + /** * Extension point for subclasses to employ different token generators. * @return a token generating metamodel visitor. @@ -81,7 +107,7 @@ protected AbstractMetamodelVisitor createMetamodelVisitor() { * @param type is the token type. * @param source is the corresponding {@link EObject} for which the token is added. */ - void addToken(MetamodelTokenType type, EObject source) { + protected void addToken(TokenType type, EObject source) { MetamodelToken token = new MetamodelToken(type, currentFile, source); tokens.add(treeView.convertToMetadataEnrichedToken(token)); } diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelElementTokenizer.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelElementTokenizer.java new file mode 100644 index 000000000..f5f7c66c1 --- /dev/null +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelElementTokenizer.java @@ -0,0 +1,114 @@ +package de.jplag.emf.parser; + +import java.util.Set; + +import org.eclipse.emf.ecore.EAnnotation; +import org.eclipse.emf.ecore.EAttribute; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EDataType; +import org.eclipse.emf.ecore.EEnum; +import org.eclipse.emf.ecore.EEnumLiteral; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EOperation; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EParameter; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.ETypeParameter; +import org.eclipse.emf.ecore.util.EcoreSwitch; + +import de.jplag.TokenType; +import de.jplag.emf.MetamodelTokenType; + +/** + * Tokenizer for metamodel elements. Maps any {@link EObject} to a {@link MetamodelTokenType}. + */ +public class MetamodelElementTokenizer extends EcoreSwitch implements ModelingElementTokenizer { + + @Override + public TokenType element2Token(EObject modelElement) { + return doSwitch(modelElement); + } + + @Override + public MetamodelTokenType caseEAnnotation(EAnnotation eAnnotation) { + return MetamodelTokenType.ANNOTATION; + } + + @Override + public MetamodelTokenType caseEAttribute(EAttribute eAttribute) { + if (eAttribute.isID()) { + return MetamodelTokenType.ID_ATTRIBUTE; + } else { + return MetamodelTokenType.ATTRIBUTE; + } + } + + @Override + public MetamodelTokenType caseEClass(EClass eClass) { + if (eClass.isInterface()) { + return MetamodelTokenType.INTERFACE; + } else if (eClass.isAbstract()) { + return MetamodelTokenType.ABSTRACT_CLASS; + } else { + return MetamodelTokenType.CLASS; + } + } + + @Override + public MetamodelTokenType caseEDataType(EDataType eDataType) { + return MetamodelTokenType.DATATYPE; + } + + @Override + public MetamodelTokenType caseETypeParameter(ETypeParameter eTypeParameter) { + return MetamodelTokenType.TYPE_PARAMETER; + } + + @Override + public MetamodelTokenType caseEParameter(EParameter eParameter) { + return MetamodelTokenType.PARAMETER; + } + + @Override + public MetamodelTokenType caseEOperation(EOperation eOperation) { + return MetamodelTokenType.OPERATION; + } + + @Override + public MetamodelTokenType caseEPackage(EPackage ePackage) { + return MetamodelTokenType.PACKAGE; + } + + @Override + public MetamodelTokenType caseEEnumLiteral(EEnumLiteral eEnumLiteral) { + return MetamodelTokenType.ENUM_LITERAL; + } + + @Override + public MetamodelTokenType caseEEnum(EEnum eEnum) { + return MetamodelTokenType.ENUM; + } + + @Override + public MetamodelTokenType caseEReference(EReference eReference) { + if (eReference.isContainment()) { + if (eReference.getUpperBound() == 1) { + return MetamodelTokenType.CONTAINMENT; + } else { + return MetamodelTokenType.CONTAINMENT_MULT; + } + } else { + if (eReference.getUpperBound() == 1) { + return MetamodelTokenType.REFERENCE; + } else { + return MetamodelTokenType.REFERENCE_MULT; + } + } + } + + @Override + public Set allTokenTypes() { + return Set.of(MetamodelTokenType.values()); + } + +} diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java index 95b23c90d..c038c9068 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java @@ -1,135 +1,56 @@ package de.jplag.emf.parser; -import static de.jplag.emf.MetamodelTokenType.ABSTRACT_CLASS; import static de.jplag.emf.MetamodelTokenType.ABSTRACT_CLASS_END; -import static de.jplag.emf.MetamodelTokenType.ANNOTATION; -import static de.jplag.emf.MetamodelTokenType.ATTRIBUTE; import static de.jplag.emf.MetamodelTokenType.BOUND; -import static de.jplag.emf.MetamodelTokenType.CLASS; import static de.jplag.emf.MetamodelTokenType.CLASS_END; -import static de.jplag.emf.MetamodelTokenType.CONTAINMENT; -import static de.jplag.emf.MetamodelTokenType.DATATYPE; -import static de.jplag.emf.MetamodelTokenType.ENUM; import static de.jplag.emf.MetamodelTokenType.ENUM_END; -import static de.jplag.emf.MetamodelTokenType.ENUM_LITERAL; -import static de.jplag.emf.MetamodelTokenType.ID_ATTRIBUTE; -import static de.jplag.emf.MetamodelTokenType.INTERFACE; import static de.jplag.emf.MetamodelTokenType.INTERFACE_END; -import static de.jplag.emf.MetamodelTokenType.OPERATION; -import static de.jplag.emf.MetamodelTokenType.OPERATION_END; -import static de.jplag.emf.MetamodelTokenType.PACKAGE; import static de.jplag.emf.MetamodelTokenType.PACKAGE_END; -import static de.jplag.emf.MetamodelTokenType.PARAMETER; -import static de.jplag.emf.MetamodelTokenType.REFERENCE; import static de.jplag.emf.MetamodelTokenType.RETURN_TYPE; -import static de.jplag.emf.MetamodelTokenType.SUPER_TYPE; import static de.jplag.emf.MetamodelTokenType.THROWS_DECLARATION; -import static de.jplag.emf.MetamodelTokenType.TYPE_PARAMETER; -import org.eclipse.emf.ecore.EAnnotation; -import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClass; -import org.eclipse.emf.ecore.EDataType; import org.eclipse.emf.ecore.EEnum; -import org.eclipse.emf.ecore.EEnumLiteral; +import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EOperation; import org.eclipse.emf.ecore.EPackage; -import org.eclipse.emf.ecore.EParameter; -import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.ETypeParameter; import de.jplag.emf.util.AbstractMetamodelVisitor; /** - * Visits a metamodel containment tree and extracts the relevant token. + * Visits a metamodel containment tree and extracts the relevant token. See also {@link MetamodelElementTokenizer}. * @author Timur Saglam */ public class MetamodelTokenGenerator extends AbstractMetamodelVisitor { - private EcoreParser parser; + private final EcoreParser parser; + private final ModelingElementTokenizer tokenizer; /** * Creates the visitor. * @param parser is the parser which receives the generated tokens. */ public MetamodelTokenGenerator(EcoreParser parser) { - super(true); this.parser = parser; + tokenizer = new MetamodelElementTokenizer(); } @Override - protected void visitEAnnotation(EAnnotation eAnnotation) { - parser.addToken(ANNOTATION, eAnnotation); - } - - @Override - protected void visitEAttribute(EAttribute eAttribute) { - if (eAttribute.isID()) { - parser.addToken(ID_ATTRIBUTE, eAttribute); - } else { - parser.addToken(ATTRIBUTE, eAttribute); - } - } - - @Override - protected void visitEClass(EClass eClass) { - if (eClass.isInterface()) { - parser.addToken(INTERFACE, eClass); - } else if (eClass.isAbstract()) { - parser.addToken(ABSTRACT_CLASS, eClass); - } else { - parser.addToken(CLASS, eClass); - } - eClass.getESuperTypes().forEach(it -> parser.addToken(SUPER_TYPE, eClass)); - } - - @Override - protected void visitEDataType(EDataType eDataType) { - if (!(eDataType instanceof EEnum)) { - parser.addToken(DATATYPE, eDataType); - } - } - - @Override - protected void visitEEnum(EEnum eEnum) { - parser.addToken(ENUM, eEnum); - } - - @Override - protected void visitEEnumLiteral(EEnumLiteral eEnumLiteral) { - parser.addToken(ENUM_LITERAL, eEnumLiteral); + protected void visitEObject(EObject eObject) { + // Create begin tokens for elements that directly map to a token. + tokenizer.element2OptionalToken(eObject).ifPresent(it -> parser.addToken(it, eObject)); } @Override protected void visitEOperation(EOperation eOperation) { - parser.addToken(OPERATION, eOperation); if (eOperation.getEType() != null) { parser.addToken(RETURN_TYPE, eOperation); } eOperation.getEExceptions().forEach(it -> parser.addToken(THROWS_DECLARATION, it)); } - @Override - protected void visitEPackage(EPackage ePackage) { - parser.addToken(PACKAGE, ePackage); - } - - @Override - protected void visitEParameter(EParameter eParameter) { - parser.addToken(PARAMETER, eParameter); - } - - @Override - protected void visitEReference(EReference eReference) { - if (eReference.isContainment()) { - parser.addToken(CONTAINMENT, eReference); - } else { - parser.addToken(REFERENCE, eReference); - } - } - @Override protected void visitETypeParameter(ETypeParameter eTypeParameter) { - parser.addToken(TYPE_PARAMETER, eTypeParameter); eTypeParameter.getEBounds().forEach(it -> parser.addToken(BOUND, it)); } @@ -154,9 +75,4 @@ protected void leaveEEnum(EEnum eEnum) { parser.addToken(ENUM_END, eEnum); } - @Override - protected void leaveEOperation(EOperation eOperation) { - parser.addToken(OPERATION_END, eOperation); - } - } diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/ModelingElementTokenizer.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/ModelingElementTokenizer.java new file mode 100644 index 000000000..bed7e55b3 --- /dev/null +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/parser/ModelingElementTokenizer.java @@ -0,0 +1,48 @@ +package de.jplag.emf.parser; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.emf.ecore.EObject; + +import de.jplag.TokenType; + +/** + * Tokenizer for EMF modeling elements. Maps any {@link EObject} to a {@link TokenType}. + */ +public interface ModelingElementTokenizer { + + /** + * Returns the corresponding token type for a model element. + * @param modelElement is the model element. + * @return the token type or null if no token is extracted for that element. + */ + TokenType element2Token(EObject modelElement); + + /** + * Returns the corresponding the token types for a list of model elements. See + * {@link ModelingElementTokenizer#element2Token(EObject)}. + * @param modelElements contains the model elements. + * @return the list of corresponding token types, might contain less entries than elements. + */ + default List elements2Tokens(List modelElements) { + return modelElements.stream().map(this::element2Token).filter(Objects::nonNull).toList(); + } + + /** + * Returns the corresponding token type for a model element. See + * {@link ModelingElementTokenizer#element2Token(EObject)}. + * @param modelElement is the model element. + * @return the optional token type. + */ + default Optional element2OptionalToken(EObject modelElement) { + return Optional.ofNullable(element2Token(modelElement)); + } + + /** + * @return the set of all known token types. + */ + Set allTokenTypes(); +} diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java index 2f15f4086..f957bd0dc 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java @@ -1,7 +1,5 @@ package de.jplag.emf.util; -import java.util.ArrayList; - import org.eclipse.emf.ecore.EAnnotation; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClass; @@ -26,12 +24,6 @@ */ public abstract class AbstractMetamodelVisitor { - private final boolean sortContainmentsByType; - - protected AbstractMetamodelVisitor(boolean sortContainmentsByType) { - this.sortContainmentsByType = sortContainmentsByType; - } - private int currentTreeDepth; /** @@ -48,6 +40,7 @@ public int getCurrentTreeDepth() { * @param eObject is the EObject to visit. */ public final void visit(EObject eObject) { + visitEObject(eObject); if (eObject instanceof EPackage ePackage) { visitEPackage(ePackage); @@ -98,13 +91,8 @@ public final void visit(EObject eObject) { visitENamedElement(eNamedElement); } - var children = new ArrayList<>(eObject.eContents()); - if (sortContainmentsByType) { - children.sort((first, second) -> first.eClass().getName().compareTo(second.eClass().getName())); - } - currentTreeDepth++; - for (EObject child : children) { + for (EObject child : eObject.eContents()) { visit(child); } currentTreeDepth--; diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractModelView.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractModelView.java index e0dcbf11b..d5261bc92 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractModelView.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/AbstractModelView.java @@ -8,22 +8,32 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import de.jplag.emf.MetamodelToken; + /** * Textual representation of a model for the depiction of matches in submissions. - * @author Timur Saglam */ -public class AbstractModelView { +public abstract class AbstractModelView { protected final File file; protected final Logger logger; protected final StringBuilder viewBuilder; - public AbstractModelView(File file) { + protected AbstractModelView(File file) { this.file = file; logger = LoggerFactory.getLogger(this.getClass()); viewBuilder = new StringBuilder(); } + /** + * Creates a token with tracing information based on an existing one without. The token information may also be used to + * build up the model view. This means a model view may be only complete after passing every token to the view to + * enrich. + * @param token is the existing token without tracing information. + * @return the enriched token, with the tracing information corresponding to this view. + */ + public abstract MetamodelToken convertToMetadataEnrichedToken(MetamodelToken token); + /** * Writes the tree view into a file. * @param suffix is the suffix of the file to be written. diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/EmfaticModelView.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/EmfaticModelView.java index c2ae5c6ae..7f3445093 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/EmfaticModelView.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/EmfaticModelView.java @@ -15,6 +15,7 @@ import org.eclipse.emf.emfatic.core.generator.emfatic.Writer; import de.jplag.Token; +import de.jplag.TokenTrace; import de.jplag.emf.MetamodelToken; import de.jplag.emf.MetamodelTokenType; @@ -22,7 +23,6 @@ * Textual view of an EMF metamodel based on Emfatic. Emfatic code is generated for the metamodel and the model elements * are then traced to line in the code. The tracing is done via hashes as model element names and keyword detection via * regex matching. The tracing is requires, as Emfatic does not provide it itself. - * @author Timur Saglam */ public final class EmfaticModelView extends AbstractModelView { // The following regular expressions match keywords of the Emfatic syntax: @@ -37,7 +37,7 @@ public final class EmfaticModelView extends AbstractModelView { private final List hashedLines; // code for model element tracing lookup private final Map elementToLine; // maps model elements to Emfatic code line numbers - private Copier modelCopier; // Allows to trace between original and copied elements + private final Copier modelCopier; // Allows to trace between original and copied elements private int lastLineIndex; // last line given to a token /** @@ -62,6 +62,7 @@ public EmfaticModelView(File file, Resource modelResource) { * @param token is the existing token without tracing information. * @return the enriched token, with the tracing information corresponding to this view. */ + @Override public MetamodelToken convertToMetadataEnrichedToken(MetamodelToken token) { int lineIndex = calculateLineIndexOf(token); String line = lines.get(lineIndex); @@ -72,7 +73,8 @@ public MetamodelToken convertToMetadataEnrichedToken(MetamodelToken token) { lineIndex++; columnIndex += columnIndex == Token.NO_VALUE ? 0 : 1; - return new MetamodelToken(token.getType(), token.getFile(), lineIndex, columnIndex, length, token.getEObject()); + TokenTrace trace = new TokenTrace(lineIndex, columnIndex, length); + return new MetamodelToken(token.getType(), token.getFile(), trace, token.getEObject()); } /** @@ -80,7 +82,7 @@ public MetamodelToken convertToMetadataEnrichedToken(MetamodelToken token) { * elements in subsequently generated Emfatic code while avoiding name collisions. */ private final void replaceElementNamesWithHashes(Resource copiedResource) { - AbstractMetamodelVisitor renamer = new AbstractMetamodelVisitor(false) { + AbstractMetamodelVisitor renamer = new AbstractMetamodelVisitor() { @Override protected void visitENamedElement(ENamedElement eNamedElement) { eNamedElement.setName(Integer.toString(eNamedElement.hashCode())); diff --git a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java index 36a736e01..55f3647bc 100644 --- a/languages/emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java +++ b/languages/emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java @@ -1,75 +1,73 @@ package de.jplag.emf.util; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.eclipse.emf.ecore.ENamedElement; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; -import de.jplag.Token; +import de.jplag.TokenTrace; import de.jplag.emf.MetamodelToken; /** - * Simplistic tree view representation of an EMF metamodel. - * @author Timur Saglam + * Very basic tree view representation of an EMF metamodel or model. */ public class MetamodelTreeView extends AbstractModelView { - - private int lineIndex; - private int columnIndex; - - private static final String INDENTATION = " "; + private final List lines; + private final Map objectToLine; /** * Creates a tree view for a metamodel. * @param file is the path to the metamodel. */ - public MetamodelTreeView(File file) { + public MetamodelTreeView(File file, Resource modelResource) { super(file); + lines = new ArrayList<>(); + objectToLine = new HashMap<>(); + TreeViewBuilder visitor = new TreeViewBuilder(); + modelResource.getContents().forEach(visitor::visit); } /** * Adds a token to the view, thus adding the index information to the token. Returns a new token enriched with the index * metadata. * @param token is the token to add. - * @param treeDepth is the current containment tree depth, required for the indentation. */ - public MetamodelToken convertToMetadataEnrichedTokenAndAdd(MetamodelToken token, int treeDepth, String prefix) { - int length = Token.NO_VALUE; - int line = Token.NO_VALUE; - int column = Token.NO_VALUE; + @Override + public MetamodelToken convertToMetadataEnrichedToken(MetamodelToken token) { Optional optionalEObject = token.getEObject(); if (optionalEObject.isPresent()) { - EObject eObject = optionalEObject.get(); - if (prefix.isEmpty() && treeDepth > 0) { - lineIndex++; - columnIndex = 0; - viewBuilder.append(System.lineSeparator()); - } + EObject object = optionalEObject.get(); + TokenTrace trace = objectToLine.get(object); + return new MetamodelToken(token.getType(), token.getFile(), trace, optionalEObject); + } + return new MetamodelToken(token.getType(), token.getFile()); + } - String tokenText = token.getType().getDescription(); - if (eObject instanceof ENamedElement element) { - tokenText = element.getName() + " : " + tokenText; - } - length = tokenText.length(); + private final class TreeViewBuilder extends AbstractMetamodelVisitor { + private static final String INDENTATION = " "; + private static final String NAME_SEPARATOR = " : "; - if (prefix.isEmpty()) { - for (int i = 0; i < treeDepth; i++) { - viewBuilder.append(INDENTATION); - columnIndex += INDENTATION.length(); - } - viewBuilder.append(tokenText); - } else { - viewBuilder.append(prefix + tokenText); - columnIndex += prefix.length(); + @Override + protected void visitEObject(EObject eObject) { + String prefix = INDENTATION.repeat(getCurrentTreeDepth()); + String line = prefix; + if (eObject instanceof ENamedElement element) { + line += element.getName() + NAME_SEPARATOR; } + line += eObject.eClass().getName(); - line = lineIndex + 1; - column = columnIndex + 1; - - columnIndex += tokenText.length(); + lines.add(line); + viewBuilder.append(line + System.lineSeparator()); + // line and column values are one-indexed + TokenTrace trace = new TokenTrace(lines.size(), prefix.length() + 1, line.trim().length()); + objectToLine.put(eObject, trace); } - return new MetamodelToken(token.getType(), token.getFile(), line, column, length, token.getEObject()); } } diff --git a/languages/emf-metamodel/src/test/java/de/jplag/emf/AbstractEmfTest.java b/languages/emf-metamodel/src/test/java/de/jplag/emf/AbstractEmfTest.java index 0abb90def..0626026ab 100644 --- a/languages/emf-metamodel/src/test/java/de/jplag/emf/AbstractEmfTest.java +++ b/languages/emf-metamodel/src/test/java/de/jplag/emf/AbstractEmfTest.java @@ -1,11 +1,20 @@ package de.jplag.emf; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import org.eclipse.emf.ecore.resource.Resource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import de.jplag.emf.util.EMFUtil; import de.jplag.testutils.FileUtil; /** @@ -35,4 +44,34 @@ protected void setUp() { protected void tearDown() { FileUtil.clearFiles(new File(BASE_PATH.toString()), Language.VIEW_FILE_SUFFIX); } + + /** + * Load (meta)model from file and assert it is correctly loaded. + * @param modelFile is the file to load. + * @return the loaded resource. + */ + protected Resource loadAndVerifyModel(File modelFile) { + assertTrue(modelFile.exists()); + Resource modelResource = EMFUtil.loadModelResource(modelFile); + assertNotNull(modelResource); + return modelResource; + } + + /** + * Compares the generated view file of a meta(model) with an expected one. + * @param modelFile is the file of the meta(model). + * @param viewFileSuffix is the suffix of the view file. + * @param directoryOfExpectedViews is the name of the folder where the expected view files are located. + */ + protected void assertViewFilesMatch(File modelFile, String viewFileSuffix, String directoryOfExpectedViews) { + File viewFile = new File(modelFile.getPath() + viewFileSuffix); + File expectedViewFile = BASE_PATH.resolveSibling(Path.of(directoryOfExpectedViews, viewFile.getName())).toFile(); + assertTrue(viewFile.exists()); + assertTrue(expectedViewFile.exists()); + try { + assertIterableEquals(Files.readAllLines(expectedViewFile.toPath()), Files.readAllLines(viewFile.toPath())); + } catch (IOException exception) { + fail(exception); + } + } } diff --git a/languages/emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java b/languages/emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java index 7284803d3..8089d664c 100644 --- a/languages/emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java +++ b/languages/emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java @@ -33,8 +33,8 @@ void testBookstoreMetamodels() throws ParsingException { logger.debug(TokenPrinter.printTokens(result, baseDirectory, Optional.of(Language.VIEW_FILE_SUFFIX))); List tokenTypes = result.stream().map(Token::getType).toList(); logger.info("Parsed token types: " + tokenTypes.stream().map(TokenType::getDescription).toList().toString()); - assertEquals(82, tokenTypes.size()); - assertEquals(13, new HashSet<>(tokenTypes).size()); + assertEquals(80, tokenTypes.size()); + assertEquals(12, new HashSet<>(tokenTypes).size()); var originalTokens = TokenUtils.tokenTypesByFile(result, testFiles.get(0)); var renamedTokens = TokenUtils.tokenTypesByFile(result, testFiles.get(3)); diff --git a/languages/emf-metamodel/src/test/java/de/jplag/emf/util/EmfaticModelViewTest.java b/languages/emf-metamodel/src/test/java/de/jplag/emf/util/EmfaticModelViewTest.java index d6f178c00..3c6d116c3 100644 --- a/languages/emf-metamodel/src/test/java/de/jplag/emf/util/EmfaticModelViewTest.java +++ b/languages/emf-metamodel/src/test/java/de/jplag/emf/util/EmfaticModelViewTest.java @@ -1,14 +1,6 @@ package de.jplag.emf.util; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -34,23 +26,13 @@ private static List provideModelNames() { void testEmfaticViewFiles(String modelName) { // Load model: File modelFile = new File(baseDirectory, modelName); - assertTrue(modelFile.exists()); - Resource modelResource = EMFUtil.loadModelResource(modelFile); - assertNotNull(modelResource); + Resource modelResource = loadAndVerifyModel(modelFile); // Generate emfatic view: EmfaticModelView view = new EmfaticModelView(modelFile, modelResource); view.writeToFile(Language.VIEW_FILE_SUFFIX); // Compare expected vs. actual view file: - File viewFile = new File(modelFile.getPath() + Language.VIEW_FILE_SUFFIX); - File expectedViewFile = BASE_PATH.resolveSibling(Path.of(EXPECTED_VIEW_FOLDER, viewFile.getName())).toFile(); - assertTrue(viewFile.exists()); - assertTrue(expectedViewFile.exists()); - try { - assertEquals(Files.readAllLines(expectedViewFile.toPath()), Files.readAllLines(viewFile.toPath())); - } catch (IOException exception) { - fail(exception); - } + assertViewFilesMatch(modelFile, Language.VIEW_FILE_SUFFIX, EXPECTED_VIEW_FOLDER); } } diff --git a/languages/emf-metamodel/src/test/java/de/jplag/emf/util/MetamodelTreeViewTest.java b/languages/emf-metamodel/src/test/java/de/jplag/emf/util/MetamodelTreeViewTest.java new file mode 100644 index 000000000..9a2976974 --- /dev/null +++ b/languages/emf-metamodel/src/test/java/de/jplag/emf/util/MetamodelTreeViewTest.java @@ -0,0 +1,47 @@ +package de.jplag.emf.util; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.emf.ecore.resource.Resource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import de.jplag.emf.AbstractEmfTest; +import de.jplag.testutils.FileUtil; + +class MetamodelTreeViewTest extends AbstractEmfTest { + + private static final String VIEW_FILE_SUFFIX = ".treeview"; + private static final String EXPECTED_VIEW_FOLDER = "treeview"; + + private static List provideModelNames() { + return Arrays.asList(TEST_SUBJECTS); + } + + @ParameterizedTest + @DisplayName("Test content of emfatic view files of example metamodels") + @MethodSource("provideModelNames") + void testEmfaticViewFiles(String modelName) { + // Load model: + File modelFile = new File(baseDirectory, modelName); + Resource modelResource = loadAndVerifyModel(modelFile); + + // Generate emfatic view: + MetamodelTreeView view = new MetamodelTreeView(modelFile, modelResource); + view.writeToFile(VIEW_FILE_SUFFIX); + + // Compare expected vs. actual view file: + assertViewFilesMatch(modelFile, VIEW_FILE_SUFFIX, EXPECTED_VIEW_FOLDER); + } + + @AfterEach + @Override + protected void tearDown() { + FileUtil.clearFiles(new File(BASE_PATH.toString()), VIEW_FILE_SUFFIX); + } + +} diff --git a/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStore.ecore.treeview b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStore.ecore.treeview new file mode 100644 index 000000000..ce6ad5331 --- /dev/null +++ b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStore.ecore.treeview @@ -0,0 +1,13 @@ +BookStorePackage : EPackage + BookStore : EClass + owner : EAttribute + EGenericType + location : EAttribute + EGenericType + books : EReference + EGenericType + Book : EClass + name : EAttribute + EGenericType + isbn : EAttribute + EGenericType diff --git a/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreExtended.ecore.treeview b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreExtended.ecore.treeview new file mode 100644 index 000000000..4fdd2c505 --- /dev/null +++ b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreExtended.ecore.treeview @@ -0,0 +1,35 @@ +BookStorePackage : EPackage + store : EPackage + BookStore : EClass + owner : EReference + EGenericType + name : EAttribute + EGenericType + location : EAttribute + EGenericType + books : EReference + EGenericType + Book : EClass + name : EAttribute + EGenericType + isbn : EAttribute + EGenericType + author : EReference + EGenericType + genre : EAttribute + EGenericType + Genre : EEnum + NOVEL : EEnumLiteral + COOKBOOK : EEnumLiteral + BIOGRAPHY : EEnumLiteral + TEXTBOOK : EEnumLiteral + person : EPackage + Author : EClass + isStageName : EAttribute + EGenericType + EGenericType + Person : EClass + firstName : EAttribute + EGenericType + lastName : EAttribute + EGenericType diff --git a/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreExtendedRefactor.ecore.treeview b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreExtendedRefactor.ecore.treeview new file mode 100644 index 000000000..ca127fe5a --- /dev/null +++ b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreExtendedRefactor.ecore.treeview @@ -0,0 +1,29 @@ +BookStorePackage : EPackage + store : EPackage + Store : EClass + owner : EReference + EGenericType + name : EAttribute + EGenericType + location : EAttribute + EGenericType + BookStore : EClass + books : EReference + EGenericType + EGenericType + Book : EClass + title : EAttribute + EGenericType + isbn : EAttribute + EGenericType + author : EReference + EGenericType + category : EAttribute + EGenericType + Person : EClass + firstName : EAttribute + EGenericType + lastName : EAttribute + EGenericType + isStageName : EAttribute + EGenericType diff --git a/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreRenamed.ecore.treeview b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreRenamed.ecore.treeview new file mode 100644 index 000000000..4ee5941e3 --- /dev/null +++ b/languages/emf-metamodel/src/test/resources/de/jplag/treeview/bookStoreRenamed.ecore.treeview @@ -0,0 +1,13 @@ +BookStorePackage : EPackage + Store : EClass + nameOfOwner : EAttribute + EGenericType + city : EAttribute + EGenericType + soldItems : EReference + EGenericType + Item : EClass + title : EAttribute + EGenericType + identifier : EAttribute + EGenericType diff --git a/languages/emf-model/pom.xml b/languages/emf-model/pom.xml new file mode 100644 index 000000000..763a528c0 --- /dev/null +++ b/languages/emf-model/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + de.jplag + languages + ${revision} + + emf-model + + + + de.jplag + emf-metamodel-dynamic + ${revision} + + + de.jplag + language-testutils + ${revision} + test-jar + test + + + org.eclipse.emf + org.eclipse.emf.ecore + ${emf.version} + + + org.eclipse.emf + org.eclipse.emf.common + ${emf.ecore.version} + + + org.eclipse.emf + org.eclipse.emf.ecore.xmi + ${emf.ecore.xmi.version} + + + diff --git a/languages/emf-model/src/main/java/de/jplag/emf/model/Language.java b/languages/emf-model/src/main/java/de/jplag/emf/model/Language.java new file mode 100644 index 000000000..092fb2af5 --- /dev/null +++ b/languages/emf-model/src/main/java/de/jplag/emf/model/Language.java @@ -0,0 +1,58 @@ +package de.jplag.emf.model; + +import java.io.File; +import java.util.Comparator; +import java.util.List; + +import org.kohsuke.MetaInfServices; + +import de.jplag.emf.model.parser.DynamicModelParser; + +/** + * Language for EMF metamodels from the Eclipse Modeling Framework (EMF). This language is based on a dynamically + * created token set. + * @author Timur Saglam + */ +@MetaInfServices(de.jplag.Language.class) +public class Language extends de.jplag.emf.dynamic.Language { + private static final String NAME = "EMF models (dynamically created token set)"; + private static final String IDENTIFIER = "emf-model"; + + public static final String VIEW_FILE_SUFFIX = ".treeview"; + + public Language() { + super(new DynamicModelParser()); + } + + @Override + public String[] suffixes() { + return new String[] {}; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getIdentifier() { + return IDENTIFIER; + } + + @Override + public String viewFileSuffix() { + return VIEW_FILE_SUFFIX; + } + + @Override + public boolean expectsSubmissionOrder() { + return true; + } + + @Override + public List customizeSubmissionOrder(List sub) { + Comparator fileEndingComparator = (first, second) -> Boolean.compare(second.getName().endsWith(FILE_ENDING), + first.getName().endsWith(FILE_ENDING)); + return sub.stream().sorted(fileEndingComparator).toList(); + } +} diff --git a/languages/emf-model/src/main/java/de/jplag/emf/model/parser/DynamicModelParser.java b/languages/emf-model/src/main/java/de/jplag/emf/model/parser/DynamicModelParser.java new file mode 100644 index 000000000..4bc23cf72 --- /dev/null +++ b/languages/emf-model/src/main/java/de/jplag/emf/model/parser/DynamicModelParser.java @@ -0,0 +1,78 @@ +package de.jplag.emf.model.parser; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.resource.Resource; + +import de.jplag.ParsingException; +import de.jplag.emf.dynamic.parser.DynamicEcoreParser; +import de.jplag.emf.model.Language; +import de.jplag.emf.util.AbstractModelView; +import de.jplag.emf.util.EMFUtil; +import de.jplag.emf.util.MetamodelTreeView; + +/** + * Parser for EMF metamodels based on dynamically created tokens. + */ +public class DynamicModelParser extends DynamicEcoreParser { + private static final String VIEW_FILE_WARNING = "Skipping view file {} as submission!"; + private static final String METAPACKAGE_WARNING = "Loading model instance {} without any metamodel!"; + private static final String METAPACKAGE_ERROR = "Error, not a metapackage: "; + private static final String METAMODEL_LOADING_ERROR = "Could not load metamodel file!"; + + private static final List metapackages = new ArrayList<>(); + private static final String ALL_EXTENSIONS = "*"; + + /** + * Creates the parser. + */ + public DynamicModelParser() { + EMFUtil.registerModelExtension(ALL_EXTENSIONS); + } + + @Override + protected void parseModelFile(File file) throws ParsingException { + // implicit assumption: Metamodel gets parsed first! + if (file.getName().endsWith(de.jplag.emf.Language.FILE_ENDING)) { + parseMetamodelFile(file); + } else if (file.getName().endsWith(Language.VIEW_FILE_SUFFIX)) { + logger.warn(VIEW_FILE_WARNING, file.getName()); + } else { + if (metapackages.isEmpty()) { + logger.warn(METAPACKAGE_WARNING, file.getName()); + } + super.parseModelFile(file); + } + } + + @Override + protected String getCorrespondingViewFileSuffix() { + return Language.VIEW_FILE_SUFFIX; + } + + @Override + protected AbstractModelView createView(File file, Resource modelResource) { + return new MetamodelTreeView(file, modelResource); + } + + private void parseMetamodelFile(File file) throws ParsingException { + metapackages.clear(); + Resource modelResource = EMFUtil.loadModelResource(file); + if (modelResource == null) { + throw new ParsingException(file, METAMODEL_LOADING_ERROR); + } else { + for (EObject object : modelResource.getContents()) { + if (object instanceof EPackage ePackage) { + metapackages.add(ePackage); + } else { + logger.error(METAPACKAGE_ERROR, object); + } + } + EMFUtil.registerEPackageURIs(metapackages); + } + } +} diff --git a/languages/emf-model/src/test/java/de/jplag/emf/model/BookStoreFactory.java b/languages/emf-model/src/test/java/de/jplag/emf/model/BookStoreFactory.java new file mode 100644 index 000000000..5f755defa --- /dev/null +++ b/languages/emf-model/src/test/java/de/jplag/emf/model/BookStoreFactory.java @@ -0,0 +1,120 @@ +package de.jplag.emf.model; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EAttribute; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EFactory; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.ETypedElement; +import org.eclipse.emf.ecore.EcoreFactory; +import org.eclipse.emf.ecore.EcorePackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emf.ecore.xmi.impl.XMLResourceFactoryImpl; + +public class BookStoreFactory { + + private static final String MODEL_NAME = "bookStore.xml"; + private static final String METAMODEL_NAME = "bookStore.ecore"; + + public static void generateAll(String filePath) { + File file = new File(filePath); + if (!file.exists()) { + file.mkdirs(); + } + BookStoreFactory.createMetamodelAndModelInstance(filePath); + } + + @SuppressWarnings("unchecked") + private static void createMetamodelAndModelInstance(String baseBath) { + /* + * Create metamodel: + */ + EcoreFactory theCoreFactory = EcoreFactory.eINSTANCE; + + EClass bookStoreEClass = theCoreFactory.createEClass(); + bookStoreEClass.setName("BookStore"); + + EClass bookEClass = theCoreFactory.createEClass(); + bookEClass.setName("Book"); + + EPackage bookStoreEPackage = theCoreFactory.createEPackage(); + bookStoreEPackage.setName("BookStorePackage"); + bookStoreEPackage.setNsPrefix("bookStore"); + bookStoreEPackage.setNsURI("http:///com.ibm.dynamic.example.bookstore.ecore"); + + EcorePackage theCorePackage = EcorePackage.eINSTANCE; + + EAttribute bookStoreOwner = theCoreFactory.createEAttribute(); + bookStoreOwner.setName("owner"); + bookStoreOwner.setEType(theCorePackage.getEString()); + EAttribute bookStoreLocation = theCoreFactory.createEAttribute(); + bookStoreLocation.setName("location"); + bookStoreLocation.setEType(theCorePackage.getEString()); + EReference bookStore_Books = theCoreFactory.createEReference(); + bookStore_Books.setName("books"); + bookStore_Books.setEType(bookEClass); + bookStore_Books.setUpperBound(ETypedElement.UNBOUNDED_MULTIPLICITY); + bookStore_Books.setContainment(true); + + EAttribute bookName = theCoreFactory.createEAttribute(); + bookName.setName("name"); + bookName.setEType(theCorePackage.getEString()); + EAttribute bookISBN = theCoreFactory.createEAttribute(); + bookISBN.setName("isbn"); + bookISBN.setID(true); + bookISBN.setEType(theCorePackage.getEInt()); + + bookStoreEClass.getEStructuralFeatures().add(bookStoreOwner); + bookStoreEClass.getEStructuralFeatures().add(bookStoreLocation); + bookStoreEClass.getEStructuralFeatures().add(bookStore_Books); + + bookEClass.getEStructuralFeatures().add(bookName); + bookEClass.getEStructuralFeatures().add(bookISBN); + + bookStoreEPackage.getEClassifiers().add(bookStoreEClass); + bookStoreEPackage.getEClassifiers().add(bookEClass); + + /* + * Create model instance: + */ + EFactory bookFactoryInstance = bookStoreEPackage.getEFactoryInstance(); + + EObject bookObject = bookFactoryInstance.create(bookEClass); + EObject bookStoreObject = bookFactoryInstance.create(bookStoreEClass); + + bookStoreObject.eSet(bookStoreOwner, "David Brown"); + bookStoreObject.eSet(bookStoreLocation, "Street#12, Top Town, NY"); + ((List) bookStoreObject.eGet(bookStore_Books)).add(bookObject); + + bookObject.eSet(bookName, "Harry Potter and the Deathly Hallows"); + bookObject.eSet(bookISBN, 157221); + + /* + * Save model instance and metamodel: + */ + persist(baseBath, bookStoreObject, MODEL_NAME, "*"); + persist(baseBath, bookStoreEPackage, METAMODEL_NAME, EcorePackage.eNAME); + } + + private static void persist(String baseBath, EObject eObject, String name, String extension) { + ResourceSet metaResourceSet = new ResourceSetImpl(); + metaResourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(extension, new XMLResourceFactoryImpl()); + Resource metaResource = metaResourceSet.createResource(URI.createFileURI(baseBath + File.separator + name)); + metaResource.getContents().add(eObject); + + try { + metaResource.save(null); + } catch (IOException exception) { + exception.printStackTrace(); + } + } + +} diff --git a/languages/emf-model/src/test/java/de/jplag/emf/model/MinimalModelInstanceTest.java b/languages/emf-model/src/test/java/de/jplag/emf/model/MinimalModelInstanceTest.java new file mode 100644 index 000000000..8d22eddb7 --- /dev/null +++ b/languages/emf-model/src/test/java/de/jplag/emf/model/MinimalModelInstanceTest.java @@ -0,0 +1,66 @@ +package de.jplag.emf.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.TreeSet; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.jplag.ParsingException; +import de.jplag.Token; +import de.jplag.TokenPrinter; +import de.jplag.testutils.FileUtil; + +class MinimalModelInstanceTest { + private final Logger logger = LoggerFactory.getLogger(MinimalModelInstanceTest.class); + + private static final Path BASE_PATH = Path.of("src", "test", "resources", "de", "jplag", "books"); + private static final String[] TEST_SUBJECTS = {"bookStore.ecore", "bookStore.xml", "bookStore2.xml"}; + + private Language language; + private File baseDirectory; + + @BeforeEach + public void setUp() { + language = new Language(); + baseDirectory = BASE_PATH.toFile(); + FileUtil.assertDirectory(baseDirectory, TEST_SUBJECTS); + } + + @Test + @DisplayName("Test tokens extracted from generated example instances") + void testBookStoreInstances() { + File baseFile = new File(BASE_PATH.toString()); + List baseFiles = new ArrayList<>(Arrays.asList(baseFile.listFiles())); + var sortedFiles = new TreeSet<>(language.customizeSubmissionOrder(baseFiles)); + try { + List tokens = language.parse(sortedFiles); + assertNotEquals(0, tokens.size()); + logger.debug(TokenPrinter.printTokens(tokens, baseDirectory, Optional.of(de.jplag.emf.Language.VIEW_FILE_SUFFIX))); + logger.info("Parsed tokens: " + tokens); + assertEquals(7, tokens.size()); + } catch (ParsingException e) { + fail("Parsing failed: " + e.getMessage(), e); + } + + } + + @AfterEach + public void tearDown() { + FileUtil.clearFiles(new File(BASE_PATH.toString()), Language.VIEW_FILE_SUFFIX); + } + +} diff --git a/languages/emf-model/src/test/resources/de/jplag/books/bookStore.ecore b/languages/emf-model/src/test/resources/de/jplag/books/bookStore.ecore new file mode 100644 index 000000000..e50d0ead5 --- /dev/null +++ b/languages/emf-model/src/test/resources/de/jplag/books/bookStore.ecore @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/languages/emf-model/src/test/resources/de/jplag/books/bookStore.xml b/languages/emf-model/src/test/resources/de/jplag/books/bookStore.xml new file mode 100644 index 000000000..4cd26c39e --- /dev/null +++ b/languages/emf-model/src/test/resources/de/jplag/books/bookStore.xml @@ -0,0 +1,4 @@ + + + + diff --git a/languages/emf-model/src/test/resources/de/jplag/books/bookStore2.xml b/languages/emf-model/src/test/resources/de/jplag/books/bookStore2.xml new file mode 100644 index 000000000..ac7aa3959 --- /dev/null +++ b/languages/emf-model/src/test/resources/de/jplag/books/bookStore2.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/languages/pom.xml b/languages/pom.xml index d1932bf7b..f241a3205 100644 --- a/languages/pom.xml +++ b/languages/pom.xml @@ -14,6 +14,7 @@ csharp emf-metamodel emf-metamodel-dynamic + emf-model golang java kotlin diff --git a/pom.xml b/pom.xml index af485f582..fd20a5c57 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,7 @@ 2.33.0 2.28.0 2.18.0 + 1.0.0 4.4.0-SNAPSHOT