package com.anjlab.eclipse.tapestry5; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.jar.Manifest; 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.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.IJarEntryResource; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchConstants; import com.anjlab.eclipse.tapestry5.DeclarationReference.NonJavaReference; import com.anjlab.eclipse.tapestry5.DeclarationReference.ProjectSettingsReference; import com.anjlab.eclipse.tapestry5.TapestryService.ServiceDefinition; import com.anjlab.eclipse.tapestry5.internal.CaseInsensitiveMap; import com.anjlab.eclipse.tapestry5.internal.Orderable; import com.anjlab.eclipse.tapestry5.internal.Orderer; import com.anjlab.eclipse.tapestry5.internal.SymbolExpansion; import com.anjlab.eclipse.tapestry5.templates.ProjectSettings; import com.anjlab.eclipse.tapestry5.templates.ProjectSettings.TapestryModuleSettings; import com.anjlab.eclipse.tapestry5.templates.ProjectSettings.TapestryServiceSettings; import com.anjlab.eclipse.tapestry5.watchdog.WebXmlReader.WebXml; public class TapestryProject { private IProject project; private String tapestryVersion; private boolean tapestryVersionResolved; private volatile List<TapestryModule> modules; public TapestryProject(IProject project) { this.project = project; } public IProject getProject() { return project; } private static final Pattern VERSION_MAJOR_MINOR_PATTERN = Pattern.compile("^\\d+\\.\\d+"); public String getTapestryVersionMajorMinor() { String version = getTapestryVersion(); if (StringUtils.isEmpty(version)) { return null; } Matcher matcher = VERSION_MAJOR_MINOR_PATTERN.matcher(version); if (matcher.find()) { return matcher.group(); } return null; } public String getTapestryVersion() { if (tapestryVersionResolved) { // May be null return tapestryVersion; } for (TapestryModule module : modules()) { if (module.isTapestryCoreModule()) { TapestryFile file = module.findClasspathFileCaseInsensitive( "META-INF/gradle/org.apache.tapestry/tapestry-core/project.properties"); if (file instanceof JarEntryFile) { try { Properties properties = new Properties(); properties.load(((JarEntryFile) file).getJarEntry().getContents()); tapestryVersion = properties.getProperty("version"); } catch (Exception e) { Activator.getDefault().logError("Error reading tapestry version string", e); } } break; } } tapestryVersionResolved = true; return tapestryVersion; } public List<TapestryModule> modules() { if (modules == null) { initialize(new NullProgressMonitor()); } return modules; } public void initialize(IProgressMonitor monitor) { findModules(monitor); // Now when we know tapestry version for this project, // try to locate correct version of project settings final ProjectSettings projectSettings = TapestryUtils.readProjectSettings(this); for (Entry<String, TapestryModuleSettings> moduleEntry : projectSettings.getTapestryModules().entrySet()) { String moduleClassName = moduleEntry.getKey(); TapestryModule module = addModule(monitor, modules, project, moduleClassName, new ObjectCallback<TapestryModule, RuntimeException>() { @Override public void callback(TapestryModule module) { module.addReference(new TapestryModuleReference(new NonJavaReference(module), true) { @Override public String getLabel() { return "via " + projectSettings.getReferenceLabel(); } }); } }); if (module != null) { TapestryModuleSettings moduleSettings = moduleEntry.getValue(); for (Entry<String, TapestryServiceSettings> serviceEntry : moduleSettings.getTapestryServices().entrySet()) { String serviceLabel = serviceEntry.getKey(); TapestryServiceSettings serviceSettings = serviceEntry.getValue(); if (StringUtils.isNotEmpty(serviceSettings.getDiscovery())) { // Support just one discovery rule for now if (serviceSettings.getDiscovery().equals("intf-impl-pattern")) { addServicesViaIntfImplPatternDiscovery(projectSettings, module, serviceLabel, serviceSettings, monitor); } else { Activator.getDefault().logWarning("Unsupported discovery '" + serviceSettings.getDiscovery()); } } else { // Clone and add new service ServiceDefinition definition = serviceSettings.clone(); String intfClass; if (StringUtils.isEmpty(serviceSettings.getIntfClass())) { // Treat serviceLabel as intfClass intfClass = serviceLabel; } else { intfClass = serviceSettings.getIntfClass(); } if (StringUtils.isEmpty(definition.getId())) { definition.setId(TapestryUtils.getSimpleName(intfClass)); } definition.setIntfClass(intfClass); addService(projectSettings, module, definition); } } } else { Activator.getDefault().logWarning("Tapestry module '" + moduleClassName + "' not found"); } } // TODO Tapestry 5.4: Register javascriptModules as contributions to ModuleManager? findSymbols(monitor, projectSettings); markJavaScriptStackOverrides(); } private void addServicesViaIntfImplPatternDiscovery( final ProjectSettings projectSettings, final TapestryModule module, final String serviceLabel, final TapestryServiceSettings serviceSettings, final IProgressMonitor monitor) { // Note: This rule can only search between classes that are located // in the same project/JAR as this module class if (StringUtils.isEmpty(serviceSettings.getIntfClass()) || StringUtils.isEmpty(serviceSettings.getImplClass())) { Activator.getDefault().logError("Both intfClass and implClass must not be null with auto-discovery rule '" + serviceSettings.getDiscovery() + "' (" + projectSettings.getReferenceLabel() + ", tapestryModule: " + module.getName() + ", serviceLabel: " + serviceLabel + ", intfClass: " + serviceSettings.getIntfClass() + ", implClass: " + serviceSettings.getImplClass() + ")"); return; } final Pattern intfPattern = Pattern.compile(serviceSettings.getIntfClass()); module.enumJavaClassesRecursively(monitor, "", new ObjectCallback<Object, RuntimeException>() { @Override public void callback(Object obj) throws RuntimeException { String className; if (obj instanceof IFile) { className = EclipseUtils.getClassName((IFile) obj); } else if (obj instanceof IClassFile) { className = ((IClassFile) obj).getElementName(); } else { return; } Matcher matcher = intfPattern.matcher(className); if (matcher.find()) { ServiceDefinition definition = serviceSettings.clone(); String intfClass = className; String implClass = matcher.replaceAll(definition.getImplClass()); // Check if implClass actually exists IType implClassType = EclipseUtils .findTypeDeclaration(getProject(), IJavaSearchConstants.CLASS, implClass); if (implClassType == null) { Activator.getDefault().logWarning( "Implementation class '" + implClass + "' not found for service interface '" + intfClass + "'"); return; } definition.setIntfClass(intfClass); definition.setImplClass(implClass); definition.setId(StringUtils.isEmpty(definition.getId()) ? TapestryUtils.getSimpleName(className) : matcher.replaceAll(definition.getId())); addService(projectSettings, module, definition); } } }); } private void addService(ProjectSettings projectSettings, TapestryModule module, ServiceDefinition definition) { definition.resolveMarkers(module); TapestryService service = new TapestryService( module, definition, new ProjectSettingsReference(projectSettings)); // TODO Make sure to replace existing service that could be added via config previously // (caching issue) module.addService(service); } private SymbolExpansion expansion; public String expandSymbols(String input) throws RuntimeException { if (expansion == null) { expansion = new SymbolExpansion(symbols()); } return expansion.expandSymbols(input); } private Map<String, List<TapestrySymbol>> symbols; public Map<String, List<TapestrySymbol>> symbols() { if (symbols == null) { findSymbols(new NullProgressMonitor(), TapestryUtils.readProjectSettings(this)); } return symbols; } private synchronized void findSymbols(IProgressMonitor monitor, ProjectSettings projectSettings) { if (symbols != null) { return; } final List<TapestryModule> modules = modules(); monitor.beginTask("Resolving symbol providers", modules.size()); final Orderer<TapestryService> orderer = new Orderer<TapestryService>(); for (final TapestryModule module : modules) { monitor.subTask(module.getModuleClass().getFullyQualifiedName()); List<Orderable<TapestryService>> symbolProviders = module.symbolProviders(monitor); for (Orderable<TapestryService> orderable : symbolProviders) { orderer.add(orderable); } monitor.worked(1); } final List<TapestryService> symbolProviders = orderer.getOrdered(); monitor.beginTask("Resolving symbols", modules.size()); final Map<TapestryService, List<TapestrySymbol>> providersToSymbols = new HashMap<TapestryService, List<TapestrySymbol>>(); for (final TapestryModule module : modules) { monitor.subTask(module.getModuleClass().getFullyQualifiedName()); module.buildProvidersToSymbolsMap(symbolProviders, monitor); for (final TapestryService symbolProvider : symbolProviders) { // Values from the same provider may override each other List<TapestrySymbol> providerSymbols = module.symbolsFrom(symbolProvider, monitor); if (providerSymbols != null) { for (int i = 0; i < providerSymbols.size(); i++) { TapestrySymbol symbol = providerSymbols.get(i); for (int j = i + 1; j < providerSymbols.size(); j++) { TapestrySymbol other = providerSymbols.get(j); if (StringUtils.equals(symbol.getName(), other.getName())) { if (other.isOverride()) { // Clone symbol to keep cached version untouched symbol = symbol.clone(); symbol.setOverridden(true); providerSymbols.set(i, symbol); } } } } List<TapestrySymbol> symbols = providersToSymbols.get(symbolProvider); if (symbols == null) { symbols = new ArrayList<TapestrySymbol>(); providersToSymbols.put(symbolProvider, symbols); } symbols.addAll(providerSymbols); } } monitor.worked(1); } // Override symbols using values from project settings Map<String, TapestryService> symbolProvidersLookup = new CaseInsensitiveMap<TapestryService>(); for (TapestryService symbolProvider : symbolProviders) { symbolProvidersLookup.put(symbolProvider.getDefinition().getId(), symbolProvider); } for (Entry<String, Map<String, String>> additionalProviderSymbols : projectSettings.getSymbols().entrySet()) { String providerName = additionalProviderSymbols.getKey(); TapestryService symbolProvider = symbolProvidersLookup.get(providerName); if (symbolProvider == null) { Activator.getDefault().logWarning("Symbol provider '" + providerName + "' not found"); continue; } List<TapestrySymbol> symbols = providersToSymbols.get(symbolProvider); if (symbols == null) { symbols = new ArrayList<TapestrySymbol>(); providersToSymbols.put(symbolProvider, symbols); } Map<String, String> additionalSymbols = additionalProviderSymbols.getValue(); for (Entry<String, String> symbolEntry : additionalSymbols.entrySet()) { // Add to the top of the list to win during symbol expansion symbols.add(0, new TapestrySymbol( symbolEntry.getKey(), symbolEntry.getValue(), false, new ProjectSettingsReference(projectSettings), symbolProvider)); } } symbols = new HashMap<String, List<TapestrySymbol>>(); // Symbol providers ordered by override rule, // symbols in the first provider override values of the second, // second overrides third, etc. for (TapestryService symbolProvider : symbolProviders) { List<TapestrySymbol> providerSymbols = providersToSymbols.get(symbolProvider); if (providerSymbols != null) { for (TapestrySymbol symbol : providerSymbols) { List<TapestrySymbol> values = symbols.get(symbol.getName()); if (values == null) { values = new ArrayList<TapestrySymbol>(); symbols.put(symbol.getName(), values); } else { for (TapestrySymbol existing : values) { if (!existing.isOverridden() && !(existing.getReference() instanceof ProjectSettingsReference) // Some modules may be added conditionally, like QA/DEV/Production // we should treat them all as equal, they souldn't override each other, // because usually only one of them enabled at run-time && !existing.getReference().getTapestryModule().isConditional()) { // Clone symbol to keep cached version untouched symbol = symbol.clone(); symbol.setOverridden(true); break; } } } values.add(symbol); } } } } public TapestryService findService( com.anjlab.eclipse.tapestry5.TapestryService.Matcher matcher) { for (TapestryModule module : modules()) { for (TapestryService service : module.services()) { if (matcher.matches(service)) { return service; } } } return null; } private void markJavaScriptStackOverrides() { Map<String, List<JavaScriptStack>> stacks = new HashMap<String, List<JavaScriptStack>>(); for (TapestryModule module : modules()) { for (JavaScriptStack stack : module.javaScriptStacks()) { List<JavaScriptStack> configs = stacks.get(stack.getName()); if (configs == null) { configs = new ArrayList<JavaScriptStack>(); stacks.put(stack.getName(), configs); } configs.add(stack); } } for (String stackName : stacks.keySet()) { List<JavaScriptStack> configs = stacks.get(stackName); boolean hasOverride = false; for (JavaScriptStack stack : configs) { if (stack.isOverride()) { hasOverride = true; } } if (hasOverride) { for (JavaScriptStack stack : configs) { if (!stack.isOverride()) { stack.setOverridden(true); } } } } } private synchronized void findModules(IProgressMonitor monitor) { if (modules != null) { return; } modules = new ArrayList<TapestryModule>(); String appPackage = TapestryUtils.getAppPackage(project); if (appPackage == null) { return; } final WebXml webXml = Activator.getDefault().getWebXml(project); if (webXml == null) { return; } for (String filterName : webXml.getFilterNames()) { final String localFilterName = filterName; TapestryModule appModule = addModule(monitor, modules, project, appPackage + ".services." + StringUtils.capitalize(filterName) + "Module", new ObjectCallback<TapestryModule, RuntimeException>() { @Override public void callback(TapestryModule module) { module.setAppModule(true); module.addReference(new TapestryModuleReference(new NonJavaReference(module), false) { @Override public String getLabel() { return "Your Application's Module (via " + webXml.getFilterClassName(localFilterName) + " in web.xml)"; } }); } }); if (appModule != null) { break; } } for (String paramName : webXml.getParamNames()) { if (paramName.matches("tapestry\\.[^-]+-modules")) { final String tapestryModeModules = paramName; String modeModules = webXml.getParamValue(tapestryModeModules); for (String moduleClassName : modeModules.split(",")) { addModule(monitor, modules, project, moduleClassName.trim(), new ObjectCallback<TapestryModule, RuntimeException>() { @Override public void callback(TapestryModule obj) { obj.addReference(new TapestryModuleReference(new NonJavaReference(obj), true) { @Override public String getLabel() { return "via " + tapestryModeModules + " in web.xml"; } }); } }); } } } // Handle new t5.4 TapestryModule class location ObjectCallback<TapestryModule, RuntimeException> coreObjectCallback = new ObjectCallback<TapestryModule, RuntimeException>() { @Override public void callback(TapestryModule module) { module.setTapestryCoreModule(true); module.addReference(new TapestryModuleReference(new NonJavaReference(module), false) { @Override public String getLabel() { final String version = TapestryProject.this.getTapestryVersion(); return "Tapestry Core Module" + (StringUtils.isEmpty(version) ? "" : " version " + version); } }); } }; // T5.3 addModule(monitor, modules, project, "org.apache.tapestry5.services.TapestryModule", coreObjectCallback); // T5.4 addModule(monitor, modules, project, "org.apache.tapestry5.modules.TapestryModule", coreObjectCallback); final ObjectCallback<TapestryModule, RuntimeException> iocModuleCallback = new ObjectCallback<TapestryModule, RuntimeException>() { @Override public void callback(TapestryModule module) { module.addReference(new TapestryModuleReference(new NonJavaReference(module), false) { @Override public String getLabel() { return "Tapestry IoC Module"; } }); } }; // T5.3 addModule(monitor, modules, project, "org.apache.tapestry5.ioc.services.TapestryIOCModule", iocModuleCallback); // T5.4 addModule(monitor, modules, project, "org.apache.tapestry5.ioc.modules.TapestryIOCModule", iocModuleCallback); try { for (IPackageFragmentRoot root : JavaCore.create(project).getAllPackageFragmentRoots()) { findModules(monitor, modules, root); } } catch (CoreException e) { Activator.getDefault().logError("Error searching tapestry modules", e); } } private void findModules(IProgressMonitor monitor, List<TapestryModule> modules, final IPackageFragmentRoot root) throws CoreException { monitor.subTask("Reading " + root.getElementName() + "..."); for (Object obj : root.getNonJavaResources()) { if (obj instanceof IJarEntryResource) { IJarEntryResource jarEntry = (IJarEntryResource) obj; if ("META-INF".equals(jarEntry.getName())) { for (IJarEntryResource child : jarEntry.getChildren()) { if ("MANIFEST.MF".equals(child.getName())) { InputStream contents = child.getContents(); try { Manifest manifest = new Manifest(contents); String classes = manifest.getMainAttributes().getValue("Tapestry-Module-Classes"); if (classes != null) { for (String className : classes.split(",")) { addModule(monitor, modules, project, className, new ObjectCallback<TapestryModule, RuntimeException>() { @Override public void callback(TapestryModule obj) { obj.addReference(new TapestryModuleReference(new NonJavaReference(obj), false) { @Override public String getLabel() { return "via " + root.getElementName() + "/META-INF/MANIFEST.MF"; } }); } }); } } } catch (IOException e) { if (contents != null) { try { contents.close(); } catch (IOException t) { } } } } } } } } } private TapestryModule addModule(IProgressMonitor monitor, List<TapestryModule> modules, IProject project, String moduleClassName, ObjectCallback<TapestryModule, RuntimeException> moduleCreated) { if (monitor.isCanceled()) { return null; } monitor.subTask("Locating " + moduleClassName + "..."); IType moduleClass = EclipseUtils.findTypeDeclaration( project, IJavaSearchConstants.CLASS, moduleClassName); if (moduleClass == null) { return null; } TapestryModule module = Activator.getDefault() .getTapestryModuleFactory() .createTapestryModule(this, moduleClass, moduleCreated); addModule(monitor, modules, module, moduleCreated); return module; } private void addModule(IProgressMonitor monitor, List<TapestryModule> modules, TapestryModule module, ObjectCallback<TapestryModule, RuntimeException> moduleCreated) { if (monitor.isCanceled()) { return; } int index = modules.indexOf(module); if (index != -1) { TapestryModule existingModule = modules.get(index); if (existingModule != module) { for (TapestryModuleReference reference : module.references()) { existingModule.addReference(reference); } } else { moduleCreated.callback(existingModule); } return; } module.initialize(monitor); modules.add(module); for (TapestryModule subModule : module.subModules()) { addModule(monitor, modules, subModule, moduleCreated); } } public boolean contains(IProject project) { for (TapestryModule module : modules()) { if (project.equals(module.getModuleClass().getJavaProject().getProject())) { return true; } } return false; } public TapestryContext findComponentContext(String componentName) throws JavaModelException { String libraryPrefix = ""; String componentNameWithoutPrefix = componentName; int index = componentName.indexOf('.'); if (index < 0) { index = componentName.indexOf('/'); } if (index >= 0) { libraryPrefix = componentName.substring(0, index); if (index + 1 >= componentName.length()) { return null; } componentNameWithoutPrefix = componentName.substring(index + 1); } for (TapestryModule module : modules) { if (module.isAppModule()) { TapestryContext context = findComponentContext( module, TapestryUtils.getComponentsPackage(module.getEclipseProject()), componentName); if (context != null) { return context; } } for (LibraryMapping mapping : module.libraryMappings()) { if (libraryPrefix.equals(mapping.getPathPrefix())) { TapestryContext context = findComponentContext( module, mapping.getRootPackage() + ".components", componentNameWithoutPrefix); if (context != null) { return context; } } } } if ("".equals(libraryPrefix)) { return findComponentContext("core/" + componentNameWithoutPrefix); } return null; } private TapestryContext findComponentContext(TapestryModule module, String appPackage, String componentNameWithoutPrefix) { // subpackage.componentName String componentPath = getComponentPath(module, appPackage, componentNameWithoutPrefix); // TODO Look in module.getComponents() instead? It's cached TapestryFile file = module.findClasspathFileCaseInsensitive(componentPath); if (file == null) { File parentFile = new File(componentPath).getParentFile(); if (parentFile != null) { // subpackage.componentNameSubpackage componentPath = getComponentPath(module, appPackage, componentNameWithoutPrefix + parentFile.getName()); file = module.findClasspathFileCaseInsensitive(componentPath); if (file == null) { // subpackage.subpackageComponentName componentPath = getComponentPath(module, appPackage, prepend(componentNameWithoutPrefix, parentFile.getName())); file = module.findClasspathFileCaseInsensitive(componentPath); } } } return file != null ? file.getContext() : null; } private String prepend(String componentNameWithoutPrefix, String parentName) { StringBuilder builder = new StringBuilder(componentNameWithoutPrefix); int index = componentNameWithoutPrefix.lastIndexOf("."); return builder.insert(index + 1, parentName).toString(); } protected String getComponentPath(TapestryModule module, String appPackage, String componentNameWithoutPrefix) { return TapestryUtils.joinPath(appPackage.replace('.', '/'), componentNameWithoutPrefix.replace('.', '/') + (module instanceof LocalTapestryModule ? ".java" : ".class")); } public JavaScriptStack findStack(String stackName) { List<JavaScriptStack> stacks = new ArrayList<JavaScriptStack>(); for (TapestryModule module : modules()) { for (JavaScriptStack stack : module.javaScriptStacks()) { if (stackName.equals(stack.getName())) { stacks.add(stack); } } } // Find first overridden stack (if any) for (JavaScriptStack stack : stacks) { if (stack.isOverride()) { return stack; } } return stacks.isEmpty() ? null : stacks.get(0); } public LibraryMapping findLibraryMapping(String packageName) { List<LibraryMapping> mappings = new ArrayList<LibraryMapping>(); for (TapestryModule module : modules()) { for (LibraryMapping mapping : module.libraryMappings()) { if (packageName.startsWith(mapping.getRootPackage())) { mappings.add(mapping); } } } return mappings.size() > 0 ? mappings.get(0) : null; } }