/* Copyright (c) 2008 Google Inc.
*
* 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.google.gdata.model;
import com.google.gdata.util.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gdata.model.Schema.RootKey;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Set;
/**
* A mutable, thread-safe registry for metadata. This should be used
* to construct the default metadata for elements and attributes, the transforms
* on those default metadata instances, and any adaptations on the element
* types. The {@link #createSchema} method can be called to construct an
* immutable {@link Schema} from this builder.
*
* <p>Thread safety is guaranteed by always guarding access to the element and
* attribute maps by synchronizing on the builder itself. The individual
* element and attribute registries are also thread safe.
*
*
*/
public final class MetadataRegistry {
// The attribute registries that store the attribute data, indexed by ID.
private final Map<RootKey, AttributeMetadataRegistryBuilder> attributes;
// The element registries that store the element data, indexed by ID and
// base element type.
private final Map<RootKey, ElementMetadataRegistryBuilder> elements;
// The cached schema built from this builder. Will be set to null if the
// registry has been modified since last being built.
private volatile Schema cachedSchema;
/**
* Constructs a new empty metadata registry.
*/
public MetadataRegistry() {
this.attributes = Maps.newHashMap();
this.elements = Maps.newHashMap();
}
/**
* Copy constructor, creates a copy of the given metadata registry. This is
* equivalent to {@code new MetadataRegistry().merge(source)}.
*/
public MetadataRegistry(MetadataRegistry source) {
this();
merge(source);
}
/**
* Merges another metadata registry into this metadata registry. Both
* registries are locked during this time, first this registry and then the
* other registry. Do not attempt to do a.merge(b) and b.merge(a) in separate
* threads at the same time or a deadlock may occur.
*/
public synchronized MetadataRegistry merge(MetadataRegistry other) {
synchronized (other) {
for (Map.Entry<RootKey, AttributeMetadataRegistryBuilder> entry
: other.attributes.entrySet()) {
RootKey key = entry.getKey();
AttributeMetadataRegistryBuilder builder = attributes.get(key);
if (builder == null) {
builder = new AttributeMetadataRegistryBuilder(this);
attributes.put(key, builder);
}
builder.merge(entry.getValue());
}
for (Map.Entry<RootKey, ElementMetadataRegistryBuilder> entry
: other.elements.entrySet()) {
RootKey key = entry.getKey();
ElementMetadataRegistryBuilder builder = elements.get(key);
if (builder == null) {
builder = new ElementMetadataRegistryBuilder(this);
elements.put(key, builder);
}
builder.merge(entry.getValue());
}
}
return this;
}
/**
* Registers the metadata for an element key. This will call the
* "registerMetadata" method on the {@link Element} subclass that the key
* refers to, if it refers to a subclass. If it refers to Element directly
* it will just build the key.
*
* @throws IllegalArgumentException if the element type does not support a
* registration method.
*/
public MetadataRegistry register(ElementKey<?, ?> key) {
if (key != null) {
Class<? extends Element> elementType = key.getElementType();
if (Element.class == elementType) {
build(key);
} else {
registerClass(elementType);
}
}
return this;
}
/**
* Registers the metadata from an element subclass using its
* "registerMetadata" method through reflection.
*/
public synchronized MetadataRegistry registerClass(
Class<? extends Element> clazz) {
// Element itself does not require registration, so skip it.
if (Element.class == clazz) {
return this;
}
try {
Method registerMethod = clazz.getDeclaredMethod(
"registerMetadata", MetadataRegistry.class);
if (!Modifier.isStatic(registerMethod.getModifiers())) {
throw new IllegalArgumentException("Class " + clazz
+ " had a non-static registerMetadata(MetadataRegistry) method.");
}
registerMethod.invoke(null, this);
return this;
} catch (SecurityException e) {
// Something fishy is going on, rethrow a runtime exception.
throw new IllegalArgumentException(e);
} catch (NoSuchMethodException e) {
// No "registerMetadata" method found, throw an exception.
throw new IllegalArgumentException(
"Class " + clazz + " doesn't support metadata registration.", e);
} catch (IllegalAccessException e) {
// Method wasn't public, throw an illegal argument exception.
throw new IllegalArgumentException(e);
} catch (InvocationTargetException e) {
// Our registration method threw an exception! Rethrown the nested
// exception in an illegal argument exception.
throw new IllegalArgumentException(e.getCause());
}
}
/**
* Creates a {@link Schema} out of this registry. Will return a cached
* instance if the registry has not changed since last time create() was
* called.
*/
public Schema createSchema() {
Schema instance = cachedSchema;
return (instance != null) ? instance : buildSchema();
}
/**
* Synchronized to ensure memory visibility into the attribute and element
* maps and also to guarantee that no changes to the registry can occur while
* the immutable schema is being built.
*/
private synchronized Schema buildSchema() {
Schema instance = cachedSchema;
if (instance == null) {
whitelistAttributes();
whitelistElements();
cachedSchema = instance = new Schema(this);
}
return instance;
}
/**
* Dirty this registry. Whoever is modifying this builder must also hold
* onto this builder's lock while modifying it, by using a
* {@code sychronized(registry) ...} block, or this method will throw
* an {@link IllegalStateException}.
*/
void dirty() {
Preconditions.checkState(Thread.holdsLock(this),
"Cannot call dirty() without holding the lock on the registry.");
cachedSchema = null;
}
/**
* Here is what we do to deal with whitelisted attributes:
* 1) Find each transform(parent, child, context)-> whitelisted attributes
* 2) Find composite(parent, child, context)-> all attributes.
* 3) For each attribute not in the whitelist -> hide the attribute.
*/
private void whitelistAttributes() {
for (ElementMetadataRegistryBuilder builder : elements.values()) {
Map<TransformKey, Set<AttributeKey<?>>> whitelistMap =
Maps.newLinkedHashMap();
Map<TransformKey, ElementCreatorImpl> creators = builder.getCreators();
for (Map.Entry<TransformKey, ElementCreatorImpl> entry
: creators.entrySet()) {
TransformKey key = entry.getKey();
ElementCreatorImpl element = entry.getValue();
if (element.getAttributeWhitelist() != null) {
whitelistMap.put(key, element.getAttributeWhitelist());
}
}
for (Map.Entry<TransformKey, Set<AttributeKey<?>>> whitelistEntry
: whitelistMap.entrySet()) {
TransformKey key = whitelistEntry.getKey();
Set<AttributeKey<?>> whitelist = whitelistEntry.getValue();
Set<QName> whitelistNames = Sets.newHashSet();
for (AttributeKey<?> whitelistKey : whitelist) {
whitelistNames.add(whitelistKey.getId());
}
Set<AttributeKey<?>> allAttributes = Sets.newHashSet();
for (Map.Entry<TransformKey, ElementCreatorImpl> entry
: creators.entrySet()) {
if (entry.getKey().matches(key)) {
allAttributes.addAll(entry.getValue().getAttributeSet());
}
}
if (!allAttributes.containsAll(whitelist)) {
Set<AttributeKey<?>> missing = Sets.newHashSet(whitelist);
missing.removeAll(allAttributes);
throw new IllegalStateException(
"Missing attributes! Whitelist specified " + missing
+ " but did not find those attributes.");
}
for (AttributeKey<?> attribute : allAttributes) {
if (!whitelistNames.contains(attribute.getId())) {
ElementKey<?, ?> parent = (ElementKey<?, ?>) key.getKey();
build(parent, attribute, key.getContext()).setVisible(false);
}
}
}
}
}
/**
* Here is what we do to deal with whitelisted children:
* 1) Find each transform(parent, child, context)-> whitelisted children
* 2) Find composite(parent, child, context)-> all children.
* 3) For each child element not in the whitelist -> hide the child element.
*/
private void whitelistElements() {
for (Map.Entry<RootKey, ElementMetadataRegistryBuilder> rootEntry
: elements.entrySet()) {
ElementMetadataRegistryBuilder builder = rootEntry.getValue();
Map<TransformKey, Set<ElementKey<?, ?>>> whitelistMap =
Maps.newLinkedHashMap();
Map<TransformKey, ElementCreatorImpl> creators = builder.getCreators();
for (Map.Entry<TransformKey, ElementCreatorImpl> entry
: creators.entrySet()) {
TransformKey key = entry.getKey();
ElementCreatorImpl element = entry.getValue();
if (element.getElementWhitelist() != null) {
whitelistMap.put(key, element.getElementWhitelist());
}
}
for (Map.Entry<TransformKey, Set<ElementKey<?, ?>>> whitelistEntry
: whitelistMap.entrySet()) {
TransformKey key = whitelistEntry.getKey();
Set<ElementKey<?, ?>> whitelist = whitelistEntry.getValue();
Set<QName> whitelistNames = Sets.newHashSet();
for (ElementKey<?, ?> whitelistKey : whitelist) {
whitelistNames.add(whitelistKey.getId());
}
Set<ElementKey<?, ?>> allChildren = Sets.newHashSet();
for (Map.Entry<TransformKey, ElementCreatorImpl> entry
: creators.entrySet()) {
if (entry.getKey().matches(key)) {
allChildren.addAll(entry.getValue().getElementSet());
}
}
if (!allChildren.containsAll(whitelist)) {
Set<ElementKey<?, ?>> missing = Sets.newHashSet(whitelist);
missing.removeAll(allChildren);
throw new IllegalStateException(
"Missing children! Whitelist specified " + missing
+ " but did not find those child elements.");
}
for (ElementKey<?, ?> child : allChildren) {
if (!whitelistNames.contains(child.getId())) {
ElementKey<?, ?> parent = (ElementKey<?, ?>) key.getKey();
build(parent, child, key.getContext()).setVisible(false);
}
}
}
}
}
/**
* Returns {@code true} if the given key has already been registered. This
* is used to prevent reentrant metadata registration (and cycles).
*/
public boolean isRegistered(ElementKey<?, ?> key) {
RootKey rootKey = Schema.getRootKey(key);
ElementMetadataRegistryBuilder elementRegistry = elements.get(rootKey);
if (elementRegistry != null) {
return elementRegistry.isRegistered(null, key, null);
}
return false;
}
/**
* Builds element metadata for the given key. This will create the default
* metadata for that key. If the key's element type has a superclass with
* existing metadata registered at the same ID, the new key will inherit
* any existing properties.
*/
public ElementCreator build(ElementKey<?, ?> element) {
return build(null, element, null);
}
/**
* Builds metadata for when the {@code key} is inside the {@code parent}.
* Note that this will not declare the element as part of the parent element,
* that must be done using {@link ElementCreator#addElement(ElementKey)} or
* {@link ElementCreator#replaceElement(ElementKey)}.
*/
public ElementCreator build(ElementKey<?, ?> parent,
ElementKey<?, ?> element) {
return build(parent, element, null);
}
/**
* Builds metadata for when the {@code key} is used in a context compatible
* with {@code context}.
*/
public ElementCreator build(ElementKey<?, ?> element,
MetadataContext context) {
return build(null, element, context);
}
/**
* Builds metadata for when the {@code key} is inside the {@code parent} and
* used in a context compatible with {@code context}. Note that this will not
* declare the element as part of the parent element, that must be done using
* {@link ElementCreator#addElement(ElementKey)} or
* {@link ElementCreator#replaceElement(ElementKey)}.
*
* <p>This will also guarantee that any element types that are referenced
* have been registered in this registry.
*/
public ElementCreator build(ElementKey<?, ?> parent,
ElementKey<?, ?> element, MetadataContext context) {
ElementCreatorImpl creator = getOrCreateElement(element).build(
parent, element, context);
// If there is a parent or context this isn't a basic registration, so force
// basic registration to take place.
if (parent != null || context != null) {
register(element);
}
return creator;
}
/**
* Returns the existing element registry for the given key, or creates it if
* it does not already exist.
*/
private synchronized ElementMetadataRegistryBuilder getOrCreateElement(
ElementKey<?, ?> key) {
RootKey rootKey = Schema.getRootKey(key);
ElementMetadataRegistryBuilder elementRegistry = elements.get(rootKey);
if (elementRegistry == null) {
elementRegistry = new ElementMetadataRegistryBuilder(this);
elements.put(rootKey, elementRegistry);
}
dirty();
return elementRegistry;
}
/**
* Builds the metadata for the attribute inside the parent. Note that this
* will not declare the attribute as part of the parent element, that must be
* done using {@link ElementCreator#addAttribute(AttributeKey)} or
* {@link ElementCreator#replaceAttribute(AttributeKey)}.
*/
public AttributeCreator build(
ElementKey<?, ?> parent, AttributeKey<?> attribute) {
return build(parent, attribute, null);
}
/**
* Builds the metadata for the attribute inside the parent, during the
* context. Note that this will not declare the attribute as part of the
* parent element, that must be done using
* {@link ElementCreator#addAttribute(AttributeKey)} or
* {@link ElementCreator#replaceAttribute(AttributeKey)}.
*/
public AttributeCreator build(ElementKey<?, ?> parent,
AttributeKey<?> attribute, MetadataContext context) {
return getOrCreateAttribute(attribute).build(parent, attribute, context);
}
/**
* Returns the existing element registry for the given key, or creates it if
* it does not already exist.
*/
private synchronized AttributeMetadataRegistryBuilder getOrCreateAttribute(
AttributeKey<?> key) {
RootKey rootKey = Schema.getRootKey(key);
AttributeMetadataRegistryBuilder attRegistry = attributes.get(rootKey);
if (attRegistry == null) {
attRegistry = new AttributeMetadataRegistryBuilder(this);
attributes.put(rootKey, attRegistry);
}
dirty();
return attRegistry;
}
/**
* Adapts from the source type to the adaptation type on the given kind.
*/
public <D, E extends Element> void adapt(ElementKey<D, E> source,
String kind, ElementKey<? extends D, ? extends E> adaptation) {
build(source).adapt(kind, adaptation);
}
/**
* Package level read-only access to the attributes.
*/
Map<RootKey, AttributeMetadataRegistryBuilder> getAttributes() {
return ImmutableMap.copyOf(attributes);
}
/**
* Package level read-only access to the elements.
*/
Map<RootKey, ElementMetadataRegistryBuilder> getElements() {
return ImmutableMap.copyOf(elements);
}
}