示例#1
1
  /** Fetch the classloader for the given ApplicationID. */
  static URLClassLoader getUrlClassLoader(ApplicationID appId, Map input) {
    NCube cpCube = getCube(appId, CLASSPATH_CUBE);
    if (cpCube
        == null) { // No sys.classpath cube exists, just create regular GroovyClassLoader with no
                   // URLs set into it.
      // Scope the GroovyClassLoader per ApplicationID
      return getLocalClassloader(appId);
    }

    final String envLevel = SystemUtilities.getExternalVariable("ENV_LEVEL");
    if (!input.containsKey("env")
        && StringUtilities.hasContent(
            envLevel)) { // Add in the 'ENV_LEVEL" environment variable when looking up sys.* cubes,
      // if there was not already an entry for it.
      input.put("env", envLevel);
    }
    if (!input.containsKey("username")) { // same as ENV_LEVEL, add it in if not already there.
      input.put("username", System.getProperty("user.name"));
    }
    Object urlCpLoader = cpCube.getCell(input);

    if (urlCpLoader instanceof URLClassLoader) {
      return (URLClassLoader) urlCpLoader;
    }

    throw new IllegalStateException(
        "If the sys.classpath cube exists it must return a URLClassLoader.");
  }
示例#2
0
  /** Associate Advice to all n-cubes that match the passed in regular expression. */
  public static void addAdvice(ApplicationID appId, String wildcard, Advice advice) {
    validateAppId(appId);
    ConcurrentMap<String, Advice> current = advices.get(appId);
    if (current == null) {
      current = new ConcurrentHashMap<>();
      ConcurrentMap<String, Advice> mapRef = advices.putIfAbsent(appId, current);
      if (mapRef != null) {
        current = mapRef;
      }
    }

    current.put(advice.getName() + '/' + wildcard, advice);

    // Apply newly added advice to any fully loaded (hydrated) cubes.
    String regex = StringUtilities.wildcardToRegexString(wildcard);
    Map<String, Object> cubes = getCacheForApp(appId);

    for (Object value : cubes.values()) {
      if (value instanceof NCube) { // apply advice to hydrated cubes
        NCube ncube = (NCube) value;
        Axis axis = ncube.getAxis("method");
        addAdviceToMatchedCube(advice, regex, ncube, axis);
      }
    }
  }
示例#3
0
  /**
   * Clear the cube (and other internal caches) for a given ApplicationID. This will remove all the
   * n-cubes from memory, compiled Groovy code, caches related to expressions, caches related to
   * method support, advice caches, and local classes loaders (used when no sys.classpath is
   * present).
   *
   * @param appId ApplicationID for which the cache is to be cleared.
   */
  public static void clearCache(ApplicationID appId) {
    synchronized (ncubeCache) {
      validateAppId(appId);

      Map<String, Object> appCache = getCacheForApp(appId);
      clearGroovyClassLoaderCache(appCache);

      appCache.clear();
      GroovyBase.clearCache(appId);
      NCubeGroovyController.clearCache(appId);

      // Clear Advice cache
      Map<String, Advice> adviceCache = advices.get(appId);
      if (adviceCache != null) {
        adviceCache.clear();
      }

      // Clear ClassLoader cache
      GroovyClassLoader classLoader = localClassLoaders.get(appId);
      if (classLoader != null) {
        classLoader.clearCache();
        localClassLoaders.remove(appId);
      }
    }
  }
示例#4
0
 /** Testing API (Cache validation) */
 static boolean isCubeCached(ApplicationID appId, String cubeName) {
   validateAppId(appId);
   NCube.validateCubeName(cubeName);
   Map<String, Object> ncubes = getCacheForApp(appId);
   Object cachedItem = ncubes.get(cubeName.toLowerCase());
   return cachedItem instanceof NCube || cachedItem instanceof NCubeInfoDto;
 }
