[KARAF-2930] Support for user defined repositories during resolution
Project: http://git-wip-us.apache.org/repos/asf/karaf/repo Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/bc05096b Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/bc05096b Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/bc05096b Branch: refs/heads/master Commit: bc05096b5e214d4a51c6c3663efdb9d08aa7275b Parents: 3574b44 Author: Guillaume Nodet <[email protected]> Authored: Wed Apr 23 15:28:23 2014 +0200 Committer: Guillaume Nodet <[email protected]> Committed: Wed Apr 23 15:48:54 2014 +0200 ---------------------------------------------------------------------- .../download/simple/SimpleDownloader.java | 4 +- .../karaf/features/internal/osgi/Activator.java | 26 ++++++- .../region/SubsystemResolveContext.java | 82 +++++++++++++++++++- .../internal/region/SubsystemResolver.java | 8 +- .../internal/service/FeaturesServiceImpl.java | 18 +++-- .../karaf/features/FeaturesServiceTest.java | 8 +- .../features/internal/region/SubsystemTest.java | 9 ++- .../service/FeaturesServiceImplTest.java | 8 +- 8 files changed, 138 insertions(+), 25 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/main/java/org/apache/karaf/features/internal/download/simple/SimpleDownloader.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/download/simple/SimpleDownloader.java b/features/core/src/main/java/org/apache/karaf/features/internal/download/simple/SimpleDownloader.java index 9c35199..1255fb6 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/download/simple/SimpleDownloader.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/download/simple/SimpleDownloader.java @@ -59,7 +59,9 @@ public class SimpleDownloader implements DownloadManager, Downloader { providers.putIfAbsent(location, createProvider(location)); } try { - downloadCallback.downloaded(providers.get(location)); + if (downloadCallback != null) { + downloadCallback.downloaded(providers.get(location)); + } } catch (Exception e) { exception.addException(e); } http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java index b2cdd6e..de6b44d 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java @@ -23,13 +23,18 @@ import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Dictionary; import java.util.Hashtable; +import java.util.List; import java.util.Properties; import org.apache.felix.resolver.ResolverImpl; import org.apache.karaf.features.FeaturesListener; import org.apache.karaf.features.FeaturesService; +import org.apache.karaf.features.internal.repository.AggregateRepository; +import org.apache.karaf.features.internal.repository.JsonRepository; +import org.apache.karaf.features.internal.repository.XmlRepository; import org.apache.karaf.features.internal.resolver.Slf4jResolverLog; import org.apache.karaf.features.internal.service.EventAdminListener; import org.apache.karaf.features.internal.service.FeatureConfigInstaller; @@ -39,7 +44,6 @@ import org.apache.karaf.features.internal.service.FeaturesServiceImpl; import org.apache.karaf.features.internal.service.StateStorage; import org.apache.karaf.features.internal.management.FeaturesServiceMBeanImpl; import org.apache.karaf.util.tracker.BaseActivator; -import org.apache.karaf.util.tracker.SingleServiceTracker; import org.eclipse.equinox.internal.region.DigraphHelper; import org.eclipse.equinox.internal.region.StandardRegionDigraph; import org.eclipse.equinox.internal.region.management.StandardManageableRegionDigraph; @@ -50,6 +54,7 @@ import org.osgi.framework.hooks.bundle.CollisionHook; import org.osgi.framework.hooks.resolver.ResolverHookFactory; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.cm.ManagedService; +import org.osgi.service.repository.Repository; import org.osgi.service.resolver.Resolver; import org.osgi.service.url.URLStreamHandlerService; import org.osgi.util.tracker.ServiceTracker; @@ -120,7 +125,21 @@ public class Activator extends BaseActivator { props.put(Constants.SERVICE_PID, FEATURES_REPOS_PID); register(ManagedService.class, featureFinder, props); - FeatureConfigInstaller configInstaller = new FeatureConfigInstaller(configurationAdmin); + List<Repository> repositories = new ArrayList<Repository>(); + String[] resourceRepositories = getString("resourceRepositories", "").split(","); + for (String url : resourceRepositories) { + url = url.trim(); + if (url.startsWith("json:")) { + repositories.add(new JsonRepository(url.substring("json:".length()))); + } else if (url.startsWith("xml:")) { + repositories.add(new XmlRepository(url.substring("xml:".length()))); + } else { + logger.warn("Unrecognized resource repository: " + url); + } + } + Repository globalRepository = repositories.isEmpty() ? null : new AggregateRepository(repositories); + + FeatureConfigInstaller configInstaller = new FeatureConfigInstaller(configurationAdmin); String overrides = getString("overrides", new File(System.getProperty("karaf.etc"), "overrides.properties").toURI().toString()); String featureResolutionRange = getString("featureResolutionRange", FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE); String bundleUpdateRange = getString("bundleUpdateRange", FeaturesServiceImpl.DEFAULT_BUNDLE_UPDATE_RANGE); @@ -159,7 +178,8 @@ public class Activator extends BaseActivator { overrides, featureResolutionRange, bundleUpdateRange, - updateSnapshots); + updateSnapshots, + globalRepository); register(FeaturesService.class, featuresService); featuresListenerTracker = new ServiceTracker<FeaturesListener, FeaturesListener>( http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java index 949e37a..ef9c420 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java @@ -16,6 +16,7 @@ */ package org.apache.karaf.features.internal.region; +import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -25,7 +26,11 @@ import java.util.List; import java.util.Map; import org.apache.felix.resolver.Util; +import org.apache.karaf.features.internal.download.Downloader; import org.apache.karaf.features.internal.repository.BaseRepository; +import org.apache.karaf.features.internal.resolver.CapabilityImpl; +import org.apache.karaf.features.internal.resolver.RequirementImpl; +import org.apache.karaf.features.internal.resolver.ResourceImpl; import org.eclipse.equinox.region.Region; import org.eclipse.equinox.region.RegionDigraph; import org.eclipse.equinox.region.RegionFilter; @@ -38,6 +43,8 @@ import org.osgi.service.repository.Repository; import org.osgi.service.resolver.HostedCapability; import org.osgi.service.resolver.ResolveContext; +import static org.apache.karaf.features.internal.resolver.ResourceUtils.addIdentityRequirement; +import static org.apache.karaf.features.internal.resolver.ResourceUtils.getUri; import static org.eclipse.equinox.region.RegionFilter.VISIBLE_BUNDLE_NAMESPACE; import static org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE; import static org.osgi.framework.Constants.BUNDLE_VERSION_ATTRIBUTE; @@ -54,11 +61,14 @@ public class SubsystemResolveContext extends ResolveContext { private final Map<Resource, Subsystem> resToSub = new HashMap<Resource, Subsystem>(); private final Repository repository; + private final Repository globalRepository; + private final Downloader downloader; - - public SubsystemResolveContext(Subsystem root, RegionDigraph digraph) throws BundleException { + public SubsystemResolveContext(Subsystem root, RegionDigraph digraph, Repository globalRepository, Downloader downloader) throws BundleException { this.root = root; this.digraph = digraph; + this.globalRepository = globalRepository != null ? new SubsystemRepository(globalRepository) : null; + this.downloader = downloader; prepare(root); repository = new BaseRepository(resToSub.keySet()); @@ -87,9 +97,16 @@ public class SubsystemResolveContext extends ResolveContext { Map<Requirement, Collection<Capability>> resMap = repository.findProviders(Collections.singleton(requirement)); Collection<Capability> res = resMap != null ? resMap.get(requirement) : null; - if (res != null) { + if (res != null && !res.isEmpty()) { caps.addAll(res); + } else if (globalRepository != null) { + resMap = globalRepository.findProviders(Collections.singleton(requirement)); + res = resMap != null ? resMap.get(requirement) : null; + if (res != null && !res.isEmpty()) { + caps.addAll(res); + } } + // Use the digraph to prune non visible capabilities Visitor visitor = new Visitor(caps); requirerRegion.visitSubgraph(visitor); @@ -191,4 +208,63 @@ public class SubsystemResolveContext extends ResolveContext { } + class SubsystemRepository implements Repository { + + private final Repository repository; + private final Map<Subsystem, Map<Capability, Capability>> mapping = new HashMap<Subsystem, Map<Capability, Capability>>(); + + public SubsystemRepository(Repository repository) { + this.repository = repository; + } + + @Override + public Map<Requirement, Collection<Capability>> findProviders(Collection<? extends Requirement> requirements) { + Map<Requirement, Collection<Capability>> base = repository.findProviders(requirements); + Map<Requirement, Collection<Capability>> result = new HashMap<Requirement, Collection<Capability>>(); + for (Map.Entry<Requirement, Collection<Capability>> entry : base.entrySet()) { + List<Capability> caps = new ArrayList<Capability>(); + Subsystem ss = getSubsystem(entry.getKey().getResource()); + while (!ss.isAcceptDependencies()) { + ss = ss.getParent(); + } + Map<Capability, Capability> map = mapping.get(ss); + if (map == null) { + map = new HashMap<Capability, Capability>(); + mapping.put(ss, map); + } + for (Capability cap : entry.getValue()) { + Capability wrapped = map.get(cap); + if (wrapped == null) { + wrap(map, ss, cap.getResource()); + wrapped = map.get(cap); + } + caps.add(wrapped); + } + result.put(entry.getKey(), caps); + } + return result; + } + + private void wrap(Map<Capability, Capability> map, Subsystem subsystem, Resource resource) { + ResourceImpl wrapped = new ResourceImpl(); + for (Capability cap : resource.getCapabilities(null)) { + CapabilityImpl wCap = new CapabilityImpl(wrapped, cap.getNamespace(), cap.getDirectives(), cap.getAttributes()); + map.put(cap, wCap); + wrapped.addCapability(wCap); + } + for (Requirement req : resource.getRequirements(null)) { + RequirementImpl wReq = new RequirementImpl(wrapped, req.getNamespace(), req.getDirectives(), req.getAttributes()); + wrapped.addRequirement(wReq); + } + addIdentityRequirement(wrapped, subsystem, false); + resToSub.put(wrapped, subsystem); + // TODO: use RepositoryContent ? + try { + downloader.download(getUri(wrapped), null); + } catch (MalformedURLException e) { + throw new IllegalStateException("Unable to download resource: " + getUri(wrapped)); + } + } + } + } http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java index 190a67d..8f42dda 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java @@ -30,6 +30,7 @@ import org.apache.felix.resolver.Util; import org.apache.karaf.features.Feature; import org.apache.karaf.features.Repository; import org.apache.karaf.features.internal.download.DownloadManager; +import org.apache.karaf.features.internal.download.Downloader; import org.apache.karaf.features.internal.download.StreamProvider; import org.apache.karaf.features.internal.download.simple.SimpleDownloader; import org.apache.karaf.features.internal.resolver.CapabilitySet; @@ -80,7 +81,8 @@ public class SubsystemResolver { Map<String, Set<String>> features, Map<String, Set<BundleRevision>> system, Set<String> overrides, - String featureResolutionRange + String featureResolutionRange, + org.osgi.service.repository.Repository globalRepository ) throws Exception { // Build subsystems on the fly for (Map.Entry<String, Set<String>> entry : features.entrySet()) { @@ -141,7 +143,9 @@ public class SubsystemResolver { populateDigraph(digraph, root); Resolver resolver = new ResolverImpl(new Slf4jResolverLog(LOGGER)); - wiring = resolver.resolve(new SubsystemResolveContext(root, digraph)); + Downloader downloader = manager.createDownloader(); + wiring = resolver.resolve(new SubsystemResolveContext(root, digraph, globalRepository, downloader)); + downloader.await(); // Fragments are always wired to their host only, so create fake wiring to // the subsystem the host is wired to http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java ---------------------------------------------------------------------- diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java index bb3c00f..7e42df9 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java @@ -152,6 +152,11 @@ public class FeaturesServiceImpl implements FeaturesService { */ private final String updateSnaphots; + /** + * Optional global repository + */ + private final org.osgi.service.repository.Repository globalRepository; + private final List<FeaturesListener> listeners = new CopyOnWriteArrayIdentityList<FeaturesListener>(); // Synchronized on lock @@ -171,7 +176,8 @@ public class FeaturesServiceImpl implements FeaturesService { String overrides, String featureResolutionRange, String bundleUpdateRange, - String updateSnaphots) { + String updateSnaphots, + org.osgi.service.repository.Repository globalRepository) { this.bundle = bundle; this.systemBundleContext = systemBundleContext; this.storage = storage; @@ -183,6 +189,7 @@ public class FeaturesServiceImpl implements FeaturesService { this.featureResolutionRange = featureResolutionRange; this.bundleUpdateRange = bundleUpdateRange; this.updateSnaphots = updateSnaphots; + this.globalRepository = globalRepository; loadState(); } @@ -700,7 +707,7 @@ public class FeaturesServiceImpl implements FeaturesService { Set<String> fl = required.get(region); if (fl == null) { fl = new HashSet<String>(); - required.put(region,fl); + required.put(region, fl); } List<String> featuresToRemove = new ArrayList<String>(); for (String feature : new HashSet<String>(features)) { @@ -749,7 +756,7 @@ public class FeaturesServiceImpl implements FeaturesService { print(sb.toString(), options.contains(Option.Verbose)); fl.removeAll(featuresToRemove); if (fl.isEmpty()) { - required.remove(fl); + required.remove(region); } doInstallFeaturesInThread(required, state, options); } @@ -872,7 +879,8 @@ public class FeaturesServiceImpl implements FeaturesService { features, unmanagedBundles, Overrides.loadOverrides(this.overrides), - featureResolutionRange); + featureResolutionRange, + globalRepository); Collection<Resource> allResources = resolution.keySet(); Map<String, StreamProvider> providers = resolver.getProviders(); @@ -957,7 +965,7 @@ public class FeaturesServiceImpl implements FeaturesService { List<Wire> newWires = resolution.get(wiring.getRevision()); if (newWires != null) { for (Wire wire : newWires) { - Bundle b = null; + Bundle b; if (wire.getProvider() instanceof BundleRevision) { b = ((BundleRevision) wire.getProvider()).getBundle(); } else { http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java ---------------------------------------------------------------------- diff --git a/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java b/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java index d94d9b7..2609c8d 100644 --- a/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java +++ b/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java @@ -346,7 +346,7 @@ public class FeaturesServiceTest extends TestBase { + " <feature name='f2' version='0.2'><bundle>bundle2</bundle></feature>" + "</features>"); - FeaturesServiceImpl svc = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, null, null, null, null); + FeaturesServiceImpl svc = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, null, null, null, null, null); svc.addRepository(uri); assertEquals(feature("f2", "0.2"), svc.getFeature("f2", "[0.1,0.3)")); @@ -366,7 +366,7 @@ public class FeaturesServiceTest extends TestBase { expect(bundleContext.getBundles()).andReturn(new Bundle[0]); replay(bundleContext); - FeaturesServiceImpl svc = new FeaturesServiceImpl(null, bundleContext, new Storage(), null, null, null, null, null, null, null, null); + FeaturesServiceImpl svc = new FeaturesServiceImpl(null, bundleContext, new Storage(), null, null, null, null, null, null, null, null, null); svc.addRepository(uri); try { List<String> features = new ArrayList<String>(); @@ -391,7 +391,7 @@ public class FeaturesServiceTest extends TestBase { URI uri = createTempRepo("<features name='test' xmlns='http://karaf.apache.org/xmlns/features/v1.0.0'>" + " <featur><bundle>somebundle</bundle></featur></features>"); - FeaturesServiceImpl svc = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, null, null, null, null); + FeaturesServiceImpl svc = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, null, null, null, null, null); try { svc.addRepository(uri); fail("exception expected"); @@ -409,7 +409,7 @@ public class FeaturesServiceTest extends TestBase { + " <feature name='f1'><bundle>file:bundle1</bundle><bundle>file:bundle2</bundle></feature>" + "</features>"); - FeaturesServiceImpl svc = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, null, null, null, null); + FeaturesServiceImpl svc = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, null, null, null, null, null); svc.addRepository(uri); Feature feature = svc.getFeature("f1"); Assert.assertNotNull("No feature named fi found", feature); http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java ---------------------------------------------------------------------- diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java index d64bb65..a027290 100644 --- a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java +++ b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java @@ -66,7 +66,8 @@ public class SubsystemTest { features, Collections.<String, Set<BundleRevision>>emptyMap(), Collections.<String>emptySet(), - FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE); + FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE, + null); verify(resolver, expected); } @@ -96,7 +97,8 @@ public class SubsystemTest { features, Collections.<String, Set<BundleRevision>>emptyMap(), Collections.<String>emptySet(), - FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE); + FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE, + null); verify(resolver, expected); } @@ -116,7 +118,8 @@ public class SubsystemTest { features, Collections.<String, Set<BundleRevision>>emptyMap(), Collections.singleton("b"), - FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE); + FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE, + null); verify(resolver, expected); } http://git-wip-us.apache.org/repos/asf/karaf/blob/bc05096b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java ---------------------------------------------------------------------- diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java index 3627c0f..7f1f358 100644 --- a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java +++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java @@ -49,7 +49,7 @@ public class FeaturesServiceImplTest extends TestBase { public void testGetFeature() throws Exception { Feature transactionFeature = feature("transaction", "1.0.0"); final Map<String, Map<String, Feature>> features = features(transactionFeature); - final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null) { + final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null, null) { protected Map<String,Map<String,Feature>> getFeatures() throws Exception { return features; } @@ -60,7 +60,7 @@ public class FeaturesServiceImplTest extends TestBase { @Test public void testGetFeatureStripVersion() throws Exception { - final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null) { + final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null, null) { protected Map<String,Map<String,Feature>> getFeatures() throws Exception { return features(feature("transaction", "1.0.0")); } @@ -72,7 +72,7 @@ public class FeaturesServiceImplTest extends TestBase { @Test public void testGetFeatureNotAvailable() throws Exception { - final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null) { + final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null, null) { protected Map<String,Map<String,Feature>> getFeatures() throws Exception { return features(feature("transaction", "1.0.0")); } @@ -86,7 +86,7 @@ public class FeaturesServiceImplTest extends TestBase { feature("transaction", "1.0.0"), feature("transaction", "2.0.0") ); - final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null) { + final FeaturesServiceImpl impl = new FeaturesServiceImpl(null, null, new Storage(), null, null, null, null, "", null, null, null, null) { protected Map<String,Map<String,Feature>> getFeatures() throws Exception { return features; }
