You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2015/06/11 08:31:40 UTC

[2/2] karaf git commit: [KARAF-3759] Provide tooling to store a resolution attempt that failed so that it can be replayed offline for analysis

[KARAF-3759] Provide tooling to store a resolution attempt that failed so that it can be replayed offline for analysis


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/9770185c
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/9770185c
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/9770185c

Branch: refs/heads/master
Commit: 9770185c85d0d38d78e3e9d0854582ef8529dd3b
Parents: 947d2eb
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Mon Jun 8 10:25:52 2015 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Jun 10 21:13:35 2015 +0200

----------------------------------------------------------------------
 .../features/command/InstallFeatureCommand.java |   4 +
 features/core/pom.xml                           |   6 +
 .../apache/karaf/features/FeaturesService.java  |   2 +
 .../internal/region/OfflineResolver.java        | 167 +++++++++++++++++++
 .../region/SubsystemResolveContext.java         |   8 +
 .../internal/region/SubsystemResolver.java      |  72 +++++++-
 .../internal/resolver/ResourceBuilder.java      |  83 ++++++++-
 .../features/internal/service/Deployer.java     |   4 +-
 .../internal/service/FeaturesServiceImpl.java   |  25 ++-
 .../features/internal/util/JsonWriter.java      |  15 +-
 .../features/internal/region/SubsystemTest.java |  16 +-
 11 files changed, 380 insertions(+), 22 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/features/command/src/main/java/org/apache/karaf/features/command/InstallFeatureCommand.java
----------------------------------------------------------------------
diff --git a/features/command/src/main/java/org/apache/karaf/features/command/InstallFeatureCommand.java b/features/command/src/main/java/org/apache/karaf/features/command/InstallFeatureCommand.java
index 600310b..571a713 100644
--- a/features/command/src/main/java/org/apache/karaf/features/command/InstallFeatureCommand.java
+++ b/features/command/src/main/java/org/apache/karaf/features/command/InstallFeatureCommand.java
@@ -52,6 +52,9 @@ public class InstallFeatureCommand extends FeaturesCommandSupport {
     @Option(name = "-t", aliases = "--simulate", description = "Perform a simulation only", required = false, multiValued = false)
     boolean simulate;
 
+    @Option(name = "--store", description = "Store the resolution into the given file and result for offline analysis")
+    String outputFile;
+
     @Option(name = "-g", aliases = "--region", description = "Region to install to")
     String region;
 
@@ -61,6 +64,7 @@ public class InstallFeatureCommand extends FeaturesCommandSupport {
         addOption(FeaturesService.Option.NoAutoRefreshBundles, noRefresh);
         addOption(FeaturesService.Option.NoAutoManageBundles, noManage);
         addOption(FeaturesService.Option.Verbose, verbose);
+        admin.setResolutionOutputFile(outputFile);
         admin.installFeatures(new HashSet<String>(features), region, options);
     }
 }

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/features/core/pom.xml
----------------------------------------------------------------------
diff --git a/features/core/pom.xml b/features/core/pom.xml
index d45feb5..767fe12 100644
--- a/features/core/pom.xml
+++ b/features/core/pom.xml
@@ -52,6 +52,11 @@
 
         <dependency>
             <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.resolver</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.utils</artifactId>
             <scope>provided</scope>
         </dependency>
@@ -135,6 +140,7 @@
                             org.eclipse.equinox.region.*
                         </Export-Package>
                         <Import-Package>
