/*******************************************************************************
* Copyright (c) 2006-2013
* Software Technology Group, Dresden University of Technology
* DevBoost GmbH, Berlin, Amtsgericht Charlottenburg, HRB 140026
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Software Technology Group - TU Dresden, Germany;
* DevBoost GmbH - Berlin, Germany
* - initial API and implementation
******************************************************************************/
package org.emftext.language.java.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.Resource.Diagnostic;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.Diagnostician;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.MalformedTreeException;
import org.emftext.language.java.JavaClasspath;
import org.emftext.language.java.classifiers.Annotation;
import org.emftext.language.java.classifiers.Classifier;
import org.emftext.language.java.classifiers.Enumeration;
import org.emftext.language.java.classifiers.Interface;
import org.emftext.language.java.commons.NamedElement;
import org.emftext.language.java.containers.CompilationUnit;
import org.emftext.language.java.containers.JavaRoot;
import org.emftext.language.java.generics.TypeParameter;
import org.emftext.language.java.members.Constructor;
import org.emftext.language.java.members.Member;
import org.emftext.language.java.members.MemberContainer;
import org.emftext.language.java.members.Method;
import org.emftext.language.java.modifiers.AnnotationInstanceOrModifier;
import org.emftext.language.java.modifiers.Modifier;
import org.emftext.language.java.modifiers.Public;
import org.emftext.language.java.resource.JavaSourceOrClassFileResourceFactoryImpl;
import org.emftext.language.java.resource.java.IJavaTextDiagnostic;
import org.emftext.language.java.resource.java.mopp.JavaResource;
import org.emftext.language.java.resource.java.util.JavaResourceUtil;
import org.emftext.language.java.resource.java.util.JavaUnicodeConverter;
import org.emftext.language.java.types.NamespaceClassifierReference;
/**
* Abstract superclass that provides some frequently used assert and helper methods.
*/
public abstract class AbstractJavaParserTestCase {
protected static final String TEST_OUTPUT_FOLDER = "output";
public AbstractJavaParserTestCase() {
Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put(
"java", new JavaSourceOrClassFileResourceFactoryImpl());
}
protected void registerInClassPath(String file) throws Exception {
String inputFolderPath = "." + File.separator + getTestInputFolder();
File inputFolder = new File(inputFolderPath);
File inputFile = new File(file);
CompilationUnit cu = (CompilationUnit) parseResource(inputFile.getPath());
inputFile = new File(inputFolder + File.separator + file);
JavaClasspath.get(cu).registerClassifierSource(cu, URI.createFileURI(inputFile.getAbsolutePath().toString()));
}
/**
* All test files that were parsed by the method parseResource(String relativePath).
*/
private static List<File> parsedResources = new ArrayList<File>();
private static List<File> reprintedResources = new ArrayList<File>();
protected JavaRoot parseResource(String filename, String inputFolderName) throws Exception {
File inputFolder = new File("./" + inputFolderName);
File file = new File(inputFolder, filename);
assertTrue("File " + file + " must exist.", file.exists());
addParsedResource(file);
return loadResource(file.getAbsolutePath());
}
protected JavaRoot parseResource(ZipFile file, ZipEntry entry) throws Exception {
return loadResource(URI.createURI("archive:file:///" + new File(".").getAbsoluteFile().toURI().getRawPath() + file.getName().replaceAll("\\\\", "/") + "!/" + entry.getName()));
}
protected JavaRoot loadResource(String filePath) throws Exception {
URI uri = URI.createFileURI(filePath);
return loadResource(uri);
}
protected JavaRoot loadResource(URI uri) throws Exception {
JavaResource resource = (JavaResource) getResourceSet().createResource(uri);
resource.load(getLoadOptions());
assertNoErrors(uri.toString(), resource);
assertNoWarnings(uri.toString(), resource);
EList<EObject> contents = resource.getContents();
assertEquals("The resource must have one content element.", 1, contents.size());
EObject content = contents.get(0);
assertTrue("File '" + uri.toString() + "' was parsed to CompilationUnit.", content instanceof JavaRoot);
JavaRoot root = (JavaRoot) content;
return root;
}
protected void addFileToClasspath(File file, ResourceSet resourceSet) throws Exception {
JavaClasspath cp = JavaClasspath.get(resourceSet);
String fullName = file.getPath().substring(getTestInputFolder().length() + 3, file.getPath().length() - 5);
fullName = fullName.replace(File.separator, ".");
int idx = fullName.lastIndexOf(".");
String packageName;
String classifierName;
if (idx == -1) {
packageName = "";
classifierName = fullName;
} else {
packageName = fullName.substring(0, idx);
classifierName = fullName.substring(idx + 1);
}
cp.registerClassifier(packageName, classifierName, URI.createFileURI(file.getAbsolutePath()));
}
protected Map<? extends Object, ? extends Object> getLoadOptions() {
Map<Object, Object> map = new HashMap<Object, Object>();
map.put(JavaClasspath.OPTION_USE_LOCAL_CLASSPATH, Boolean.TRUE);
return map;
}
private static void assertNoErrors(String fileIdentifier, JavaResource resource) {
List<Diagnostic> errors = new BasicEList<Diagnostic>(resource.getErrors());
printErrors(fileIdentifier, errors);
assertTrue("The resource should be parsed without errors.", errors.isEmpty());
}
private static void assertNoWarnings(String fileIdentifier, JavaResource resource) {
List<Diagnostic> warnings = resource.getWarnings();
printWarnings(fileIdentifier, warnings);
assertTrue("The resource should be parsed without warnings.", warnings.isEmpty());
}
protected static void printErrors(String filename, List<Diagnostic> errors) {
printDiagnostics(filename, errors, "Errors");
}
protected static void printWarnings(String filename, List<Diagnostic> warnings) {
printDiagnostics(filename, warnings, "Warnings");
}
private static void printDiagnostics(String filename, List<Diagnostic> errors, String diagnosticType) {
if (errors.size() == 0) {
return;
}
StringBuffer buffer = new StringBuffer();
buffer.append(diagnosticType + " while parsing resource '" + filename
+ "':\n");
for (Diagnostic diagnostic : errors) {
String text;
if (diagnostic instanceof IJavaTextDiagnostic) {
IJavaTextDiagnostic textDiagnostic = (IJavaTextDiagnostic) diagnostic;
text = textDiagnostic.getMessage() + " at ("
+ textDiagnostic.getLine() + ","
+ textDiagnostic.getColumn() + ")";
} else {
text = diagnostic.getMessage();
}
buffer.append("\t" + text + "\n");
}
System.out.println(buffer.toString());
}
protected void parseAndReprint(ZipFile file, ZipEntry entry, String outputFolderName, String libFolderName)
throws Exception {
String entryName = entry.getName();
String outputFileName = "./" + outputFolderName + File.separator + entryName;
File outputFile = prepareOutputFile(outputFileName);
URI archiveURI = URI.createURI("archive:file:///" + new File(".").getAbsoluteFile().toURI().getRawPath() + file.getName().replaceAll("\\\\", "/") + "!/" + entry.getName());
ResourceSet resourceSet = getResourceSet();
if (prefixUsedInZipFile()) {
String prefix = entry.getName().substring(0, entry.getName().indexOf("/") + 1);
registerLibs(libFolderName, JavaClasspath.get(resourceSet), prefix);
}
Resource resource = resourceSet.createResource(archiveURI);
resource.load(getLoadOptions());
if (!ignoreSemanticErrors(entry.getName())) {
// This will not work if external resources are not yet registered (order of tests)
assertResolveAllProxies(resource);
}
if (isExcludedFromReprintTest(entry.getName())) {
return;
}
//addReprintedResource(inputFile);
resource.setURI(URI.createFileURI(outputFile.getAbsolutePath()));
resource.save(null);
assertTrue("File " + outputFile.getAbsolutePath() + " must exist.", outputFile.exists());
compareTextContents(file.getInputStream(entry), new FileInputStream(outputFile));
}
protected boolean prefixUsedInZipFile() {
return false;
}
public static void registerLibs(String libdir, JavaClasspath classpath, String prefix) throws IOException, CoreException {
File libFolder = new File("." + File.separator + libdir);
List<File> allLibFiles = collectAllFilesRecursive(libFolder, "jar");
for (File libFile : allLibFiles) {
String libPath = libFile.getAbsolutePath();
URI libURI = URI.createFileURI(libPath);
classpath.registerClassifierJar(libURI, prefix);
}
}
protected void parseAndReprint(String filename, String inputFolderName, String outputFolderName) throws Exception {
String path = "." + File.separator + inputFolderName + File.separator + filename;
File file = new File(path);
parseAndReprint(file, inputFolderName, outputFolderName);
}
protected void parseAndReprint(File file, String inputFolderName,
String outputFolderName) throws Exception {
File inputFile = file;
assertTrue("File " + inputFile.getAbsolutePath() + " exists.",
inputFile.exists());
String outputFileName = calculateOutputFilename(inputFile,
inputFolderName, outputFolderName);
File outputFile = prepareOutputFile(outputFileName);
Resource resource = getResourceSet().createResource(URI.createFileURI(inputFile.getAbsolutePath().toString()));
resource.load(getLoadOptions());
assertNoErrors(resource.getURI().toString(), (JavaResource) resource);
addParsedResource(inputFile);
if (!ignoreSemanticErrors(file.getPath())) {
// This will not work if external resources are not yet registered (order of tests)
assertResolveAllProxies(resource);
// Default EMF vaildation should not fail
assertModelValid(resource);
}
if (isExcludedFromReprintTest(file.getPath())) {
return;
}
addReprintedResource(inputFile);
resource.setURI(URI.createFileURI(outputFileName));
resource.save(null);
assertTrue("File " + outputFile.getAbsolutePath() + " exists.",
outputFile.exists());
compareTextContents(new FileInputStream(inputFile),
new FileInputStream(outputFile));
}
protected abstract boolean isExcludedFromReprintTest(String filename);
protected boolean ignoreSemanticErrors(String filename) {
return false;
}
private static boolean compareTextContents(InputStream inputStream,
InputStream inputStream2) throws MalformedTreeException,
BadLocationException, IOException {
//converter unicode
inputStream = new JavaUnicodeConverter(inputStream);
inputStream2 = new JavaUnicodeConverter(inputStream2);
org.eclipse.jdt.core.dom.CompilationUnit unit1 = parseWithJDT(inputStream);
removeJavadoc(unit1);
org.eclipse.jdt.core.dom.CompilationUnit unit2 = parseWithJDT(inputStream2);
removeJavadoc(unit2);
TalkativeASTMatcher matcher = new TalkativeASTMatcher(true);
boolean result = unit1.subtreeMatch(matcher, unit2);
assertTrue(
"Reprint not equal: " + matcher.getDiff(),
result);
return result;
}
private static org.eclipse.jdt.core.dom.CompilationUnit parseWithJDT(
InputStream inputStream) {
@SuppressWarnings("deprecation")
ASTParser jdtParser = ASTParser.newParser(AST.JLS3);
char[] charArray = readTextContents(inputStream).toCharArray();
jdtParser.setSource(charArray);
Map<String, Object> options = new HashMap<String, Object>();
options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_5);
jdtParser.setCompilerOptions(options);
org.eclipse.jdt.core.dom.CompilationUnit result1 =
(org.eclipse.jdt.core.dom.CompilationUnit) jdtParser.createAST(null);
return result1;
}
private static void removeJavadoc(
org.eclipse.jdt.core.dom.CompilationUnit result1) {
final List<Javadoc> javadocNodes = new ArrayList<Javadoc>();
result1.accept(new ASTVisitor() {
public boolean visit(Javadoc node) {
javadocNodes.add(node);
return true;
}
});
for (Javadoc node : javadocNodes) {
node.delete();
}
}
protected static String calculateOutputFilename(File inputFile,
String inputFolderName, String outputFolderName) {
File inputPath = new File("." + File.separator + inputFolderName);
int trimOffset = inputPath.getAbsolutePath().length();
File outputFolder = new File("." + File.separator + outputFolderName);
File outputFile = new File(outputFolder + File.separator
+ inputFile.getAbsolutePath().substring(trimOffset));
return outputFile.getAbsolutePath();
}
private static File prepareOutputFile(String outputFileName) {
File outputFile = new File(URI.createFileURI(outputFileName).toFileString());
File parent = outputFile.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
return outputFile;
}
private static String readTextContents(InputStream inputStream) {
StringBuffer contents = new StringBuffer();
try {
BufferedReader input = new BufferedReader(new InputStreamReader(
inputStream));
try {
String line = null; // not declared within while loop
while ((line = input.readLine()) != null) {
contents.append(line);
contents.append(System.getProperty("line.separator"));
}
} finally {
input.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
return contents.toString();
}
protected static List<File> collectAllFilesRecursive(File startFolder, String extension)
throws CoreException {
if (!startFolder.isDirectory())
return Collections.emptyList();
List<File> allFiles = new ArrayList<File>();
//add sub dirs first
for (File member : startFolder.listFiles()) {
if (member.isDirectory()) {
if (!member.getName().equals(".svn")) {
allFiles.addAll(collectAllFilesRecursive(member, extension));
}
}
}
//bin.jar in the end
for (File member : startFolder.listFiles()) {
if (member.isFile()) {
if (member.getName().endsWith(extension)) {
allFiles.add(member);
}
}
}
return allFiles;
}
protected void assertClassTypeParameterCount(Member member,
int expectedNumberOfTypeArguments) {
assertType(member, org.emftext.language.java.classifiers.Class.class);
org.emftext.language.java.classifiers.Class clazz = (org.emftext.language.java.classifiers.Class) member;
List<TypeParameter> typeParameters = clazz.getTypeParameters();
assertEquals("Expected " + expectedNumberOfTypeArguments
+ " type parameter(s).", expectedNumberOfTypeArguments,
typeParameters.size());
}
protected void assertInterfaceTypeParameterCount(Member member,
int expectedNumberOfTypeArguments) {
assertType(member, Interface.class);
Interface interfaze = (Interface) member;
List<TypeParameter> typeParameters = interfaze.getTypeParameters();
assertEquals("Expected " + expectedNumberOfTypeArguments
+ " type parameter(s).", expectedNumberOfTypeArguments,
typeParameters.size());
}
protected void assertMethodThrowsCount(Member member,
int expectedNumberOfThrownExceptions) {
assertType(member, Method.class);
Method method = (Method) member;
List<NamespaceClassifierReference> exceptions = method.getExceptions();
assertEquals("Expected " + expectedNumberOfThrownExceptions
+ " exception(s).", expectedNumberOfThrownExceptions,
exceptions.size());
}
protected void assertMethodTypeParameterCount(Member member,
int expectedNumberOfTypeArguments) {
assertType(member, Method.class);
Method method = (Method) member;
List<TypeParameter> typeParameters = method.getTypeParameters();
assertEquals("Expected " + expectedNumberOfTypeArguments
+ " type parameter(s).", expectedNumberOfTypeArguments,
typeParameters.size());
}
protected void assertConstructorThrowsCount(Member member,
int expectedNumberOfThrownExceptions) {
assertType(member, Constructor.class);
Constructor constructor = (Constructor) member;
List<NamespaceClassifierReference> exceptions = constructor.getExceptions();
assertEquals("Expected " + expectedNumberOfThrownExceptions
+ " exception(s).", expectedNumberOfThrownExceptions,
exceptions.size());
}
protected void assertConstructorTypeParameterCount(Member member,
int expectedNumberOfTypeArguments) {
assertType(member, Constructor.class);
Constructor constructor = (Constructor) member;
List<TypeParameter> typeParameters = constructor.getTypeParameters();
assertEquals("Expected " + expectedNumberOfTypeArguments
+ " type parameter(s).", expectedNumberOfTypeArguments,
typeParameters.size());
}
protected void assertIsClass(Classifier classifier) {
assertType(classifier,
org.emftext.language.java.classifiers.Class.class);
}
protected void assertIsInterface(Classifier classifier) {
assertType(classifier, Interface.class);
}
protected Constructor assertIsConstructor(Member member) {
assertType(member, Constructor.class);
return (Constructor) member;
}
protected void assertType(EObject object, Class<?> expectedType) {
assertTrue("The object should have type '"
+ expectedType.getSimpleName() + "', but was "
+ object.getClass().getSimpleName(), expectedType
.isInstance(object));
}
protected void assertClassifierName(Classifier declaration,
String expectedName) {
assertEquals("The name of the declared classifier should equal '"
+ expectedName + "'", expectedName, declaration.getName());
}
protected void assertNumberOfClassifiers(CompilationUnit model,
int expectedCount) {
assertEquals("The compilation unit should contain " + expectedCount
+ " classifier declaration(s).", expectedCount, model
.getClassifiers().size());
}
protected void assertModifierCount(Method method,
int expectedNumberOfModifiers) {
List<Modifier> annotationsAndModifiers = getModifiers(method);
assertEquals("Method '" + method.getName() + "' should have "
+ expectedNumberOfModifiers + " modifier(s).",
expectedNumberOfModifiers, annotationsAndModifiers.size());
}
private List<Modifier> getModifiers(Method method) {
EList<AnnotationInstanceOrModifier> annotationsAndModifiers = method.getAnnotationsAndModifiers();
List<Modifier> modifiers = new ArrayList<Modifier>();
for (AnnotationInstanceOrModifier annotationInstanceOrModifier : annotationsAndModifiers) {
if (annotationInstanceOrModifier instanceof Modifier) {
modifiers.add((Modifier) annotationInstanceOrModifier);
}
}
return modifiers;
}
protected void assertIsPublic(Method method) {
assertTrue("Method '" + method.getName() + "' should be public.",
getModifiers(method).get(0) instanceof Public);
}
protected Enumeration assertParsesToEnumeration(String typename)
throws Exception {
return assertParsesToEnumeration("", typename);
}
protected Enumeration assertParsesToEnumeration(String pkgFolder, String typename)
throws Exception {
return assertParsesToType(pkgFolder, typename, Enumeration.class);
}
protected Interface assertParsesToInterface(String typename)
throws Exception {
return assertParsesToInterface("", typename);
}
protected Interface assertParsesToInterface(String pkgFolder, String typename)
throws Exception {
return assertParsesToType(pkgFolder, typename, Interface.class);
}
protected Annotation assertParsesToAnnotation(String typename)
throws Exception {
return assertParsesToAnnotation("", typename);
}
protected Annotation assertParsesToAnnotation(String pkgFolder, String typename)
throws Exception {
return assertParsesToType(pkgFolder, typename, Annotation.class);
}
protected <T> T assertParsesToType(String pkgFolder, String typename, String folder,
Class<T> expectedType) throws Exception {
String filename = pkgFolder + File.separator + typename + ".java";
JavaRoot model = parseResource(filename, folder);
if (model instanceof CompilationUnit) {
CompilationUnit cu = (CompilationUnit) model;
assertNumberOfClassifiers(cu, 1);
Classifier declaration = cu.getClassifiers().get(0);
assertClassifierName(declaration, typename);
assertType(declaration, expectedType);
return expectedType.cast(declaration);
}
else {
return null;
}
}
protected <T> T assertParsesToType(String typename, Class<T> expectedType)
throws Exception {
return assertParsesToType("", typename, expectedType);
}
protected <T> T assertParsesToType(String pkgFolder, String typename, Class<T> expectedType)
throws Exception {
return assertParsesToType(pkgFolder, typename, getTestInputFolder(),
expectedType);
}
protected abstract String getTestInputFolder();
protected JavaRoot parseResource(String filename) throws Exception {
return parseResource(filename, getTestInputFolder());
}
protected void parseAndReprint(String filename) throws Exception {
parseAndReprint(filename, getTestInputFolder(), TEST_OUTPUT_FOLDER);
}
protected void parseAndReprint(File file) throws Exception {
parseAndReprint(file, getTestInputFolder(), TEST_OUTPUT_FOLDER);
}
protected org.emftext.language.java.classifiers.Class assertParsesToClass(
String typename) throws Exception {
return assertParsesToClass("", typename);
}
protected org.emftext.language.java.classifiers.Class assertParsesToClass(
String pkgFolder, String typename) throws Exception {
return assertParsesToType(pkgFolder, typename,
org.emftext.language.java.classifiers.Class.class);
}
protected void assertMemberCount(
MemberContainer container,
int expectedCount) {
String name = container.toString();
if (container instanceof NamedElement) {
name = ((NamedElement) container).getName();
}
assertEquals(name + " should have " + expectedCount
+ " member(s).", expectedCount, container.getMembers().size());
}
protected void assertParsesToClass(String typename, int expectedMembers)
throws Exception, IOException, BadLocationException {
assertParsesToClass("", typename, expectedMembers);
}
protected void assertParsesToClass(String pkgFolder, String typename, int expectedMembers)
throws Exception, IOException, BadLocationException {
String filename = typename + ".java";
org.emftext.language.java.classifiers.Class clazz = assertParsesToClass(pkgFolder, typename);
assertEquals(typename + " should have " + expectedMembers
+ " member(s).", expectedMembers, clazz.getMembers().size());
parseAndReprint(filename);
}
protected void assertResolveAllProxies(EObject element) {
assertResolveAllProxies(element.eResource());
}
protected ResourceSet getResourceSet() throws Exception {
ResourceSet rs = new ResourceSetImpl();
setUpClasspath(rs);
rs.getLoadOptions().putAll(getLoadOptions());
return rs;
}
protected void setUpClasspath(ResourceSet resourceSet) throws Exception {
// Sub class can override this method
}
protected boolean assertResolveAllProxies(Resource resource) {
Set<EObject> unresolvedProxies = JavaResourceUtil.findUnresolvedProxies(resource);
boolean failure = false;
String msg="";
for (EObject next : unresolvedProxies) {
InternalEObject nextElement = (InternalEObject) next;
assertFalse("Can not resolve: " + nextElement.eProxyURI(), nextElement.eIsProxy());
for (EObject crElement : nextElement.eCrossReferences()) {
crElement = EcoreUtil.resolve(crElement, resource);
if (crElement.eIsProxy()) {
msg += "\nCan not resolve: " + ((InternalEObject) crElement).eProxyURI();
failure = true;
}
}
}
assertFalse(msg, failure);
return failure;
}
private void assertModelValid(Resource resource) {
org.eclipse.emf.common.util.Diagnostic result = Diagnostician.INSTANCE.validate(resource.getContents().get(0));
String msg = "EMF validation problems found:";
for (org.eclipse.emf.common.util.Diagnostic childResult : result.getChildren()) {
System.out.println(childResult.getMessage());
msg += "\n" + childResult.getMessage();
}
assertTrue(msg, result.getChildren().isEmpty());
}
public static List<File> getParsedResources() {
return parsedResources;
}
public static List<File> getReprintedResources() {
return reprintedResources;
}
public void addParsedResource(File file) {
if (!parsedResources.contains(file)) {
parsedResources.add(file);
}
}
public void addReprintedResource(File file) {
if(!reprintedResources.contains(file)) {
reprintedResources.add(file);
}
}
}