/*
* Copyright (c) 2013-2017 Cinchapi 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.cinchapi.concourse.server.plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.LogManager;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import com.cinchapi.concourse.lang.Criteria;
import com.cinchapi.concourse.lang.Language;
import com.cinchapi.concourse.server.plugin.data.ObjectResultDataset;
import com.cinchapi.concourse.server.plugin.data.TObjectResultDataset;
import com.cinchapi.concourse.server.plugin.io.PluginSerializer;
import com.cinchapi.concourse.thrift.ComplexTObject;
import com.cinchapi.concourse.util.ConcurrentMaps;
import com.cinchapi.concourse.util.Convert;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.matcher.Matchers;
/**
* Every Plugin application has a single instance of the
* {@link ConcourseRuntime} class that allows the Plugin to interact with the
* local Concourse Server node from which the Plugin was launched.
*
* <p>
* The current runtime can be obtained using provided the {@link #getRuntime()}
* method. For convenience, each {@link Plugin} provides access to the runtime
* via the {@link Plugin#runtime} variable which is available to all
* implementing applications.
* </p>
*
* @author Jeff Nelson
*/
public class ConcourseRuntime extends StatefulConcourseService {
static {
// turn off logging from java.util.logging
LogManager.getLogManager().reset();
}
/**
* Responsible for taking arbitrary objects and turning them into binary so
* they can be sent across the wire.
*/
private static PluginSerializer serializer = new PluginSerializer();
/**
* Return the runtime instance associated with the current plugin.
*
* @return the {@link ConcourseRuntime} instance associated with the current
* plugin
*/
@NoGuice
public static ConcourseRuntime getRuntime() {
return INSTANCE;
}
/**
* Return a {@link ConcourseRuntime} that is configured to intercept method
* calls and proxy them through the {@link #invokeServer(String, Object...)}
* method.
*
* @return a {@link ConcourseRuntime} instance
*/
@NoGuice
private static ConcourseRuntime init() {
Injector injector = Guice.createInjector(new ServerInvokerModule());
ConcourseRuntime runtime = injector.getInstance(ConcourseRuntime.class);
return runtime;
}
/**
* Invoke {@code method} with {@code args} on the local Concourse Server
* instance that is associated with this {@link ConcourseRuntime runtime}.
*
* @param method the name of the method to invoke
* @param args the args to pass to the method
* @return the result of the method invocation
*/
@NoGuice
@SuppressWarnings("unchecked")
private static <T> T invokeServer(String method, Object... args) {
try {
ConcourseRuntimeAuthorized thread = (ConcourseRuntimeAuthorized) Thread
.currentThread();
List<ComplexTObject> targs = Lists
.newArrayListWithCapacity(args.length);
Collection<Integer> valueTransform = VALUE_TRANSFORM.get(method);
Collection<Integer> criteriaTransform = CRITERIA_TRANSFORM
.get(method);
for (int i = 0; i < args.length; ++i) {
// Must go through each parameters and transform generic value
// objects into TObjects and all Criteria into TCriteria.
Object arg = args[i];
if(valueTransform.contains(i)) {
if(arg instanceof List) {
arg = Convert.javaListToThrift((List<Object>) arg);
}
else if(arg instanceof Set) {
arg = Convert.javaSetToThrift((Set<Object>) arg);
}
else if(arg instanceof Map) {
arg = Convert.javaMapToThrift((Map<?, Object>) arg);
}
else {
arg = Convert.javaToThrift(arg);
}
}
else if(criteriaTransform.contains(i)) {
arg = Language.translateToThriftCriteria((Criteria) arg);
}
targs.add(ComplexTObject.fromJavaObject(arg));
}
// Send a RemoteMethodRequest to the server, asking that the locally
// invoked method be executed. The result will be placed on the
// current thread's response queue
RemoteMethodResponse response;
synchronized (thread.accessToken()) {
RemoteMethodRequest request = new RemoteMethodRequest(method,
thread.accessToken(), thread.transactionToken(),
thread.environment(), targs);
ByteBuffer buffer = serializer.serialize(request);
thread.outgoing().write(buffer);
response = ConcurrentMaps.waitAndRemove(thread.responses(),
thread.accessToken());
}
if(!response.isError()) {
Object ret = response.response.getJavaObject();
if(ret instanceof ByteBuffer) {
// CON-509: PluginSerializable objects will be wrapped
// within a ComplexTObject as BINARY data
ret = serializer.deserialize((ByteBuffer) ret);
}
if(RETURN_TRANSFORM.contains(method)) {
// Must transform the TObject(s) from the server into
// standard java objects to conform with the
// StatefulConcourseService interface.
if(ret instanceof TObjectResultDataset) {
ret = new ObjectResultDataset(
(TObjectResultDataset) ret);
}
else {
ret = Convert.possibleThriftToJava(ret);
}
}
return (T) ret;
}
else {
throw Throwables.propagate(response.error);
}
}
catch (ClassCastException e) {
throw new RuntimeException("Illegal attempt to use "
+ ConcourseRuntime.class.getSimpleName()
+ " from an unsupported thread");
}
}
/**
* Singleton instance so that multiple plugins in the same distribution do
* not unnecessarily create multiple runtime objects.
*/
private static final ConcourseRuntime INSTANCE = init();
/**
* Construct a new instance.
*/
@Inject
protected ConcourseRuntime() {
// NOTE: Routing to the correct Concourse Server instance is handled via
// communication channels stored in each thread that accesses this
// instance.
}
/**
* An internal annotation used to denote that a method call should not be
* proxied via the {@link ServerInvoker}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NoGuice {}
/**
* An object that intercepts method invocations and proxies them to the
* {@link ConcourseRuntime#invokeServer(String, Object...)} method.
*
* @author Jeff Nelson
*/
static class ServerInvoker implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return invokeServer(invocation.getMethod().getName(),
invocation.getArguments());
}
}
/**
* A module that binds all methods not annotated with {@link NoGuice} to use
* the {@link ServerInvoker} intercepter.
*
* @author Jeff Nelson
*/
static class ServerInvokerModule extends AbstractModule {
@Override
protected void configure() {
bindInterceptor(Matchers.subclassesOf(ConcourseRuntime.class),
Matchers.not(Matchers.annotatedWith(NoGuice.class)),
new ServerInvoker());
}
}
}