示例#5
0
  private static void cacheCubes(ApplicationID appId, List<NCubeInfoDto> cubes) {
    Map<String, Object> appCache = getCacheForApp(appId);

    for (NCubeInfoDto cubeInfo : cubes) {
      String key = cubeInfo.name.toLowerCase();

      if (!cubeInfo.revision.startsWith("-")) {
        Object cachedItem = appCache.get(key);
        if (cachedItem == null
            || cachedItem
                instanceof
                NCubeInfoDto) { // If cube not in cache or already in cache as infoDto, overwrite it
          appCache.put(key, cubeInfo);
        } else if (cachedItem
            instanceof
            NCube) { // If cube is already cached, make sure the SHA1's match - if not, then cache
                     // the new cubeInfo
          NCube ncube = (NCube) cachedItem;
          if (!ncube.sha1().equals(cubeInfo.sha1)) {
            appCache.put(key, cubeInfo);
          }
        }
      }
    }
  }
示例#6
0
 /**
  * Load n-cube, bypassing any caching. This is necessary for n-cube-editor (IDE time usage). If
  * the IDE environment is clustered, cannot be getting stale copies from cache. Any advices in the
  * manager will be applied to the n-cube.
  *
  * @return NCube of the specified name from the specified AppID, or null if not found.
  */
 public static NCube loadCube(ApplicationID appId, String cubeName) {
   NCube ncube = getPersister().loadCube(appId, cubeName);
   if (ncube == null) {
     return null;
   }
   applyAdvices(ncube.getApplicationID(), ncube);
   Map<String, Object> cubes = getCacheForApp(appId);
   cubes.put(cubeName.toLowerCase(), ncube); // Update cache
   return ncube;
 }
示例#7
0
  public static boolean mergeAcceptMine(ApplicationID appId, String cubeName, String username) {
    validateAppId(appId);
    appId.validateBranchIsNotHead();
    appId.validateStatusIsNotRelease();

    boolean ret = getPersister().mergeAcceptMine(appId, cubeName, username);

    Map<String, Object> appCache = getCacheForApp(appId);
    appCache.remove(cubeName.toLowerCase());

    return ret;
  }
示例#8
0
  public static String getNotes(ApplicationID appId, String cubeName) {
    validateAppId(appId);
    NCube.validateCubeName(cubeName);
    Map<String, Object> options = new HashMap<>();
    options.put(SEARCH_INCLUDE_NOTES, true);
    options.put(SEARCH_EXACT_MATCH_NAME, true);

    List<NCubeInfoDto> infos = search(appId, cubeName, null, options);
    if (infos.size() == 0) {
      throw new IllegalArgumentException(
          "Could not fetch notes, no cube: " + cubeName + " in app: " + appId);
    }
    return infos.get(0).notes;
  }
示例#9
0
  /**
   * Apply existing advices loaded into the NCubeManager, to the passed in n-cube. This allows
   * advices to be added first, and then let them be applied 'on demand' as an n-cube is loaded
   * later.
   *
   * @param appId ApplicationID
   * @param ncube NCube to which all matching advices will be applied.
   */
  private static void applyAdvices(ApplicationID appId, NCube ncube) {
    final Map<String, Advice> appAdvices = advices.get(appId);

    if (MapUtilities.isEmpty(appAdvices)) {
      return;
    }
    for (Map.Entry<String, Advice> entry : appAdvices.entrySet()) {
      final Advice advice = entry.getValue();
      final String wildcard = entry.getKey().replace(advice.getName() + '/', "");
      final String regex = StringUtilities.wildcardToRegexString(wildcard);
      final Axis axis = ncube.getAxis("method");
      addAdviceToMatchedCube(advice, regex, ncube, axis);
    }
  }
