package org.springframework.roo.metadata;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.ReferenceStrategy;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.metadata.internal.AbstractMetadataCache;
import org.springframework.roo.metadata.internal.MetadataDependencyRegistryTracker;
/**
* Default implementation of {@link MetadataService}.
* <p>
* This implementation is not thread safe. It should only be accessed by a
* single thread at a time. This is enforced by the process manager semantics,
* so we avoid the cost of re-synchronization here.
*
* @author Ben Alex
* @author Enrique Ruiz at DISID Corporation S.L.
* @since 1.0
*/
@Component
@Service
@Reference(name = "metadataProvider", strategy = ReferenceStrategy.EVENT,
policy = ReferencePolicy.DYNAMIC, referenceInterface = MetadataProvider.class,
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE)
public class DefaultMetadataService extends AbstractMetadataCache implements MetadataService {
@Reference
private MetadataLogger metadataLogger;
// Request control
// List to assist output "stacks"which show the order of requests
private final List<String> activeRequests = new ArrayList<String>();
private int cacheEvictions = 0;
private int cacheHits = 0;
private int cacheMisses = 0;
private int cachePuts = 0;
// List to help us verify correct operation through logs (predictable
// ordering)
private final List<String> keysToRetry = new ArrayList<String>();
// Mutex
private final Object lock = new Object();
private final Map<String, MetadataProvider> providerMap = new HashMap<String, MetadataProvider>();
private final Set<MetadataProvider> providers = new HashSet<MetadataProvider>();
private int recursiveGets = 0;
private int validGets = 0;
protected MetadataDependencyRegistryTracker registryTracker = null;
/**
* This service is being activated so setup it:
* <ul>
* <li>Create and open the {@link MetadataDependencyRegistryTracker}.</li>
* </ul>
*/
protected void activate(final ComponentContext context) {
this.registryTracker = new MetadataDependencyRegistryTracker(context.getBundleContext(), this);
this.registryTracker.open();
}
/**
* This service is being deactivated so unregister upstream-downstream
* dependencies, triggers, matchers and listeners.
*
* @param context
*/
protected void deactivate(final ComponentContext context) {
MetadataDependencyRegistry registry = this.registryTracker.getService();
registry.removeNotificationListener(this);
this.registryTracker.close();
}
protected void bindMetadataProvider(final MetadataProvider mp) {
synchronized (lock) {
Validate.notNull(mp, "Metadata provider required");
final String mid = mp.getProvidesType();
Validate.isTrue(MetadataIdentificationUtils.isIdentifyingClass(mid),
"Metadata provider '%s' violated interface contract by returning '%s'", mp, mid);
Validate.isTrue(!providerMap.containsKey(mid),
"Metadata provider '%s' already is providing metadata for '%s'", providerMap.get(mid),
mid);
providers.add(mp);
providerMap.put(mid, mp);
}
}
@Override
public void evict(final String metadataIdentificationString) {
synchronized (lock) {
// Clear my own cache (which also verifies the argument is valid at
// the same time)
super.evict(metadataIdentificationString);
// Finally, evict downstream dependencies (ie metadata that
// previously depended on this now-evicted metadata)
MetadataDependencyRegistry registry = this.registryTracker.getService();
for (final String downstream : registry.getDownstream(metadataIdentificationString)) {
// We only need to evict if it is an instance, as only an
// instance will ever go into the cache
if (MetadataIdentificationUtils.isIdentifyingInstance(downstream)) {
evict(downstream);
}
}
}
}
@Override
public void evictAll() {
synchronized (lock) {
// Clear my own cache
super.evictAll();
// Clear the caches of any metadata providers which support the
// interface
for (final MetadataProvider p : providers) {
if (p instanceof MetadataCache) {
((MetadataCache) p).evictAll();
}
}
}
}
@SuppressWarnings("unchecked")
public <T extends MetadataItem> T evictAndGet(final String metadataIdentificationString) {
return (T) getInternal(metadataIdentificationString, true, false);
}
@SuppressWarnings("unchecked")
public <T extends MetadataItem> T get(final String metadataIdentificationString) {
return (T) get(metadataIdentificationString, false);
}
public MetadataItem get(final String metadataIdentificationString, final boolean evictCache) {
return getInternal(metadataIdentificationString, evictCache, true);
}
private MetadataItem getInternal(final String metadataIdentificationString,
final boolean evictCache, final boolean cacheRetrievalAllowed) {
Validate.isTrue(
MetadataIdentificationUtils.isIdentifyingInstance(metadataIdentificationString),
"Metadata identification string '%s' does not identify a metadata instance",
metadataIdentificationString);
synchronized (lock) {
validGets++;
try {
metadataLogger.startEvent();
// Do some cache eviction if the caller requested it
if (evictCache) {
evict(metadataIdentificationString);
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Evicting " + metadataIdentificationString);
}
cacheEvictions++;
}
// We can use the cache even for a recursive get (unless of
// course the caller has prevented it)
if (cacheRetrievalAllowed) {
// Try the cache first
final MetadataItem result = getFromCache(metadataIdentificationString);
if (result != null) {
cacheHits++;
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Cache hit " + metadataIdentificationString);
}
return result;
}
}
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Cache miss " + metadataIdentificationString);
}
cacheMisses++;
// Determine if this MID was already requested earlier. We need
// to stop these infinite requests from occurring.
if (activeRequests.contains(metadataIdentificationString)) {
recursiveGets++;
if (!keysToRetry.contains(metadataIdentificationString)) {
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Blocked recursive request for " + metadataIdentificationString);
}
keysToRetry.add(metadataIdentificationString);
}
return null;
}
// Get the destination
final String mdClassId =
MetadataIdentificationUtils.getMetadataClassId(metadataIdentificationString);
final MetadataProvider p = providerMap.get(mdClassId);
Validate
.notNull(
p,
"No metadata provider is currently registered to provide metadata for identifier '%s' (class '%s')",
metadataIdentificationString, mdClassId);
// Infinite loop management
activeRequests.add(metadataIdentificationString);
// Obtain the item
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Get " + metadataIdentificationString + " from "
+ p.getClass().getName());
}
MetadataItem result = null;
try {
metadataLogger.startTimer(p.getClass().getName());
result = p.get(metadataIdentificationString);
} finally {
metadataLogger.stopTimer();
}
// If the item isn't available, evict it from the cache (unless
// we did so at the start of the method already)
if (result == null && !evictCache) {
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Evicting unavailable item " + metadataIdentificationString);
}
evict(metadataIdentificationString);
cacheEvictions++;
}
// Put into the cache, provided it isn't null
if (result != null) {
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Caching " + metadataIdentificationString);
}
super.put(result);
cachePuts++;
}
activeRequests.remove(metadataIdentificationString);
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Returning " + metadataIdentificationString);
}
return result;
} catch (final Exception e) {
activeRequests.remove(metadataIdentificationString);
throw new IllegalStateException(e);
} finally {
// We use another try..finally block as we want to ensure
// exceptions don't prevent our metadataLogger.stopEvent()
try {
// Have we processed all requests? If so, handle any retries
// we recorded
if (activeRequests.isEmpty()) {
final List<String> thisRetry = new ArrayList<String>();
thisRetry.addAll(keysToRetry);
keysToRetry.clear();
if (metadataLogger.getTraceLevel() > 0 && thisRetry.size() > 0) {
metadataLogger.log(thisRetry.size() + " keys to retry: " + thisRetry);
}
for (final String retryMid : thisRetry) {
// Important: we should not evict any prior version
// from the cache (an interim version is
// acceptable).
// We discard the result of the get; this is purely
// to facilitate updating metadata stored in memory
// and on-disk
if (metadataLogger.getTraceLevel() > 0) {
metadataLogger.log("Retrying " + retryMid);
}
if (ObjectUtils.equals(retryMid, metadataIdentificationString)) {
// Avoid infinite recursion loop
continue;
}
getInternal(retryMid, false, false);
}
if (metadataLogger.getTraceLevel() > 0 && thisRetry.size() > 0) {
metadataLogger.log("Retry group completed " + metadataIdentificationString);
}
}
} finally {
metadataLogger.stopEvent();
}
}
}
}
public void notify(final String upstreamDependency, final String downstreamDependency) {
Validate.isTrue(MetadataIdentificationUtils.isValid(upstreamDependency),
"Upstream dependency is an invalid metadata identification string ('%s')",
upstreamDependency);
Validate.isTrue(MetadataIdentificationUtils.isValid(downstreamDependency),
"Downstream dependency is an invalid metadata identification string ('%s')",
downstreamDependency);
MetadataDependencyRegistry registry = this.registryTracker.getService();
synchronized (lock) {
// Get the destination
final String mdClassId = MetadataIdentificationUtils.getMetadataClassId(downstreamDependency);
final MetadataProvider p = providerMap.get(mdClassId);
if (p == null) {
// No known provider that can consume this notification, so just
// return as per the interface contract
return;
}
if (p instanceof MetadataNotificationListener) {
// The provider can directly handle this notification, so we
// just need to delegate directly to it.
// We rely on the provider to evict items from the cache if
// applicable.
((MetadataNotificationListener) p).notify(upstreamDependency, downstreamDependency);
} else {
// As per interface contract, we just ensure we evict the item
// and recreate it
// However, we only do this if the destination is an instance -
// if it's a class, "get" is not a meaningful operation.
if (MetadataIdentificationUtils.isIdentifyingInstance(downstreamDependency)) {
get(downstreamDependency, true);
}
// As per interface contract, we now notify any listeners this
// downstream instance has probably now changed
registry.notifyDownstream(downstreamDependency);
}
}
}
@Override
public void put(final MetadataItem metadataItem) {
super.put(metadataItem);
cachePuts++;
}
@Override
public final String toString() {
final ToStringBuilder builder = new ToStringBuilder(this);
builder.append("validGets", validGets);
builder.append("recursiveGets", recursiveGets);
builder.append("cachePuts", cachePuts);
builder.append("cacheHits", cacheHits);
builder.append("cacheMisses", cacheMisses);
builder.append("cacheEvictions", cacheEvictions);
builder.append("cacheCurrentSize", getCacheSize());
builder.append("cacheMaximumSize", getMaxCapacity());
return builder.toString().replaceFirst("@[0-9a-f]+", ":");
}
protected void unbindMetadataProvider(final MetadataProvider mp) {
synchronized (lock) {
final String mid = mp.getProvidesType();
providers.remove(mp);
providerMap.remove(mid);
}
}
}