void exportSyncRecords(PrismsCenter center, DBRecordKeeper keeper, JsonStreamWriter jsw)
     throws java.io.IOException, PrismsRecordException {
   jsw.startObject();
   jsw.startProperty("id");
   jsw.writeNumber(Integer.valueOf(center.getID()));
   jsw.startProperty("centerID");
   jsw.writeNumber(Integer.valueOf(center.getCenterID()));
   jsw.startProperty("syncRecords");
   jsw.startArray();
   for (SyncRecord record : keeper.getSyncRecords(center, null)) {
     jsw.startObject();
     jsw.startProperty("id");
     jsw.writeNumber(Integer.valueOf(record.getID()));
     jsw.startProperty("parallelID");
     jsw.writeNumber(Integer.valueOf(record.getParallelID()));
     jsw.startProperty("syncType");
     jsw.writeString(record.getSyncType().toString());
     jsw.startProperty("time");
     jsw.writeNumber(Long.valueOf(record.getSyncTime()));
     jsw.startProperty("isImport");
     jsw.writeBoolean(record.isImport());
     jsw.startProperty("syncError");
     jsw.writeString(record.getSyncError());
     jsw.startProperty("associated");
     jsw.startArray();
     for (long assoc : keeper.getChangeIDs(null, null, null, null, record, null)) {
       jsw.startObject();
       jsw.startProperty("id");
       jsw.writeNumber(Long.valueOf(assoc));
       jsw.startProperty("error");
       jsw.writeBoolean(!keeper.hasSuccessfulChange(assoc));
       jsw.endObject();
     }
     jsw.endArray();
     jsw.endObject();
   }
   jsw.endArray();
   jsw.endObject();
 }
    void importData(
        PrismsApplication app,
        PrismsSynchronizer sync,
        String version,
        JsonSerialReader jsr,
        StringBuilder message)
        throws java.io.IOException, SAJParser.ParseException {
      SyncRecord syncRecord =
          new SyncRecord(
              new PrismsCenter("Export"),
              SyncRecord.Type.AUTOMATIC,
              System.currentTimeMillis(),
              false);
      PrismsSynchronizer.SyncTransaction trans =
          sync.transact(
              version, true, syncRecord, false, new prisms.ui.UI.DefaultProgressInformer());

      // Items
      PrismsSynchronizer.PS2ItemReader itemReader = new PrismsSynchronizer.PS2ItemReader(trans);
      if (!jsr.goToProperty("items")) {
        message.append("\n\t\tNo items property in sync data");
        return;
      }
      int items = 0;
      org.json.simple.JSONObject obj;
      jsr.startArray();
      obj = jsr.parseObject();
      while (obj != null) {
        items++;
        try {
          itemReader.read(obj);
        } catch (PrismsRecordException e) {
          log.error("Error parsing item", e);
        }
        obj = jsr.parseObject();
      }
      jsr.endArray(null);
      message.append("\n\t\t").append(items).append(" items");

      PrismsCenter[] centers;
      try {
        centers = sync.getKeeper().getCenters();
      } catch (PrismsRecordException e) {
        message.append("\n\t\t").append("Could not get centers");
        log.error("Could not get centers", e);
        centers = null;
      }
      if (centers != null && trans.getImpl().getCentersProperty() != null) {
        PrismsCenter[] appCenters = centers;
        for (int i = 0; i < appCenters.length; i++)
          if (centers[i].getID() == 0) {
            // Don't include the "Here" center in the UI value
            appCenters = prisms.util.ArrayUtils.remove(appCenters, i);
            break;
          }
        app.setGlobalProperty(
            trans.getImpl().getCentersProperty(),
            appCenters,
            RecordUtils.TRANSACTION_EVENT_PROP,
            new prisms.records.RecordsTransaction());
      }

      // Changes
      PrismsSynchronizer.ChangeReader changeReader =
          new PrismsSynchronizer.ChangeReader(trans, null, itemReader, 0);
      if (!jsr.goToProperty("changes")) {
        message.append("\n\t\tNo changes property in sync data");
        return;
      }
      int changes = 0;
      jsr.startArray();
      obj = jsr.parseObject();
      while (obj != null) {
        changes++;
        changeReader.readChange(obj);
        obj = jsr.parseObject();
      }
      jsr.endArray(null);
      message.append(", ").append(changes).append(" changes");
      changeReader = null;
      itemReader = null;

      // Sync records
      if (centers != null) {
        if (jsr.goToProperty("syncRecords")) {
          int syncRecords = 0;
          jsr.startArray();
          while (jsr.getNextItem(true, false) instanceof JsonSerialReader.ObjectItem) {
            JsonSerialReader.StructState centerState = jsr.save();
            try {
              if (!jsr.goToProperty("id")) {
                message.append("\n\t\tID missing in sync records group");
                break;
              }
              int centerID = jsr.parseInt();
              PrismsCenter center = null;
              for (PrismsCenter c : centers)
                if (c.getID() == centerID) {
                  center = c;
                  break;
                }
              if (center == null) {
                log.warn("No such center with ID " + centerID);
                continue;
              }
              if (!jsr.goToProperty("syncRecords")) {
                message.append("\n\t\tsyncRecords missing in sync records group");
                continue;
              }
              jsr.startArray();
              while (jsr.getNextItem(true, false) instanceof JsonSerialReader.ObjectItem) {
                JsonSerialReader.StructState srState = jsr.save();
                try {
                  if (!jsr.goToProperty("id")) {
                    log.warn("No id property in sync record");
                    continue;
                  }
                  int srID = jsr.parseInt();
                  if (!jsr.goToProperty("parallelID")) {
                    log.warn("No parallelID property in sync record");
                    continue;
                  }
                  int parallelID = jsr.parseInt();
                  if (!jsr.goToProperty("syncType")) {
                    log.warn("No syncType property in sync record");
                    continue;
                  }
                  String typeName = jsr.parseString();
                  SyncRecord.Type type = SyncRecord.Type.byName(typeName);
                  if (type == null) {
                    log.warn("Unrecognized sync type in sync record: " + typeName);
                    continue;
                  }
                  if (!jsr.goToProperty("time")) {
                    log.warn("No time property in sync record");
                    continue;
                  }
                  long time = jsr.parseLong();
                  if (!jsr.goToProperty("isImport")) {
                    log.warn("No isImport property in sync record");
                    continue;
                  }
                  boolean isImport = jsr.parseBoolean();
                  if (!jsr.goToProperty("syncError")) {
                    log.warn("No syncError property in sync record");
                    continue;
                  }
                  Object syncError = jsr.parseNext(false);
                  SyncRecord sr = new SyncRecord(center, type, time, isImport);
                  sr.setID(srID);
                  sr.setParallelID(parallelID);
                  if (syncError instanceof String) sr.setSyncError((String) syncError);
                  else if (syncError == JsonSerialReader.NULL) sr.setSyncError(null);
                  else String.class.cast(syncError);
                  try {
                    sync.getKeeper().putSyncRecord(sr);
                  } catch (PrismsRecordException e) {
                    log.error("Could not persist sync record", e);
                    continue;
                  }
                  syncRecords++;
                  if (!jsr.goToProperty("associated")) {
                    log.warn("No associated property in sync record");
                    continue;
                  }
                  prisms.util.LongList assocIDs = new prisms.util.LongList();
                  prisms.util.BooleanList assocErrors = new prisms.util.BooleanList();
                  jsr.startArray();
                  while (jsr.getNextItem(true, false) instanceof JsonSerialReader.ObjectItem) {
                    if (!jsr.goToProperty("id")) {
                      log.warn("No id for associated change");
                      jsr.endObject(null);
                      continue;
                    }
                    assocIDs.add(jsr.parseLong());
                    if (jsr.goToProperty("error")) assocErrors.add(jsr.parseBoolean());
                    else assocErrors.add(false);
                    jsr.endObject(null);
                  }
                  jsr.endArray(null);
                  ChangeRecord[] assocChanges;
                  try {
                    assocChanges = sync.getKeeper().getItems(assocIDs.toArray());
                  } catch (PrismsRecordException e) {
                    log.error("Could not get associated changes", e);
                    continue;
                  }
                  for (int i = 0; i < assocChanges.length; i++) {
                    if (assocChanges[i] != null)
                      try {
                        sync.getKeeper().associate(assocChanges[i], sr, assocErrors.get(i));
                      } catch (PrismsRecordException e) {
                        log.error("Could not associated change", e);
                      }
                  }
                } finally {
                  jsr.endObject(srState);
                }
              }
              jsr.endArray(null);
            } finally {
              jsr.endObject(centerState);
            }
          }
          jsr.endArray(null);
          message.append(", ").append(syncRecords).append(" sync records");
        }
      }

      // Latest changes
      if (jsr.goToProperty("latestChanges")) {
        JsonSerialReader.StructState rootState = jsr.startArray();
        while (jsr.getNextItem(true, false) instanceof JsonSerialReader.ObjectItem) {
          JsonSerialReader.StructState lcState = jsr.save();
          try {
            if (!jsr.goToProperty("center")) {
              log.warn("No center for latest change entry");
              continue;
            }
            int centerID = jsr.parseInt();
            if (!jsr.goToProperty("subjectCenter")) {
              log.warn("No subjectCenter for latest change entry");
              continue;
            }
            int subjectCenter = jsr.parseInt();
            if (!jsr.goToProperty("latestChange")) {
              log.warn("No latestChange for latest change entry");
              continue;
            }
            long changeTime = jsr.parseLong();
            try {
              sync.getKeeper().setLatestChange(centerID, subjectCenter, changeTime);
            } catch (PrismsRecordException e) {
              log.error("Could not set latest change", e);
            }
          } finally {
            jsr.endObject(lcState);
          }
        }
        jsr.endArray(rootState);
      }
    }
    void exportData(
        prisms.ui.UI ui,
        PrismsSynchronizer sync,
        JsonStreamWriter jsw,
        java.util.HashSet<String> namespaces,
        prisms.ui.UI.DefaultProgressInformer pi,
        boolean global)
        throws java.io.IOException, PrismsRecordException {
      DBRecordKeeper keeper = (DBRecordKeeper) sync.getKeeper();
      String namespace = keeper.getNamespace();
      if (namespaces.contains(namespace)) return;
      namespaces.add(namespace);
      for (PrismsSynchronizer depend : sync.getDepends())
        exportData(ui, depend, jsw, namespaces, pi, global);
      jsw.startObject();
      jsw.startProperty("namespace");
      jsw.writeString(namespace);
      jsw.startProperty("version");
      String v = null;
      SynchronizeImpl impl = null;
      for (SynchronizeImpl imp : sync.getImpls())
        if (prisms.arch.PrismsConfig.compareVersions(imp.getVersion(), v) > 0) {
          impl = imp;
          v = impl.getVersion();
        }

      jsw.writeString(v);
      SyncRecord syncRecord =
          new SyncRecord(
              new PrismsCenter("Export"),
              SyncRecord.Type.AUTOMATIC,
              System.currentTimeMillis(),
              false);
      pi.setProgressText("Exporting " + namespace + " items");
      ui.startTimedTask(pi);
      PrismsSynchronizer.SyncTransaction syncTrans = sync.transact(v, false, syncRecord, false, pi);
      PrismsSynchronizer.PS2ItemWriter itemWriter =
          new PrismsSynchronizer.PS2ItemWriter(
              syncTrans, jsw, new prisms.records.LatestCenterChange[0]);

      int[] centerIDs = global ? keeper.getAllCenterIDs() : new int[] {keeper.getCenterID()};
      SynchronizeImpl.ItemIterator iter = impl.getAllItems(centerIDs, syncRecord.getCenter());
      jsw.startProperty("items");
      jsw.startArray();
      while (iter.hasNext()) itemWriter.writeItem(iter.next());
      for (PrismsCenter center : keeper.getCenters()) itemWriter.writeItem(center);
      if (keeper.getAutoPurger() != null) itemWriter.writeItem(keeper.getAutoPurger());
      jsw.endArray();

      pi.setProgressText("Exporting " + namespace + " changes");
      prisms.util.Sorter<RecordKeeper.ChangeField> sorter;
      sorter = new prisms.util.Sorter<RecordKeeper.ChangeField>();
      sorter.addSort(RecordKeeper.ChangeField.CHANGE_TIME, true);
      prisms.util.Search changeSearch =
          global
              ? null
              : new prisms.records.ChangeSearch.IDRange(
                      Long.valueOf(IDGenerator.getMinID(keeper.getCenterID())),
                      Long.valueOf(IDGenerator.getMaxID(keeper.getCenterID())))
                  .and(new prisms.records.ChangeSearch.LocalOnlySearch(null));
      prisms.util.LongList changeIDs =
          new prisms.util.LongList(keeper.search(changeSearch, sorter));
      long[] batch = new long[changeIDs.size() < 200 ? changeIDs.size() : 200];
      jsw.startProperty("changes");
      jsw.startArray();
      for (int i = 0; i < changeIDs.size(); i += batch.length) {
        if (changeIDs.size() - i < batch.length) batch = new long[changeIDs.size() - i];
        changeIDs.arrayCopy(i, batch, 0, batch.length);
        ChangeRecord[] changes = keeper.getItems(batch);
        for (ChangeRecord change : changes)
          if (change != null
              && (change.type.subjectType instanceof PrismsChange || impl.shouldSend(change)))
            itemWriter.writeChange(change, false);
      }
      jsw.endArray();

      if (global) {
        pi.setProgressText("Exporting " + namespace + " sync records");
        jsw.startProperty("syncRecords");
        jsw.startArray();
        for (PrismsCenter center : keeper.getCenters()) exportSyncRecords(center, keeper, jsw);
        jsw.endArray();

        pi.setProgressText("Exporting " + namespace + " metadata");
        jsw.startProperty("latestChanges");
        jsw.startArray();
        prisms.util.IntList allCenterIDs = new prisms.util.IntList(keeper.getAllCenterIDs());
        for (int i = 0; i < allCenterIDs.size(); i++)
          for (int j = 0; j < allCenterIDs.size(); j++) {
            long changeTime = keeper.getLatestChange(allCenterIDs.get(i), allCenterIDs.get(j));
            if (changeTime > 0) {
              jsw.startObject();
              jsw.startProperty("center");
              jsw.writeNumber(Integer.valueOf(allCenterIDs.get(i)));
              jsw.startProperty("subjectCenter");
              jsw.writeNumber(Integer.valueOf(allCenterIDs.get(i)));
              jsw.startProperty("latestChange");
              jsw.writeNumber(Long.valueOf(changeTime));
              jsw.endObject();
            }
          }
        jsw.endArray();
      }
      jsw.endObject();
    }