示例#10
0
  /** Duplicate the given n-cube specified by oldAppId and oldName to new ApplicationID and name, */
  public static void duplicate(
      ApplicationID oldAppId,
      ApplicationID newAppId,
      String oldName,
      String newName,
      String username) {
    validateAppId(oldAppId);
    validateAppId(newAppId);

    newAppId.validateBranchIsNotHead();

    if (newAppId.isRelease()) {
      throw new IllegalArgumentException(
          "Cubes cannot be duplicated into a "
              + ReleaseStatus.RELEASE
              + " version, cube: "
              + newName
              + ", app: "
              + newAppId);
    }

    NCube.validateCubeName(oldName);
    NCube.validateCubeName(newName);

    if (oldName.equalsIgnoreCase(newName) && oldAppId.equals(newAppId)) {
      throw new IllegalArgumentException(
          "Could not duplicate, old name cannot be the same as the new name when oldAppId matches newAppId, name: "
              + oldName
              + ", app: "
              + oldAppId);
    }

    getPersister().duplicateCube(oldAppId, newAppId, oldName, newName, username);

    if (CLASSPATH_CUBE.equalsIgnoreCase(
        newName)) { // If another cube is renamed into sys.classpath,
      // then the entire class loader must be dropped (and then lazily rebuilt).
      clearCache(newAppId);
    } else {
      Map<String, Object> appCache = getCacheForApp(newAppId);
      appCache.remove(newName.toLowerCase());
    }

    broadcast(newAppId);
  }
示例#11
0
  public static boolean renameCube(
      ApplicationID appId, String oldName, String newName, String username) {
    validateAppId(appId);
    appId.validateBranchIsNotHead();

    if (appId.isRelease()) {
      throw new IllegalArgumentException(
          "Cannot rename a "
              + ReleaseStatus.RELEASE
              + " cube, cube: "
              + oldName
              + ", app: "
              + appId);
    }

    NCube.validateCubeName(oldName);
    NCube.validateCubeName(newName);

    if (oldName.equalsIgnoreCase(newName)) {
      throw new IllegalArgumentException(
          "Could not rename, old name cannot be the same as the new name, name: "
              + oldName
              + ", app: "
              + appId);
    }

    boolean result = getPersister().renameCube(appId, oldName, newName, username);

    if (CLASSPATH_CUBE.equalsIgnoreCase(oldName)
        || CLASSPATH_CUBE.equalsIgnoreCase(
            newName)) { // If the sys.classpath cube is renamed, or another cube is renamed into
                        // sys.classpath,
      // then the entire class loader must be dropped (and then lazily rebuilt).
      clearCache(appId);
    } else {
      Map<String, Object> appCache = getCacheForApp(appId);
      appCache.remove(oldName.toLowerCase());
      appCache.remove(newName.toLowerCase());
    }

    broadcast(appId);
    return result;
  }
示例#12
0
  static boolean deleteCubes(
      ApplicationID appId, Object[] cubeNames, boolean allowDelete, String username) {
    validateAppId(appId);
    if (!allowDelete) {
      if (appId.isRelease()) {
        throw new IllegalArgumentException(
            ReleaseStatus.RELEASE + " cubes cannot be hard-deleted, app: " + appId);
      }
    }

    if (getPersister().deleteCubes(appId, cubeNames, allowDelete, username)) {
      Map<String, Object> appCache = getCacheForApp(appId);
      for (int i = 0; i < cubeNames.length; i++) {
        appCache.remove(((String) cubeNames[i]).toLowerCase());
      }
      broadcast(appId);
      return true;
    }
    return false;
  }
示例#13
0
 private static void clearGroovyClassLoaderCache(Map<String, Object> appCache) {
   Object cube = appCache.get(CLASSPATH_CUBE);
   if (cube instanceof NCube) {
     NCube cpCube = (NCube) cube;
     for (Object content : cpCube.cells.values()) {
       if (content instanceof UrlCommandCell) {
         ((UrlCommandCell) content).clearClassLoaderCache();
       }
     }
   }
 }
