All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openksavi.sponge.remoteapi.client.BaseSpongeClient Maven / Gradle / Ivy

There is a newer version: 1.18.0
Show newest version
/*
 * Copyright 2016-2018 The Sponge authors.
 *
 * 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 org.openksavi.sponge.remoteapi.client;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

import org.apache.commons.lang3.Validate;

import org.openksavi.sponge.action.InactiveActionException;
import org.openksavi.sponge.action.ProvideArgsParameters;
import org.openksavi.sponge.remoteapi.RemoteApiConstants;
import org.openksavi.sponge.remoteapi.client.listener.OnRequestSerializedListener;
import org.openksavi.sponge.remoteapi.client.listener.OnResponseDeserializedListener;
import org.openksavi.sponge.remoteapi.feature.converter.DefaultFeatureConverter;
import org.openksavi.sponge.remoteapi.feature.converter.FeatureConverter;
import org.openksavi.sponge.remoteapi.feature.converter.FeaturesUtils;
import org.openksavi.sponge.remoteapi.model.RemoteActionMeta;
import org.openksavi.sponge.remoteapi.model.RemoteEvent;
import org.openksavi.sponge.remoteapi.model.RemoteKnowledgeBaseMeta;
import org.openksavi.sponge.remoteapi.model.request.ActionCallRequest;
import org.openksavi.sponge.remoteapi.model.request.ActionCallRequest.ActionCallParams;
import org.openksavi.sponge.remoteapi.model.request.ActionExecutionInfo;
import org.openksavi.sponge.remoteapi.model.request.GetActionsRequest;
import org.openksavi.sponge.remoteapi.model.request.GetActionsRequest.GetActionsParams;
import org.openksavi.sponge.remoteapi.model.request.GetEventTypesRequest;
import org.openksavi.sponge.remoteapi.model.request.GetEventTypesRequest.GetEventTypesParams;
import org.openksavi.sponge.remoteapi.model.request.GetFeaturesRequest;
import org.openksavi.sponge.remoteapi.model.request.GetKnowledgeBasesRequest;
import org.openksavi.sponge.remoteapi.model.request.GetVersionRequest;
import org.openksavi.sponge.remoteapi.model.request.IsActionActiveRequest;
import org.openksavi.sponge.remoteapi.model.request.IsActionActiveRequest.IsActionActiveEntry;
import org.openksavi.sponge.remoteapi.model.request.IsActionActiveRequest.IsActionActiveParams;
import org.openksavi.sponge.remoteapi.model.request.LoginRequest;
import org.openksavi.sponge.remoteapi.model.request.LogoutRequest;
import org.openksavi.sponge.remoteapi.model.request.ProvideActionArgsRequest;
import org.openksavi.sponge.remoteapi.model.request.ProvideActionArgsRequest.ProvideActionArgsParams;
import org.openksavi.sponge.remoteapi.model.request.ReloadRequest;
import org.openksavi.sponge.remoteapi.model.request.RequestHeader;
import org.openksavi.sponge.remoteapi.model.request.SendEventRequest;
import org.openksavi.sponge.remoteapi.model.request.SendEventRequest.SendEventParams;
import org.openksavi.sponge.remoteapi.model.request.SpongeRequest;
import org.openksavi.sponge.remoteapi.model.response.ActionCallResponse;
import org.openksavi.sponge.remoteapi.model.response.GetActionsResponse;
import org.openksavi.sponge.remoteapi.model.response.GetActionsResponse.GetActionsResult;
import org.openksavi.sponge.remoteapi.model.response.GetEventTypesResponse;
import org.openksavi.sponge.remoteapi.model.response.GetFeaturesResponse;
import org.openksavi.sponge.remoteapi.model.response.GetKnowledgeBasesResponse;
import org.openksavi.sponge.remoteapi.model.response.GetVersionResponse;
import org.openksavi.sponge.remoteapi.model.response.IsActionActiveResponse;
import org.openksavi.sponge.remoteapi.model.response.LoginResponse;
import org.openksavi.sponge.remoteapi.model.response.LogoutResponse;
import org.openksavi.sponge.remoteapi.model.response.ProvideActionArgsResponse;
import org.openksavi.sponge.remoteapi.model.response.ReloadResponse;
import org.openksavi.sponge.remoteapi.model.response.ResponseError;
import org.openksavi.sponge.remoteapi.model.response.ResponseHeader;
import org.openksavi.sponge.remoteapi.model.response.SendEventResponse;
import org.openksavi.sponge.remoteapi.model.response.SpongeResponse;
import org.openksavi.sponge.remoteapi.type.converter.BaseTypeConverter;
import org.openksavi.sponge.remoteapi.type.converter.DefaultTypeConverter;
import org.openksavi.sponge.remoteapi.type.converter.TypeConverter;
import org.openksavi.sponge.remoteapi.type.converter.unit.ObjectTypeUnitConverter;
import org.openksavi.sponge.remoteapi.util.RemoteApiUtils;
import org.openksavi.sponge.type.DataType;
import org.openksavi.sponge.type.DataTypeKind;
import org.openksavi.sponge.type.ListType;
import org.openksavi.sponge.type.RecordType;
import org.openksavi.sponge.type.TypeType;
import org.openksavi.sponge.type.provided.ProvidedValue;
import org.openksavi.sponge.type.value.AnnotatedValue;
import org.openksavi.sponge.util.DataTypeUtils;
import org.openksavi.sponge.util.SpongeApiUtils;

/**
 * A base Sponge Remote API client.
 */
