private void resolveRoots(ModelTreeNode modelTreeNode) throws IOException {
    ZdoModel model = modelTreeNode.getModel();

    // Get kdr version of this doc
    String kdrUrl = model.get(ZdoTerms.kdrObject);
    ZdoModel kdrDoc = store.get(kdrUrl);

    // If there is also a published version of this doc, find it, remove it and adopt all its
    // children
    String oldModelUrl = kdrDoc.get(ZdoTerms.newestPublished);
    if (oldModelUrl != null) {
      // Mark the old published doc as unpublished
      ZdoModel oldModel = store.get(oldModelUrl);
      if (ZdoGroup.ZDO.name().equals(oldModel.get(ZdoTerms.group))) {
        markAsUnpublished(oldModel, kdrDoc);
        store.update(oldModel);
      }
    }
    resolveChildren(modelTreeNode, oldModelUrl);

    // Set to KDR doc that this will be its published version
    kdrDoc.replaceValueOfProperty(
        ZdoTerms.newestPublished, store.removeTransactionFromUrl(model.getUrl()));
    store.update(kdrDoc);
  }
  private void resolveChildren(ModelTreeNode modelTreeNode, String oldModelUrl) throws IOException {
    ZdoModel model = modelTreeNode.getModel();

    // If there is also a published version of this doc, find it, remove it and adopt all its
    // children
    if (oldModelUrl != null) {
      // Change its children to be ours
      String queryString =
          "SELECT ?subject WHERE {\n"
              + " ?subject <"
              + DCTerms.isPartOf.getURI()
              + "> <"
              + oldModelUrl
              + ">.\n"
              + "}";
      QueryExecution queryExecution =
          QueryExecutionFactory.sparqlService(SPARQL_ENDPOINT, queryString);
      ResultSet resultSet = queryExecution.execSelect();
      while (resultSet.hasNext()) {
        QuerySolution querySolution = resultSet.next();
        RDFNode childNode = querySolution.get("subject");
        String childToAdoptUrl = childNode.asResource().getURI();
        ZdoModel childModel = store.get(childToAdoptUrl);
        childModel.replaceValueOfProperty(
            DCTerms.isPartOf, store.removeTransactionFromUrl(model.getUrl()));

        // If this children was published
        if (ZdoGroup.ZDO.name().equals(childModel.get(ZdoTerms.group))) {
          // Is this children getting replaced by newer version?
          if (modelTreeNodeKdrIndex.containsKey(childModel.get(ZdoTerms.kdrObject))) {
            // Yes, unpublish it
            ZdoModel childKdrObject = store.get(childModel.get(ZdoTerms.kdrObject));
            markAsUnpublished(childModel, childKdrObject);
            store.update(childKdrObject);
          } else { // No, it should be added to our tree to solr
            ModelTreeNode childModelTreeNode = new ModelTreeNode();
            childModelTreeNode.setModel(childModel);
            modelTreeNodeKdrIndex.put(childModel.get(ZdoTerms.kdrObject), childModelTreeNode);
            modelTreeNodeIndex.put(
                store.removeTransactionFromUrl(childModel.getUrl()), modelTreeNode);
            modelTreeNode.getChildren().add(childModelTreeNode);
          }
        }
        store.update(childModel);
      }
    }

    // Recurse on children
    for (ModelTreeNode childNode : modelTreeNode.getChildren()) {
      // Get kdr version of this doc
      ZdoModel kdrDoc = store.get(childNode.getModel().get(ZdoTerms.kdrObject));
      resolveChildren(childNode, kdrDoc.get(ZdoTerms.newestPublished));
      kdrDoc.replaceValueOfProperty(
          ZdoTerms.newestPublished, store.removeTransactionFromUrl(childNode.getModel().getUrl()));
      store.update(kdrDoc);
    }
  }
  private ModelTreeNode addNodeToStructure(ZdoModel model) throws IOException {
    // Was this node already parsed?
    ModelTreeNode modelTreeNode =
        modelTreeNodeIndex.get(store.removeTransactionFromUrl(model.getUrl()));
    if (modelTreeNode != null) {
      return modelTreeNode;
    }
    // Can it be published?
    if (!"true".equals(model.get(ZdoTerms.validToPublish))) {
      throw new RuntimeException("This model is not ready for publishing!");
    }

    // Create tree structure with model for Solr
    modelTreeNode = new ModelTreeNode();
    modelTreeNode.setModel(model);
    modelTreeNodeIndex.put(store.removeTransactionFromUrl(model.getUrl()), modelTreeNode);
    modelTreeNodeKdrIndex.put(model.get(ZdoTerms.kdrObject), modelTreeNode);

    // Parse ancestors and add us as their children
    String parentUrl = model.get(DCTerms.isPartOf);
    if (parentUrl != null) {
      ZdoModel parent = store.get(parentUrl);
      ModelTreeNode parentNode = addNodeToStructure(parent);
      parentNode.getChildren().add(modelTreeNode);
    } else {
      // This is a root
      dataToIndex.add(modelTreeNode);
    }

    // Published periodicals and volumes shouldn't have batch
    if (!ZdoType.isBranchEndCategory(model.get(ZdoTerms.zdoType))) {
      model.removeAllValuesOfProperty(ZdoTerms.batchId);
    }

    // Mark doc as published
    model.replaceValueOfProperty(ZdoTerms.group, ZdoGroup.ZDO.name());
    store.update(model);

    // Leaf children like page and binary don't need to be changed, but images must be tiled
    if (ZdoType.isBranchEndCategory(model.get(ZdoTerms.zdoType))) {
      imagesToTile.addAll(
          triplestoreStuff.fetchUrlsOfTileableImageDescendants(model.get(ZdoTerms.kdrObject)));
    }

    return modelTreeNode;
  }
  // We cant wait for this because fedora transaction times out
  public void process(String url) {
    try {
      if (url == null || outputFolder == null) {
        logger.error("Missing source url or output folder.");
        return;
      }

      File targetFolder =
          new File(outputFolder, dataStore.createDeepPath(dataStore.getOnlyIdFromUrl(url)));
      if (targetFolder.exists()) {
        logger.debug("Image is already tiled, skipping.");
        return;
      }

      ZdoModel metadata = dataStore.get(url + "/fcr:metadata");

      if (metadata == null) {
        throw new IOException("Image metadata does not exist.");
      }

      ImageType type;
      switch (metadata.get(ZdoTerms.mimeType)) {
        case "image/jpeg":
          type = ImageType.JPEG;
          break;
        case "image/jp2":
          type = ImageType.JPEG2000;
          break;
        default:
          throw new IOException("Unsupported image format '{" + url + "}' detected.");
      }

      try (InputStream in = new URL(url).openStream()) {
        TiledImage image = tiler.tileImage(in, type);
        imageWriter.output(image, targetFolder);
      }

    } catch (final IOException e) {
      logger.error("Error processing JMS event!", e);
    }
  }
  @Transactional
  public void publishBatch(int batchId) throws IOException {
    if (userUnderWhichToPublish == null) {
      userUnderWhichToPublish = plAccess.getUser().getLoginName();
    }

    ZdoBatch batch = em.find(ZdoBatch.class, batchId);

    if (batch == null
        || batch.isDeleted() /*todo || !batch.getOwner().equals(identity.getAccount().getId())*/) {
      throw new RuntimeException("Bad batch id to publish.");
    }

    if (!batch.getState().equals(ZdoBatch.BatchState.unfinished)) {
      throw new RuntimeException("Cant publish discarded or published batch.");
    }

    // Statistics collectors
    Map<String, Integer> zdoTypesCount = new HashMap<>();
    Map<String, Integer> documentTypesCount = new HashMap<>();
    Map<String, Integer> documentSubTypesCount = new HashMap<>();

    rootsToDelete = new ArrayList<>();
    imagesToTile = new ArrayList<>();
    modelTreeNodeIndex = new HashMap<>();
    modelTreeNodeKdrIndex = new HashMap<>();
    dataToIndex = new ArrayList<>();
    List<FeedDataHolder> feedData =
        new ArrayList<>(); // Rss and Atom feed data about newly published documents
    // Construct a forest from models that need to be published - bottom to top
    for (String docId : batch.getDocuments()) {
      ZdoModel model = store.get(store.createUrl(docId));

      storeStatistic(model, ZdoTerms.zdoType, zdoTypesCount);

      feedData.add(createFeedData(model));
      addNodeToStructure(model);
    }

    // Store stats - doctypes of root models
    for (ModelTreeNode modelTreeNode : dataToIndex) {
      ZdoModel model = modelTreeNode.getModel();
      if (ZdoType.periodical
          .name()
          .equals(
              model.get(
                  ZdoTerms
                      .zdoType))) { // periodicals are not counted when end branch docs are, so we
        // must count them now
        storeStatistic(model, ZdoTerms.zdoType, zdoTypesCount);
      }
      storeStatistic(model, ZdoTerms.documentType, documentTypesCount);
      storeStatistic(model, ZdoTerms.documentSubType, documentSubTypesCount);
    }

    // Unpublish documents being replaced and adopt their children - top to bottom
    for (ModelTreeNode modelTreeNode : dataToIndex) {
      resolveRoots(modelTreeNode);
    }

    // After all is done in fedora, index it in Solr
    rootsToDelete.forEach(ExWrapper.accept(indexer::remove));
    dataToIndex.forEach(ExWrapper.accept(indexer::update));

    // Aaand, tile the images
    Response response =
        ClientBuilder.newClient()
            .target(IP_ENDPOINT + "process")
            .request()
            .post(Entity.json(imagesToTile));
    if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
      throw new RuntimeException("Failed to call image processing war.");
    }

    batch.setState(ZdoBatch.BatchState.published);
    batch.setModified(LocalDateTime.now());

    // Record statistics
    String org =
        plAccess.getOrganizationOfUser(plAccess.getUser(userUnderWhichToPublish)).getName();
    statsAccess.incrementUserDocsPublished(userUnderWhichToPublish, batch.getNumDocs());
    statsAccess.incrementOrganizationDocsPublished(org, batch.getNumDocs());
    statsAccessCommon.documentPublished(userUnderWhichToPublish, org, batch.getNumDocs());
    for (String zdoTypeStr : zdoTypesCount.keySet()) {
      statsAccess.incrementZdoTypeUsage(
          ZdoType.valueOf(zdoTypeStr), org, zdoTypesCount.get(zdoTypeStr));
    }
    for (String docTypeStr : documentTypesCount.keySet()) {
      statsAccess.incrementDocTypeUsage(
          Integer.valueOf(docTypeStr), org, documentTypesCount.get(docTypeStr));
    }
    for (String docSubTypeStr : documentSubTypesCount.keySet()) {
      statsAccess.incrementDocSubTypeUsage(
          Integer.valueOf(docSubTypeStr), org, documentSubTypesCount.get(docSubTypeStr));
    }

    createFeedEntryAboutBatch(feedData);
  }