示例#14
0
  private static NCube checkForConflicts(
      ApplicationID appId,
      Map<String, Map> errors,
      String message,
      NCubeInfoDto info,
      NCubeInfoDto head,
      boolean reverse) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("message", message);
    map.put("sha1", info.sha1);
    map.put("headSha1", head != null ? head.sha1 : null);

    try {
      if (head != null) {
        long branchCubeId = (long) Converter.convert(info.id, long.class);
        long headCubeId = (long) Converter.convert(head.id, long.class);
        NCube branchCube = getPersister().loadCubeById(branchCubeId);
        NCube headCube = getPersister().loadCubeById(headCubeId);

        if (info.headSha1 != null) {
          NCube baseCube = getPersister().loadCubeBySha1(appId, info.name, info.headSha1);

          Map delta1 = baseCube.getDelta(branchCube);
          Map delta2 = baseCube.getDelta(headCube);

          if (NCube.areDeltaSetsCompatible(delta1, delta2)) {
            if (reverse) {
              headCube.mergeCellChangeSet(delta1);
              return headCube;
            } else {
              branchCube.mergeCellChangeSet(delta2);
              return branchCube;
            }
          }
        }

        List<Delta> diff = branchCube.getDeltaDescription(headCube);
        if (diff.size() > 0) {
          map.put("diff", diff);
        } else {
          return branchCube;
        }
      } else {
        map.put("diff", null);
      }
    } catch (Exception e) {
      map.put("diff", e.getMessage());
    }
    errors.put(info.name, map);
    return null;
  }
示例#15
0
  /**
   * Fetch all the n-cube names for the given ApplicationID. This API will load all cube records for
   * the ApplicationID (NCubeInfoDtos), and then get the names from them.
   *
   * @return Set<String> n-cube names. If an empty Set is returned, then there are no persisted
   *     n-cubes for the passed in ApplicationID.
   */
  public static Set<String> getCubeNames(ApplicationID appId) {
    Map<String, Object> options = new HashMap<>();
    options.put(SEARCH_ACTIVE_RECORDS_ONLY, true);
    List<NCubeInfoDto> cubeInfos = search(appId, null, null, options);
    Set<String> names = new TreeSet<>();

    for (NCubeInfoDto info : cubeInfos) {
      names.add(info.name);
    }

    if (names.isEmpty()) { // Support tests that load cubes from JSON files...
      // can only be in there as ncubes, not ncubeDtoInfo
      for (Object value : getCacheForApp(appId).values()) {
        if (value instanceof NCube) {
          NCube cube = (NCube) value;
          names.add(cube.getName());
        }
      }
    }
    return new CaseInsensitiveSet<>(names);
  }
示例#16
0
  /**
   * Fetch an n-cube by name from the given ApplicationID. If no n-cubes are loaded, then a
   * loadCubes() call is performed and then the internal cache is checked again. If the cube is not
   * found, null is returned.
   */
  public static NCube getCube(ApplicationID appId, String name) {
    validateAppId(appId);
    NCube.validateCubeName(name);
    Map<String, Object> cubes = getCacheForApp(appId);
    final String lowerCubeName = name.toLowerCase();

    if (cubes.containsKey(lowerCubeName)) { // pull from cache
      final Object cube = cubes.get(lowerCubeName);
      return Boolean.FALSE == cube ? null : ensureLoaded(cube);
    }

    // now even items with metaProperties(cache = 'false') can be retrieved
    // and normal app processing doesn't do two queries anymore.
    // used to do getCubeInfoRecords() -> dto
    // and then dto -> loadCube(id)
    NCube ncube = getPersister().loadCube(appId, name);
    if (ncube == null) {
      cubes.put(lowerCubeName, Boolean.FALSE);
      return null;
    }
    return prepareCube(ncube);
  }
