/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.aries.subsystem.core.internal; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; 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.aries.subsystem.ContentHandler; import org.apache.aries.subsystem.core.archive.AriesProvisionDependenciesDirective; import org.apache.aries.subsystem.core.archive.Attribute; import org.apache.aries.subsystem.core.archive.DeploymentManifest; import org.apache.aries.subsystem.core.archive.GenericHeader; import org.apache.aries.subsystem.core.archive.Header; import org.apache.aries.subsystem.core.archive.ImportPackageHeader; import org.apache.aries.subsystem.core.archive.RequireBundleHeader; import org.apache.aries.subsystem.core.archive.RequireCapabilityHeader; import org.apache.aries.subsystem.core.archive.SubsystemContentHeader; import org.apache.aries.subsystem.core.archive.SubsystemContentHeader.Clause; import org.apache.aries.subsystem.core.archive.SubsystemImportServiceHeader; import org.apache.aries.subsystem.core.archive.SubsystemLocalizationHeader; import org.apache.aries.subsystem.core.archive.SubsystemManifest; import org.apache.aries.subsystem.core.archive.SubsystemSymbolicNameHeader; import org.apache.aries.subsystem.core.archive.SubsystemTypeHeader; import org.apache.aries.subsystem.core.archive.SubsystemVersionHeader; import org.apache.aries.subsystem.core.capabilityset.SimpleFilter; import org.apache.aries.util.filesystem.FileSystem; import org.apache.aries.util.filesystem.IDirectory; import org.apache.aries.util.filesystem.IFile; import org.apache.aries.util.io.IOUtils; import org.apache.aries.util.manifest.ManifestHeaderProcessor; import org.apache.aries.util.manifest.ManifestProcessor; import org.osgi.framework.Version; import org.osgi.framework.namespace.BundleNamespace; import org.osgi.framework.namespace.ExecutionEnvironmentNamespace; import org.osgi.framework.namespace.IdentityNamespace; import org.osgi.framework.namespace.PackageNamespace; import org.osgi.namespace.service.ServiceNamespace; import org.osgi.resource.Capability; import org.osgi.resource.Requirement; import org.osgi.resource.Resource; import org.osgi.service.resolver.ResolutionException; import org.osgi.service.subsystem.Subsystem.State; import org.osgi.service.subsystem.SubsystemConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RawSubsystemResource implements Resource { private static final Logger logger = LoggerFactory.getLogger(RawSubsystemResource.class); private static final Pattern PATTERN = Pattern.compile("([^@/\\\\]+)(?:@(.+))?.esa"); private static final String APPLICATION_IMPORT_SERVICE_HEADER = "Application-ImportService"; private static SubsystemManifest computeExistingSubsystemManifest(IDirectory directory) throws IOException { Manifest manifest = ManifestProcessor.obtainManifestFromAppDir(directory, "OSGI-INF/SUBSYSTEM.MF"); if (manifest == null) return null; return new SubsystemManifest(manifest); } private static SubsystemManifest computeNewSubsystemManifest() { return new SubsystemManifest.Builder().build(); } private static SubsystemManifest computeSubsystemManifest(IDirectory directory) throws IOException { SubsystemManifest result = computeExistingSubsystemManifest(directory); if (result == null) result = computeNewSubsystemManifest(); return result; } private static String convertFileToLocation(IFile file) throws MalformedURLException { String result = convertFileNameToLocation(file.getName()); if (result == null) result = file.toURL().toString(); return result; } private static String convertFileNameToLocation(String fileName) { Matcher matcher = PATTERN.matcher(fileName); if (!matcher.matches()) return null; String version = matcher.group(2); return new SubsystemUri(matcher.group(1), version == null ? null : Version.parseVersion(version), null).toString(); } private final List<Capability> capabilities; private final DeploymentManifest deploymentManifest; private final long id; private final org.apache.aries.subsystem.core.repository.Repository localRepository; private final Location location; private final BasicSubsystem parentSubsystem; private final List<Requirement> requirements; private final Collection<Resource> resources; private final Resource fakeImportServiceResource; private final SubsystemManifest subsystemManifest; private final Collection<TranslationFile> translations; public RawSubsystemResource(String location, IDirectory content, BasicSubsystem parent) throws URISyntaxException, IOException, ResolutionException { id = SubsystemIdentifier.getNextId(); this.location = new Location(location); this.parentSubsystem = parent; if (content == null) content = this.location.open(); try { SubsystemManifest manifest = computeSubsystemManifest(content); resources = computeResources(content, manifest); fakeImportServiceResource = createFakeResource(manifest); localRepository = computeLocalRepository(); manifest = computeSubsystemManifestBeforeRequirements(content, manifest); requirements = computeRequirements(manifest); subsystemManifest = computeSubsystemManifestAfterRequirements(manifest); capabilities = computeCapabilities(); deploymentManifest = computeDeploymentManifest(content); translations = computeTranslations(content); } finally { IOUtils.close(content.toCloseable()); } } public RawSubsystemResource(File file, BasicSubsystem parent) throws IOException, URISyntaxException, ResolutionException { this(FileSystem.getFSRoot(file), parent); } public RawSubsystemResource(IDirectory idir, BasicSubsystem parent) throws IOException, URISyntaxException, ResolutionException { subsystemManifest = initializeSubsystemManifest(idir); requirements = subsystemManifest.toRequirements(this); capabilities = subsystemManifest.toCapabilities(this); deploymentManifest = initializeDeploymentManifest(idir); id = Long.parseLong(deploymentManifest.getHeaders().get(DeploymentManifest.ARIESSUBSYSTEM_ID).getValue()); location = new Location(deploymentManifest.getHeaders().get(DeploymentManifest.ARIESSUBSYSTEM_LOCATION).getValue()); parentSubsystem = parent; translations = Collections.emptyList(); Map<String, Header<?>> headers = deploymentManifest.getHeaders(); if (State.INSTALLING.equals( State.valueOf( headers.get( DeploymentManifest.ARIESSUBSYSTEM_STATE).getValue())) && subsystemManifest.getSubsystemTypeHeader().getAriesProvisionDependenciesDirective().isResolve()) { URL url = new URL(headers.get(Constants.AriesSubsystemOriginalContent).getValue()); Collection<Resource> resources; try { resources = computeResources(FileSystem.getFSRoot(new File(url.toURI())), subsystemManifest); } catch (IllegalArgumentException e) { // Thrown by File if the URI is not hierarchical. For example, // when handling a JAR URL. resources = computeResources(FileSystem.getFSRoot(url.openStream()), subsystemManifest); } this.resources = resources; fakeImportServiceResource = createFakeResource(subsystemManifest); } else { resources = Collections.emptyList(); fakeImportServiceResource = null; } localRepository = computeLocalRepository(); } private static Resource createFakeResource(SubsystemManifest manifest) { Header<?> importServiceHeader = manifest.getHeaders().get(APPLICATION_IMPORT_SERVICE_HEADER); if (importServiceHeader == null) { return null; } List<Capability> modifiableCaps = new ArrayList<Capability>(); final List<Capability> fakeCapabilities = Collections.unmodifiableList(modifiableCaps); Resource fakeResource = new Resource() { @Override public List<Capability> getCapabilities(String namespace) { if (namespace == null) { return fakeCapabilities; } List<Capability> results = new ArrayList<Capability>(); for (Capability capability : fakeCapabilities) { if (namespace.equals(capability.getNamespace())) { results.add(capability); } } return results; } @Override public List<Requirement> getRequirements(String namespace) { return Collections.emptyList(); } }; modifiableCaps.add(new OsgiIdentityCapability(fakeResource, Constants.ResourceTypeSynthesized, new Version(1,0,0), Constants.ResourceTypeSynthesized)); Map<String, Map<String, String>> serviceImports = ManifestHeaderProcessor.parseImportString(importServiceHeader.getValue()); for (Entry<String, Map<String, String>> serviceImport : serviceImports.entrySet()) { Collection<String> objectClasses = new ArrayList<String>(Arrays.asList(serviceImport.getKey())); String filter = serviceImport.getValue().get(IdentityNamespace.REQUIREMENT_FILTER_DIRECTIVE); BasicCapability.Builder capBuilder = new BasicCapability.Builder(); capBuilder.namespace(ServiceNamespace.SERVICE_NAMESPACE); capBuilder.attribute(ServiceNamespace.CAPABILITY_OBJECTCLASS_ATTRIBUTE, objectClasses); if (filter != null) capBuilder.attributes(new HashMap<String, Object>(SimpleFilter.attributes(filter))); capBuilder.attribute("service.imported", ""); capBuilder.resource(fakeResource); modifiableCaps.add(capBuilder.build()); } return fakeResource; } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof RawSubsystemResource)) return false; RawSubsystemResource that = (RawSubsystemResource)o; return getLocation().equals(that.getLocation()); } @Override public List<Capability> getCapabilities(String namespace) { if (namespace == null) return Collections.unmodifiableList(capabilities); ArrayList<Capability> result = new ArrayList<Capability>(capabilities.size()); for (Capability capability : capabilities) if (namespace.equals(capability.getNamespace())) result.add(capability); result.trimToSize(); return Collections.unmodifiableList(result); } public DeploymentManifest getDeploymentManifest() { return deploymentManifest; } public long getId() { return id; } public org.apache.aries.subsystem.core.repository.Repository getLocalRepository() { return localRepository; } public Location getLocation() { return location; } @Override public List<Requirement> getRequirements(String namespace) { if (namespace == null) return Collections.unmodifiableList(requirements); ArrayList<Requirement> result = new ArrayList<Requirement>(requirements.size()); for (Requirement requirement : requirements) if (namespace.equals(requirement.getNamespace())) result.add(requirement); result.trimToSize(); return Collections.unmodifiableList(result); } public SubsystemManifest getSubsystemManifest() { return subsystemManifest; } public Collection<TranslationFile> getTranslations() { return translations; } @Override public int hashCode() { int result = 17; result = 31 * result + getLocation().hashCode(); return result; } private void addHeader(SubsystemManifest.Builder builder, Header<?> header) { if (header == null) return; builder.header(header); } private void addImportPackageHeader(SubsystemManifest.Builder builder) { addHeader(builder, computeImportPackageHeader()); } private void addRequireBundleHeader(SubsystemManifest.Builder builder) { addHeader(builder, computeRequireBundleHeader()); } private void addRequireCapabilityHeader(SubsystemManifest.Builder builder) { addHeader(builder, computeRequireCapabilityHeader()); } private void addSubsystemContentHeader(SubsystemManifest.Builder builder, SubsystemManifest manifest) { addHeader(builder, computeSubsystemContentHeader(manifest)); } private void addSubsystemImportServiceHeader(SubsystemManifest.Builder builder) { addHeader(builder, computeSubsystemImportServiceHeader()); } private void addSubsystemSymbolicNameHeader(SubsystemManifest.Builder builder, SubsystemManifest manifest) { addHeader(builder, computeSubsystemSymbolicNameHeader(manifest)); } private void addSubsystemTypeHeader(SubsystemManifest.Builder builder, SubsystemManifest manifest) { addHeader(builder, computeSubsystemTypeHeader(manifest)); } private void addSubsystemVersionHeader(SubsystemManifest.Builder builder, SubsystemManifest manifest) { addHeader(builder, computeSubsystemVersionHeader(manifest)); } private List<Capability> computeCapabilities() { return subsystemManifest.toCapabilities(this); } private DeploymentManifest computeDeploymentManifest(IDirectory directory) throws IOException { return computeExistingDeploymentManifest(directory); } private DeploymentManifest computeExistingDeploymentManifest(IDirectory directory) throws IOException { Manifest manifest = ManifestProcessor.obtainManifestFromAppDir(directory, "OSGI-INF/DEPLOYMENT.MF"); if (manifest == null) return null; return new DeploymentManifest(manifest); } private ImportPackageHeader computeImportPackageHeader() { if (requirements.isEmpty()) return null; ArrayList<ImportPackageHeader.Clause> clauses = new ArrayList<ImportPackageHeader.Clause>(requirements.size()); for (Requirement requirement : requirements) { if (!PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace())) continue; clauses.add(ImportPackageHeader.Clause.valueOf(requirement)); } if (clauses.isEmpty()) return null; clauses.trimToSize(); return new ImportPackageHeader(clauses); } private org.apache.aries.subsystem.core.repository.Repository computeLocalRepository() { if (fakeImportServiceResource != null) { Collection<Resource> temp = new ArrayList<Resource>(resources); temp.add(fakeImportServiceResource); return new LocalRepository(temp); } return new LocalRepository(resources); } private RequireBundleHeader computeRequireBundleHeader() { if (requirements.isEmpty()) return null; ArrayList<RequireBundleHeader.Clause> clauses = new ArrayList<RequireBundleHeader.Clause>(requirements.size()); for (Requirement requirement : requirements) { if (!BundleNamespace.BUNDLE_NAMESPACE.equals(requirement.getNamespace())) continue; clauses.add(RequireBundleHeader.Clause.valueOf(requirement)); } if (clauses.isEmpty()) return null; clauses.trimToSize(); return new RequireBundleHeader(clauses); } private RequireCapabilityHeader computeRequireCapabilityHeader() { if (requirements.isEmpty()) return null; ArrayList<RequireCapabilityHeader.Clause> clauses = new ArrayList<RequireCapabilityHeader.Clause>(); for (Requirement requirement : requirements) { String namespace = requirement.getNamespace(); if (namespace.startsWith("osgi.") && !( // Don't filter out the osgi.ee namespace... namespace.equals(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE) || // ...or the osgi.service namespace. namespace.equals(ServiceNamespace.SERVICE_NAMESPACE))) continue; clauses.add(RequireCapabilityHeader.Clause.valueOf(requirement)); } if (clauses.isEmpty()) return null; clauses.trimToSize(); return new RequireCapabilityHeader(clauses); } private List<Requirement> computeRequirements(SubsystemManifest manifest) throws ResolutionException { if (isComposite(manifest)) { // Composites determine their own requirements. return manifest.toRequirements(this); } // Gather up all of the content resources for the subsystem. SubsystemContentHeader header = manifest.getSubsystemContentHeader(); if (header == null) { // Empty subsystems (i.e. subsystems with no content) are allowed. return Collections.emptyList(); } List<Requirement> requirements = header.toRequirements(this); List<Resource> resources = new ArrayList<Resource>(requirements.size()); // TODO Do we need the system repository in here (e.g., for features)? // What about the preferred provider repository? // Search the local repository and service repositories for content. RepositoryServiceRepository serviceRepo = new RepositoryServiceRepository(); // TODO Should we search the service repositories first, the assumption // being they will contain more current content than the subsystem // archive? CompositeRepository compositeRepo = new CompositeRepository(localRepository, serviceRepo); for (Requirement requirement : requirements) { Collection<Capability> capabilities = compositeRepo.findProviders(requirement); if (!capabilities.isEmpty()) { resources.add(capabilities.iterator().next().getResource()); } } if (fakeImportServiceResource != null) { // Add the fake resource so the dependency calculator knows not to // return service requirements that are included in // Application-ImportService. resources.add(fakeImportServiceResource); } // Now compute the dependencies of the content resources. These are // dependencies not satisfied by the content resources themselves. return new DependencyCalculator(resources).calculateDependencies(); } private Collection<Resource> computeResources(IDirectory directory, SubsystemManifest manifest) throws IOException, URISyntaxException, ResolutionException { List<IFile> files = directory.listFiles(); if (files.isEmpty()) return Collections.emptyList(); ArrayList<Resource> result = new ArrayList<Resource>(files.size()); for (IFile file : directory.listFiles()) { if (file.isFile()) { addResource(file, file.convertNested(), manifest, result); } else { addResource(file, file.convert(), manifest, result); } } result.trimToSize(); return result; } private void addResource(IFile file, IDirectory content, SubsystemManifest manifest, ArrayList<Resource> result) throws URISyntaxException, IOException, ResolutionException, MalformedURLException { String name = file.getName(); if (name.endsWith(".esa")) { result.add(new RawSubsystemResource(convertFileToLocation(file), content, parentSubsystem)); } else if (name.endsWith(".jar")) { result.add(new BundleResource(file)); } else { // This is a different type of file. Add a file resource for it if there is a custom content handler for it. FileResource fr = new FileResource(file); fr.setCapabilities(computeFileCapabilities(fr, file, manifest)); List<Capability> idcaps = fr.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE); if (idcaps.size() > 0) { Capability idcap = idcaps.get(0); Object type = idcap.getAttributes().get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE); if (type instanceof String && parentSubsystem != null) { if (CustomResources.getCustomContentHandler(parentSubsystem, (String) type) != null) { // Yes, there is a custom content handler, add it. result.add(fr); return; } } } // There is no custom handler for this resource, let's check if it turns out to be a bundle try { result.add(new BundleResource(file)); } catch (Exception e) { // Ignore if the resource is an invalid bundle or not a bundle at all. if (logger.isDebugEnabled()) { logger.debug("File \"" + file.getName() + "\" in subsystem with location \"" + location + "\" will be ignored because it is not recognized as a supported resource", e); } } } } private List<Capability> computeFileCapabilities(FileResource resource, IFile file, SubsystemManifest manifest) { SubsystemContentHeader ssch = manifest.getSubsystemContentHeader(); if (ssch == null) return Collections.emptyList(); for (Clause c : ssch.getClauses()) { Attribute er = c.getAttribute(ContentHandler.EMBEDDED_RESOURCE_ATTRIBUTE); if (er != null) { if (file.getName().equals(er.getValue())) { Map<String, Object> attrs = new HashMap<String, Object>(); attrs.put(ContentHandler.EMBEDDED_RESOURCE_ATTRIBUTE, er.getValue()); return Collections.<Capability> singletonList( new OsgiIdentityCapability(resource, c.getSymbolicName(), c.getVersionRange().getLeft(), c.getType(), attrs)); } } } return Collections.emptyList(); } private SubsystemContentHeader computeSubsystemContentHeader(SubsystemManifest manifest) { SubsystemContentHeader header = manifest.getSubsystemContentHeader(); if (header == null && !resources.isEmpty()) header = SubsystemContentHeader.newInstance(resources); return header; } private SubsystemImportServiceHeader computeSubsystemImportServiceHeader() { if (requirements.isEmpty()) return null; ArrayList<SubsystemImportServiceHeader.Clause> clauses = new ArrayList<SubsystemImportServiceHeader.Clause>(requirements.size()); for (Requirement requirement : requirements) { if (!ServiceNamespace.SERVICE_NAMESPACE.equals(requirement.getNamespace())) continue; clauses.add(SubsystemImportServiceHeader.Clause.valueOf(requirement)); } if (clauses.isEmpty()) return null; clauses.trimToSize(); return new SubsystemImportServiceHeader(clauses); } private SubsystemManifest computeSubsystemManifestAfterRequirements(SubsystemManifest manifest) { if (isComposite(manifest)) return manifest; SubsystemManifest.Builder builder = new SubsystemManifest.Builder().manifest(manifest); addImportPackageHeader(builder); addRequireBundleHeader(builder); addRequireCapabilityHeader(builder); addSubsystemImportServiceHeader(builder); return builder.build(); } private SubsystemManifest computeSubsystemManifestBeforeRequirements(IDirectory content, SubsystemManifest manifest) throws MalformedURLException { SubsystemManifest.Builder builder = new SubsystemManifest.Builder().manifest(manifest); addSubsystemSymbolicNameHeader(builder, manifest); addSubsystemVersionHeader(builder, manifest); addSubsystemTypeHeader(builder, manifest); addSubsystemContentHeader(builder, manifest); builder.header(new GenericHeader(Constants.AriesSubsystemOriginalContent, String.valueOf(content.toURL()))); return builder.build(); } private SubsystemSymbolicNameHeader computeSubsystemSymbolicNameHeader(SubsystemManifest manifest) { SubsystemSymbolicNameHeader header = manifest.getSubsystemSymbolicNameHeader(); if (header != null) return header; String symbolicName = location.getSymbolicName(); if (symbolicName == null) symbolicName = "org.apache.aries.subsystem." + id; return new SubsystemSymbolicNameHeader(symbolicName); } private SubsystemTypeHeader computeSubsystemTypeHeader(SubsystemManifest manifest) { SubsystemTypeHeader header = manifest.getSubsystemTypeHeader(); AriesProvisionDependenciesDirective directive = header.getAriesProvisionDependenciesDirective(); if (directive != null) { // Nothing to do because the directive was specified in the original // manifest. Validation of the value occurs later. return header; } // The directive was not specified in the original manifest. The value // of the parent directive becomes the default. SubsystemManifest parentManifest = ((BasicSubsystem)parentSubsystem).getSubsystemManifest(); SubsystemTypeHeader parentHeader = parentManifest.getSubsystemTypeHeader(); directive = parentHeader.getAriesProvisionDependenciesDirective(); header = new SubsystemTypeHeader(header.getValue() + ';' + directive); return header; } private SubsystemVersionHeader computeSubsystemVersionHeader(SubsystemManifest manifest) { SubsystemVersionHeader header = manifest.getSubsystemVersionHeader(); if (header.getVersion().equals(Version.emptyVersion) && location.getVersion() != null) header = new SubsystemVersionHeader(location.getVersion()); return header; } private Collection<TranslationFile> computeTranslations(IDirectory directory) throws IOException { SubsystemManifest manifest = getSubsystemManifest(); SubsystemLocalizationHeader header = manifest.getSubsystemLocalizationHeader(); String directoryName = header.getDirectoryName(); // TODO Assumes the ZIP file includes directory entries. Issues? IFile file = directoryName == null ? directory : directory.getFile(directoryName); if (file == null || !file.isDirectory()) return Collections.emptyList(); List<IFile> files = file.convert().listFiles(); if (files == null || files.isEmpty()) return Collections.emptyList(); ArrayList<TranslationFile> result = new ArrayList<TranslationFile>(files.size()); for (IFile f : files) { Properties properties = new Properties(); InputStream is = f.open(); try { properties.load(is); result.add(new TranslationFile(f.getName(), properties)); } finally { is.close(); } } result.trimToSize(); return result; } private DeploymentManifest initializeDeploymentManifest(IDirectory idir) throws IOException { Manifest manifest = ManifestProcessor.obtainManifestFromAppDir(idir, "OSGI-INF/DEPLOYMENT.MF"); if (manifest != null) return new DeploymentManifest(manifest); else return new DeploymentManifest.Builder() .manifest(getSubsystemManifest()) .location(BasicSubsystem.ROOT_LOCATION).autostart(true).id(0) .lastId(SubsystemIdentifier.getLastId()) .state(State.INSTALLING) .build(); } private SubsystemManifest initializeSubsystemManifest(IDirectory idir) throws IOException { Manifest manifest = ManifestProcessor.obtainManifestFromAppDir(idir, "OSGI-INF/SUBSYSTEM.MF"); if (manifest != null) return new SubsystemManifest(manifest); else return new SubsystemManifest.Builder() .symbolicName(BasicSubsystem.ROOT_SYMBOLIC_NAME) .version(BasicSubsystem.ROOT_VERSION) .type(SubsystemTypeHeader.TYPE_APPLICATION + ';' + SubsystemTypeHeader.DIRECTIVE_PROVISION_POLICY + ":=" + SubsystemTypeHeader.PROVISION_POLICY_ACCEPT_DEPENDENCIES + ';' + AriesProvisionDependenciesDirective.INSTALL.toString()) .build(); } private boolean isComposite(SubsystemManifest manifest) { return SubsystemConstants.SUBSYSTEM_TYPE_COMPOSITE.equals(manifest.getSubsystemTypeHeader().getType()); } Collection<Resource> getResources() { return resources; } }