/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.everrest.groovy;
import groovy.lang.GroovyCodeSource;
import org.everrest.core.DependencySupplier;
import org.everrest.core.ObjectFactory;
import org.everrest.core.PerRequestObjectFactory;
import org.everrest.core.ResourceBinder;
import org.everrest.core.ResourcePublicationException;
import org.everrest.core.resource.ResourceDescriptor;
import org.everrest.core.uri.UriPattern;
import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
/**
* Manage via {@link ResourceBinder} Groovy based RESTful services.
*
* @author andrew00x
*/
public class GroovyResourcePublisher {
/** Default character set name. */
protected static final String DEFAULT_CHARSET_NAME = "UTF-8";
/** Default character set. */
protected static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_CHARSET_NAME);
protected final ResourceBinder binder;
protected final Map<ResourceId, String> resources = Collections.synchronizedMap(new HashMap<>());
protected final GroovyClassLoaderProvider classLoaderProvider;
protected final DependencySupplier dependencies;
protected final Comparator<Constructor<?>> CONSTRUCTOR_COMPARATOR = new Comparator<Constructor<?>>() {
@Override
public int compare(Constructor<?> o1, Constructor<?> o2) {
return o2.getParameterTypes().length - o1.getParameterTypes().length;
}
};
/**
* Create GroovyJaxrsPublisher which is able publish per-request and singleton resources. Any required dependencies for per-request
* resource injected by {@link PerRequestObjectFactory}.
*
* @param binder
* resource binder
* @param classLoaderProvider
* GroovyClassLoaderProvider
* @param dependencies
* dependencies resolver
* @see DependencySupplier
*/
protected GroovyResourcePublisher(ResourceBinder binder, GroovyClassLoaderProvider classLoaderProvider,
DependencySupplier dependencies) {
this.binder = binder;
this.classLoaderProvider = classLoaderProvider;
this.dependencies = dependencies;
}
/**
* Create GroovyJaxrsPublisher which is able publish per-request and singleton resources. Any required dependencies for per-request
* resource injected by {@link PerRequestObjectFactory}.
*
* @param binder
* resource binder
* @param dependencies
* dependencies resolver
* @see DependencySupplier
*/
public GroovyResourcePublisher(ResourceBinder binder, DependencySupplier dependencies) {
this(binder, new GroovyClassLoaderProvider(), dependencies);
}
/**
* Get resource corresponded to specified id <code>resourceId</code> .
*
* @param resourceId
* resource id
* @return resource or <code>null</code>
*/
public ObjectFactory<ResourceDescriptor> getResource(ResourceId resourceId) {
String path = resources.get(resourceId);
if (path == null) {
return null;
}
UriPattern pattern = new UriPattern(path);
for (ObjectFactory<ResourceDescriptor> res : binder.getResources()) {
if (res.getObjectModel().getUriPattern().equals(pattern)) {
return res;
}
}
// If resource not exists any more but still in mapping.
resources.remove(resourceId);
return null;
}
/**
* Check is groovy resource with specified id is published or not
*
* @param resourceId
* id of resource to be checked
* @return <code>true</code> if resource is published and <code>false</code>* otherwise
*/
public boolean isPublished(ResourceId resourceId) {
return null != getResource(resourceId);
}
/**
* Parse given stream and publish result as per-request RESTful service.
*
* @param in
* stream which contains groovy source code of RESTful service
* @param resourceId
* id to be assigned to resource
* @param properties
* optional resource properties. This parameter may be <code>null</code>
* @param src
* additional path to Groovy sources
* @param files
* Groovy source files to be added in build path directly
* @throws ResourcePublicationException
* see {@link ResourceBinder#addResource(Class, MultivaluedMap)}
*/
public void publishPerRequest(InputStream in, ResourceId resourceId, MultivaluedMap<String, String> properties,
SourceFolder[] src, SourceFile[] files) {
Class<?> rc;
try {
rc = classLoaderProvider.getGroovyClassLoader(src).parseClass(in, resourceId.getId(), files);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e.getMessage());
}
binder.addResource(rc, properties);
resources.put(resourceId, rc.getAnnotation(Path.class).value());
}
/**
* Parse given <code>source</code> and publish result as per-request RESTful service.
*
* @param source
* groovy source code of RESTful service
* @param resourceId
* id to be assigned to resource
* @param properties
* optional resource properties. This parameter may be <code>null</code>
* @param src
* additional path to Groovy sources
* @param files
* Groovy source files to be added in build path directly
* @throws ResourcePublicationException
* see {@link ResourceBinder#addResource(Class, MultivaluedMap)}
*/
public final void publishPerRequest(String source, ResourceId resourceId, MultivaluedMap<String, String> properties,
SourceFolder[] src, SourceFile[] files) {
publishPerRequest(source, DEFAULT_CHARSET, resourceId, properties, src, files);
}
/**
* Parse given <code>source</code> and publish result as per-request RESTful
* service.
*
* @param source
* groovy source code of RESTful service
* @param charset
* source string charset. May be <code>null</code> than default charset will be in use
* @param resourceId
* id to be assigned to resource
* @param properties
* optional resource properties. This parameter may be <code>null</code>.
* @param src
* additional path to Groovy sources
* @param files
* Groovy source files to be added in build path directly
* @throws UnsupportedCharsetException
* if <code>charset</code> is unsupported
* @throws ResourcePublicationException
* see {@link ResourceBinder#addResource(Class, MultivaluedMap)}
*/
public final void publishPerRequest(String source, String charset, ResourceId resourceId,
MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files) {
publishPerRequest(source, charset == null ? DEFAULT_CHARSET : Charset.forName(charset), resourceId, properties, src, files);
}
/**
* Parse given stream and publish result as singleton RESTful service.
*
* @param in
* stream which contains groovy source code of RESTful service
* @param resourceId
* id to be assigned to resource
* @param properties
* optional resource properties. This parameter may be <code>null</code>
* @param src
* additional path to Groovy sources
* @param files
* Groovy source files to be added in build path directly
* @throws ResourcePublicationException
* see {@link ResourceBinder#addResource(Object, MultivaluedMap)}
*/
public void publishSingleton(InputStream in, ResourceId resourceId, MultivaluedMap<String, String> properties,
SourceFolder[] src, SourceFile[] files) {
Class<?> rc;
try {
rc = classLoaderProvider.getGroovyClassLoader(src).parseClass(in, resourceId.getId(), files);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e.getMessage());
}
Object r;
try {
r = createInstance(rc);
} catch (IllegalArgumentException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new ResourcePublicationException(e.getMessage());
}
binder.addResource(r, properties);
resources.put(resourceId, r.getClass().getAnnotation(Path.class).value());
}
/**
* Parse given <code>source</code> and publish result as singleton RESTful service.
*
* @param source
* groovy source code of RESTful service
* @param resourceId
* name of resource
* @param properties
* optional resource properties. This parameter may be
* <code>null</code>.
* @param src
* additional path to Groovy sources
* @param files
* Groovy source files to be added in build path directly
* @throws ResourcePublicationException
* see {@link ResourceBinder#addResource(Object, MultivaluedMap)}
*/
public final void publishSingleton(String source, ResourceId resourceId, MultivaluedMap<String, String> properties,
SourceFolder[] src, SourceFile[] files) {
publishSingleton(source, DEFAULT_CHARSET, resourceId, properties, src, files);
}
/**
* Parse given <code>source</code> and publish result as singleton RESTful service.
*
* @param source
* groovy source code of RESTful service
* @param charset
* source string charset. May be <code>null</code> than default charset will be in use
* @param resourceId
* name of resource
* @param properties
* optional resource properties. This parameter may be <code>null</code>.
* @param src
* additional path to Groovy sources
* @param files
* Groovy source files to be added in build path directly
* @throws UnsupportedCharsetException
* if <code>charset</code> is unsupported
* @throws ResourcePublicationException
* see {@link ResourceBinder#addResource(Object, MultivaluedMap)}
*/
public final void publishSingleton(String source, String charset, ResourceId resourceId,
MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files) {
publishSingleton(source, charset == null ? DEFAULT_CHARSET : Charset.forName(charset), resourceId, properties, src, files);
}
/**
* Unpublish resource with specified id.
*
* @param resourceId
* id of resource to be unpublished
* @return <code>true</code> if resource was published and <code>false</code> otherwise, e.g. because there is not resource corresponded
* to supplied <code>resourceId</code>
*/
public ObjectFactory<ResourceDescriptor> unpublishResource(ResourceId resourceId) {
String path = resources.get(resourceId);
if (path == null) {
return null;
}
ObjectFactory<ResourceDescriptor> resource = binder.removeResource(path);
if (resource != null) {
resources.remove(resourceId);
}
return resource;
}
private void publishPerRequest(String source, Charset charset, ResourceId resourceId,
MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files) {
byte[] bytes = source.getBytes(charset);
publishPerRequest(new ByteArrayInputStream(bytes), resourceId, properties, src, files);
}
private void publishSingleton(String source, Charset charset, ResourceId resourceId,
MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files) {
byte[] bytes = source.getBytes(charset);
publishSingleton(new ByteArrayInputStream(bytes), resourceId, properties, src, files);
}
protected Object createInstance(Class<?> clazz)
throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<?>[] constructors = clazz.getConstructors();
//Sort constructors by number of parameters. With more parameters must be first.
Arrays.sort(constructors, CONSTRUCTOR_COMPARATOR);
l:
for (Constructor<?> c : constructors) {
Class<?>[] parameterTypes = c.getParameterTypes();
if (parameterTypes.length == 0) {
return c.newInstance();
}
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Object param = dependencies.getInstance(parameterTypes[i]);
if (param == null) {
continue l;
}
parameters[i] = param;
}
return c.newInstance(parameters);
}
throw new ResourcePublicationException(String.format("Unable create instance of class %s. Required constructor's dependencies can't be resolved. ", clazz.getName()));
}
/**
* Create {@link GroovyCodeSource} from given stream and name. Code base 'file:/groovy/script/jaxrs' will be used.
*
* @param in
* groovy source code stream
* @param name
* code source name
* @return GroovyCodeSource
*/
protected GroovyCodeSource createCodeSource(InputStream in, String name) {
GroovyCodeSource gcs = new GroovyCodeSource(new BufferedReader(new InputStreamReader(in)), name, "/groovy/script/jaxrs");
gcs.setCachable(false);
return gcs;
}
}