/*******************************************************************************
* 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.wink.server.internal.registry;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.wink.common.DynamicResource;
import org.apache.wink.common.RuntimeContext;
import org.apache.wink.common.internal.i18n.Messages;
import org.apache.wink.common.internal.lifecycle.LifecycleManagersRegistry;
import org.apache.wink.common.internal.lifecycle.ObjectFactory;
import org.apache.wink.common.internal.registry.Injectable;
import org.apache.wink.common.internal.registry.Injectable.ParamType;
import org.apache.wink.common.internal.registry.metadata.ClassMetadata;
import org.apache.wink.common.internal.registry.metadata.MethodMetadata;
import org.apache.wink.common.internal.registry.metadata.ResourceMetadataCollector;
import org.apache.wink.common.internal.uritemplate.UriTemplateProcessor;
import org.apache.wink.server.internal.ServerCustomProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ResourceRecordFactory {
private static final Logger logger =
LoggerFactory
.getLogger(ResourceRecordFactory.class);
private final LifecycleManagersRegistry lifecycleManagerRegistry;
private final Map<Class<?>, ResourceRecord> cacheByClass;
private Lock readersLock;
private Lock writersLock;
final private boolean isStrictConsumesProduces;
public ResourceRecordFactory(LifecycleManagersRegistry lifecycleManagerRegistry) {
this(lifecycleManagerRegistry, new Properties());
}
public ResourceRecordFactory(LifecycleManagersRegistry lifecycleManagerRegistry,
Properties customProperties) {
if (lifecycleManagerRegistry == null) {
throw new NullPointerException("lifecycleManagerRegistry"); //$NON-NLS-1$
}
this.lifecycleManagerRegistry = lifecycleManagerRegistry;
this.cacheByClass = new HashMap<Class<?>, ResourceRecord>();
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readersLock = readWriteLock.readLock();
writersLock = readWriteLock.writeLock();
if (customProperties == null) {
customProperties = new Properties();
}
String value =
customProperties
.getProperty(ServerCustomProperties.STRICT_INTERPRET_CONSUMES_PRODUCES_SPEC_CUSTOM_PROPERTY
.getPropertyName(),
ServerCustomProperties.STRICT_INTERPRET_CONSUMES_PRODUCES_SPEC_CUSTOM_PROPERTY
.getDefaultValue());
isStrictConsumesProduces = Boolean.valueOf(value);
}
/**
* Gets a resource record from a cache of records for the specified resource
* class. If there is no record in the cache, then a new record is created
*
* @param cls the resource class to get the record for
* @return ResourceRecord for the resource class
*/
public ResourceRecord getResourceRecord(Class<?> cls) {
readersLock.lock();
try {
ResourceRecord record = cacheByClass.get(cls);
if (record == null) {
ObjectFactory<?> of = lifecycleManagerRegistry.getObjectFactory(cls);
readersLock.unlock();
try {
record = createStaticResourceRecord(cls, of);
} finally {
readersLock.lock();
}
}
return record;
} finally {
readersLock.unlock();
}
}
/**
* Gets a root resource record from a cache of records for the specified
* resource instance. This is a shortcut for {@code
* getResourceRecord(instance, true)}
*
* @param instance the resource instance to get the record for
* @return ResourceRecord for the resource instance
*/
public ResourceRecord getResourceRecord(Object instance) {
return getResourceRecord(instance, true);
}
/**
* Gets a resource record from a cache of records for the specified resource
* instance. If there is no record in the cache, or if the instance is a
* dynamic resource, then a new record is created
*
* @param instance the resource instance to get the record for
* @param isRootResource specifies whether the instance is a root resource
* (true) or sub-resource (false)
* @return ResourceRecord for the resource instance
*/
public ResourceRecord getResourceRecord(Object instance, boolean isRootResource) {
Class<? extends Object> cls = instance.getClass();
ResourceRecord record = null;
readersLock.lock();
try {
// if this is a root resource
if (isRootResource) {
if (ResourceMetadataCollector.isStaticResource(cls)) {
// if this is a static resource, and use cache
record = cacheByClass.get(cls);
if (record == null) {
ObjectFactory<?> of = lifecycleManagerRegistry.getObjectFactory(instance);
readersLock.unlock();
try {
record = createStaticResourceRecord(cls, of);
} finally {
readersLock.lock();
}
}
} else if (ResourceMetadataCollector.isDynamicResource(cls)) {
// if this is a dynamic resource, don't use cache
ObjectFactory<?> of = lifecycleManagerRegistry.getObjectFactory(instance);
readersLock.unlock();
try {
record = createDynamicResourceRecord((DynamicResource)instance, of);
} finally {
readersLock.lock();
}
} else {
throw new IllegalArgumentException(Messages
.getMessage("rootResourceInstanceIsAnInvalidResource", instance.getClass() //$NON-NLS-1$
.getCanonicalName()));
}
} else {
// if this is a sub-resource, don't use cache, and don't use the
// life-cycle manager
ObjectFactory<?> of = new InstanceObjectFactory<Object>(instance);
readersLock.unlock();
try {
record = createSubResourceRecord(instance, of);
} finally {
readersLock.lock();
}
}
return record;
} finally {
readersLock.unlock();
}
}
private ResourceRecord createStaticResourceRecord(Class<? extends Object> cls,
ObjectFactory<?> of) {
ClassMetadata metadata = createMetadata(cls);
UriTemplateProcessor processor = createUriTemplateProcessor(metadata);
ResourceRecord record = new ResourceRecord(metadata, of, processor);
writersLock.lock();
try {
// double check so as not to put the same resource twice
if (cacheByClass.get(cls) == null) {
cacheByClass.put(cls, record);
}
} finally {
writersLock.unlock();
}
return record;
}
private ResourceRecord createDynamicResourceRecord(DynamicResource instance, ObjectFactory<?> of) {
Class<? extends Object> cls = instance.getClass();
ClassMetadata metadata = createMetadata(cls);
metadata = fixInstanceMetadata(metadata, instance);
UriTemplateProcessor processor = createUriTemplateProcessor(metadata);
return new ResourceRecord(metadata, of, processor);
}
private ResourceRecord createSubResourceRecord(Object instance, ObjectFactory<?> of) {
Class<? extends Object> cls = instance.getClass();
ClassMetadata metadata = createMetadata(cls);
return new ResourceRecord(metadata, of, null);
}
private ClassMetadata createMetadata(Class<? extends Object> cls) {
ClassMetadata md = ResourceMetadataCollector.collectMetadata(cls);
md = fixConsumesAndProduces(md);
return md;
}
private UriTemplateProcessor createUriTemplateProcessor(ClassMetadata metadata) {
// create the resource path using the parents paths
StringBuilder path = new StringBuilder();
// Recursively append parent paths
appendPathWithParent(metadata, path);
// create the processor
return UriTemplateProcessor.newNormalizedInstance(path.toString());
}
private void appendPathWithParent(ClassMetadata metadata, StringBuilder pathStr) {
ResourceRecord parentRecord = getParent(metadata);
if (parentRecord != null) {
ClassMetadata parentMetadata = parentRecord.getMetadata();
appendPathWithParent(parentMetadata, pathStr);
}
String path = UriTemplateProcessor.normalizeUri(metadata.getPath());
if (!path.endsWith("/")) { //$NON-NLS-1$
pathStr.append("/"); //$NON-NLS-1$
}
pathStr.append(path);
}
private ResourceRecord getParent(ClassMetadata metadata) {
Class<?> parent = metadata.getParent();
Object parentInstance = metadata.getParentInstance();
ResourceRecord parentRecord = null;
if (parent != null) {
parentRecord = getResourceRecord(parent);
} else if (parentInstance != null) {
parentRecord = getResourceRecord(parentInstance);
}
return parentRecord;
}
/**
* Fixed the metadata to reflect the information stored on the instance of
* the dynamic resource.
*
* @param classMetadata
* @param instance
* @return
*/
private ClassMetadata fixInstanceMetadata(ClassMetadata classMetadata,
DynamicResource dynamicResource) {
String path = dynamicResource.getPath();
if (path != null) {
classMetadata.addPath(path);
if (logger.isTraceEnabled()) {
logger.trace("Adding dispatched path from instance: {}", path); //$NON-NLS-1$
}
}
Object parent = dynamicResource.getParent();
if (parent != null) {
classMetadata.getParentInstances().add(parent);
if (logger.isTraceEnabled()) {
logger.trace("Adding parent beans from instance: {}", parent); //$NON-NLS-1$
}
}
String workspaceTitle = dynamicResource.getWorkspaceTitle();
if (workspaceTitle != null) {
classMetadata.setWorkspaceName(workspaceTitle);
}
String collectionTitle = dynamicResource.getCollectionTitle();
if (collectionTitle != null) {
classMetadata.setCollectionTitle(collectionTitle);
}
return classMetadata;
}
/**
* This method will go through each method and "fix" the method metadata to
* "ignore" inherited {@link Consumes} and {@link Produces} annotations when
* appropriate. For Produces, if the return type is void, then ignore the
* Produces annotation. For Consumes, if there are no entity parameters,
* ignore the Consumes annotation.
*
* @param classMetadata
* @return
*/
ClassMetadata fixConsumesAndProduces(ClassMetadata classMetadata) {
logger.trace("fixConsumesAndProduces({}) entry", classMetadata);
if (isStrictConsumesProduces) {
logger
.trace("fixConsumesAndProduces() exit returning because custom property {} is set to true.",
ServerCustomProperties.STRICT_INTERPRET_CONSUMES_PRODUCES_SPEC_CUSTOM_PROPERTY
.getPropertyName());
return classMetadata;
}
Set<MediaType> produces = classMetadata.getProduces();
Set<MediaType> consumes = classMetadata.getConsumes();
Set<MethodMetadata> allMethodMetadata = new HashSet<MethodMetadata>();
allMethodMetadata.addAll(classMetadata.getResourceMethods());
allMethodMetadata.addAll(classMetadata.getSubResourceMethods());
/*
* Ignore subresource locators because a) they have to return a non-void
* and b) they aren't allowed to have an entity parameter.
*/
/* fix the produces */
for (MethodMetadata methodMetadata : allMethodMetadata) {
Method method = methodMetadata.getReflectionMethod();
if (Void.TYPE.equals(method.getReturnType())) {
if (produces.size() > 0 && methodMetadata.getProduces().equals(produces)) {
/*
* let's assume this was inherited now. weird case would be
* they repeated the annotation values in both the class and
* method.
*/
methodMetadata.addProduces(MediaType.WILDCARD_TYPE);
logger
.trace("Method has a @Produces value but also a void return type so adding a */* to allow any response: {} ",
methodMetadata);
}
}
}
/* fix the consumes */
for (MethodMetadata methodMetadata : allMethodMetadata) {
if (consumes.size() > 0 && methodMetadata.getConsumes().equals(consumes)) {
List<Injectable> params = methodMetadata.getFormalParameters();
boolean isEntityParamFound = false;
for (Injectable p : params) {
if (ParamType.ENTITY.equals(p.getParamType())) {
isEntityParamFound = true;
}
}
/*
* let's assume this was inherited now. weird case would be they
* repeated the annotation values in both the class and method.
*/
if (!isEntityParamFound) {
methodMetadata.addConsumes(MediaType.WILDCARD_TYPE);
logger
.trace("Method has a @Consumes value but no entity parameter so adding a */* to allow any request: {} ",
methodMetadata);
}
}
}
logger.trace("fixConsumesAndProduces() exit returning {}", classMetadata);
return classMetadata;
}
private static class InstanceObjectFactory<T> implements ObjectFactory<T> {
private final T object;
public InstanceObjectFactory(T object) {
this.object = object;
}
public T getInstance(RuntimeContext context) {
return object;
}
@SuppressWarnings("unchecked")
public Class<T> getInstanceClass() {
return (Class<T>)object.getClass();
}
@Override
public String toString() {
return String.format("InstanceObjectFactory: %s", getInstanceClass()); //$NON-NLS-1$
}
public void releaseInstance(T instance, RuntimeContext context) {
/* do nothing */
}
public void releaseAll(RuntimeContext context) {
/* do nothing */
}
}
}