/*
* Copyright (c) 2010-2016 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.repo.cache;
import com.evolveum.midpoint.prism.Containerable;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.repo.api.RepoAddOptions;
import com.evolveum.midpoint.repo.api.RepoModifyOptions;
import com.evolveum.midpoint.repo.api.RepositoryService;
import com.evolveum.midpoint.schema.*;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import org.apache.commons.lang.Validate;
import org.jetbrains.annotations.NotNull;
import javax.xml.namespace.QName;
import java.util.Collection;
import java.util.List;
/**
* Read-through write-through per-session repository cache.
*
* TODO doc
* TODO logging perf measurements
*
* @author Radovan Semancik
*
*/
public class RepositoryCache implements RepositoryService {
private static ThreadLocal<Cache> cacheInstance = new ThreadLocal<>();
private RepositoryService repository;
private static final Trace LOGGER = TraceManager.getTrace(RepositoryCache.class);
private static final Trace PERFORMANCE_ADVISOR = TraceManager.getPerformanceAdvisorTrace();
private PrismContext prismContext;
public RepositoryCache() {
}
public void setRepository(RepositoryService service, PrismContext prismContext) {
Validate.notNull(service, "Repository service must not be null.");
Validate.notNull(prismContext, "Prism context service must not be null.");
this.repository = service;
this.prismContext = prismContext;
}
private static Cache getCache() {
return cacheInstance.get();
}
public static void init() {
}
public static void destroy() {
Cache.destroy(cacheInstance, LOGGER);
}
public static void enter() {
Cache.enter(cacheInstance, Cache.class, LOGGER);
}
public static void exit() {
Cache.exit(cacheInstance, LOGGER);
}
public static boolean exists() {
return Cache.exists(cacheInstance);
}
public static String debugDump() {
return Cache.debugDump(cacheInstance);
}
@Override
public <T extends ObjectType> PrismObject<T> getObject(Class<T> type, String oid,
Collection<SelectorOptions<GetOperationOptions>> options, OperationResult parentResult) throws ObjectNotFoundException, SchemaException {
if (!isCacheable(type) || !nullOrHarmlessOptions(options)) {
log("Cache: PASS {} ({})", oid, type.getSimpleName());
return repository.getObject(type, oid, options, parentResult);
}
Cache cache = getCache();
boolean readOnly = GetOperationOptions.isReadOnly(SelectorOptions.findRootOptions(options));
if (cache == null) {
log("Cache: NULL {} ({})", oid, type.getSimpleName());
} else {
PrismObject<T> object = (PrismObject) cache.getObject(oid);
if (object != null) {
// TODO: result?
if (readOnly) {
log("Cache: HIT {} ({})", oid, type.getSimpleName());
return object;
} else {
log("Cache: HIT(clone) {} ({})", oid, type.getSimpleName());
return object.clone();
}
}
log("Cache: MISS {} ({})", oid, type.getSimpleName());
}
PrismObject<T> object = repository.getObject(type, oid, null, parentResult);
cacheObject(cache, object, readOnly);
return object;
}
private boolean isCacheable(Class<?> type) {
if (type.equals(TaskType.class)) {
return false;
}
// if (ShadowType.class.isAssignableFrom(type)) {
// return false;
// }
return true;
}
@Override
public <T extends ObjectType> String addObject(PrismObject<T> object, RepoAddOptions options, OperationResult parentResult)
throws ObjectAlreadyExistsException, SchemaException {
String oid = repository.addObject(object, options, parentResult);
Cache cache = getCache();
// DON't cache the object here. The object may not have proper "JAXB" form, e.g. some pieces may be
// DOM element instead of JAXB elements. Not to cache it is safer and the performance loss
// is acceptable.
if (cache != null) {
// Invalidate the cache entry if it happens to be there
cache.removeObject(oid);
cache.clearQueryResults(object.getCompileTimeClass());
}
return oid;
}
@NotNull
@Override
public <T extends ObjectType> SearchResultList<PrismObject<T>> searchObjects(Class<T> type, ObjectQuery query,
Collection<SelectorOptions<GetOperationOptions>> options, OperationResult parentResult) throws SchemaException {
if (!isCacheable(type) || !nullOrHarmlessOptions(options)) {
log("Cache: PASS ({})", type.getSimpleName());
return repository.searchObjects(type, query, options, parentResult);
}
Cache cache = getCache();
boolean readOnly = GetOperationOptions.isReadOnly(SelectorOptions.findRootOptions(options));
if (cache == null) {
log("Cache: NULL ({})", type.getSimpleName());
} else {
SearchResultList queryResult = cache.getQueryResult(type, query, prismContext);
if (queryResult != null) {
if (readOnly) {
log("Cache: HIT {} ({})", query, type.getSimpleName());
return queryResult;
} else {
log("Cache: HIT(clone) {} ({})", query, type.getSimpleName());
return queryResult.clone();
}
}
log("Cache: MISS {} ({})", query, type.getSimpleName());
}
// Cannot satisfy from cache, pass down to repository
SearchResultList<PrismObject<T>> objects = repository.searchObjects(type, query, options, parentResult);
if (cache != null && options == null) {
for (PrismObject<T> object : objects) {
cacheObject(cache, object, readOnly);
}
// TODO cloning before storing into cache?
cache.putQueryResult(type, query, objects, prismContext);
}
return objects;
}
@Override
public <T extends Containerable> SearchResultList<T> searchContainers(Class<T> type, ObjectQuery query, Collection<SelectorOptions<GetOperationOptions>> options, OperationResult parentResult) throws SchemaException {
return repository.searchContainers(type, query, options, parentResult);
}
/* (non-Javadoc)
* @see com.evolveum.midpoint.repo.api.RepositoryService#searchObjectsIterative(java.lang.Class, com.evolveum.midpoint.prism.query.ObjectQuery, com.evolveum.midpoint.schema.ResultHandler, com.evolveum.midpoint.schema.result.OperationResult)
*/
@Override
public <T extends ObjectType> SearchResultMetadata searchObjectsIterative(Class<T> type, ObjectQuery query,
final ResultHandler<T> handler, final Collection<SelectorOptions<GetOperationOptions>> options,
boolean strictlySequential, OperationResult parentResult) throws SchemaException {
// TODO use cached query result if applicable
log("Cache: PASS searchObjectsIterative ({})", type.getSimpleName());
final Cache cache = getCache();
ResultHandler<T> myHandler = new ResultHandler<T>() {
@Override
public boolean handle(PrismObject<T> object, OperationResult parentResult) {
cacheObject(cache, object, GetOperationOptions.isReadOnly(SelectorOptions.findRootOptions(options)));
return handler.handle(object, parentResult);
}
};
return repository.searchObjectsIterative(type, query, myHandler, options, strictlySequential, parentResult);
}
@Override
public <T extends ObjectType> int countObjects(Class<T> type, ObjectQuery query, OperationResult parentResult)
throws SchemaException {
// TODO use cached query result if applicable
log("Cache: PASS countObjects ({})", type.getSimpleName());
return repository.countObjects(type, query, parentResult);
}
@Override
public <T extends ObjectType> int countObjects(Class<T> type, ObjectQuery query,
Collection<SelectorOptions<GetOperationOptions>> options, OperationResult parentResult)
throws SchemaException {
// TODO use cached query result if applicable
log("Cache: PASS countObjects ({})", type.getSimpleName());
return repository.countObjects(type, query, options, parentResult);
}
public <T extends ObjectType> void modifyObject(Class<T> type, String oid, Collection<? extends ItemDelta> modifications,
OperationResult parentResult) throws ObjectNotFoundException, SchemaException, ObjectAlreadyExistsException {
modifyObject(type, oid, modifications, null, parentResult);
}
@Override
public <T extends ObjectType> void modifyObject(Class<T> type, String oid, Collection<? extends ItemDelta> modifications,
RepoModifyOptions options, OperationResult parentResult) throws ObjectNotFoundException, SchemaException, ObjectAlreadyExistsException {
try {
repository.modifyObject(type, oid, modifications, options, parentResult);
} finally {
// this changes the object. We are too lazy to apply changes ourselves, so just invalidate
// the object in cache
invalidateCacheEntry(type, oid);
}
}
protected <T extends ObjectType> void invalidateCacheEntry(Class<T> type, String oid) {
Cache cache = getCache();
if (cache != null) {
cache.removeObject(oid);
cache.clearQueryResults(type);
}
}
@Override
public <T extends ObjectType> void deleteObject(Class<T> type, String oid, OperationResult parentResult)
throws ObjectNotFoundException {
try {
repository.deleteObject(type, oid, parentResult);
} finally {
invalidateCacheEntry(type, oid);
}
}
@Override
public <F extends FocusType> PrismObject<F> searchShadowOwner(
String shadowOid, Collection<SelectorOptions<GetOperationOptions>> options, OperationResult parentResult) {
// TODO cache the search operation?
PrismObject<F> ownerObject = repository.searchShadowOwner(shadowOid, options, parentResult);
if (ownerObject != null && nullOrHarmlessOptions(options)) {
cacheObject(getCache(), ownerObject, GetOperationOptions.isReadOnly(SelectorOptions.findRootOptions(options)));
}
return ownerObject;
}
private boolean nullOrHarmlessOptions(Collection<SelectorOptions<GetOperationOptions>> options) {
if (options == null || options.isEmpty()) {
return true;
}
if (options.size() > 1) {
return false;
}
SelectorOptions<GetOperationOptions> selectorOptions = options.iterator().next();
if (!selectorOptions.isRoot()) {
return false;
}
GetOperationOptions options1 = selectorOptions.getOptions();
if (options1 == null || options1.equals(new GetOperationOptions()) || options1.equals(GetOperationOptions.createAllowNotFound())) {
return true;
}
return false;
}
@Override
@Deprecated
public PrismObject<UserType> listAccountShadowOwner(String accountOid, OperationResult parentResult)
throws ObjectNotFoundException {
return repository.listAccountShadowOwner(accountOid, parentResult);
}
@Override
public <T extends ShadowType> List<PrismObject<T>> listResourceObjectShadows(String resourceOid,
Class<T> resourceObjectShadowType, OperationResult parentResult) throws ObjectNotFoundException,
SchemaException {
return repository.listResourceObjectShadows(resourceOid, resourceObjectShadowType, parentResult);
}
/* (non-Javadoc)
* @see com.evolveum.midpoint.repo.api.RepositoryService#getVersion(java.lang.Class, java.lang.String, com.evolveum.midpoint.schema.result.OperationResult)
*/
@Override
public <T extends ObjectType> String getVersion(Class<T> type, String oid, OperationResult parentResult)
throws ObjectNotFoundException, SchemaException {
if (!isCacheable(type)) {
log("Cache: PASS {} ({})", oid, type.getSimpleName());
return repository.getVersion(type, oid, parentResult);
}
Cache cache = getCache();
if (cache == null) {
log("Cache: NULL {} ({})", oid, type.getSimpleName());
} else {
String version = cache.getObjectVersion(oid);
if (version != null) {
log("Cache: HIT {} ({})", oid, type.getSimpleName());
return version;
}
log("Cache: MISS {} ({})", oid, type.getSimpleName());
}
String version = repository.getVersion(type, oid, parentResult);
cacheObjectVersion(cache, oid, version);
return version;
}
/* (non-Javadoc)
* @see com.evolveum.midpoint.repo.api.RepositoryService#getRepositoryDiag()
*/
@Override
public RepositoryDiag getRepositoryDiag() {
return repository.getRepositoryDiag();
}
/* (non-Javadoc)
* @see com.evolveum.midpoint.repo.api.RepositoryService#repositorySelfTest(com.evolveum.midpoint.schema.result.OperationResult)
*/
@Override
public void repositorySelfTest(OperationResult parentResult) {
repository.repositorySelfTest(parentResult);
}
@Override
public void testOrgClosureConsistency(boolean repairIfNecessary, OperationResult testResult) {
repository.testOrgClosureConsistency(repairIfNecessary, testResult);
}
private <T extends ObjectType> void cacheObject(Cache cache, PrismObject<T> object, boolean readOnly) {
if (cache != null) {
PrismObject<ObjectType> objectToCache;
if (readOnly) {
object.setImmutable(true);
objectToCache = (PrismObject<ObjectType>) object;
} else {
objectToCache = (PrismObject<ObjectType>) object.clone();
}
cache.putObject(object.getOid(), objectToCache);
}
}
private <T extends ObjectType> void cacheObjectVersion(Cache cache, String oid, String version) {
if (cache != null) {
cache.putObjectVersion(oid, version);
}
}
@Override
public boolean isAnySubordinate(String upperOrgOid, Collection<String> lowerObjectOids)
throws SchemaException {
return repository.isAnySubordinate(upperOrgOid, lowerObjectOids);
}
@Override
public <O extends ObjectType> boolean isDescendant(PrismObject<O> object, String orgOid)
throws SchemaException {
return repository.isDescendant(object, orgOid);
}
@Override
public <O extends ObjectType> boolean isAncestor(PrismObject<O> object, String oid)
throws SchemaException {
return repository.isAncestor(object, oid);
}
@Override
public <O extends ObjectType> boolean selectorMatches(ObjectSelectorType objectSelector,
PrismObject<O> object, Trace logger, String logMessagePrefix) throws SchemaException {
return repository.selectorMatches(objectSelector, object, logger, logMessagePrefix);
}
private void log(String message, Object... params) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(message, params);
}
if (PERFORMANCE_ADVISOR.isTraceEnabled()) {
PERFORMANCE_ADVISOR.trace(message, params);
}
}
@Override
public long advanceSequence(String oid, OperationResult parentResult) throws ObjectNotFoundException,
SchemaException {
try {
return repository.advanceSequence(oid, parentResult);
} finally {
invalidateCacheEntry(SequenceType.class, oid);
}
}
@Override
public void returnUnusedValuesToSequence(String oid, Collection<Long> unusedValues, OperationResult parentResult)
throws ObjectNotFoundException, SchemaException {
try {
repository.returnUnusedValuesToSequence(oid, unusedValues, parentResult);
} finally {
invalidateCacheEntry(SequenceType.class, oid);
}
}
@Override
public RepositoryQueryDiagResponse executeQueryDiagnostics(RepositoryQueryDiagRequest request, OperationResult result) {
return repository.executeQueryDiagnostics(request, result);
}
@Override
public QName getApproximateSupportedMatchingRule(Class<?> dataType, QName originalMatchingRule) {
return repository.getApproximateSupportedMatchingRule(dataType, originalMatchingRule);
}
@Override
public void applyFullTextSearchConfiguration(FullTextSearchConfigurationType fullTextSearch) {
repository.applyFullTextSearchConfiguration(fullTextSearch);
}
@Override
public FullTextSearchConfigurationType getFullTextSearchConfiguration() {
return repository.getFullTextSearchConfiguration();
}
@Override
public void postInit(OperationResult result) throws SchemaException {
repository.postInit(result);
}
}