示例#17
0
  /**
   * Fetch an array of NCubeInfoDto's where the cube names match the cubeNamePattern (contains) and
   * the content (in JSON format) 'contains' the passed in content String.
   *
   * @param appId ApplicationID on which we are working
   * @param cubeNamePattern cubeNamePattern String pattern to match cube names
   * @param content String value that is 'contained' within the cube's JSON
   * @param options map with possible keys: changedRecordsOnly - default false -> Only searches
   *     changed records if true. activeRecordsOnly - default false -> Only searches non-deleted
   *     records if true. deletedRecordsOnly - default false -> Only searches deleted records if
   *     true. cacheResult - default false -> Cache the cubes that match this result..
   * @return List<NCubeInfoDto>
   */
  public static List<NCubeInfoDto> search(
      ApplicationID appId, String cubeNamePattern, String content, Map options) {
    validateAppId(appId);

    if (options == null) {
      options = new HashMap();
    }

    List<NCubeInfoDto> cubes = getPersister().search(appId, cubeNamePattern, content, options);
    Boolean result = (Boolean) options.get(SEARCH_CACHE_RESULT);

    if (result == null || result) {
      cacheCubes(appId, cubes);
    }
    return cubes;
  }
示例#18
0
  /**
   * Update a branch from the HEAD. Changes from the HEAD are merged into the supplied branch. If
   * the merge cannot be done perfectly, an exception is thrown indicating the cubes that are in
   * conflict.
   */
  public static Map<String, Object> updateBranch(ApplicationID appId, String username) {
    validateAppId(appId);
    appId.validateBranchIsNotHead();
    appId.validateStatusIsNotRelease();

    ApplicationID headAppId = appId.asHead();

    Map<String, Object> options = new HashMap<>();
    options.put(SEARCH_ACTIVE_RECORDS_ONLY, false);
    List<NCubeInfoDto> records = search(appId, null, null, options);
    Map<String, NCubeInfoDto> branchRecordMap = new CaseInsensitiveMap<>();

    for (NCubeInfoDto info : records) {
      branchRecordMap.put(info.name, info);
    }

    List<NCubeInfoDto> updates = new ArrayList<>();
    List<NCubeInfoDto> dtosMerged = new ArrayList<>();
    Map<String, Map> conflicts = new CaseInsensitiveMap<>();
    List<NCubeInfoDto> headRecords = search(headAppId, null, null, options);

    for (NCubeInfoDto head : headRecords) {
      NCubeInfoDto info = branchRecordMap.get(head.name);

      if (info == null) { // HEAD has cube that branch does not have
        updates.add(head);
        continue;
      }

      long infoRev = (long) Converter.convert(info.revision, long.class);
      long headRev = (long) Converter.convert(head.revision, long.class);
      boolean activeStatusMatches = (infoRev < 0) == (headRev < 0);

      // Did branch change?
      if (!info.isChanged()) { // No change on branch
        if (!activeStatusMatches
            || !StringUtilities.equalsIgnoreCase(
                info.headSha1, head.sha1)) { // 1. The active/deleted statuses don't match, or
          // 2. HEAD has different SHA1 but branch cube did not change, safe to update branch (fast
          // forward)
          // In both cases, the cube was marked NOT changed in the branch, so safe to update.
          updates.add(head);
        }
      } else if (StringUtilities.equalsIgnoreCase(
          info.sha1,
          head.sha1)) { // If branch is 'changed' but has same SHA-1 as head, then see if branch
                        // needs Fast-Forward
        if (!StringUtilities.equalsIgnoreCase(info.headSha1, head.sha1)) { // Fast-Forward branch
          // Update HEAD SHA-1 on branch directly (no need to insert)
          getPersister()
              .updateBranchCubeHeadSha1((Long) Converter.convert(info.id, Long.class), head.sha1);
        }
      } else {
        if (!StringUtilities.equalsIgnoreCase(
            info.headSha1,
            head.sha1)) { // Cube is different than HEAD, AND it is not based on same HEAD cube, but
                          // it could be merge-able.
          String message = "Cube was changed in both branch and HEAD";
          NCube cube = checkForConflicts(appId, conflicts, message, info, head, true);

          if (cube != null) {
            NCubeInfoDto mergedDto =
                getPersister().commitMergedCubeToBranch(appId, cube, head.sha1, username);
            dtosMerged.add(mergedDto);
          }
        }
      }
    }

    List<NCubeInfoDto> finalUpdates = new ArrayList<>(updates.size());

    Object[] ids = new Object[updates.size()];
    int i = 0;
    for (NCubeInfoDto dto : updates) {
      ids[i++] = dto.id;
    }
    finalUpdates.addAll(getPersister().pullToBranch(appId, ids, username));

    clearCache(appId);

    Map<String, Object> ret = new LinkedHashMap<>();
    ret.put(BRANCH_UPDATES, finalUpdates);
    ret.put(BRANCH_MERGES, dtosMerged);
    ret.put(BRANCH_CONFLICTS, conflicts);
    return ret;
  }