@SuppressWarnings("rawtypes")
public abstract class BaseSpongeClient implements SpongeClient {

    protected static final boolean DEFAULT_ALLOW_FETCH_METADATA = true;

    protected static final boolean DEFAULT_ALLOW_FETCH_EVENT_TYPE = true;

    private SpongeClientConfiguration configuration;

    private AtomicLong currentRequestId = new AtomicLong(0);

    private AtomicReference currentAuthToken = new AtomicReference<>();

    private TypeConverter typeConverter;

    private FeatureConverter featureConverter;

    private Lock lock = new ReentrantLock(true);

    private LoadingCache actionMetaCache;

    private LoadingCache eventTypeCache;

    private Map featuresCache;

    protected List onRequestSerializedListeners = new CopyOnWriteArrayList<>();

    protected List onResponseDeserializedListeners = new CopyOnWriteArrayList<>();

    public BaseSpongeClient(SpongeClientConfiguration configuration) {
        setConfiguration(configuration);
    }

    @Override
    public SpongeClientConfiguration getConfiguration() {
        return configuration;
    }

    protected void setConfiguration(SpongeClientConfiguration configuration) {
        this.configuration = configuration;

        applyConfiguration();

        initActionMetaCache();
        initEventTypeCache();
    }

    private void applyConfiguration() {
        ObjectMapper mapper = RemoteApiUtils.createObjectMapper();
        mapper.configure(SerializationFeature.INDENT_OUTPUT, configuration.isPrettyPrint());
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

        typeConverter = new DefaultTypeConverter(mapper);
        initObjectTypeMarshalers(typeConverter);

        featureConverter = new DefaultFeatureConverter(mapper);

        typeConverter.setFeatureConverter(featureConverter);
    }

    protected void initObjectTypeMarshalers(TypeConverter typeConverter) {
        ObjectTypeUnitConverter objectConverter =
                (ObjectTypeUnitConverter) ((BaseTypeConverter) typeConverter).getInternalUnitConverter(DataTypeKind.OBJECT);

        if (objectConverter == null) {
            return;
        }

        // Add RemoteEvent marshaler and unmarshaler.
        objectConverter.addMarshaler(RemoteApiConstants.REMOTE_EVENT_OBJECT_TYPE_CLASS_NAME, (TypeConverter converter,
                Object value) -> RemoteApiUtils.marshalRemoteEvent((RemoteEvent) value, converter, eventName -> getEventType(eventName)));
        objectConverter.addUnmarshaler(RemoteApiConstants.REMOTE_EVENT_OBJECT_TYPE_CLASS_NAME, (TypeConverter converter,
                Object value) -> RemoteApiUtils.unmarshalRemoteEvent(value, converter, eventName -> getEventType(eventName)));
    }

    @Override
    public TypeConverter getTypeConverter() {
        return typeConverter;
    }

    @Override
    public void setTypeConverter(TypeConverter typeConverter) {
        this.typeConverter = typeConverter;
    }

    @Override
    public FeatureConverter getFeatureConverter() {
        return featureConverter;
    }

    @Override
    public void setFeatureConverter(FeatureConverter featureConverter) {
        this.featureConverter = featureConverter;
    }

    @Override
    public void addOnRequestSerializedListener(OnRequestSerializedListener listener) {
        onRequestSerializedListeners.add(listener);
    }

    @Override
    public boolean removeOnRequestSerializedListener(OnRequestSerializedListener listener) {
        return onRequestSerializedListeners.remove(listener);
    }

    @Override
    public void addOnResponseDeserializedListener(OnResponseDeserializedListener listener) {
        onResponseDeserializedListeners.add(listener);
    }

    @Override
    public boolean removeOnResponseDeserializedListener(OnResponseDeserializedListener listener) {
        return onResponseDeserializedListeners.remove(listener);
    }

