/********************************************************************************* | |
* Copyright (c) 2017-2020 Robert Bosch GmbH and others. | |
* | |
* This program and the accompanying materials are made | |
* available under the terms of the Eclipse Public License 2.0 | |
* which is available at https://www.eclipse.org/legal/epl-2.0/ | |
* | |
* SPDX-License-Identifier: EPL-2.0 | |
* | |
* Contributors: | |
* Robert Bosch GmbH - initial API and implementation | |
******************************************************************************** | |
*/ | |
package org.eclipse.app4mc.amalthea.converters.common.tests; | |
import static org.junit.Assert.assertEquals; | |
import static org.junit.Assert.assertTrue; | |
import static org.junit.Assert.fail; | |
import java.io.File; | |
import java.io.IOException; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.Comparator; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.stream.Stream; | |
import org.eclipse.app4mc.amalthea.converters.common.MigrationHelper; | |
import org.eclipse.app4mc.amalthea.converters.common.ServiceConstants; | |
import org.eclipse.app4mc.amalthea.converters.common.base.ICache; | |
import org.eclipse.app4mc.amalthea.converters.common.base.IConverter; | |
import org.eclipse.app4mc.amalthea.converters.common.base.IPostProcessor; | |
import org.eclipse.app4mc.amalthea.converters.common.utils.AmaltheaNamespaceRegistry; | |
import org.eclipse.app4mc.amalthea.converters.common.utils.HelperUtil; | |
import org.eclipse.app4mc.amalthea.converters.common.utils.ModelVersion; | |
import org.eclipse.app4mc.util.sessionlog.SessionLogger; | |
import org.jdom2.Attribute; | |
import org.jdom2.Document; | |
import org.jdom2.Element; | |
import org.jdom2.Namespace; | |
import org.junit.Rule; | |
import org.junit.rules.TestName; | |
public abstract class AbstractConverterTest { | |
public static final String GLOBAL_TEST_INPUT_DIRECTORY = "./TestModels/input"; | |
public static final String GLOBAL_TEST_OUTPUT_DIRECTORY = "./TestModels/output"; | |
protected final HashMap<File, Document> fileDocumentMapping = new HashMap<>(); | |
protected final HashMap<String, String> filenameInputOutputMapping = new HashMap<>(); | |
protected final boolean canExecuteTestCase; | |
protected String localOutputDirectory; | |
public AbstractConverterTest(final boolean canExecuteTestCase, final String... xmlFilesRelative) { | |
for (final String xmlFileRelativeLocation : xmlFilesRelative) { | |
final String inputXmlFilePath = GLOBAL_TEST_INPUT_DIRECTORY + File.separator + xmlFileRelativeLocation; | |
final String outputXmlFilePath = GLOBAL_TEST_OUTPUT_DIRECTORY + File.separator + xmlFileRelativeLocation; | |
this.filenameInputOutputMapping.put(inputXmlFilePath, outputXmlFilePath); | |
} | |
this.localOutputDirectory = new File(GLOBAL_TEST_OUTPUT_DIRECTORY, xmlFilesRelative[0]).getParent(); | |
this.canExecuteTestCase = canExecuteTestCase; | |
} | |
public abstract ModelVersion getInputModelVersion(); | |
public abstract ModelVersion getOutputModelVersion(); | |
public void parseInputXMLFiles() { | |
this.fileDocumentMapping.clear(); | |
if (!this.canExecuteTestCase) { | |
return; | |
} | |
try { | |
Set<String> inputFiles = this.filenameInputOutputMapping.keySet(); | |
for (String inputFilePath : inputFiles) { | |
File inputFile = new File(inputFilePath); | |
// verify that the model version of the input file matches the configured input model version | |
String modelVersion = MigrationHelper.getModelVersion(inputFile); | |
assertEquals(getInputModelVersion().getVersion(), modelVersion); | |
this.fileDocumentMapping.put(inputFile, HelperUtil.loadFile(inputFilePath)); | |
} | |
} | |
catch (final Exception e) { | |
fail(e.getMessage()); | |
} | |
} | |
public void parseGeneratedXMLFiles() { | |
this.fileDocumentMapping.clear(); | |
if (!this.canExecuteTestCase) { | |
return; | |
} | |
try { | |
final Collection<String> outputFiles = this.filenameInputOutputMapping.values(); | |
for (final String outputFilePath : outputFiles) { | |
/* | |
* Special handling for fetching the files is implemented as in certain cases, | |
* file extensions are updated during model migration | |
*/ | |
File targetFile = new File(outputFilePath); | |
if (!targetFile.exists()) { | |
String fileExtension = targetFile.getName().substring(targetFile.getName().lastIndexOf('.') + 1); | |
if (!fileExtension.equalsIgnoreCase("amxmi")) { | |
targetFile = new File(outputFilePath + ".amxmi"); | |
} | |
} | |
// verify that the model version of the output file matches the configured output model version | |
String modelVersion = MigrationHelper.getModelVersion(targetFile); | |
assertEquals(getOutputModelVersion().getVersion(), modelVersion); | |
this.fileDocumentMapping.put(targetFile, HelperUtil.loadFile(outputFilePath)); | |
} | |
} | |
catch (final Exception e) { | |
fail(e.getMessage()); | |
} | |
} | |
protected List<ICache> buildCaches() { | |
final List<ICache> caches = new ArrayList<>(); | |
return caches; | |
} | |
// accessibility updates in test cases are ok here, at runtime the activate method is called by the SCR | |
@SuppressWarnings("squid:S3011") | |
@SafeVarargs | |
final protected void invokeDependentConverters_ForAll_Files( | |
Set<File> files, | |
Map<File, Document> filename2documentMap, | |
List<ICache> caches, | |
Class<?>... converterClasss) | |
throws InstantiationException, IllegalAccessException, Exception { | |
if (converterClasss != null) { | |
SessionLogger logger = new SessionLogger(); | |
for (Class<?> class1 : converterClasss) { | |
if (IConverter.class.isAssignableFrom(class1)) { | |
IConverter newInstance = (IConverter) class1.getDeclaredConstructor().newInstance(); | |
// provide service properties programmatically | |
HashMap<String, Object> properties = new HashMap<>(); | |
properties.put(ServiceConstants.INPUT_MODEL_VERSION_PROPERTY, getInputModelVersion().getVersion()); | |
properties.put(ServiceConstants.OUTPUT_MODEL_VERSION_PROPERTY, getOutputModelVersion().getVersion()); | |
Method[] methods = newInstance.getClass().getDeclaredMethods(); | |
Method activateMethod = Arrays.stream(methods).filter(m -> "activate".equals(m.getName())).findFirst().orElse(null); | |
if (activateMethod != null) { | |
activateMethod.setAccessible(true); | |
if (activateMethod.getParameterCount() == 1) { | |
activateMethod.invoke(newInstance, properties); | |
} else if (activateMethod.getParameterCount() == 2) { | |
activateMethod.invoke(newInstance, properties, null); | |
} | |
} | |
Field[] fields = newInstance.getClass().getDeclaredFields(); | |
Field loggerField = Arrays.stream(fields).filter(m -> "logger".equals(m.getName())).findFirst().orElse(null); | |
if (loggerField != null) { | |
loggerField.setAccessible(true); | |
loggerField.set(newInstance, logger); | |
} | |
//set cache | |
setCache(newInstance, caches); | |
for (File file : files) { | |
newInstance.convert(file, filename2documentMap, caches); | |
} | |
} | |
} | |
} | |
} | |
protected void invoke_postProcessor_ForAll_Files(Set<File> keySet, | |
Map<File, Document> filename2documentMap) { | |
List<IPostProcessor> postProcessors = buildPostProcessors(); | |
for (IPostProcessor iPostProcessor : postProcessors) { | |
iPostProcessor.process(filename2documentMap); | |
} | |
} | |
protected List<IPostProcessor> buildPostProcessors() { | |
final List<IPostProcessor> processors = new ArrayList<IPostProcessor>(); | |
return processors; | |
} | |
/** | |
* This method is used to verify if the AMALTHEA namespaces used in this document are of AMALTHEA 0.9.2 | |
* | |
* @param document | |
*/ | |
protected void namespaceVerification(final Document document) { | |
final List<Namespace> namespacesInScope = document.getRootElement().getNamespacesInScope(); | |
for (final Namespace namespace : namespacesInScope) { | |
final String prefix = namespace.getPrefix(); | |
if (prefix.equals("xmlns") || prefix.equals("xmi") || prefix.equals("xsi") || prefix.equals("") | |
|| prefix.equals("xml")) { | |
continue; | |
} | |
final boolean isAmaltheaNameSpacePrefix = AmaltheaNamespaceRegistry.isPrefixForVersion(getOutputModelVersion(), prefix); | |
if (isAmaltheaNameSpacePrefix) { | |
final boolean nsAvailable = AmaltheaNamespaceRegistry.isNamespaceAvailable(getOutputModelVersion(), namespace); | |
assertTrue("Expected namespace fom AMALTHEA " + getOutputModelVersion().getVersion() + " : " + namespace.getURI() + " --> " | |
+ document.getBaseURI(), nsAvailable); | |
} | |
else { | |
assertTrue("Invalid namespace is present in AMALTHEA model file : " + namespace.getURI(), false); | |
} | |
} | |
} | |
/** | |
* This method is used to save all migrated files | |
* | |
* @throws Exception | |
*/ | |
protected void saveMigratedFiles() throws IOException { | |
saveFiles(true, this.localOutputDirectory); | |
} | |
private void saveFiles(final boolean updateFileNames, final String outputDirectoryLocation) throws IOException { | |
HelperUtil.updateFileName(this.fileDocumentMapping); | |
final List<File> outputFiles = new ArrayList<>(); | |
final Set<File> keySet = this.fileDocumentMapping.keySet(); | |
for (final File inputFile : keySet) { | |
String convertedFileName = inputFile.getName(); | |
if (updateFileNames) { | |
// add amxmi extension to file names which do not have it (and are migrated to versions 1.1.1 or higher) | |
final int indexOfDot = convertedFileName.lastIndexOf('.'); | |
final String extension = convertedFileName.substring(indexOfDot + 1); | |
if (extension.startsWith("amxmi") && !extension.equals("amxmi")) { | |
convertedFileName = convertedFileName + ".amxmi"; | |
} | |
} | |
File outputFile = null; | |
if (outputDirectoryLocation != null && !outputDirectoryLocation.equals("")) { | |
final String location = outputDirectoryLocation + File.separator + convertedFileName; | |
HelperUtil.saveFile(this.fileDocumentMapping.get(inputFile), location, true, true); | |
outputFile = new File(location); | |
/* adding new file path */ | |
outputFiles.add(outputFile); | |
} | |
else { | |
final String location = inputFile.getParentFile().getAbsolutePath() + File.separator | |
+ convertedFileName; | |
HelperUtil.saveFile(this.fileDocumentMapping.get(inputFile), location, true, true); | |
/* adding new file path */ | |
outputFile = new File(location); | |
outputFiles.add(outputFile); | |
} | |
} | |
} | |
@Rule | |
public TestName testName = new TestName(); | |
protected Namespace[] getNameSpaces() { | |
return AmaltheaNamespaceRegistry.getAllNamespacesBefore(getOutputModelVersion(), true, true); | |
} | |
/** | |
* This method is used to apply the Xpath and query the contents | |
* | |
* Note: This method is used only to query the Nodes, not the attributes | |
* | |
* @param element | |
* @param xpath | |
* @return | |
*/ | |
protected List<Element> getXpathResult(final Element element, final String xpath) { | |
final StringBuilder xpathBuffer = new StringBuilder(); | |
xpathBuffer.append(xpath); | |
final List<Element> elements = HelperUtil.getXpathResult(element, xpathBuffer.toString(), Element.class, getNameSpaces()); | |
return elements; | |
} | |
/** | |
* This method is used to apply the Xpath and query the contents | |
* | |
* Note: This method is used only to query the attributes, not the elements | |
* | |
* @param element | |
* @param xpath | |
* @return | |
*/ | |
protected List<Attribute> getXpathResult_Attributes(final Element element, final String xpath) { | |
final StringBuilder xpathBuffer = new StringBuilder(); | |
xpathBuffer.append(xpath); | |
final List<Attribute> attributes = HelperUtil.getXpathResult(element, xpathBuffer.toString(), Attribute.class, getNameSpaces()); | |
return attributes; | |
} | |
public void testConversion(final Class<?>... classes) { | |
if (!this.canExecuteTestCase) { | |
return; | |
} | |
try { | |
parseInputXMLFiles(); | |
final List<ICache> caches = buildCaches(); | |
invokeDependentConverters_ForAll_Files(this.fileDocumentMapping.keySet(), this.fileDocumentMapping, | |
caches, classes); | |
invoke_postProcessor_ForAll_Files(this.fileDocumentMapping.keySet(),this.fileDocumentMapping); | |
saveMigratedFiles(); | |
} | |
catch (final Exception e) { | |
fail(e.getMessage()); | |
} | |
} | |
public void verification() { | |
if (!this.canExecuteTestCase) { | |
return; | |
} | |
parseGeneratedXMLFiles(); | |
final Collection<Document> values = this.fileDocumentMapping.values(); | |
for (final Document document : values) { | |
/*- verifying the namespace */ | |
namespaceVerification(document); | |
modelFileVerificationHook(document); | |
} | |
} | |
protected void modelFileVerificationHook(final Document document) { | |
} | |
public static void cleanOutputDirectory(String testOutputDir) { | |
// check if the local output directory already exists and clear it if it is not empty | |
Path localOutput = Paths.get(GLOBAL_TEST_OUTPUT_DIRECTORY + File.separator + testOutputDir); | |
if (Files.exists(localOutput)) { | |
try (Stream<Path> directoryStream = Files.list(localOutput)) { | |
if (directoryStream.findAny().isPresent()) { | |
try (Stream<Path> files = Files.walk(localOutput)) { | |
files | |
.sorted(Comparator.reverseOrder()) | |
.forEach(path -> { | |
try { | |
Files.delete(path); | |
} catch (IOException e) { | |
fail("Clearing local output directory failed: " + e.getLocalizedMessage()); | |
} | |
}); | |
} | |
} | |
} catch (IOException e) { | |
fail("Clearing local output directory failed: " + e.getLocalizedMessage()); | |
} | |
} | |
} | |
protected void setCache(IConverter converter, List<ICache> caches) | |
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { | |
//subclasses will override | |
} | |
} |