示例#19
0
  /**
   * Commit the passed in changed cube records identified by NCubeInfoDtos.
   *
   * @return array of NCubeInfoDtos that are to be committed.
   */
  public static List<NCubeInfoDto> commitBranch(
      ApplicationID appId, Object[] infoDtos, String username) {
    validateAppId(appId);
    appId.validateBranchIsNotHead();
    appId.validateStatusIsNotRelease();

    ApplicationID headAppId = appId.asHead();
    Map<String, NCubeInfoDto> headMap = new TreeMap<>();
    Map<String, Object> options = new HashMap<>();
    options.put(SEARCH_ACTIVE_RECORDS_ONLY, false);
    List<NCubeInfoDto> headInfo = search(headAppId, null, null, options);

    //  build map of head objects for reference.
    for (NCubeInfoDto info : headInfo) {
      headMap.put(info.name, info);
    }

    List<NCubeInfoDto> dtosToUpdate = new ArrayList<>(infoDtos.length);
    List<NCubeInfoDto> dtosMerged = new ArrayList<>();

    Map<String, Map> errors = new LinkedHashMap<>();

    for (Object dto : infoDtos) {
      NCubeInfoDto branchCubeInfo = (NCubeInfoDto) dto;

      if (!branchCubeInfo.isChanged()) {
        continue;
      }
      if (branchCubeInfo.sha1 == null) {
        branchCubeInfo.sha1 = "";
      }

      // All changes go through here.
      NCubeInfoDto headCubeInfo = headMap.get(branchCubeInfo.name);
      long infoRev = (long) Converter.convert(branchCubeInfo.revision, long.class);

      if (headCubeInfo == null) { // No matching head cube, CREATE case
        if (infoRev
            >= 0) { // Only create if the cube in the branch is active (revision number not
                    // negative)
          dtosToUpdate.add(branchCubeInfo);
        }
      } else if (StringUtilities.equalsIgnoreCase(
          branchCubeInfo.headSha1,
          headCubeInfo
              .sha1)) { // HEAD cube has not changed (at least in terms of SHA-1 it could have it's
                        // revision sign changed)
        if (StringUtilities.equalsIgnoreCase(
            branchCubeInfo.sha1,
            branchCubeInfo
                .headSha1)) { // Cubes are same, but active status could be opposite (delete or
                              // restore case)
          long headRev = (long) Converter.convert(headCubeInfo.revision, long.class);
          if ((infoRev < 0) != (headRev < 0)) {
            dtosToUpdate.add(branchCubeInfo);
          }
        } else { // Regular update case (branch updated cube that was not touched in HEAD)
          dtosToUpdate.add(branchCubeInfo);
        }
      } else if (StringUtilities.equalsIgnoreCase(
          branchCubeInfo.sha1,
          headCubeInfo
              .sha1)) { // Branch headSha1 does not match HEAD sha1, but it's SHA-1 matches the HEAD
                        // SHA-1.
        // This means that the branch cube and HEAD cube are identical, but the HEAD was
        // different when the branch was created.
        dtosToUpdate.add(branchCubeInfo);
      } else {
        String msg;
        if (branchCubeInfo.headSha1 == null) {
          msg = ". A cube with the same name was added to HEAD since your branch was created.";
        } else {
          msg = ". The cube changed since your last update branch.";
        }
        String message = "Conflict merging " + branchCubeInfo.name + msg;
        NCube mergedCube =
            checkForConflicts(appId, errors, message, branchCubeInfo, headCubeInfo, false);
        if (mergedCube != null) {
          NCubeInfoDto mergedDto =
              getPersister().commitMergedCubeToHead(appId, mergedCube, username);
          dtosMerged.add(mergedDto);
        }
      }
    }

    if (!errors.isEmpty()) {
      throw new BranchMergeException(
          errors.size()
              + " merge conflict(s) committing branch.  Update your branch and retry commit.",
          errors);
    }

    List<NCubeInfoDto> committedCubes = new ArrayList<>(dtosToUpdate.size());
    Object[] ids = new Object[dtosToUpdate.size()];
    int i = 0;
    for (NCubeInfoDto dto : dtosToUpdate) {
      ids[i++] = dto.id;
    }

    committedCubes.addAll(getPersister().commitCubes(appId, ids, username));
    committedCubes.addAll(dtosMerged);
    clearCache(appId);
    clearCache(headAppId);
    broadcast(appId);
    return committedCubes;
  }
