package com.anjlab.eclipse.tapestry5; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.core.IAnnotation; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IMemberValuePair; import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.internal.ui.text.javadoc.JavadocContentAccess2; import com.anjlab.eclipse.tapestry5.internal.CompilationUnitContext; import com.anjlab.eclipse.tapestry5.internal.CompilationUnitContext.CompilationUnitLifecycle; import com.anjlab.eclipse.tapestry5.internal.CompilationUnitContext.ContextRunnable; @SuppressWarnings("restriction") public abstract class TapestryContext { public static interface FileNameBuilder { String getFileName(String fileName, String fileExtension); } private List<TapestryFile> files; private TapestryFile initialFile; public static TapestryContext emptyContext() { return new TapestryContext() { @Override protected Map<String, String> codeDesignExtensionMappings() { return null; } @Override public String getPackageName() { return null; } @Override protected CompilationUnitLifecycle getCompilationUnit() { return new CompilationUnitLifecycle() { @Override public ICompilationUnit createCompilationUnit() { return null; } }; } @Override public List<TapestryFile> findTapestryFiles(TapestryFile forFile, boolean findFirst, FileNameBuilder fileNameBuilder) { return Collections.emptyList(); } @Override public boolean isReadOnly() { return true; } @Override public TapestryComponentSpecification getSpecification() { return TapestryComponentSpecification.EMPTY; } @Override public IType getJavaType() { return null; } @Override public FileLookup createLookup() { return new FileLookup() { @Override public TapestryFile findClasspathFileCaseInsensitive(String path) { return null; } @Override public String findClasspathRelativePath(TapestryFile file) throws JavaModelException { return null; } }; } }; } public TapestryContext() { this.files = new ArrayList<TapestryFile>(); } protected void initFromFile(TapestryFile file) { if (this != file.getContext()) { throw new IllegalStateException("File is not from this context"); } if (initialFile != null) { throw new IllegalStateException("Already initialized"); } initialFile = file; if (file.isJavaFile() || file.isTemplateFile()) { initFromJavaOrTemplateFile(file); } else if (file.isPropertiesFile()) { initFromPropertiesFile(file); } else if (file.isJavaScriptFile() || file.isStyleSheetFile()) { initFromImportedFile(file); } } private void analyzeCompilationUnit() { CompilationUnitContext context = new CompilationUnitContext(getCompilationUnit()); try { addImports(context); addInjectedAssets(context); addRequireJSModules(context); } finally { context.dispose(); } } private void analyzeCompilationUnit( String operationName, CompilationUnitContext context, ContextRunnable runnable) { try { if (context.getCompilationUnit() != null) { runnable.run(context); } } catch (JavaModelException e) { Activator.getDefault().logError("Error during " + operationName, e); } } protected abstract CompilationUnitLifecycle getCompilationUnit(); private void addInjectedAssets(CompilationUnitContext context) { // context.accept(new ASTVisitor() // { // @Override // public boolean visit(SingleMemberAnnotation node) // { // // TODO Evaluate @Path expressions that are not available via IMemberValuePair (see below) // return super.visit(node); // } // }); analyzeCompilationUnit("analyzing @Inject'ed assets", context, new ContextRunnable() { @Override public void run(CompilationUnitContext context) throws JavaModelException { for (IType type : context.getCompilationUnit().getAllTypes()) { for (IField field : type.getFields()) { IAnnotation[] annotations = field.getAnnotations(); IAnnotation inject = TapestryUtils.findAnnotation( annotations, TapestryUtils.ORG_APACHE_TAPESTRY5_IOC_ANNOTATIONS_INJECT); if (inject == null) { continue; } final IAnnotation path = TapestryUtils.findAnnotation( annotations, TapestryUtils.ORG_APACHE_TAPESTRY5_ANNOTATIONS_PATH); if (path == null) { continue; } EclipseUtils.readValueFromAnnotation(path, "value", getProject(), context.getAST(), new ObjectCallback<String, JavaModelException>() { @Override public void callback(String value) throws JavaModelException { // This is the path of @Inject'ed Asset files.add(new AssetReference(getJavaFile(), path.getSourceRange(), value)); } }); } } } }); } private static class RequireDefinition { private String moduleName; private String functionName; } private void addRequireJSModules(CompilationUnitContext context) { analyzeCompilationUnit("analyzing RequireJS modules", context, new ContextRunnable() { @Override public void run(CompilationUnitContext context) throws JavaModelException { context.accept(new ASTVisitor() { private RequireDefinition requireDefinition; private RequireDefinition requireDefinition() { if (requireDefinition == null) { requireDefinition = new RequireDefinition(); } return requireDefinition; } private boolean analyzeInvocationChain(MethodInvocation node) { if (node.getExpression() instanceof MethodInvocation) { visit((MethodInvocation) node.getExpression()); } if (requireDefinition != null && StringUtils.isNotEmpty(requireDefinition.moduleName)) { String reference = StringUtils.isEmpty(requireDefinition.functionName) ? requireDefinition.moduleName : requireDefinition.moduleName + ":" + requireDefinition.functionName; final int offset = node.getStartPosition(); final int length = node.getLength(); files.add(new JavaScriptModuleReference(getJavaFile(), new ISourceRange() { @Override public int getOffset() { return offset; } @Override public int getLength() { return length; } }, reference)); } requireDefinition = null; return false; } @Override public boolean visit(MethodInvocation node) { String identifier = node.getName().getIdentifier(); if ("require".equals(identifier)) { if (node.arguments().size() != 1) { return false; } requireDefinition().moduleName = EclipseUtils.evalExpression( getProject(), node.arguments().get(0)); return analyzeInvocationChain(node); } else if ("invoke".equals(identifier)) { if (node.arguments().size() != 1) { return false; } requireDefinition().functionName = EclipseUtils.evalExpression( getProject(), node.arguments().get(0)); return analyzeInvocationChain(node); } return super.visit(node); } }); } }); } private void addImports(CompilationUnitContext context) { analyzeCompilationUnit("analyzing @Imports", context, new ContextRunnable() { @Override public void run(CompilationUnitContext context) throws JavaModelException { for (IType type : context.getCompilationUnit().getAllTypes()) { IAnnotation annotation = TapestryUtils.findAnnotation( type.getAnnotations(), "org.apache.tapestry5.annotations.Import"); if (annotation == null) { continue; } IMemberValuePair[] pairs = annotation.getMemberValuePairs(); for (IMemberValuePair pair : pairs) { if ("library".equals(pair.getMemberName()) || "stylesheet".equals(pair.getMemberName()) || "stack".equals(pair.getMemberName()) || "module".equals(pair.getMemberName())) { processImport( annotation, pair.getMemberName(), pair.getValue(), pair.getValueKind(), context.getAST()); } } } } }); } // TODO Rewrite with callback private void processImport(IAnnotation annotation, String type, Object value, int valueKind, AST ast) { if (value instanceof Object[]) { for (Object item : (Object[])value) { processImportedFile(annotation, type, eval(item, valueKind, ast)); } } else if (value instanceof String) { processImportedFile(annotation, type, eval(value, valueKind, ast)); } } private String eval(Object value, int valueKind, AST ast) { return EclipseUtils.eval(value, valueKind, ast, getProject()); } private void processImportedFile(IAnnotation annotation, String type, String fileName) { ISourceRange sourceRange = null; try { sourceRange = annotation.getSourceRange(); } catch (JavaModelException e) { Activator.getDefault().logError("Error getting annotation location", e); } if ("stack".equals(type)) { files.add(new JavaScriptStackReference(getJavaFile(), fileName, sourceRange)); } else if ("module".equals(type)) { files.add(new JavaScriptModuleReference(getJavaFile(), sourceRange, fileName)); } else { files.add(new AssetReference(getJavaFile(), sourceRange, fileName)); } } public List<TapestryFile> getFiles() { return Collections.unmodifiableList(files); } public TapestryFile getJavaFile() { for (TapestryFile file : files) { if (file.isJavaFile()) { return file; } } return null; } public TapestryFile getTemplateFile() { for (TapestryFile file : files) { if (file.isTemplateFile()) { return file; } } return null; } private void initFromImportedFile(TapestryFile file) { List<TapestryFile> files = findTapestryFiles(file, true, new TapestryContext.FileNameBuilder() { @Override public String getFileName(String fileName, String fileExtension) { return fileName.substring(0, fileName.lastIndexOf(fileExtension)) + codeDesignExtensionMappings().get("tml"); } }); if (files.isEmpty()) { // Support alternative naming of the asset files: lower-case-with-dashes files = findTapestryFiles(file, true, new TapestryContext.FileNameBuilder() { @Override public String getFileName(String fileName, String fileExtension) { StringBuilder builder = new StringBuilder(); String[] pathParts = fileName.split("/"); for (int i = 0; i < pathParts.length - 1; i++) { builder.append(pathParts[i]).append("/"); } String[] parts = pathParts[pathParts.length - 1].split("-"); for (String part : parts) { builder.append(Character.toUpperCase(part.charAt(0))) .append(part.substring(1)); } fileName = builder.toString(); return fileName.substring(0, fileName.lastIndexOf(fileExtension)) + codeDesignExtensionMappings().get("tml"); } }); } if (!files.isEmpty()) { TapestryFile javaFile = files.get(0); addWithComplementFile(javaFile); analyzeCompilationUnit(); if (!contains(file)) { // Assumption was wrong: Original file not from this context this.files.clear(); this.files.add(file); } } } private void initFromPropertiesFile(TapestryFile file) { List<TapestryFile> files = findTapestryFiles(file, true, new TapestryContext.FileNameBuilder() { @Override public String getFileName(String fileName, String fileExtension) { Matcher matcher = getLocalizedPropertiesPattern().matcher(fileName); String codeExtension = codeDesignExtensionMappings().get("tml"); if (matcher.find()) { return matcher.group(1) + "." + codeExtension; } return fileName.substring(0, fileName.lastIndexOf(fileExtension)) + codeExtension; } }); if (!files.isEmpty()) { TapestryFile javaFile = files.get(0); addWithComplementFile(javaFile); } addPropertiesFiles(file); analyzeCompilationUnit(); } private void initFromJavaOrTemplateFile(TapestryFile file) { addWithComplementFile(file); addPropertiesFiles(file); analyzeCompilationUnit(); } private void addWithComplementFile(TapestryFile file) { this.files.add(file); TapestryFile complementFile = findComplementFile(file); if (complementFile != null) { if (complementFile.isJavaFile()) { // Keep Java file on top of the list this.files.add(0, complementFile); } else { this.files.add(complementFile); } } } private void addPropertiesFiles(TapestryFile file) { List<TapestryFile> propertiesFiles = findTapestryFiles(file, false, new TapestryContext.FileNameBuilder() { @Override public String getFileName(String fileName, String fileExtension) { Matcher matcher = getLocalizedPropertiesPattern().matcher(fileName); String propertiesSuffix = "(|_.*)\\.properties"; if (matcher.find()) { return matcher.group(1) + propertiesSuffix; } return fileName.substring(0, fileName.lastIndexOf(fileExtension) - 1) + propertiesSuffix; } }); for (TapestryFile properties : propertiesFiles) { this.files.add(properties); } } private Pattern getLocalizedPropertiesPattern() { return Pattern.compile("([^_]*)(_.*)+\\.properties"); } public boolean contains(IFile file) { return contains(new LocalFile(this, file)); } public boolean contains(TapestryFile file) { if (file == null) { return false; } for (TapestryFile f : files) { if (f.equals(file)) { return true; } } return false; } public void validate() { for (TapestryFile file : files) { if (file instanceof TapestryFileReference) { try { ((TapestryFileReference) file).resolveFile(true); } catch (UnresolvableReferenceException e) { // Ignore } } } } public static void deleteMarkers(IResource project) { try { IMarker[] markers = project.findMarkers(IMarker.PROBLEM, false, IResource.DEPTH_INFINITE); for (IMarker marker : markers) { // TODO Support other markers too if (marker.getAttribute(AssetReference.MARKER_NAME) != null || marker.getAttribute(JavaScriptStackReference.MARKER_NAME) != null || marker.getAttribute(JavaScriptModuleReference.MARKER_NAME) != null) { marker.delete(); } } } catch (CoreException e) { Activator.getDefault().logError("Error deleting asset problem markers", e); } } public IProject getProject() { for (TapestryFile file : files) { return file.getProject(); } return null; } public boolean contains(String fileName) { for (TapestryFile file : files) { if (fileName.equals(file.getName())) { return true; } } return false; } public String getName() { for (TapestryFile file : files) { if (file.isJavaFile() || file.isTemplateFile()) { return file.getName().substring(0, file.getName().length() - file.getFileExtension().length() - 1); } } return null; } public boolean isEmpty() { return files.isEmpty(); } public abstract String getPackageName(); public void remove(IFile file) { remove(new LocalFile(this, file)); } public void remove(TapestryFile file) { if (this.files.remove(file) && file.isJavaFile()) { // Remove all @Imports, because Java file removed // and assets could be only traversed from the Java file Iterator<TapestryFile> iterator = files.iterator(); while (iterator.hasNext()) { TapestryFile f = iterator.next(); if (!f.isTemplateFile() && !f.isPropertiesFile()) { iterator.remove(); } } } } public abstract List<TapestryFile> findTapestryFiles(TapestryFile forFile, boolean findFirst, FileNameBuilder fileNameBuilder); public TapestryFile findComplementFile(TapestryFile file) { List<TapestryFile> files = findTapestryFiles(file, true, new TapestryContext.FileNameBuilder() { @Override public String getFileName(String fileName, String fileExtension) { String complementExtension = codeDesignExtensionMappings().get(fileExtension); if (complementExtension == null) { throw new IllegalArgumentException(); } return fileName.substring(0, fileName.lastIndexOf(fileExtension)) + complementExtension; } }); return !files.isEmpty() ? files.get(0) : null; } protected abstract Map<String, String> codeDesignExtensionMappings(); public TapestryFile getInitialFile() { return initialFile; } public abstract boolean isReadOnly(); public abstract TapestryComponentSpecification getSpecification(); public abstract IType getJavaType(); public String getJavadoc() { try { IType javaType = getJavaType(); return javaType != null ? JavadocContentAccess2.getHTMLContent(javaType, true) : null; } catch (CoreException e) { return null; } } public abstract FileLookup createLookup(); public boolean isPage() { String pagesPackage = TapestryUtils.getPagesPackage(getProject()); return pagesPackage != null ? StringUtils.startsWith(getPackageName(), pagesPackage) : false; } public boolean isComponent() { String componentsPackage = TapestryUtils.getComponentsPackage(getProject()); return componentsPackage != null ? StringUtils.startsWith(getPackageName(), componentsPackage) : false; } public boolean isMixin() { String mixinsPackage = TapestryUtils.getMixinsPackage(getProject()); return mixinsPackage != null ? StringUtils.startsWith(getPackageName(), mixinsPackage) : false; } }