class ChecklistExporter {

    public void updateChecklist(final WriterOpener writerOpener,
                                final Session session,
                                final Checklist checklist,
                                final ForcedRootUpdatePolicy updatePolicy)
            throws IOException, RepositoryException {

        final List<JcrNs> chkNs = new ArrayList<>();
        // first attempt to remap JCR namespaces in the session, if necessary.
        if (checklist != null && checklist.getJcrNamespaces() != null) {
            chkNs.addAll(checklist.getJcrNamespaces());
        }

        final NamespaceMapping origMapping = JsonCnd.toNamespaceMapping(chkNs);
        final NamespaceMapping remapping = JsonCnd.toNamespaceMapping(jcrNamespaces);

        ensureNamespaces(session, origMapping);
        ensureNamespaces(session, remapping);

        // try to find the roots. If any error occurs there, we want to fail fast before committing to other
        // potentially expensive, destructive, or error-prone logic.
        final List<ForcedRoot> newRoots = findRoots(session);

        // construct a stream filter for retaining existing forced roots
        Predicate<ForcedRoot> retainFilter = getRetainFilter(updatePolicy);

        final JsonObjectBuilder builder = Json.createObjectBuilder();
        final Map<String, ForcedRoot> existing = new LinkedHashMap<>();
        final List<PrivilegeDefinition> privileges = new ArrayList<>();

        // remap the names of existing jcr definitions to match the new jcr namespaces
        if (checklist != null) {
            checklist.toJson().forEach(builder::add);

            privileges.addAll(checklist.getJcrPrivileges());

            checklist.getForcedRoots().stream()
                    .map(nsRemapForcedRoot(origMapping, remapping))
                    .filter(retainFilter)
                    .forEachOrdered(root -> existing.put(root.getPath(), root));
        }

        //final Set<String> finalPrefixes = new HashSet<>();
        final NamespaceMappingRequest.Builder request = new NamespaceMappingRequest.Builder();
        if (!privileges.isEmpty()) {
            builder.add(Checklist.keys().jcrPrivileges(), JsonCnd.privilegesToJson(privileges, origMapping));
            privileges.stream().flatMap(JsonCnd::namedBy).forEach(request::withQName);
        }

        newRoots.forEach(root -> existing.put(root.getPath(), root));

        final List<ForcedRoot> forcedRoots = new ArrayList<>(existing.values());
        Collections.sort(forcedRoots);

        final JsonArray forcedRootsJson = forcedRoots.stream()
                .map(ForcedRoot::toJson)
                .collect(JsonCollectors.toJsonArray());

        builder.add(Checklist.keys().forcedRoots(), forcedRootsJson);

        // begin nodetype handling

        final NamePathResolver resolver = new DefaultNamePathResolver(session);
        final Set<Name> builtinNodetypes = CndExporter.BUILTIN_NODETYPES.stream()
                .map(uncheck1(resolver::getQName))
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());

        final List<Name> foundNodeTypes = forcedRoots.stream()
                .reduce(new HashSet<>(),
                        ChecklistExporter::findNodeTypesInForcedRoot,
                        addToLeft()).stream()
                .map(uncheck1(resolver::getQName))
                .collect(Collectors.toList());

        final Map<Name, NodeTypeDefinition> exportedNodeTypes =
                CndExporter.retrieveNodeTypes(session, foundNodeTypes, exportTypeDefSelector());
        final List<QNodeTypeDefinition> qNodeTypes = exportedNodeTypes.entrySet().stream()
                .map(mapValue(JsonCnd.adaptToQ(session)))
                .filter(testKey(((Predicate<Name>) builtinNodetypes::contains).negate()))
                .map(Map.Entry::getValue)
                .collect(Collectors.toList());

        final NamespaceMapping spm = new NamespaceMapping(new SessionNamespaceResolver(session));

        if (!qNodeTypes.isEmpty()) {
            final Set<String> nsUris = qNodeTypes.stream()
                    .flatMap(JsonCnd::namedBy)
                    .map(Name::getNamespaceURI)
                    .collect(Collectors.toSet());

            final Map<String, String> uriMapping = remapping.getURIToPrefixMapping();
            nsUris.forEach(Fun.uncheckVoid1(uri -> {
                final String prefix = uriMapping.containsKey(uri) ? uriMapping.get(uri) : spm.getPrefix(uri);
                request.withRetainPrefix(prefix);
                spm.setMapping(prefix, uri);
            }));

            final JsonObject jcrNodetypes =
                    JsonCnd.toJson(qNodeTypes, new NamespaceMapping(new SessionNamespaceResolver(session)));
            builder.add(Checklist.keys().jcrNodetypes(), jcrNodetypes);
        }

        // begin namespace handling
        final Set<String> forcedRootPrefixes = forcedRoots.stream()
                .reduce(new HashSet<>(),
                        ChecklistExporter::findJcrPrefixesInForcedRoot,
                        addToLeft());
        forcedRootPrefixes.forEach(request::withRetainPrefix);

        final List<JcrNs> exportNamespaces = request.build()
                .resolveToJcrNs(spm).stream()
                .flatMap(Result::stream)
                .collect(Collectors.toList());
        if (!exportNamespaces.isEmpty()) {
            builder.add(Checklist.keys().jcrNamespaces(), JavaxJson.wrap(exportNamespaces));
        }

        final JsonObject sorted = builder.build().entrySet().stream()
                .sorted(Checklist.comparingJsonKeys(Map.Entry::getKey))
                .collect(JsonCollectors.toJsonObject());

        try (Writer writer = writerOpener.open();
             JsonWriter jsonWriter = Json
                     .createWriterFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true))
                     .createWriter(writer)) {
            jsonWriter.writeObject(sorted);
        }
    }

    private  ChecklistExporter(final List<Op> operations,
                              final List<String> exportTypeDefs,
                              final List<Rule> pathScopes,
                              final List<Rule> nodeTypeFilters,
                              final List<JcrNs> jcrNamespaces);

}