+                            !org.apache.felix.resolver,
                             !org.eclipse.osgi.service.resolver,
                             *
                         </Import-Package>

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
index 82d246d..57e7014 100644
--- a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
@@ -83,6 +83,8 @@ public interface FeaturesService {
 
     String getRepositoryName(URI uri) throws Exception;
 
+    void setResolutionOutputFile(String outputFile);
+
     void installFeature(String name) throws Exception;
 
     void installFeature(String name, EnumSet<Option> options) throws Exception;

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/features/core/src/main/java/org/apache/karaf/features/internal/region/OfflineResolver.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/OfflineResolver.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/OfflineResolver.java
new file mode 100644
index 0000000..1c89dc2
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/OfflineResolver.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.karaf.features.internal.region;
+
+import java.io.BufferedReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.resolver.Logger;
+import org.apache.felix.resolver.ResolverImpl;
+import org.apache.karaf.features.internal.repository.BaseRepository;
+import org.apache.karaf.features.internal.resolver.RequirementImpl;
+import org.apache.karaf.features.internal.resolver.ResourceBuilder;
+import org.apache.karaf.features.internal.resolver.ResourceImpl;
+import org.apache.karaf.features.internal.resolver.SimpleFilter;
+import org.apache.karaf.features.internal.util.JsonReader;
+import org.osgi.framework.BundleException;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.resource.Wire;
+import org.osgi.resource.Wiring;
+import org.osgi.service.repository.Repository;
+import org.osgi.service.resolver.HostedCapability;
+import org.osgi.service.resolver.ResolveContext;
+import org.osgi.service.resolver.Resolver;
+
+import static org.osgi.framework.Constants.RESOLUTION_DIRECTIVE;
+import static org.osgi.framework.Constants.RESOLUTION_OPTIONAL;
+import static org.osgi.framework.namespace.IdentityNamespace.IDENTITY_NAMESPACE;
+
+public class OfflineResolver {
+
+    public static void main(String[] args) throws Exception {
+        if (args == null || args.length != 1) {
+            throw new IllegalArgumentException("File path expected");
+        }
+        resolve(args[0]);
+    }
+
+    public static void resolve(String resolutionFile) throws Exception {
+        Map<String, Object> resolution;
+        try (BufferedReader reader = Files.newBufferedReader(Paths.get(resolutionFile), StandardCharsets.UTF_8)) {
+            resolution = (Map<String, Object>) JsonReader.read(reader);
+        }
+
+        final Repository globalRepository;
+        if (resolution.containsKey("globalRepository")) {
+            globalRepository = readRepository(resolution.get("globalRepository"));
+        } else {
+            globalRepository = null;
+        }
+        final Repository repository = readRepository(resolution.get("repository"));
+
+        Resolver resolver = new ResolverImpl(new Logger(Logger.LOG_ERROR));
+        Map<Resource, List<Wire>> wiring = resolver.resolve(new ResolveContext() {
+            private final Set<Resource> mandatory = new HashSet<>();
+            private final CandidateComparator candidateComparator = new CandidateComparator(mandatory);
+
+            @Override
+            public Collection<Resource> getMandatoryResources() {
+                List<Resource> resources = new ArrayList<Resource>();
+                Requirement req = new RequirementImpl(
+                        null,
+                        IDENTITY_NAMESPACE,
+                        Collections.<String, String>emptyMap(),
+                        Collections.<String, Object>emptyMap(),
+                        SimpleFilter.parse("(" + IDENTITY_NAMESPACE + "=root)"));
+                Collection<Capability> identities = repository.findProviders(Collections.singleton(req)).get(req);
+                for (Capability identity : identities) {
+                    resources.add(identity.getResource());
+                }
+                return resources;
+            }
+
+            @Override
+            public List<Capability> findProviders(Requirement requirement) {
+                List<Capability> caps = new ArrayList<>();
+                Map<Requirement, Collection<Capability>> resMap =
+                        repository.findProviders(Collections.singleton(requirement));
+                Collection<Capability> res = resMap != null ? resMap.get(requirement) : null;
+                if (res != null && !res.isEmpty()) {
+                    caps.addAll(res);
+                } else if (globalRepository != null) {
+                    // Only bring in external resources for non optional requirements
+                    if (!RESOLUTION_OPTIONAL.equals(requirement.getDirectives().get(RESOLUTION_DIRECTIVE))) {
+                        resMap = globalRepository.findProviders(Collections.singleton(requirement));
+                        res = resMap != null ? resMap.get(requirement) : null;
+                        if (res != null && !res.isEmpty()) {
+                            caps.addAll(res);
+                        }
+                    }
+                }
+
+                // Sort caps
+                Collections.sort(caps, candidateComparator);
+                return caps;
+            }
+
+            @Override
+            public int insertHostedCapability(List<Capability> capabilities, HostedCapability hostedCapability) {
+                int idx = Collections.binarySearch(capabilities, hostedCapability, candidateComparator);
+                if (idx < 0) {
+                    idx = Math.abs(idx + 1);
+                }
+                capabilities.add(idx, hostedCapability);
+                return idx;
+            }
+
+            @Override
+            public boolean isEffective(Requirement requirement) {
+                return true;
+            }
+
+            @Override
+            public Map<Resource, Wiring> getWirings() {
+                return Collections.emptyMap();
+            }
+        });
+    }
+
+    private static Repository readRepository(Object repository) throws BundleException {
+        List<Resource> resources = new ArrayList<>();
+        Collection<Map<String, List<String>>> metadatas;
+        if (repository instanceof Map) {
+            metadatas = ((Map<String, Map<String, List<String>>>) repository).values();
+        } else {
+            metadatas = (Collection<Map<String, List<String>>>) repository;
+        }
+        for (Map<String, List<String>> metadata : metadatas) {
+            ResourceImpl res = new ResourceImpl();
+            for (String cap : metadata.get("capabilities")) {
+                res.addCapabilities(ResourceBuilder.parseCapability(res, cap));
+            }
+            if (metadata.containsKey("requirements")) {
+                for (String req : metadata.get("requirements")) {
+                    res.addRequirements(ResourceBuilder.parseRequirement(res, req));
+                }
+            }
+            resources.add(res);
+        }
+        return new BaseRepository(resources);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/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 fdf9016..de990f4 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
@@ -87,6 +87,14 @@ public class SubsystemResolveContext extends ResolveContext {
         findMandatory(root);
     }
 
+    public Repository getRepository() {
+        return repository;
+    }
+
+    public Repository getGlobalRepository() {
+        return globalRepository;
+    }
+
     void findMandatory(Resource res) {
         if (mandatory.add(res)) {
             for (Requirement req : res.getRequirements(null)) {

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/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 0402c12..d3701c7 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
@@ -16,6 +16,12 @@
  */
 package org.apache.karaf.features.internal.region;
 
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -30,12 +36,16 @@ import org.apache.karaf.features.Feature;
 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.resolver.BaseClause;
 import org.apache.karaf.features.internal.resolver.CapabilityImpl;
 import org.apache.karaf.features.internal.resolver.CapabilitySet;
+import org.apache.karaf.features.internal.resolver.RequirementImpl;
 import org.apache.karaf.features.internal.resolver.ResolverUtil;
 import org.apache.karaf.features.internal.resolver.ResourceBuilder;
 import org.apache.karaf.features.internal.resolver.ResourceImpl;
+import org.apache.karaf.features.internal.resolver.ResourceUtils;
 import org.apache.karaf.features.internal.resolver.SimpleFilter;
+import org.apache.karaf.features.internal.util.JsonWriter;
 import org.eclipse.equinox.internal.region.StandardRegionDigraph;
 import org.eclipse.equinox.region.Region;
 import org.eclipse.equinox.region.RegionDigraph;
@@ -49,6 +59,7 @@ import org.osgi.resource.Capability;
 import org.osgi.resource.Requirement;
 import org.osgi.resource.Resource;
 import org.osgi.resource.Wire;
+import org.osgi.service.repository.Repository;
 import org.osgi.service.resolver.Resolver;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -168,8 +179,9 @@ public class SubsystemResolver {
     public Map<Resource, List<Wire>> resolve(
             Set<String> overrides,
             String featureResolutionRange,
-            final org.osgi.service.repository.Repository globalRepository
-    ) throws Exception {
+            final Repository globalRepository,
+            String outputFile) throws Exception {
+
         if (root == null) {
             return Collections.emptyMap();
         }
@@ -182,7 +194,30 @@ public class SubsystemResolver {
         populateDigraph(digraph, root);
 
         Downloader downloader = manager.createDownloader();
-        wiring = resolver.resolve(new SubsystemResolveContext(root, digraph, globalRepository, downloader));
+        SubsystemResolveContext context = new SubsystemResolveContext(root, digraph, globalRepository, downloader);
+        if (outputFile != null) {
+            Map<String, Object> json = new HashMap<>();
+            if (globalRepository != null) {
+                json.put("globalRepository", toJson(globalRepository));
+            }
+            json.put("repository", toJson(context.getRepository()));
+            try {
+                wiring = resolver.resolve(context);
+                json.put("success", "true");
+            } catch (Exception e) {
+                json.put("exception", e.toString());
+                throw e;
+            } finally {
+                try (Writer writer = Files.newBufferedWriter(
+                        Paths.get(outputFile),
+                        StandardCharsets.UTF_8,
+                        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
+                    JsonWriter.write(writer, json);
+                }
+            }
+        } else {
+            wiring = resolver.resolve(context);
+        }
         downloader.await();
 
         // Remove wiring to the fake environment resource
@@ -203,6 +238,37 @@ public class SubsystemResolver {
         return wiring;
     }
 
+    private Object toJson(Repository repository) {
+        Requirement req = new RequirementImpl(
+                null,
+                IDENTITY_NAMESPACE,
+                Collections.<String, String>emptyMap(),
+                Collections.<String, Object>emptyMap(),
+                new SimpleFilter(null, null, SimpleFilter.MATCH_ALL));
+        Collection<Capability> identities = repository.findProviders(Collections.singleton(req)).get(req);
+        List<Object> resources = new ArrayList<>();
+        for (Capability identity : identities) {
+            String id = BaseClause.toString(null, identity.getNamespace(), identity.getAttributes(), identity.getDirectives());
+            resources.add(toJson(identity.getResource()));
+        }
+        return resources;
+    }
+
+    private Object toJson(Resource resource) {
+        Map<String, Object> obj = new HashMap<>();
+        List<Object> caps = new ArrayList<>();
+        List<Object> reqs = new ArrayList<>();
+        for (Capability cap : resource.getCapabilities(null)) {
+            caps.add(BaseClause.toString(null, cap.getNamespace(), cap.getAttributes(), cap.getDirectives()));
+        }
+        for (Requirement req : resource.getRequirements(null)) {
+            reqs.add(BaseClause.toString(null, req.getNamespace(), req.getAttributes(), req.getDirectives()));
+        }
+        obj.put("capabilities", caps);
+        obj.put("requirements", reqs);
+        return obj;
+    }
+
     public Map<String, Map<String, BundleInfo>> getBundleInfos() {
         if (bundleInfos == null) {
             bundleInfos = new HashMap<>();

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
index 47a0661..0dbf4e2 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
@@ -455,6 +455,87 @@ public final class ResourceBuilder {
     private static List<ParsedHeaderClause> normalizeRequireCapabilityClauses(
             List<ParsedHeaderClause> clauses) throws BundleException {
 
+        // Convert attributes into specified types.
+        for (ParsedHeaderClause clause : clauses) {
+            for (Map.Entry<String, Object> entry : clause.attrs.entrySet()) {
+                if (entry.getKey().equals("version")) {
+                    clause.attrs.put(entry.getKey(), new VersionRange(entry.getValue().toString()));
+                }
+            }
+            for (Map.Entry<String, String> entry : clause.types.entrySet()) {
+                String type = entry.getValue();
+                if (!type.equals("String")) {
+                    if (type.equals("Double")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Double(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.equals("Version")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Version(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.equals("Long")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Long(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.startsWith("List")) {
+                        int startIdx = type.indexOf('<');
+                        int endIdx = type.indexOf('>');
+                        if (((startIdx > 0) && (endIdx <= startIdx))
+                                || ((startIdx < 0) && (endIdx > 0))) {
+                            throw new BundleException(
+                                    "Invalid Provide-Capability attribute list type for '"
+                                            + entry.getKey()
+                                            + "' : "
+                                            + type
+                            );
+                        }
+
+                        String listType = "String";
+                        if (endIdx > startIdx) {
+                            listType = type.substring(startIdx + 1, endIdx).trim();
+                        }
+
+                        List<String> tokens = parseDelimitedString(
+                                clause.attrs.get(entry.getKey()).toString(), ",", false);
+                        List<Object> values = new ArrayList<>(tokens.size());
+                        for (String token : tokens) {
+                            switch (listType) {
+                            case "String":
+                                values.add(token);
+                                break;
+                            case "Double":
+                                values.add(new Double(token.trim()));
+                                break;
+                            case "Version":
+                                values.add(new Version(token.trim()));
+                                break;
+                            case "Long":
+                                values.add(new Long(token.trim()));
+                                break;
+                            default:
+                                throw new BundleException(
+                                        "Unknown Provide-Capability attribute list type for '"
+                                                + entry.getKey()
+                                                + "' : "
+                                                + type
+                                );
+                            }
+                        }
+                        clause.attrs.put(
+                                entry.getKey(),
+                                values);
+                    } else {
+                        throw new BundleException(
+                                "Unknown Provide-Capability attribute type for '"
+                                        + entry.getKey()
+                                        + "' : "
+                                        + type
+                        );
+                    }
+                }
+            }
+        }
+
         return clauses;
     }
 
@@ -550,7 +631,7 @@ public final class ResourceBuilder {
                 String filterStr = clause.dirs.get(Constants.FILTER_DIRECTIVE);
                 SimpleFilter sf = (filterStr != null)
                         ? SimpleFilter.parse(filterStr)
-                        : new SimpleFilter(null, null, SimpleFilter.MATCH_ALL);
+                        : SimpleFilter.convert(clause.attrs);
                 for (String path : clause.paths) {
                     // Create requirement and add to requirement list.
                     reqList.add(new RequirementImpl(

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
index e1d4354..e6bc258 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
@@ -163,6 +163,7 @@ public class Deployer {
         public Map<String, Set<String>> requirements;
         public Map<String, Map<String, FeatureState>> stateChanges;
         public EnumSet<FeaturesService.Option> options;
+        public String outputFile;
     }
 
     static class Deployment {
@@ -261,7 +262,8 @@ public class Deployer {
         resolver.resolve(
                 request.overrides,
                 request.featureResolutionRange,
-                request.globalRepository);
+                request.globalRepository,
+                request.outputFile);
 
         Map<String, StreamProvider> providers = resolver.getProviders();
         Map<String, Set<Resource>> featuresPerRegion = resolver.getFeaturesPerRegions();

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/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 b003331..4dcd267 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
@@ -160,6 +160,8 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
 
     private final String blacklisted;
 
+    private final ThreadLocal<String> outputFile = new ThreadLocal<>();
+
     /**
      * Optional global repository
      */
@@ -799,6 +801,11 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
 
 
     @Override
+    public void setResolutionOutputFile(String outputFile) {
+        this.outputFile.set(outputFile);
+    }
+
+    @Override
     public void installFeatures(Set<String> features, String region, EnumSet<Option> options) throws Exception {
         State state = copyState();
         Map<String, Set<String>> required = copy(state.requirements);
@@ -954,10 +961,12 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
                                     final EnumSet<Option> options) throws Exception {
         ExecutorService executor = Executors.newCachedThreadPool();
         try {
+            final String outputFile = this.outputFile.get();
+            this.outputFile.set(null);
             executor.submit(new Callable<Object>() {
                 @Override
                 public Object call() throws Exception {
-                    doProvision(requirements, stateChanges, state, options);
+                    doProvision(requirements, stateChanges, state, options, outputFile);
                     return null;
                 }
             }).get();
@@ -1026,7 +1035,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         return dstate;
     }
 
-    private Deployer.DeploymentRequest getDeploymentRequest(Map<String, Set<String>> requirements, Map<String, Map<String, FeatureState>> stateChanges, EnumSet<Option> options) {
+    private Deployer.DeploymentRequest getDeploymentRequest(Map<String, Set<String>> requirements, Map<String, Map<String, FeatureState>> stateChanges, EnumSet<Option> options, String outputFile) {
         Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
         request.bundleUpdateRange = bundleUpdateRange;
         request.featureResolutionRange = featureResolutionRange;
@@ -1036,15 +1045,17 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         request.requirements = requirements;
         request.stateChanges = stateChanges;
         request.options = options;
+        request.outputFile = outputFile;
         return request;
     }
 
 
 
-    public void doProvision(Map<String, Set<String>> requirements,                 // all requirements
-                            Map<String, Map<String, FeatureState>> stateChanges, // features state changes
-                            State state,                                           // current state
-                            EnumSet<Option> options                                // installation options
+    public void doProvision(Map<String, Set<String>> requirements,                // all requirements
+                            Map<String, Map<String, FeatureState>> stateChanges,  // features state changes
+                            State state,                                          // current state
+                            EnumSet<Option> options,                              // installation options
+                            String outputFile                                     // file to store the resolution or null
     ) throws Exception {
 
         Dictionary<String, String> props = getMavenConfig();
@@ -1057,7 +1068,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
             while (true) {
                 try {
                     Deployer.DeploymentState dstate = getDeploymentState(state);
-                    Deployer.DeploymentRequest request = getDeploymentRequest(requirements, stateChanges, options);
+                    Deployer.DeploymentRequest request = getDeploymentRequest(requirements, stateChanges, options, outputFile);
                     new Deployer(manager, this.resolver, this).deploy(dstate, request);
                     break;
                 } catch (Deployer.PartialDeploymentException e) {

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/features/core/src/main/java/org/apache/karaf/features/internal/util/JsonWriter.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/util/JsonWriter.java b/features/core/src/main/java/org/apache/karaf/features/internal/util/JsonWriter.java
index d65bc35..fb27346 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/util/JsonWriter.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/util/JsonWriter.java
@@ -76,14 +76,25 @@ public final class JsonWriter {
             char c = value.charAt(i);
             switch (c) {
             case '\"':
+                writer.append("\\\"");
+                break;
             case '\\':
+                writer.append("\\\\");
+                break;
             case '\b':
+                writer.append("\\b");
+                break;
             case '\f':
+                writer.append("\\f");
+                break;
             case '\n':
+                writer.append("\\n");
+                break;
             case '\r':
+                writer.append("\\r");
+                break;
             case '\t':
-                writer.append('\\');
-                writer.append(c);
+                writer.append("\\t");
                 break;
             default:
                 if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) {

http://git-wip-us.apache.org/repos/asf/karaf/blob/9770185c/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 965b5f6..846a2c9 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
@@ -67,7 +67,7 @@ public class SubsystemTest {
                          Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.<String>emptySet(),
                          FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                         null);
+                         null, null);
 
         verify(resolver, expected);
     }
@@ -98,7 +98,7 @@ public class SubsystemTest {
                          Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.<String>emptySet(),
                          FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                         null);
+                         null, null);
 
         verify(resolver, expected);
     }
@@ -119,7 +119,7 @@ public class SubsystemTest {
                          Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.singleton("b"),
                          FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                         null);
+                         null, null);
 
         verify(resolver, expected);
     }
@@ -139,7 +139,7 @@ public class SubsystemTest {
                          Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.<String>emptySet(),
                          FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                         null);
+                         null, null);
 
         verify(resolver, expected);
     }
@@ -161,7 +161,7 @@ public class SubsystemTest {
                          Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.<String>emptySet(),
                          FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                         null);
+                         null, null);
 
         verify(resolver, expected);
     }
@@ -183,7 +183,7 @@ public class SubsystemTest {
                 Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.<String>emptySet(),
                 FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                null);
+                null, null);
 
         verify(resolver, expected);
     }
@@ -204,7 +204,7 @@ public class SubsystemTest {
                 Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.<String>emptySet(),
                 FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                null);
+                null, null);
 
         verify(resolver, expected);
     }
@@ -226,7 +226,7 @@ public class SubsystemTest {
                 Collections.<String, Set<BundleRevision>>emptyMap());
         resolver.resolve(Collections.<String>emptySet(),
                 FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
-                null);
+                null, null);
 
         verify(resolver, expected);
     }