    private void initActionMetaCache() {
        lock.lock();
        try {
            if (!configuration.isUseActionMetaCache()) {
                actionMetaCache = null;
            } else {
                Caffeine builder = Caffeine.newBuilder();
                if (configuration.getActionMetaCacheMaxSize() > -1) {
                    builder.maximumSize(configuration.getActionMetaCacheMaxSize());
                }
                if (configuration.getActionMetaCacheExpireSeconds() > -1) {
                    builder.expireAfterWrite(configuration.getActionMetaCacheExpireSeconds(), TimeUnit.SECONDS);
                }

                actionMetaCache = builder.build(actionName -> fetchActionMeta(actionName, null));
            }
        } finally {
            lock.unlock();
        }
    }

    private void initEventTypeCache() {
        lock.lock();
        try {
            if (!configuration.isUseEventTypeCache()) {
                eventTypeCache = null;
            } else {
                Caffeine builder = Caffeine.newBuilder();
                if (configuration.getEventTypeCacheMaxSize() > -1) {
                    builder.maximumSize(configuration.getEventTypeCacheMaxSize());
                }
                if (configuration.getEventTypeCacheExpireSeconds() > -1) {
                    builder.expireAfterWrite(configuration.getEventTypeCacheExpireSeconds(), TimeUnit.SECONDS);
                }

                eventTypeCache = builder.build(eventTypeName -> fetchEventType(eventTypeName, null));
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void clearActionMetaCache() {
        lock.lock();
        try {
            if (actionMetaCache != null) {
                actionMetaCache.invalidateAll();
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void clearEventTypeCache() {
        lock.lock();
        try {
            if (eventTypeCache != null) {
                eventTypeCache.invalidateAll();
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void clearFeaturesCache() {
        lock.lock();
        try {
            featuresCache = null;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void clearCache() {
        lock.lock();
        try {
            clearActionMetaCache();
            clearEventTypeCache();
            clearFeaturesCache();
        } finally {
            lock.unlock();
        }
    }

    public LoadingCache getActionMetaCache() {
        return actionMetaCache;
    }

    public void setActionMetaCache(LoadingCache actionMetaCache) {
        this.actionMetaCache = actionMetaCache;
    }

    public LoadingCache getEventTypeCache() {
        return eventTypeCache;
    }

    public void setEventTypeCache(LoadingCache eventTypeCache) {
        this.eventTypeCache = eventTypeCache;
    }

    public ObjectMapper getObjectMapper() {
        return typeConverter.getObjectMapper();
    }

    protected final String getUrl() {
        String baseUrl = configuration.getUrl();

        return baseUrl + (baseUrl.endsWith("/") ? "" : "/") + RemoteApiConstants.ENDPOINT_JSONRPC;
    }

    protected abstract  R doExecute(T request, Class responseClass,
            SpongeRequestContext context);

    @Override
    public  T setupRequest(T request) {
        if (request.getId() == null) {
            request.setId(String.valueOf(currentRequestId.incrementAndGet()));
        }

        // Set empty header if none.
        if (request.getHeader() == null) {
            request.setHeader(new RequestHeader());
        }

        RequestHeader header = request.getHeader();

        // Must be thread-safe.
        String authToken = currentAuthToken.get();
        if (authToken != null) {
            if (header.getAuthToken() == null) {
                header.setAuthToken(authToken);
            }
        } else {
            if (configuration.getUsername() != null && header.getUsername() == null) {
                header.setUsername(configuration.getUsername());
            }

            if (configuration.getPassword() != null && header.getPassword() == null) {
                header.setPassword(configuration.getPassword());
            }
        }

        if (configuration.getFeatures() != null && header.getFeatures() == null) {
            header.setFeatures(configuration.getFeatures());
        }

        // Clear header if it is empty.
        if (header.getUsername() == null && header.getPassword() == null && header.getAuthToken() == null
                && (header.getFeatures() == null || header.getFeatures().isEmpty())) {
            request.setHeader(null);
        }

        return request;
    }

    protected  T setupResponse(String method, T response) {
        // Set empty header if none.
        if (response.getResult() != null && response.getResult().getHeader() == null) {
            response.getResult().setHeader(new ResponseHeader());
        }

        if (response.getError() != null) {
            handleErrorResponse(method, response.getError());
        }

        return response;
    }

    @Override
    public void handleErrorResponse(String method, ResponseError error) {
        if (error.getCode() != null) {
            if (configuration.isThrowExceptionOnErrorResponse()) {
                String message = error.getMessage() != null ? error.getMessage() : String.format("Error code: %s", error.getCode());

                RuntimeException exception;
                switch (error.getCode()) {
                case RemoteApiConstants.ERROR_CODE_INVALID_AUTH_TOKEN:
                    exception = new InvalidAuthTokenException(message);
                    break;
                case RemoteApiConstants.ERROR_CODE_INVALID_KB_VERSION:
                    exception = new InvalidKnowledgeBaseVersionException(message);
                    break;
                case RemoteApiConstants.ERROR_CODE_INVALID_USERNAME_PASSWORD:
                    exception = new InvalidUsernamePasswordException(message);
                    break;
                case RemoteApiConstants.ERROR_CODE_INACTIVE_ACTION:
                    exception = new InactiveActionException(message);
                    break;
                default:
                    exception = new ErrorResponseException(message);
                }

                if (exception instanceof ErrorResponseException) {
                    ((ErrorResponseException) exception).setErrorCode(error.getCode());
                    if (error.getData() != null) {
                        ((ErrorResponseException) exception).setDetailedErrorMessage(error.getData().getDetailedErrorMessage());
                    }
                }

                throw exception;
            }
        }
    }

    @Override
    public  R execute(T request, Class responseClass, SpongeRequestContext context) {
        if (context == null) {
            context = SpongeRequestContext.builder().build();
        }

        // Set empty header if none.
        if (request.getHeader() == null) {
            request.setHeader(new RequestHeader());
        }

        RequestHeader header = request.getHeader();

        final SpongeRequestContext finalContext = context;

        return executeWithAuthentication(request, header.getUsername(), header.getPassword(), header.getAuthToken(),
                (req) -> executeDelegate(req, responseClass, finalContext), () -> {
                    header.setAuthToken(null);
                    return request;
                });
    }

    @Override
    public  R execute(T request, Class responseClass) {
        return execute(request, responseClass, null);
    }

    protected boolean isRequestAnonymous(String requestUsername, String requestPassword) {
        return configuration.getUsername() == null && requestUsername == null && configuration.getPassword() == null
                && requestPassword == null;
    }

    @Override
    public  X executeWithAuthentication(T request, String requestUsername, String requestPassword, String requestAuthToken,
            Function onExecute, Supplier onClearAuthToken) {
        try {
            if (configuration.isAutoUseAuthToken() && currentAuthToken.get() == null && requestAuthToken == null
                    && !isRequestAnonymous(requestUsername, requestPassword)) {
                login();
            }

            return onExecute.apply(request);
        } catch (InvalidAuthTokenException e) {
            // Relogin if set up and necessary.
            if (currentAuthToken.get() != null && configuration.isRelogin()) {
                login();

                // Clear the request auth token and setup a new request.
                T newRequest = onClearAuthToken.get();

                return onExecute.apply(newRequest);
            } else {
                throw e;
            }
        }
    }

    protected  R executeDelegate(T request, Class responseClass,
            SpongeRequestContext context) {
        if (context == null) {
            context = SpongeRequestContext.builder().build();
        }

        return setupResponse(request.getMethod(), doExecute(setupRequest(request), responseClass, context));
    }

    @Override
    public GetVersionResponse getVersion(GetVersionRequest request, SpongeRequestContext context) {
        return execute(request, GetVersionResponse.class, context);
    }

    @Override
    public GetVersionResponse getVersion(GetVersionRequest request) {
        return getVersion(request, null);
    }

    @Override
    public String getVersion() {
        return getVersion(new GetVersionRequest()).getResult().getValue();
    }

    @Override
    public GetFeaturesResponse getFeatures(GetFeaturesRequest request, SpongeRequestContext context) {
        return execute(request, GetFeaturesResponse.class, context);
    }

    @Override
    public GetFeaturesResponse getFeatures(GetFeaturesRequest request) {
        return getFeatures(request, null);
    }

    @Override
    public Map getFeatures() {
        if (featuresCache == null) {
            lock.lock();
            try {
                if (featuresCache == null) {
                    featuresCache = getFeatures(new GetFeaturesRequest()).getResult().getValue();
                }
            } finally {
                lock.unlock();
            }
        }

        return featuresCache;
    }

    @Override
    public LoginResponse login(LoginRequest request, SpongeRequestContext context) {
        LoginResponse response;
        lock.lock();

        try {
            currentAuthToken.set(null);
            response = executeDelegate(request, LoginResponse.class, context);
            currentAuthToken.set(response.getResult().getValue().getAuthToken());
        } finally {
            lock.unlock();
        }

        return response;
    }

    @Override
    public LoginResponse login(LoginRequest request) {
        return login(request, null);
    }

    @Override
    public String login() {
        RequestHeader header = new RequestHeader();
        header.setUsername(configuration.getUsername());
        header.setPassword(configuration.getPassword());

        LoginRequest request = new LoginRequest();
        request.getParams().setHeader(header);

        return login(request).getResult().getValue().getAuthToken();
    }

    @Override
    public LogoutResponse logout(LogoutRequest request, SpongeRequestContext context) {
        LogoutResponse response;
        lock.lock();

        try {
            response = execute(request, LogoutResponse.class, context);
            currentAuthToken.set(null);
        } finally {
            lock.unlock();
        }

        return response;
    }

    @Override
    public LogoutResponse logout(LogoutRequest request) {
        return logout(request, null);
    }

    @Override
    public void logout() {
        logout(new LogoutRequest());
    }

    @Override
    public GetKnowledgeBasesResponse getKnowledgeBases(GetKnowledgeBasesRequest request, SpongeRequestContext context) {
        return execute(request, GetKnowledgeBasesResponse.class, context);
    }

    @Override
    public GetKnowledgeBasesResponse getKnowledgeBases(GetKnowledgeBasesRequest request) {
        return getKnowledgeBases(request, null);
    }

    @Override
    public List getKnowledgeBases() {
        return getKnowledgeBases(new GetKnowledgeBasesRequest()).getResult().getValue();
    }

    protected GetActionsResponse doGetActions(GetActionsRequest request, boolean populateCache, SpongeRequestContext context) {
        GetActionsResponse response = execute(request, GetActionsResponse.class, context);
        GetActionsResult result = response.getResult();

        if (result.getValue().getActions() != null) {
            result.getValue().getActions().forEach(actionMeta -> unmarshalActionMeta(actionMeta));

            // Populate the cache.
            if (populateCache && configuration.isUseActionMetaCache() && actionMetaCache != null) {
                result.getValue().getActions().forEach(actionMeta -> actionMetaCache.put(actionMeta.getName(), actionMeta));
            }
        }

        if (result.getValue().getTypes() != null) {
            result.getValue().getTypes().values().forEach(type -> unmarshalDataType(type));
        }

        return response;
    }

    protected DataType marshalDataType(DataType type) {
        return (DataType) typeConverter.marshal(new TypeType(), type);
    }

    protected DataType unmarshalDataType(DataType type) {
        return (DataType) typeConverter.unmarshal(new TypeType(), type);
    }

    protected void unmarshalActionMeta(RemoteActionMeta actionMeta) {
        if (actionMeta != null) {
            List args = actionMeta.getArgs();
            if (args != null) {
                for (int i = 0; i < args.size(); i++) {
                    args.set(i, unmarshalDataType(args.get(i)));
                }
            }

            if (actionMeta.getResult() != null) {
                actionMeta.setResult(unmarshalDataType(actionMeta.getResult()));
            }

            actionMeta.setFeatures(FeaturesUtils.unmarshal(featureConverter, actionMeta.getFeatures()));
            if (actionMeta.getCategory() != null) {
                actionMeta.getCategory().setFeatures(FeaturesUtils.unmarshal(featureConverter, actionMeta.getCategory().getFeatures()));
            }
        }
    }

    @SuppressWarnings({ "unchecked" })
    protected void unmarshalProvidedActionArgValues(RemoteActionMeta actionMeta, Map> argValues) {
        if (argValues == null || actionMeta.getArgs() == null) {
            return;
        }

        argValues.forEach((argName, argValue) -> {
            DataType argType = actionMeta.getArg(argName);
            ((ProvidedValue) argValue).setValue(typeConverter.unmarshal(argType, argValue.getValue()));

            if (argValue.getAnnotatedValueSet() != null) {
                argValue.getAnnotatedValueSet().stream().filter(Objects::nonNull)
                        .forEach(annotatedValue -> ((AnnotatedValue) annotatedValue)
                                .setValue(typeConverter.unmarshal(argType, annotatedValue.getValue())));
            }

            if (argValue.getAnnotatedElementValueSet() != null && DataTypeUtils.supportsElementValueSet(argType)) {
                argValue.getAnnotatedElementValueSet().stream().filter(Objects::nonNull).forEach(annotatedValue -> annotatedValue
                        .setValue(typeConverter.unmarshal(((ListType) argType).getElementType(), annotatedValue.getValue())));
            }
        });
    }

    @Override
    public GetActionsResponse getActions(GetActionsRequest request, SpongeRequestContext context) {
        return doGetActions(request, true, context);
    }

    @Override
    public GetActionsResponse getActions(GetActionsRequest request) {
        return getActions(request, null);
    }

    @Override
    public List getActions(String name) {
        return getActions(name, null);
    }

    @Override
    public List getActions(String name, Boolean metadataRequired) {
        GetActionsParams params = new GetActionsParams();
        params.setName(name);
        params.setMetadataRequired(metadataRequired);

        return getActions(new GetActionsRequest(params)).getResult().getValue().getActions();
    }

    @Override
    public List getActions() {
        return getActions(new GetActionsRequest()).getResult().getValue().getActions();
    }

    protected RemoteActionMeta fetchActionMeta(String actionName, SpongeRequestContext context) {
        GetActionsParams params = new GetActionsParams();
        params.setName(actionName);
        params.setMetadataRequired(true);

        List actions = doGetActions(new GetActionsRequest(params), false, context).getResult().getValue().getActions();

        return actions != null ? actions.stream().findFirst().orElse(null) : null;
    }

    @Override
    public RemoteActionMeta getActionMeta(String actionName, boolean allowFetchMetadata, SpongeRequestContext context) {
        if (configuration.isUseActionMetaCache() && actionMetaCache != null) {
            RemoteActionMeta actionMeta = actionMetaCache.getIfPresent(actionName);
            if (actionMeta != null) {
                return actionMeta;
            }

            return allowFetchMetadata ? actionMetaCache.get(actionName) : null;
        } else {
            return allowFetchMetadata ? fetchActionMeta(actionName, context) : null;
        }
    }

    @Override
    public RemoteActionMeta getActionMeta(String actionName, boolean allowFetchMetadata) {
        return getActionMeta(actionName, allowFetchMetadata, null);
    }

    @Override
    public RemoteActionMeta getActionMeta(String actionName) {
        return getActionMeta(actionName, DEFAULT_ALLOW_FETCH_METADATA);
    }

    protected void setupActionExecutionInfo(RemoteActionMeta actionMeta, ActionExecutionInfo info) {
        // Conditionally set the verification of the processor qualified version on the server side.
        if (configuration.isVerifyProcessorVersion() && actionMeta != null && info.getQualifiedVersion() == null) {
            info.setQualifiedVersion(actionMeta.getQualifiedVersion());
        }

        Validate.isTrue(actionMeta == null || Objects.equals(actionMeta.getName(), info.getName()),
                "Action name '%s' in the metadata doesn't match the action name '%s' in the request",
                actionMeta != null ? actionMeta.getName() : null, info.getName());
    }

    @SuppressWarnings("unchecked")
    protected ActionCallResponse doCall(RemoteActionMeta actionMeta, ActionCallRequest request, SpongeRequestContext context) {
        setupActionExecutionInfo(actionMeta, request.getParams());

        Object args = request.getParams().getArgs();

        if (args instanceof List || args == null) {
            validateCallArgs(actionMeta, (List) args);
            request.getParams().setArgs(marshalActionCallArgs(actionMeta, (List) args));
        } else if (args instanceof Map) {
            validateCallArgs(actionMeta, (Map) args);
            request.getParams().setArgs(marshalActionCallArgs(actionMeta, (Map) args));
        } else {
            throw new IllegalArgumentException("Action args should be an instance of a List or a Map");
        }

        ActionCallResponse response = execute(request, ActionCallResponse.class, context);

        unmarshalCallResult(actionMeta, response);

        return response;
    }

    @Override
    public void validateCallArgs(RemoteActionMeta actionMeta, List args) {
        if (actionMeta == null || actionMeta.getArgs() == null) {
            return;
        }

        SpongeApiUtils.validateActionCallArgs(actionMeta.getArgs(), args);
    }

    @Override
    public void validateCallArgs(RemoteActionMeta actionMeta, Map args) {
        if (actionMeta == null || actionMeta.getArgs() == null) {
            return;
        }

        args.forEach((name, value) -> {
            SpongeApiUtils.validateActionCallArg(actionMeta.getArgs().get(RemoteApiUtils.getActionArgIndex(actionMeta, name)), value);
        });
    }

    protected List marshalActionCallArgs(RemoteActionMeta actionMeta, List args) {
        if (args == null || actionMeta == null || actionMeta.getArgs() == null) {
            return args;
        }

        List result = new ArrayList<>(args.size());
        for (int i = 0; i < args.size(); i++) {
            result.add(typeConverter.marshal(actionMeta.getArgs().get(i), args.get(i)));
        }

        return result;
    }

    protected Map marshalActionCallArgs(RemoteActionMeta actionMeta, Map args) {
        if (args == null || actionMeta == null || actionMeta.getArgs() == null) {
            return args;
        }

        Map result = new LinkedHashMap<>(args.size());
        args.forEach((name, value) -> result.put(name, typeConverter.marshal(actionMeta.getArg(name), value)));

        return result;
    }

    protected Map marshalAuxiliaryActionArgsCurrent(RemoteActionMeta actionMeta, Map current,
            Map dynamicTypes) {
        Map marshalled = new LinkedHashMap<>();

        if (current != null) {
            if (actionMeta == null || actionMeta.getArgs() == null) {
                // Not marshalled.
                marshalled.putAll(current);
            } else {
                current.forEach((name, value) -> marshalled.put(name, typeConverter.marshal(
                        dynamicTypes != null && dynamicTypes.containsKey(name) ? dynamicTypes.get(name) : actionMeta.getArg(name), value)));
            }
        }

        return marshalled;
    }

    protected void unmarshalCallResult(RemoteActionMeta actionMeta, ActionCallResponse response) {
        if (actionMeta == null || actionMeta.getResult() == null || response.getResult() == null) {
            return;
        }

        response.getResult().setValue(typeConverter.unmarshal(actionMeta.getResult(), response.getResult().getValue()));
    }

    @Override
    public ActionCallResponse call(ActionCallRequest request, RemoteActionMeta actionMeta, boolean allowFetchMetadata,
            SpongeRequestContext context) {
        return doCall(actionMeta != null ? actionMeta : getActionMeta(request.getParams().getName(), allowFetchMetadata), request, context);
    }

    @Override
    public ActionCallResponse call(ActionCallRequest request, RemoteActionMeta actionMeta, boolean allowFetchMetadata) {
        return call(request, actionMeta, allowFetchMetadata, null);
    }

    @Override
    public ActionCallResponse call(ActionCallRequest request, RemoteActionMeta actionMeta) {
        return call(request, actionMeta, true);
    }

    @Override
    public ActionCallResponse call(ActionCallRequest request) {
        return call(request, null);
    }

    @Override
    public Object call(String actionName, List args) {
        return call(new ActionCallRequest(new ActionCallParams(actionName, args, null))).getResult().getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public  T call(Class resultClass, String actionName, List args) {
        return (T) call(actionName, args);
    }

    @Override
    public Object call(String actionName) {
        return call(actionName, Collections.emptyList());
    }

    @Override
    public  T call(Class resultClass, String actionName) {
        return call(resultClass, actionName, Collections.emptyList());
    }

    @Override
    public Object call(String actionName, Map args) {
        return call(new ActionCallRequest(new ActionCallParams(actionName, args, null))).getResult().getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public  T call(Class resultClass, String actionName, Map args) {
        return (T) call(actionName, args);
    }

    @Override
    public SendEventResponse send(SendEventRequest request, SpongeRequestContext context) {
        SendEventParams params = request.getParams();

        // Use a temporary RemoteEvent to marshal attributes and features.
        RemoteEvent event = new RemoteEvent(null, params.getName(), null, 0, params.getLabel(), params.getDescription(),
                params.getAttributes(), params.getFeatures());
        event = RemoteApiUtils.marshalRemoteEvent(event, typeConverter, eventName -> getEventType(eventName));

        // Set marshalled fields.
        params.setAttributes(event.getAttributes());
        params.setFeatures(event.getFeatures());

        return execute(request, SendEventResponse.class, context);
    }

    @Override
    public SendEventResponse send(SendEventRequest request) {
        return send(request, null);
    }

    @Override
    public String send(String eventName, Map attributes, String label, String description, Map features) {
        return send(new SendEventRequest(new SendEventParams(eventName, attributes, label, description, features))).getResult().getValue();
    }

    @Override
    public String send(String eventName, Map attributes, String label, String description) {
        return send(eventName, attributes, label, description, null);
    }

    @Override
    public String send(String eventName, Map attributes, String label) {
        return send(eventName, attributes, label, null);
    }

    @Override
    public String send(String eventName, Map attributes) {
        return send(eventName, attributes, null, null);
    }

    @Override
    public IsActionActiveResponse isActionActive(IsActionActiveRequest request, SpongeRequestContext context) {
        if (request.getParams().getEntries() != null) {
            request.getParams().getEntries().forEach(entry -> {
                RemoteActionMeta actionMeta = getActionMeta(entry.getName());
                setupActionExecutionInfo(actionMeta, entry);

                if (entry.getContextType() != null) {
                    entry.setContextType(marshalDataType(entry.getContextType()));
                }

                if (entry.getContextValue() != null && entry.getContextType() != null) {
                    entry.setContextValue(typeConverter.marshal(entry.getContextType(), entry.getContextValue()));
                }

                if (entry.getArgs() != null) {
                    entry.setArgs(marshalActionCallArgs(actionMeta, entry.getArgs()));
                }

                entry.setFeatures(FeaturesUtils.marshal(featureConverter, entry.getFeatures()));
            });
        }

        return execute(request, IsActionActiveResponse.class, context);
    }

    @Override
    public IsActionActiveResponse isActionActive(IsActionActiveRequest request) {
        return isActionActive(request, null);
    }

    @Override
    public List isActionActive(List entries) {
        boolean areActivatableActions = false;
        for (IsActionActiveEntry entry : entries) {
            RemoteActionMeta actionMeta = getActionMeta(entry.getName(), false);
            if (actionMeta != null ? actionMeta.isActivatable() : true) {
                areActivatableActions = true;
                break;
            }
        }

        // No need to connect to the server.
        if (!areActivatableActions) {
            return Stream.generate(() -> Boolean.TRUE).limit(entries.size()).collect(Collectors.toList());
        }

        // Clone all entries in order to modify their copies later.
        entries = entries.stream().map((entry) -> entry != null ? entry.clone() : null).collect(Collectors.toList());

        return isActionActive(new IsActionActiveRequest(new IsActionActiveParams(entries))).getResult().getValue();
    }

    @Override
    public ProvideActionArgsResponse provideActionArgs(ProvideActionArgsRequest request, SpongeRequestContext context) {
        RemoteActionMeta actionMeta = getActionMeta(request.getParams().getName());

        setupActionExecutionInfo(actionMeta, request.getParams());

        request.getParams().setCurrent(
                marshalAuxiliaryActionArgsCurrent(actionMeta, request.getParams().getCurrent(), request.getParams().getDynamicTypes()));

        // Clone and marshal all argument features.
        request.getParams().setArgFeatures(marshalProvideArgsFeaturesMap(request.getParams().getArgFeatures()));

        ProvideActionArgsResponse response = execute(request, ProvideActionArgsResponse.class, context);

        if (actionMeta != null) {
            unmarshalProvidedActionArgValues(actionMeta, response.getResult().getValue());
        }

        return response;
    }

    protected Map> marshalProvideArgsFeaturesMap(Map> featuresMap) {
        if (featuresMap == null) {
            return null;
        }

        Map> result = new LinkedHashMap<>();
        for (Map.Entry> entry : featuresMap.entrySet()) {
            result.put(entry.getKey(), FeaturesUtils.marshal(featureConverter, entry.getValue()));
        }

        return result;
    }

    @Override
    public ProvideActionArgsResponse provideActionArgs(ProvideActionArgsRequest request) {
        return provideActionArgs(request, null);
    }

    @Override
    public Map> provideActionArgs(String actionName, ProvideArgsParameters parameters) {
        return provideActionArgs(new ProvideActionArgsRequest(new ProvideActionArgsParams(actionName, parameters.getProvide(),
                parameters.getSubmit(), parameters.getCurrent(), parameters.getDynamicTypes(), parameters.getArgFeatures()))).getResult()
                        .getValue();
    }

    protected GetEventTypesResponse doGetEventTypes(GetEventTypesRequest request, boolean populateCache, SpongeRequestContext context) {
        GetEventTypesResponse response = execute(request, GetEventTypesResponse.class, context);

        Map eventTypes = response.getResult().getValue();
        if (eventTypes != null) {
            eventTypes.entrySet().forEach(entry -> entry.setValue((RecordType) unmarshalDataType(entry.getValue())));

            // Populate the cache.
            if (populateCache && configuration.isUseEventTypeCache() && eventTypeCache != null) {
                eventTypes.entrySet().forEach(entry -> eventTypeCache.put(entry.getKey(), entry.getValue()));
            }
        }

        return response;
    }

    @Override
    public GetEventTypesResponse getEventTypes(GetEventTypesRequest request, SpongeRequestContext context) {
        return doGetEventTypes(request, true, context);
    }

    @Override
    public GetEventTypesResponse getEventTypes(GetEventTypesRequest request) {
        return getEventTypes(request, null);
    }

    @Override
    public Map getEventTypes(String eventName) {
        return getEventTypes(new GetEventTypesRequest(new GetEventTypesParams(eventName))).getResult().getValue();
    }

    protected RecordType fetchEventType(String eventTypeName, SpongeRequestContext context) {
        return doGetEventTypes(new GetEventTypesRequest(new GetEventTypesParams(eventTypeName)), false, context).getResult().getValue()
                .get(eventTypeName);
    }

    @Override
    public RecordType getEventType(String eventTypeName, boolean allowFetchEventType, SpongeRequestContext context) {
        if (configuration.isUseEventTypeCache() && eventTypeCache != null) {
            RecordType eventType = eventTypeCache.getIfPresent(eventTypeName);
            if (eventType != null) {
                return eventType;
            }

            return allowFetchEventType ? eventTypeCache.get(eventTypeName) : null;
        } else {
            return allowFetchEventType ? fetchEventType(eventTypeName, context) : null;
        }
    }

    @Override
    public RecordType getEventType(String eventTypeName, boolean allowFetchEventType) {
        return getEventType(eventTypeName, allowFetchEventType, null);
    }

    @Override
    public RecordType getEventType(String eventTypeName) {
        return getEventType(eventTypeName, DEFAULT_ALLOW_FETCH_EVENT_TYPE);
    }

    @Override
    public ReloadResponse reload(ReloadRequest request, SpongeRequestContext context) {
        return execute(request, ReloadResponse.class, context);
    }

    @Override
    public ReloadResponse reload(ReloadRequest request) {
        return reload(request, null);
    }

    @Override
    public void reload() {
        reload(new ReloadRequest());
    }

    @Override
    public void clearSession() {
        lock.lock();

        try {
            currentAuthToken.set(null);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public Long getCurrentRequestId() {
        return currentRequestId.get();
    }

    @Override
    public String getCurrentAuthToken() {
        return currentAuthToken.get();
    }
}