/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.isis.applib.services.queryresultscache;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.isis.applib.AbstractSubscriber;
import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.events.system.FixturesInstalledEvent;
import org.apache.isis.applib.events.system.FixturesInstallingEvent;
import org.apache.isis.applib.services.WithTransactionScope;
/**
* This service (API and implementation) provides a mechanism by which idempotent query results can be cached for the duration of an interaction.
* Most commonly this allows otherwise "naive" - eg that makes a repository call many times within a loop - to
* be performance tuned. The benefit is that the algorithm of the business logic can remain easy to understand.
*
* <p>
* This implementation has no UI and there is only one implementation (this class) in applib, it is annotated with
* {@link org.apache.isis.applib.annotation.DomainService}. This means that it is automatically registered and
* available for use; no further configuration is required.
*/
@DomainService(
nature = NatureOfService.DOMAIN,
menuOrder = "" + Integer.MAX_VALUE
)
@RequestScoped
public class QueryResultsCache implements WithTransactionScope {
private static final Logger LOG = LoggerFactory.getLogger(QueryResultsCache.class);
public static class Key {
private final Class<?> callingClass;
private final String methodName;
private final Object[] keys;
public Key(Class<?> callingClass, String methodName, Object... keys) {
this.callingClass = callingClass;
this.methodName = methodName;
this.keys = keys;
}
public Class<?> getCallingClass() {
return callingClass;
}
public String getMethodName() {
return methodName;
}
public Object[] getKeys() {
return keys;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Key other = (Key) obj;
// compare callingClass
if (callingClass == null) {
if (other.callingClass != null)
return false;
} else if (!callingClass.equals(other.callingClass))
return false;
// compare methodName
if (methodName == null) {
if (other.methodName != null)
return false;
} else if (!methodName.equals(other.methodName))
return false;
// compare keys
if (!Arrays.equals(keys, other.keys))
return false;
// ok, matches
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((callingClass == null) ? 0 : callingClass.hashCode());
result = prime * result + Arrays.hashCode(keys);
result = prime * result + ((methodName == null) ? 0 : methodName.hashCode());
return result;
}
@Override
public String toString() {
return callingClass.getName() + "#" + methodName + Arrays.toString(keys);
}
}
public static class Value<T> {
private T result;
public Value(T result) {
this.result = result;
}
public T getResult() {
return result;
}
}
// //////////////////////////////////////
private final Map<Key, Value<?>> cache = Maps.newHashMap();
@Programmatic
public <T> T execute(final Callable<T> callable, final Class<?> callingClass, final String methodName, final Object... keys) {
if(control.isFixturesInstalling()) {
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
final Key cacheKey = new Key(callingClass, methodName, keys);
return executeWithCaching(callable, cacheKey);
}
@Programmatic
@SuppressWarnings("unchecked")
public <T> T execute(final Callable<T> callable, final Key cacheKey) {
if(control.isFixturesInstalling()) {
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return executeWithCaching(callable, cacheKey);
}
protected <T> T executeWithCaching(final Callable<T> callable, final Key cacheKey) {
try {
final Value<?> cacheValue = cache.get(cacheKey);
logHitOrMiss(cacheKey, cacheValue);
if(cacheValue != null) {
return (T) cacheValue.getResult();
}
// cache miss, so get the result...
T result = callable.call();
// ... and cache
//
// (it is possible that the callable just invoked might also have updated the cache, eg if there was
// some sort of recursion. However, Map#put(...) is idempotent, so valid to call more than once.
//
// note: there's no need for thread-safety synchronization... remember that QueryResultsCache is @RequestScoped
put(cacheKey, result);
return result;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Programmatic
public <T> Value<T> get(final Class<?> callingClass, final String methodName, final Object... keys) {
return get(new Key(callingClass, methodName, keys));
}
@Programmatic
@SuppressWarnings("unchecked")
public <T> Value<T> get(final Key cacheKey) {
Value<T> value = (Value<T>) cache.get(cacheKey);
logHitOrMiss(cacheKey, value);
return value;
}
@Programmatic
public <T> void put(final Key cacheKey, final T result) {
if(LOG.isDebugEnabled()) {
LOG.debug("PUT: " + cacheKey);
}
cache.put(cacheKey, new Value<T>(result));
}
private static void logHitOrMiss(final Key cacheKey, final Value<?> cacheValue) {
if(!LOG.isDebugEnabled()) {
return;
}
String hitOrMiss = cacheValue != null ? "HIT" : "MISS";
LOG.debug( hitOrMiss + ": " + cacheKey.toString());
}
/**
* Not API: for framework to call at end of transaction, to clear out the cache.
*
* <p>
* (This service really ought to be considered
* a transaction-scoped service; since that isn't yet supported by the framework, we have to manually reset).
* </p>
*/
@Programmatic
@Override
public void resetForNextTransaction() {
cache.clear();
}
/**
* In separate class because {@link QueryResultsCache} itself is request-scoped
*/
@DomainService(
nature = NatureOfService.DOMAIN,
menuOrder = "" + Integer.MAX_VALUE
)
public static class Control extends AbstractSubscriber {
@Programmatic
@Subscribe
@org.axonframework.eventhandling.annotation.EventHandler
public void on(FixturesInstallingEvent ev) {
fixturesInstalling = true;
}
@Programmatic
@Subscribe
@org.axonframework.eventhandling.annotation.EventHandler
public void on(FixturesInstalledEvent ev) {
fixturesInstalling = false;
}
private boolean fixturesInstalling;
@Programmatic
public boolean isFixturesInstalling() {
return fixturesInstalling;
}
}
@Inject
protected Control control;
}