示例#20
0
  /**
   * Get List<NCubeInfoDto> of n-cube record DTOs for the given ApplicationID (branch only). If
   * using For any cube record loaded, for which there is no entry in the app's cube cache, an entry
   * is added mapping the cube name to the cube record (NCubeInfoDto). This will be replaced by an
   * NCube if more than the name is required. one (1) character. This is universal whether using a
   * SQL perister or Mongo persister.
   */
  public static List<NCubeInfoDto> getBranchChangesFromDatabase(ApplicationID appId) {
    validateAppId(appId);
    if (appId.getBranch().equals(ApplicationID.HEAD)) {
      throw new IllegalArgumentException("Cannot get branch changes from HEAD");
    }

    ApplicationID headAppId = appId.asHead();
    Map<String, NCubeInfoDto> headMap = new TreeMap<>();

    Map<String, Object> searchChangedRecordOptions = new HashMap<>();
    searchChangedRecordOptions.put(SEARCH_CHANGED_RECORDS_ONLY, true);
    List<NCubeInfoDto> branchList = search(appId, null, null, searchChangedRecordOptions);

    Map<String, Object> options = new HashMap<>();
    options.put(SEARCH_ACTIVE_RECORDS_ONLY, false);
    List<NCubeInfoDto> headList = search(headAppId, null, null, options);

    List<NCubeInfoDto> list = new ArrayList<>();

    //  build map of head objects for reference.
    for (NCubeInfoDto info : headList) {
      headMap.put(info.name, info);
    }

    // Loop through changed (added, deleted, created, restored, updated) records
    for (NCubeInfoDto info : branchList) {
      long revision = (long) Converter.convert(info.revision, long.class);
      NCubeInfoDto head = headMap.get(info.name);

      if (head == null) {
        if (revision >= 0) {
          info.changeType = ChangeType.CREATED.getCode();
          list.add(info);
        }
      } else if (info.headSha1 == null) { //  we created this guy locally
        // someone added this one to the head already
        info.changeType = ChangeType.CONFLICT.getCode();
        list.add(info);
      } else {
        if (StringUtilities.equalsIgnoreCase(info.headSha1, head.sha1)) {
          if (StringUtilities.equalsIgnoreCase(info.sha1, info.headSha1)) {
            // only net change could be revision deleted or restored.  check head.
            long headRev = Long.parseLong(head.revision);

            if (headRev < 0 != revision < 0) {
              if (revision < 0) {
                info.changeType = ChangeType.DELETED.getCode();
              } else {
                info.changeType = ChangeType.RESTORED.getCode();
              }

              list.add(info);
            }
          } else {
            info.changeType = ChangeType.UPDATED.getCode();
            list.add(info);
          }
        } else {
          info.changeType = ChangeType.CONFLICT.getCode();
          list.add(info);
        }
      }
    }

    cacheCubes(appId, list);
    return list;
  }