/** 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."); }
/** 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); } } }
/** * 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); } } }
/** 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; }
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); } } } } }
/** * 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; }
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; }
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; }
/** * 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); } }
/** 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); }
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; }
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; }
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(); } } } }
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; }
/** * 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); }
/** * 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); }
/** * 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; }
/** * 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; }
/** * 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; }
/** * 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; }