/*
 * $#
 * FOS Weka
 *
 * Copyright (C) 2013 Feedzai SA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #$
 */
package com.feedzai.fos.impl.weka.utils;

import com.feedzai.fos.api.FOSException;
import com.feedzai.fos.api.InstanceType;
import com.feedzai.fos.impl.weka.config.WekaManagerConfig;
import com.feedzai.fos.impl.weka.config.WekaModelConfig;
import com.feedzai.fos.impl.weka.utils.setter.InstanceSetter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import weka.classifiers.Classifier;
import weka.core.FastVector;
import weka.core.Instances;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Creates a new scorer based on the weka library.
 * This should be used with only with weka classifiers that are thread safe!
 *
 * @author Marco Jorge (marco.jorge@feedzai.com)
 */
public class WekaThreadSafeScorerPassthrough implements WekaThreadSafeScorer {
    private final static Logger logger = LoggerFactory.getLogger(WekaThreadSafeScorerPassthrough.class);

    private WekaManagerConfig wekaManagerConfig;
    private Classifier classifier;
    private WekaModelConfig wekaModelConfig;
    private FastVector attributes;
    private Instances instances;
    private InstanceSetter[] instanceSetters;

    /**
     * Creates a new thread safe scorer from the given configuration parameters.
     *
     * @param wekaModelConfig   the configuration of the model
     * @param wekaManagerConfig the global configurations
     * @throws FOSException when the underlying classifier could not be instantiated
     */
    public WekaThreadSafeScorerPassthrough(WekaModelConfig wekaModelConfig,WekaManagerConfig wekaManagerConfig) throws FOSException {
        checkNotNull(wekaModelConfig, "Model config cannot be null");
        checkNotNull(wekaManagerConfig, "Manager config cannot be null");
        checkNotNull(wekaModelConfig.getAttributess(), "Model instances fields cannot be null");
        checkArgument(wekaModelConfig.getAttributess().size() > 0, "Model must have at least one field");

        this.wekaManagerConfig = wekaManagerConfig;
        this.wekaModelConfig = wekaModelConfig;

        this.attributes = WekaUtils.instanceFields2Attributes(wekaModelConfig.getClassIndex(), this.wekaModelConfig.getAttributess());
        this.instanceSetters = WekaUtils.instanceFields2ValueSetters(wekaModelConfig.getAttributess(), InstanceType.SCORING);

        this.instances = new Instances(Integer.toString(this.wekaModelConfig.hashCode()), attributes, 0 /*this set is for scoring only*/);
        this.instances.setClassIndex(this.wekaModelConfig.getClassIndex());

        try {
            this.classifier = new Cloner<Classifier>(wekaModelConfig.getModelDescriptor()).get();
        } catch (Exception e) {
            throw new FOSException(e);
        }
    }

    /**
     * The the given Object[] with this scorer (thread safe!).
     *

     * The score in bound by the configuration minScore and maxScore.
     *
     * @param scorable the scorable data to score
     * @return the score value already bound the configuration range
     * @throws FOSException when classification was not possible
     */
    @Override
    public double[] score(Object[] scorable) throws FOSException {
        return WekaUtils.score(classifier, scorable, instanceSetters, instances, attributes);
    }

    @Override
    public Classifier getClassifier() throws FOSException {
        return classifier;
    }

    /**
     * Close the resources allocated with this scorer.
     */
    @Override
    public void close() {
        /* nothing to do */
    }
}


package org.projectodd.jrapidoc.introspector;

import org.apache.commons.lang3.StringUtils;
import org.projectodd.jrapidoc.annotation.DocDescription;
import org.projectodd.jrapidoc.annotation.DocIsRequired;
import org.projectodd.jrapidoc.annotation.soap.DocReturn;
import org.projectodd.jrapidoc.annotation.soap.DocReturns;
import org.projectodd.jrapidoc.exception.JrapidocExecutionException;
import org.projectodd.jrapidoc.logger.Logger;
import org.projectodd.jrapidoc.model.*;
import org.projectodd.jrapidoc.model.param.HeaderParam;
import org.projectodd.jrapidoc.model.type.provider.TypeProvider;

import javax.jws.*;
import javax.jws.soap.SOAPBinding;
import javax.xml.ws.Holder;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

public class SEIProcessor extends AbstractWSIntrospector{

    ClassLoader loader;

    public SEIProcessor(TypeProvider typeProvider, ClassLoader loader) {
        super(typeProvider);
        this.loader = loader;
    }

    public ServiceGroup createServiceGroup(Set<Class<?>> seiClasses, ServiceGroup.ServiceGroupBuilder serviceGroupBuilder)
            throws JrapidocExecutionException {
        for (Class<?> seiClass : seiClasses) {
            if(!seiClass.isAnnotationPresent(WebService.class)){
                continue;
            }
            Logger.info("{0} processing started", seiClass.getCanonicalName());
            seiClass = getSEI(seiClass);
            Service service = createEndpoint(seiClass);
            serviceGroupBuilder.service(service);
            Logger.info("{0} processing finished", seiClass.getCanonicalName());
        }
        return serviceGroupBuilder.build();
    }

    Class<?> getSEI(Class<?> seiClass) throws JrapidocExecutionException {
        String interfaceClass = seiClass.getAnnotation(WebService.class).endpointInterface();
        if (StringUtils.isNotEmpty(interfaceClass)) {
            try {
                return loader.loadClass(interfaceClass);
            } catch (ClassNotFoundException e) {
                Logger.error("Endpoint interface {0} for {1} is not on project classpath", interfaceClass, seiClass.getCanonicalName());
                throw new JrapidocExecutionException(e.getMessage(), e);
            }
        } else {
            return seiClass;
        }
    }

    Service createEndpoint(Class<?> seiClass) {
        Service.ResourceBuilder resourceBuilder = new Service.ResourceBuilder();
        addServiceName(seiClass, resourceBuilder);
        resourceBuilder.description(getDescription(seiClass.getDeclaredAnnotations()));
        addMethods(seiClass, resourceBuilder);
        return resourceBuilder.build();
    }

    void addMethods(Class<?> seiClass, Service.ResourceBuilder resourceBuilder) {
        for (Method method : seiClass.getDeclaredMethods()) {
            if (Modifier.isPublic(method.getModifiers())) {
                WebMethod webMetAnnotation = method.getAnnotation(WebMethod.class);
                if (webMetAnnotation == null) {
                    resourceBuilder.method(createMethod(method, seiClass));
                } else if (!method.getAnnotation(WebMethod.class).exclude()) {
                    resourceBuilder.method(createMethod(method, seiClass));
                }
            }
        }
    }

    org.projectodd.jrapidoc.model.Method createMethod(Method method, Class<?> seiClass) {
        Logger.debug("{0} method processing started", method.toString());
        org.projectodd.jrapidoc.model.Method.MethodBuilder methodBuilder = new org.projectodd.jrapidoc.model.Method.MethodBuilder();
        methodBuilder.description(getDescription(method.getDeclaredAnnotations()));
        methodBuilder.isAsynchronous(true);
        addOperationName(method, methodBuilder);
        addInputHeaders(method, methodBuilder);
        addInputParams(method, methodBuilder);
        addReturn(method, methodBuilder);
        addSoapBinding(method, seiClass, methodBuilder);
        org.projectodd.jrapidoc.model.Method jrdMethod = methodBuilder.build();
        Logger.debug("{0} method processing finished", method.toString());
        return jrdMethod;
    }

    void addServiceName(Class<?> seiClass, Service.ResourceBuilder resourceBuilder) {
        WebService webServiceAnno = getAnnotation(seiClass.getDeclaredAnnotations(), WebService.class);
        String serviceName = seiClass.getSimpleName() + "Service";
        if (webServiceAnno != null) {
            if (StringUtils.isNotEmpty(webServiceAnno.serviceName())) {
                serviceName = webServiceAnno.serviceName();
            }
        }
        resourceBuilder.name(serviceName);
    }

    void addOperationName(Method method, org.projectodd.jrapidoc.model.Method.MethodBuilder methodBuilder) {
        WebMethod webMethodAnno = getAnnotation(method.getDeclaredAnnotations(), WebMethod.class);
        String operationName = method.getName();
        if (webMethodAnno != null) {
            if (StringUtils.isNotEmpty(webMethodAnno.operationName())) {
                operationName = webMethodAnno.operationName();
            }
        }
        methodBuilder.name(operationName);
    }

    void addSoapBinding(Method method, Class<?> seiClass, org.projectodd.jrapidoc.model.Method.MethodBuilder methodBuilder) {
        SOAPBinding soapBindingAnno = getAnnotation(method.getDeclaredAnnotations(), SOAPBinding.class);
        SoapBinding.SoapBindingBuilder soapBindingBuilder = new SoapBinding.SoapBindingBuilder();
        if (soapBindingAnno == null) {
            soapBindingAnno = getAnnotation(seiClass.getDeclaredAnnotations(), SOAPBinding.class);
        }
        if (soapBindingAnno != null) {
            soapBindingBuilder.parameterStyle(soapBindingAnno.parameterStyle().name()).style(soapBindingAnno.style().name())
                    .use(soapBindingAnno.use().name()).build();
        }
        methodBuilder.soapBinding(soapBindingBuilder.build());
    }

    void addReturn(Method method, org.projectodd.jrapidoc.model.Method.MethodBuilder methodBuilder) {
        if (!isOneWay(method)) {
            Return.ReturnBuilder returnBuilder = new Return.ReturnBuilder();
            addReturnOptionDescription(method, returnBuilder);
            addOutputHeaders(method, returnBuilder);
            addOutputParams(method, returnBuilder);
            HeaderParam headerParam = new HeaderParam.HeaderParamBuilder().setName(HeaderParam.CONTENT_TYPE)
                    .setOptions(new String[] { "application/xml" }).setRequired(true).build();
            returnBuilder.httpStatus(DocReturn.HTTP_STATUS_DEFAULT).headerParams(Arrays.asList(headerParam));
            Return returnOption = returnBuilder.build();
            List<Return> returnOptions = new ArrayList<Return>(Arrays.asList(new Return[] { returnOption }));
            addExceptionTypes(method, returnOptions);
            methodBuilder.returnOptions(returnOptions);
        }
    }

    /**
     * Used for non exception return option
     *
     * @param method
     * @param returnBuilder
     */
    private void addReturnOptionDescription(Method method, Return.ReturnBuilder returnBuilder) {
        DocReturn returnAnno = getNonExceptionDocReturn(method);
        String returnOptionDesc = (returnAnno == null) ? null : returnAnno.description();
        returnBuilder.description(StringUtils.isEmpty(returnOptionDesc) ? null : returnAnno.description());
    }

    private DocReturn getNonExceptionDocReturn(Method method) {
        DocReturn returnAnno = method.getAnnotation(DocReturn.class);
        DocReturns returnsAnno = method.getAnnotation(DocReturns.class);
        if (returnsAnno != null) {
            for (DocReturn docReturn : returnsAnno.value()) {
                if (docReturn.type().equals(Void.class)) {
                    returnAnno = docReturn;
                    break;
                }
            }
        }
        return returnAnno;
    }

    <T extends Annotation> T getAnnotation(Annotation[] annotations, Class<T> annotation) {
        for (Annotation a : annotations) {
            if (a.annotationType().equals(annotation)) {
                return (T) a;
            }
        }
        return null;
    }

    Type extractFromHolder(Type holder) {
        if (holder instanceof ParameterizedType) {
            ParameterizedType paramType = (ParameterizedType) holder;
            if (paramType.getRawType().equals(Holder.class)) {
                return paramType.getActualTypeArguments()[0];
            }
        }
        return holder;
    }

    void addInputHeaders(Method method, org.projectodd.jrapidoc.model.Method.MethodBuilder methodBuilder) {
        for (int i = 0; i < method.getGenericParameterTypes().length; i++) {
            Type param = extractFromHolder(method.getGenericParameterTypes()[i]);
            Annotation[] annotations = method.getParameterAnnotations()[i];
            if (isHeader(annotations)) {
                if (isInputMode(annotations)) {
                    TransportType soapInputHeader = new TransportType.TransportTypeBuilder().type(createType(param))
                            .description(getDescription(annotations)).isRequired(getIsRequired(annotations)).build();
                    methodBuilder.soapInputHeader(soapInputHeader);
                }
            }
        }
    }

    void addInputParams(Method method, org.projectodd.jrapidoc.model.Method.MethodBuilder methodBuilder) {
        List<org.projectodd.jrapidoc.model.object.type.Type> parameterTypes = new ArrayList<org.projectodd.jrapidoc.model.object.type.Type>();
        for (int i = 0; i < method.getGenericParameterTypes().length; i++) {
            Type param = extractFromHolder(method.getGenericParameterTypes()[i]);
            org.projectodd.jrapidoc.model.object.type.Type parameterType = createType(param);
            Annotation[] annotations = method.getParameterAnnotations()[i];
            if (isInputMode(annotations)) {
                if (!isHeader(annotations)) {
                    TransportType soapInputParameter = new TransportType.TransportTypeBuilder().type(createType(param))
                            .description(getDescription(annotations)).isRequired(getIsRequired(annotations)).build();
                    methodBuilder.parameter(soapInputParameter);
                }
            }
        }
    }

    void addOutputHeaders(Method method, Return.ReturnBuilder returnBuilder) {
        if (!isOneWay(method)) {
            for (int i = 0; i < method.getGenericParameterTypes().length; i++) {
                Type param = extractFromHolder(method.getGenericParameterTypes()[i]);
                Annotation[] annotations = method.getParameterAnnotations()[i];
                if (isHeader(annotations)) {
                    if (isOutputMode(annotations)) {
                        TransportType soapOutputHeader = new TransportType.TransportTypeBuilder().type(createType(param))
                                .description(getDescription(annotations)).build();
                        returnBuilder.soapOutputHeader(soapOutputHeader);
                    }
                }
            }
            addHeaderFromReturn(method, returnBuilder);
        }
    }

    void addOutputParams(Method method, Return.ReturnBuilder returnBuilder) {
        if (!isOneWay(method)) {
            List<TransportType> returnTypes = new ArrayList<TransportType>();
            for (int i = 0; i < method.getGenericParameterTypes().length; i++) {
                Type param = extractFromHolder(method.getGenericParameterTypes()[i]);
                Annotation[] annotations = method.getParameterAnnotations()[i];
                TransportType soapOutputParameter = new TransportType.TransportTypeBuilder().type(createType(param))
                        .description(getDescription(annotations)).build();
                if (isOutputMode(method.getParameterAnnotations()[i])) {
                    if (!isHeader(method.getParameterAnnotations()[i])) {
                        returnTypes.add(soapOutputParameter);
                    }
                }
            }
            addTypeFromReturn(method, returnTypes);
            returnBuilder.returnTypes(returnTypes);
        }
    }

    void addExceptionTypes(Method method, List<Return> returnOptions) {
        DocReturns docReturnsAnno = method.getAnnotation(DocReturns.class);
        if (docReturnsAnno != null) {// take exception types from DocReturns
                                     // annotation (precedence)
            for (DocReturn docReturnAnno : docReturnsAnno.value()) {
                if (!docReturnAnno.type().equals(Void.class)) {
                    String returnOptionDesc = StringUtils.isEmpty(docReturnAnno.description()) ? null : docReturnAnno.description();
                    String typeDescription = StringUtils.isEmpty(docReturnAnno.typeDescription()) ? null : docReturnAnno.typeDescription();
                    addExceptionType(returnOptionDesc, typeDescription, docReturnAnno.http(), docReturnAnno.type(), returnOptions);
                }
            }
        } else {
            // take exception types from method signature
            for (Class<?> exception : method.getExceptionTypes()) {
                addExceptionType(null, null, DocReturn.HTTP_STATUS_FAULT, exception, returnOptions);
            }
        }
    }

    void addExceptionType(String returnOptionDesc, String typeDescription, int httpStatus, Class<?> classType, List<Return> returnOptions) {
        TransportType exceptionTransport = new TransportType.TransportTypeBuilder().type(createType(classType)).description(typeDescription).build();
        List<TransportType> transportTypes = new ArrayList<TransportType>(Arrays.asList(new TransportType[] { exceptionTransport }));
        List<HeaderParam> httpHeaders = new ArrayList<HeaderParam>(Arrays.asList(new HeaderParam[] { new HeaderParam.HeaderParamBuilder()
                .setName(HeaderParam.CONTENT_TYPE).setOptions(new String[] { "application/xml" }).setRequired(true).build() }));
        Return returnException = new Return.ReturnBuilder().httpStatus(httpStatus).description(returnOptionDesc).returnTypes(transportTypes)
                .headerParams(httpHeaders).build();
        returnOptions.add(returnException);
    }

    void addTypeFromReturn(Method method, List<TransportType> returnTypes) {
        if (!isHeader(method.getDeclaredAnnotations())) {
            if (!method.getGenericReturnType().equals(Void.TYPE)) {
                DocReturn docReturnAnno = getNonExceptionDocReturn(method);
                String description = (docReturnAnno == null) ? null : docReturnAnno.typeDescription();
                TransportType soapOutputParameter = new TransportType.TransportTypeBuilder().description(description)
                        .type(createType(method.getGenericReturnType())).build();
                returnTypes.add(soapOutputParameter);
            }
        }
    }

    void addHeaderFromReturn(Method method, Return.ReturnBuilder returnBuilder) {
        if (isHeader(method.getDeclaredAnnotations())) {
            if (!method.getGenericReturnType().equals(Void.TYPE)) {
                DocReturn docReturnAnno = getNonExceptionDocReturn(method);
                String typeDescription = (docReturnAnno == null) ? null : docReturnAnno.typeDescription();
                TransportType soapOutputHeader = new TransportType.TransportTypeBuilder().description(typeDescription)
                        .type(createType(method.getGenericReturnType())).build();
                returnBuilder.soapOutputHeader(soapOutputHeader);
            }
        }
    }

    boolean isHeader(Annotation[] annotations) {
        WebParam webParamAnno = getAnnotation(annotations, WebParam.class);
        WebResult webResultAnno = getAnnotation(annotations, WebResult.class);
        if (webParamAnno == null && webResultAnno == null) {
            return false;
        }
        if (webParamAnno != null) {
            return webParamAnno.header();
        } else if (webResultAnno != null) {
            return webResultAnno.header();
        }
        return false;
    }

    boolean isInputMode(Annotation[] annotations) {
        if (annotations == null) {
            return true;
        }
        for (Annotation a : annotations) {
            if (a.annotationType().equals(WebParam.class)) {
                return ((WebParam) a).mode() != WebParam.Mode.OUT;
            }
        }
        return true;
    }

    boolean isOutputMode(Annotation[] annotations) {
        if (annotations == null) {
            return false;
        }
        for (Annotation a : annotations) {
            if (a.annotationType().equals(WebParam.class)) {
                return ((WebParam) a).mode() != WebParam.Mode.IN;
            }
        }
        return false;
    }

    boolean isOneWay(Method method) {
        return (method.getAnnotation(Oneway.class) != null);
    }

    void addMethodName(Method method, org.projectodd.jrapidoc.model.Method.MethodBuilder methodBuilder) {
        WebMethod webMetAnno = method.getAnnotation(WebMethod.class);
        if (webMetAnno == null) {
            methodBuilder.name(method.getName());
            return;
        } else if (StringUtils.isNotEmpty(webMetAnno.operationName())) {
            methodBuilder.name(webMetAnno.operationName());
            return;
        }
        methodBuilder.name(method.getName());
    }

    Boolean getIsRequired(Annotation[] annotations) {
        DocIsRequired isRequired = getAnnotation(annotations, DocIsRequired.class);
        if (isRequired == null) {
            return null;
        }
        return isRequired.value();
    }
}


/*
 * $#
 * FOS Weka
 *
 * Copyright (C) 2013 Feedzai SA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #$
 */
package com.feedzai.fos.impl.weka.utils;

import com.feedzai.fos.api.FOSException;
import com.feedzai.fos.api.InstanceType;
import com.feedzai.fos.impl.weka.config.WekaManagerConfig;
import com.feedzai.fos.impl.weka.config.WekaModelConfig;
import com.feedzai.fos.impl.weka.utils.pool.AutoPopulateGenericObjectPool;
import com.feedzai.fos.impl.weka.utils.pool.ClassifierFactory;
import com.feedzai.fos.impl.weka.utils.pool.GenericObjectPoolConfig;
import com.feedzai.fos.impl.weka.utils.setter.InstanceSetter;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import weka.classifiers.Classifier;
import weka.core.FastVector;
import weka.core.Instances;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Creates a new scorer based on the weka library.
 * Because weka classifiers are not guaranteed to be thread sate, this class wraps around the actual weka library using a pool of thread-unsafe calssifiers.
 *
 * @author Marco Jorge (marco.jorge@feedzai.com)
 */
public class WekaThreadSafeScorerPool implements WekaThreadSafeScorer {
    private final static Logger logger = LoggerFactory.getLogger(WekaThreadSafeScorerPool.class);

    private WekaManagerConfig wekaManagerConfig;
    private ObjectPool<Classifier> pool;
    private GenericObjectPool.Config poolConfig = new GenericObjectPoolConfig();
    private WekaModelConfig wekaModelConfig;
    private FastVector attributes;
    private Instances instances;
    private InstanceSetter[] instanceSetters;

    /**
     * Creates a new thread safe scorer from the given configuration parameters.
     *
     * @param wekaModelConfig   the configuration of the model
     * @param wekaManagerConfig the global configurations
     * @throws FOSException when the underlying classifier could not be instantiated
     */
    public WekaThreadSafeScorerPool(WekaModelConfig wekaModelConfig,WekaManagerConfig wekaManagerConfig) throws FOSException {
        checkNotNull(wekaModelConfig, "Model config cannot be null");
        checkNotNull(wekaManagerConfig, "Manager config cannot be null");
        checkNotNull(wekaModelConfig.getAttributess(), "Model instances fields cannot be null");
        checkArgument(wekaModelConfig.getAttributess().size() > 0, "Model must have at least one field");

        this.poolConfig = new GenericObjectPoolConfig();
        this.wekaManagerConfig = wekaManagerConfig;
        this.wekaModelConfig = wekaModelConfig;

        this.attributes = WekaUtils.instanceFields2Attributes(wekaModelConfig.getClassIndex(), wekaModelConfig.getAttributess());
        this.instanceSetters = WekaUtils.instanceFields2ValueSetters(wekaModelConfig.getAttributess(), InstanceType.SCORING);

        this.instances = new Instances(Integer.toString(this.wekaModelConfig.hashCode()), attributes, 0 /*this set is for scoring only*/);
        this.instances.setClassIndex(wekaModelConfig.getClassIndex());
        try {
            BeanUtils.populate(poolConfig, this.wekaModelConfig.getPoolConfiguration());
            this.pool = new AutoPopulateGenericObjectPool<>(new ClassifierFactory(wekaModelConfig.getModelDescriptor()), poolConfig);
        } catch (Exception e) {
            throw new FOSException(e);
        }
    }

    /**
     * The the given Object[] with this scorer (thread safe!).
     *

     * The score in bound by the configuration minScore and maxScore.
     *
     *
     * @param scorable the scorable data to score
     * @return the score value already bound the configuration range
     * @throws FOSException when classification was not possible
     */
    @Override
    public double[] score(Object[] scorable) throws FOSException {
        /* the pool can change while this is processing (reload) so assign a local variable */
        final ObjectPool<Classifier> localPool = pool;

        Classifier classifier = null;
        try {
            classifier = localPool.borrowObject();

            return WekaUtils.score(classifier, scorable, instanceSetters, instances, attributes);
        } catch (Exception e) {
            throw new FOSException(e);
        } finally {
            returnObject(localPool, classifier);
        }
    }

    @Override
    public Classifier getClassifier() throws FOSException {
        try {
            Cloner<Classifier> cloner = new Cloner<Classifier>(wekaModelConfig.getModelDescriptor());
            return cloner.get();
        } catch (Exception e) {
            throw new FOSException(e.getMessage(), e);
        }
    }

    /**
     * Returns the given object to the pool.
     *
     * @param pool   The pool to where return the object to.
     * @param object The object to be returned to the pool.
     */
    private void returnObject(ObjectPool<Classifier> pool, Classifier object) {
        try {
            pool.returnObject(object);
        } catch (Exception e) {
            logger.error("Could not return object to pool", e);
        }
    }

    /**
     * Close the resources allocated with this scorer.
     */
    @Override
    public void close() {
        try {
            pool.close();
        } catch (Exception e) {
            logger.error("Could not close pool", e);
        }
    }

}


package io.advantageous.qbit.jms;

import io.advantageous.qbit.service.Startable;
import io.advantageous.qbit.service.Stoppable;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.transport.TransportListener;

import javax.jms.*;
import java.io.IOException;
import java.lang.IllegalStateException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * This class is used to startup a JMS client that can send and receive messages via JMS.
 *
 * @author rick hightower
 */
public class JmsService implements Stoppable, Startable {

    /**
     * Supply a JMS connection. Allows you to get the connection in the most convenient way possible.
     */
    private final Supplier<Connection> connectionSupplier;

    /**
     * Supply a destination supplier. Given a queue or topic name, it supplies a destination which will be a Queue or topic.
     */
    private final Function<String, Destination> destinationSupplier;

    /**
     * Should access to the destination be transacted?
     */
    private final boolean transacted;

    /**
     * Session acknowledgement mode from java.jms.Session.
     */
    private final int acknowledgeMode;

    /**
     * Are we just looking up the connection or should we start it to.
     */
    private final boolean startConnection;

    /**
     * Name of our default destination which can be a queue or topic.
     */
    private final String defaultDestination;

    /**
     * How long should we wait to receive a message?
     */
    private final int defaultTimeout;
    /**
     * connected.
     */
    private final AtomicBoolean connected = new AtomicBoolean();
    /**
     * Holds the current connection to JMS if connected.
     */
    private Optional<Connection> connectionOption = Optional.empty();
    /**
     * Holds the current session to JMS if we have a session.
     */
    private Optional<Session> sessionOption = Optional.empty();
    /**
     * Holds a map of names to destinations.
     */
    private Map<String, Destination> destinations = new LinkedHashMap<>();
    /**
     * Holds a map of names to `MessageProducer`s.
     */
    private Map<String, MessageProducer> producers = new LinkedHashMap<>();
    /**
     * Holds a map of names to `MessageConsumer`s.
     */
    private Map<String, MessageConsumer> consumers = new LinkedHashMap<>();

    /**
     * Create a new JMS Service.
     *
     * @param connectionSupplier  connectionSupplier
     * @param destinationSupplier destinationSupplier
     * @param transacted          transacted
     * @param acknowledgeMode     acknowledgeMode
     * @param startConnection     startConnection
     * @param defaultDestination  defaultDestination
     * @param defaultTimeout      defaultTimeout
     */
    public JmsService(final Supplier<Connection> connectionSupplier,
                      final Function<String, Destination> destinationSupplier,
                      final boolean transacted,
                      final int acknowledgeMode,
                      final boolean startConnection,
                      final String defaultDestination,
                      final int defaultTimeout) {

        this.connectionSupplier = connectionSupplier;
        this.destinationSupplier = destinationSupplier;
        this.transacted = transacted;
        this.acknowledgeMode = acknowledgeMode;
        this.startConnection = startConnection;
        this.defaultDestination = defaultDestination;
        this.defaultTimeout = defaultTimeout;

        getConnection();

    }

    /**
     * Get the destination.
     *
     * @param destinationName destinationName
     * @return JMS destination which is a queue or topic.
     */
    private Destination getDestination(final String destinationName) {
        if (!destinations.containsKey(destinationName)) {
            Destination destination = destinationSupplier.apply(destinationName);
            destinations.put(destinationName, destination);
        }
        return destinations.get(destinationName);
    }

    /**
     * Get the `MessageConsumer`.
     *
     * @param destinationName destinationName
     * @return JMS `MessageConsumer`.
     */
    private MessageConsumer getConsumer(final String destinationName) {
        if (!consumers.containsKey(destinationName)) {
            Session session = getSession();

            Destination destination = getDestination(destinationName);
            try {
                MessageConsumer consumer = session.createConsumer(destination);
                consumers.put(destinationName, consumer);
            } catch (JMSException e) {
                throw new JmsException("Unable to create consumer for destination "
                        + destinationName, e);
            }
        }
        return consumers.get(destinationName);
    }

    /**
     * Get the `MessageProducer` or create one from the JMS session.
     *
     * @param destinationName destinationName
     * @return JMS `MessageProducer`.
     */
    private MessageProducer getProducer(final String destinationName) {

        if (!producers.containsKey(destinationName)) {

            Session session = getSession();
            Destination destination = getDestination(destinationName);
            MessageProducer producer;
            try {
                producer = session.createProducer(destination);
            } catch (JMSException e) {
                throw new JmsException("Unable to create producer for destination "
                        + destinationName, e);
            }
            producers.put(destinationName, producer);

        }
        return producers.get(destinationName);
    }

    /**
     * Get the current session or create one from the JMS connection.
     *
     * @return JMS Session
     */
    private Session getSession() {

        if (!sessionOption.isPresent()) {
            try {
                sessionOption = Optional.of(getConnection().createSession(transacted, acknowledgeMode));
            } catch (JMSException e) {
                throw new JmsException("Unable to get JMS session", e);
            }
        }
        return sessionOption.get();
    }

    /**
     * Get the current connection or create one using the connectionSupplier.
     *
     * @return JMS Connection
     */
    private Connection getConnection() {

        if (!connectionOption.isPresent()) {
            final Connection connection = connectionSupplier.get();

            if (connection instanceof ActiveMQConnection) {
                ((ActiveMQConnection) connection).addTransportListener(new TransportListener() {
                    @Override
                    public void onCommand(Object command) {

                    }

                    @Override
                    public void onException(IOException error) {
                    }

                    @Override
                    public void transportInterupted() {
                        connected.set(false);
                    }

                    @Override
                    public void transportResumed() {
                        connected.set(true);
                    }
                });
            }

            connected.set(true);

            if (startConnection) {
                try {
                    connection.start();
                } catch (JMSException e) {
                    throw new JmsException("Unable to start JMS connection", e);
                }
            }
            connectionOption = Optional.of(connection);
        }
        return connectionOption.get();
    }

    /**
     * Send a text message given a queue or topic name and a text message.
     *
     * @param destinationName destinationName
     * @param messageContent  messageContent
     */
    public void sendTextMessageWithDestination(final String destinationName, final String messageContent) {

        if (!this.isConnected()) {
            throw new JmsNotConnectedException("JMS connection is down " + destinationName);
        }

        final Session session = getSession();
        final MessageProducer producer = getProducer(destinationName);
        try {
            TextMessage message = session.createTextMessage(messageContent);
            producer.send(message);
        } catch (JMSException e) {
            throw new JmsException("Unable to send message to " + destinationName, e);
        }
    }

    /**
     * Send a text message to the default queue.
     *
     * @param messageContent message content
     */
    public void sendTextMessage(String messageContent) {
        sendTextMessageWithDestination(defaultDestination, messageContent);
    }

    /**
     * Listen to a message from JMS from a given destination by name.
     *
     * @param destinationName destinationName
     * @param messageListener messageListener
     */
    public void listenTextMessagesWithDestination(final String destinationName,
                                                  final Consumer<String> messageListener) {
        final MessageConsumer consumer = getConsumer(destinationName);
        try {
            consumer.setMessageListener(message -> {
                try {
                    messageListener.accept(
                            ((TextMessage) message).getText()
                    );

                    if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE) {
                        message.acknowledge();
                    }

                } catch (JMSException e) {

                    throw new JmsException("Unable to register get text from message in listener " + destinationName, e);
                } catch (Exception ex) {

                    throw new IllegalStateException("Unable handle JMS Consumer  " + destinationName, ex);
                }
            });
        } catch (JMSException e) {

            throw new JmsException("Unable to register message listener " + destinationName, e);
        }
    }

    /**
     * Listen for message on default destination.
     *
     * @param messageListener messageListener
     */
    public void listenTextMessages(final Consumer<String> messageListener) {
        listenTextMessagesWithDestination(defaultDestination, messageListener);
    }

    /**
     * Receive a message from  destination with timeout.
     *
     * @param destinationName destinationName
     * @param timeout         timeout
     * @return message
     */
    public String receiveTextMessageFromDestinationWithTimeout(final String destinationName, final int timeout) {

        if (!this.isConnected()) {
            throw new JmsNotConnectedException("Not connected");
        }
        MessageConsumer consumer = getConsumer(destinationName);
        TextMessage message;
        try {
            if (timeout == 0) {
                message = (TextMessage) consumer.receiveNoWait();
            } else {
                message = (TextMessage) consumer.receive(timeout);
            }
            if (message != null) {

                if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE) {
                    message.acknowledge();
                }
                return message.getText();
            } else {
                return null;
            }
        } catch (JMSException e) {
            throw new JmsException("Unable to receive message from " + destinationName, e);
        }
    }

    /**
     * Receive a message from  destination using default timeout.
     *
     * @param destinationName destination name
     * @return message
     */
    public String receiveTextMessageFromDestination(final String destinationName) {
        return receiveTextMessageFromDestinationWithTimeout(destinationName, defaultTimeout);
    }

    /**
     * Receive a message from  destination using default timeout and default destination.
     *
     * @return received message
     */
    public String receiveTextMessage() {
        return receiveTextMessageFromDestination(defaultDestination);
    }

    /**
     * Receive a message from  destination with timeout.
     *
     * @return received message
     */
    public String receiveTextMessageWithTimeout(final int timeout) {
        return receiveTextMessageFromDestinationWithTimeout(defaultDestination, timeout);
    }

    /**
     * Stop the service
     */
    @Override
    public void stop() {
        if (connectionOption.isPresent()) {
            try {
                if (startConnection)
                    connectionOption.get().close();
            } catch (JMSException e) {

                throw new JmsException("Unable to stop ", e);
            }
            connectionOption = Optional.empty();
            sessionOption = Optional.empty();
            producers.clear();
            consumers.clear();
            destinations.clear();
        }
    }

    /**
     * Start the service
     */
    @Override
    public void start() {
        getConnection();
    }

    public boolean isConnected() {
        return connected.get();
    }
}


/*
 * $#
 * FOS Weka
 *
 * Copyright (C) 2013 Feedzai SA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #$
 */
package com.feedzai.fos.impl.weka.utils.pmml;

import com.feedzai.fos.impl.weka.exception.PMMLConversionException;
import org.dmg.pmml.Extension;
import org.dmg.pmml.PMML;
import org.jpmml.model.JAXBUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import weka.classifiers.Classifier;

import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileOutputStream;
import java.util.zip.GZIPOutputStream;

import static com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons.ALGORITHM_EXTENSION_ELEMENT;
import static com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons.Algorithm;

/**
 * A PMML producer for Weka classifiers.
 *
 * @author Ricardo Ferreira (ricardo.ferreira@feedzai.com)
 * @since 1.0.4
 */
public class PMMLProducers {

    /**
     * The logger.
     */
    private final static Logger logger = LoggerFactory.getLogger(PMMLProducers.class);

    /**
     * Converts the given {@link weka.classifiers.Classifier} to PMML and saves it to the given file.
     *
     * @param classifier The {@link weka.classifiers.Classifier} to convert to PMML.
     * @param file       The file to save the PMML.
     * @param compress   {@code true} to save the classifier GZipped, or {@code false} otherwise.
     * @throws com.feedzai.fos.impl.weka.exception.PMMLConversionException If if fails to convert to PMML.
     */
    public static void produce(Classifier classifier, File file, boolean compress) throws PMMLConversionException {
        Algorithm algorithm = Algorithm.fromClassifier(classifier);

        PMML pmml = algorithm.getPMMLProducer().produce(classifier);
        addAlgorithm(pmml, algorithm);

        try {
            if (compress) {
                String finalPath = file.getAbsolutePath();

                logger.debug("Saving compressed file to '{}'.", finalPath);

                try (FileOutputStream fileOutputStream = new FileOutputStream(finalPath);
                     GZIPOutputStream gzipOutputStream = new GZIPOutputStream(fileOutputStream);)
                {
                    JAXBUtil.marshalPMML(pmml, new StreamResult(gzipOutputStream));
                }
            } else {
                logger.debug("Saving file to '{}'.", file.getAbsolutePath());

                try (FileOutputStream fos = new FileOutputStream(file)) {
                    JAXBUtil.marshalPMML(pmml, new StreamResult(file));
                }
            }

            logger.debug("File successfully saved.");
        } catch (Exception e) {
            logger.error("Failed to marshal the PMML to file.", e);
            throw new PMMLConversionException("Failed to marshal the PMML to file.", e);
        }
    }

    /**
     * Adds a {@link org.dmg.pmml.Extension} element to the {@link org.dmg.pmml.PMML} with the
     * algorithm used to build the classifier.
     * <p/>
     * The name of the extension is {@link com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons#ALGORITHM_EXTENSION_ELEMENT}.
     *
     * @param pmml      The {@link PMML} to which add the algorithm.
     * @param algorithm The algorithm used to build the classifier.
     */
    private static void addAlgorithm(PMML pmml, Algorithm algorithm) {
        Extension extension = new Extension();
        extension.withName(ALGORITHM_EXTENSION_ELEMENT);
        extension.withValue(algorithm.name());

        pmml.withExtensions(extension);
    }
}


package io.advantageous.qbit.example;

import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

@Service
public class RecommendationService {

    private final SimpleCache<String, User> users =
            new SimpleCache<>(10_000);

    /* Blocking version. */
//    public List<Recommendation> recommend(final String userName) {
//        System.out.println("recommend called");
//        User user = users.get(userName);
//        if (user == null) {
//            user = loadUser(userName);
//        }
//        return runRulesEngineAgainstUser(user);
//    }

//    private User loadUser(String userName) {
//        return new User("bob");
//    }

    private BlockingQueue<Runnable> callbacks = new ArrayBlockingQueue<Runnable>(10_000);

//    @Autowired

    private UserDataServiceClient userDataService;

    //    @Autowired
    public RecommendationService(UserDataServiceClient userDataService) {
        this.userDataService = userDataService;
    }

    public void recommend(final Callback<List<Recommendation>> recommendationsCallback,
                          final String userName) {

        System.out.println("recommend called");

        User user = users.get(userName);

        if (user == null) {
            userDataService.loadUser(
                    loadedUser -> {

                        System.out.println("GOT CALL BACK FROM SERVICE");
                        handleLoadFromUserDataService(loadedUser, recommendationsCallback);

                    }, userName);

            /* Load user using Callback. */
//            userDataService.loadUser(new Callback<User>() {
//                @Override
//                public void accept(final User loadedUser) {
//                        handleLoadFromUserDataService(loadedUser,
//                                recommendationsCallback);
//                }
//            }, userName);

        } else {
            recommendationsCallback.accept(runRulesEngineAgainstUser(user));
        }

    }

    /**
     * Handle defered recommendations based on user loads.
     */
    private void handleLoadFromUserDataService(final User loadedUser,
                                               final Callback<List<Recommendation>> recommendationsCallback) {

        /** Add a runnable to the callbacks list. */
        callbacks.add(() -> {
            List<Recommendation> recommendations = runRulesEngineAgainstUser(loadedUser);
            recommendationsCallback.accept(recommendations);
        });

//        callbacks.add(new Runnable() {
//            @Override
//            public void run() {
//                List<Recommendation> recommendations = runRulesEngineAgainstUser(loadedUser);
//                recommendationsCallback.accept(recommendations);
//            }
//        });
    }

    @QueueCallback({
            QueueCallbackType.EMPTY,
            QueueCallbackType.START_BATCH,
            QueueCallbackType.LIMIT})
    private void handleCallbacks() {

        flushServiceProxy(userDataService);
        Runnable runnable = callbacks.poll();

        while (runnable != null) {
            runnable.run();
            runnable = callbacks.poll();
        }
    }

    private List<Recommendation> runRulesEngineAgainstUser(final User user) {

        return Lists.list(new Recommendation("Take a walk"), new Recommendation("Read a book"),
                new Recommendation("Love more, complain less"));
    }

}


package io.advantageous.qbit.example.nocache;

import io.advantageous.qbit.admin.ManagedServiceBuilder;
import io.advantageous.qbit.annotation.RequestMapping;
import io.advantageous.qbit.annotation.http.NoCacheHeaders;
import io.advantageous.qbit.annotation.http.ResponseHeader;
import io.advantageous.qbit.http.request.HttpBinaryResponse;
import io.advantageous.qbit.http.request.HttpResponseBuilder;
import io.advantageous.qbit.http.request.HttpTextResponse;
import io.advantageous.qbit.http.request.decorator.HttpBinaryResponseHolder;
import io.advantageous.qbit.http.request.decorator.HttpResponseDecorator;
import io.advantageous.qbit.http.request.decorator.HttpTextResponseHolder;
import io.advantageous.qbit.util.MultiMap;

import static io.advantageous.qbit.admin.ManagedServiceBuilder.managedServiceBuilder;

@RequestMapping("/") @NoCacheHeaders
public class NoCacheService {

    @RequestMapping @ResponseHeader(name = "X-Attempt-Id", value="UUID")
    public String hello() {
        return "hello";
    }

//    public static void main(final String... args) {
//        managedServiceBuilder()
//                .setRootURI("/").addEndpointService(new NoCacheService()).startApplication();
//    }

    public static void main(final String... args) {
        final ManagedServiceBuilder managedServiceBuilder = managedServiceBuilder();

        managedServiceBuilder.getHttpServerBuilder().addResponseDecorator(new HttpResponseDecorator() {
            @Override
            public boolean decorateTextResponse(HttpTextResponseHolder responseHolder, String requestPath,
                                                String method,
                                                int code, String contentType, String payload,
                                                MultiMap<String, String> responseHeaders,
                                                MultiMap<String, String> requestHeaders,
                                                MultiMap<String, String> requestParams) {

                final HttpResponseBuilder responseBuilder = HttpResponseBuilder.httpResponseBuilder()
                        .setCode(code)
                        .setContentType(contentType)
                        .setBody(payload);
                if (responseHeaders != null && !responseHeaders.isEmpty()) {
                    responseBuilder.setHeaders(responseHeaders);
                }

                responseBuilder
                        .addHeader("Cache-Control", "no-cache, no-store, must-revalidate")
                        .addHeader("Pragma", "no-cache")
                        .addHeader("Expires", "0")
                        .addHeader("X-MyHeader-Hostname", "foo");

                responseHolder.setHttpTextResponse((HttpTextResponse) responseBuilder
                        .build());

                return true;
            }

            @Override
            public boolean decorateBinaryResponse(HttpBinaryResponseHolder responseHolder, String requestPath,
                                                  String method,
                                                  int code, String contentType, byte[] payload,
                                                  MultiMap<String, String> responseHeaders,
                                                  MultiMap<String, String> requestHeaders,
                                                  MultiMap<String, String> requestParams) {

                final HttpResponseBuilder responseBuilder = HttpResponseBuilder.httpResponseBuilder()
                        .setCode(code)
                        .setContentType(contentType)
                        .setBody(payload);

                if (responseHeaders != null && !responseHeaders.isEmpty()) {
                    responseBuilder.setHeaders(responseHeaders);
                }

                responseBuilder
                        .addHeader("Cache-Control", "no-cache, no-store, must-revalidate")
                        .addHeader("Pragma", "no-cache")
                        .addHeader("Expires", "0")
                        .addHeader("X-Calypso-Hostname", "foo");

                responseHolder.setHttpBinaryResponse((HttpBinaryResponse)responseBuilder
                        .build());

                return true;
            }
        });

        managedServiceBuilder.setRootURI("/").addEndpointService(new NoCacheService())
                .startApplication();
    }

}


/*
 * $#
 * FOS Weka
 *
 * Copyright (C) 2013 Feedzai SA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #$
 */
package com.feedzai.fos.impl.weka.utils.pmml;

import com.feedzai.fos.impl.weka.exception.PMMLConversionException;
import org.dmg.pmml.Extension;
import org.dmg.pmml.PMML;
import org.jpmml.model.JAXBUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import weka.classifiers.Classifier;

import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.util.zip.GZIPInputStream;

import static com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons.ALGORITHM_EXTENSION_ELEMENT;
import static com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons.Algorithm;

/**
 * A PMML consumer for Weka classifiers.
 *
 * @author Ricardo Ferreira (ricardo.ferreira@feedzai.com)
 * @since 1.0.4
 */
public class PMMLConsumers {

    /**
     * The logger.
     */
    private final static Logger logger = LoggerFactory.getLogger(PMMLConsumers.class);

    /**
     * Consumes the PMML in the given file and converts it to a Weka {@link Classifier}.
     *
     * @param file The file with the PMML representation of the classifier.
     * @return A Weka {@link Classifier}.
     * @throws PMMLConversionException If if fails to consume the PMML file.
     */
    public static Classifier consume(File file) throws PMMLConversionException {
        PMML pmml = null;
        try {
            if (isGzipped(file)) {
                logger.debug("Consuming GZipped PMML file '{}'.", file.getAbsolutePath());

                try (FileInputStream fis = new FileInputStream(file);
                     GZIPInputStream gzipInputStream = new GZIPInputStream(fis))
                {
                    pmml = JAXBUtil.unmarshalPMML(new StreamSource(gzipInputStream));
                }
            } else {
                logger.debug("Consuming PMML file '{}'.", file.getAbsolutePath());

                try (FileInputStream fis = new FileInputStream(file)) {
                    pmml = JAXBUtil.unmarshalPMML(new StreamSource(fis));
                }
            }
        } catch (Exception e) {
            throw new PMMLConversionException("Failed to unmarshal PMML file '" + file + "'. Make sure the file is a valid PMML.", e);
        }

        Algorithm algorithm = getAlgorithm(pmml);

        logger.debug("Consumed PMML file using algorithm {}.", algorithm);

        return algorithm.getPMMLConsumer().consume(pmml);
    }

    /**
     * Checks if the given file is in GZIP format.
     * <p/>
     * It checks this by checking the head for the {@link java.util.zip.GZIPInputStream#GZIP_MAGIC} number.
     *
     * @param file The file to check.
     * @return {@code true} if the file is in GZIP format, or {@code false}
     * @throws IOException
     */
    private static boolean isGzipped(File file) throws IOException {
        FileInputStream fileInputStream = new FileInputStream(file);

        int head = (fileInputStream.read() & 0xff) | ((fileInputStream.read() << 8 ) & 0xff00 );

        return GZIPInputStream.GZIP_MAGIC == head;
    }

    /**
     * Retrieves the {@link com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons.Algorithm} represented
     * as a PMML extension named {@link com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons#ALGORITHM_EXTENSION_ELEMENT}.
     *
     * @param pmml The {@link PMML}.
     * @return The {@link com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons.Algorithm} in the PMML.
     * @throws PMMLConversionException If it fails to retrieve the algorithm from the given PPML.
     */
    private static Algorithm getAlgorithm(PMML pmml) throws PMMLConversionException {
        for (Extension extension : pmml.getExtensions()) {
            if (ALGORITHM_EXTENSION_ELEMENT.equals(extension.getName())) {
                return Algorithm.valueOf(extension.getValue());
            }
        }

        throw new PMMLConversionException("Couldn't find '" + ALGORITHM_EXTENSION_ELEMENT + "' extension element in PMML.");
    }
}


package org.jboss.resteasy.util;

import javax.ws.rs.core.Context;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

/**
 * Pick
 *
 * @author Bill Burke
 * @version $Revision: 1 $
 */
public class PickConstructor {

    /**
     * Pick best constructor for a provider or resource class
     *

     * Picks constructor with most parameters. Will ignore constructors that
     * have parameters with no @Context annotation
     *
     * @param clazz
     * @return
     */
    public static Constructor pickSingletonConstructor(Class clazz) {
        Constructor<?>[] constructors = clazz.getConstructors();
        Constructor<?> constructor = null;
        // prefer a no-arg constructor
        int numParameters = 0;
        Constructor pick = null;
        boolean potentialConflict = false; // https://issues.jboss.org/browse/RESTEASY-645
        for (Constructor con : constructors) {
            if (Modifier.isPublic(con.getModifiers()) == false) {
                continue;
            }

            if (con.getParameterTypes().length >= numParameters) {
                if (con.getParameterTypes().length > numParameters) {
                    potentialConflict = false;
                }
                boolean noContextAnnotation = false;
                if (con.getParameterAnnotations() != null) {
                    for (Annotation[] ann : con.getParameterAnnotations()) {
                        if (FindAnnotation.findAnnotation(ann, Context.class) == null) {
                            noContextAnnotation = true;
                        }
                    }
                }
                if (noContextAnnotation)
                    continue;
                if (con.getParameterTypes().length == numParameters && numParameters != 0) {
                    potentialConflict = true;
                }
                numParameters = con.getParameterTypes().length;
                pick = con;

            }
        }
        if (potentialConflict) {
            // TODO logger.warn("Ambiguity constructors are found in " + clazz +
            // ". More details please refer to http://jsr311.java.net/nonav/releases/1.1/spec/spec.html");
        }
        return pick;
    }

    /**
     * Pick best constructor for a provider or resource class
     *

     * Picks constructor with most parameters. Will ignore constructors that
     * have parameters with no @Context annotation
     *
     * @param clazz
     * @return
     */
    public static Constructor pickPerRequestConstructor(Class clazz) {
        Constructor<?>[] constructors = clazz.getConstructors();
        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        Constructor<?> constructor = null;
        // prefer a no-arg constructor
        int numParameters = 0;
        Constructor pick = null;
        boolean potentialConflict = false; // https://issues.jboss.org/browse/RESTEASY-645
        for (Constructor con : constructors) {
            if (Modifier.isPublic(con.getModifiers()) == false) {
                continue;
            }
            if (con.getParameterTypes().length >= numParameters) {
                if (con.getParameterTypes().length > numParameters) {
                    potentialConflict = false;
                }

                boolean noContextAnnotation = false;
                if (con.getParameterAnnotations() != null) {
                    for (Annotation[] ann : con.getParameterAnnotations()) {
                        if (FindAnnotation.findJaxRSAnnotations(ann).length == 0) {
                            noContextAnnotation = true;
                        }
                    }
                }
                if (noContextAnnotation)
                    continue;
                if (con.getParameterTypes().length == numParameters && numParameters != 0) {
                    potentialConflict = true;
                }
                numParameters = con.getParameterTypes().length;
                pick = con;
            }
        }

        if (potentialConflict) {
            // todo logger.warn("Ambiguity constructors are found in " + clazz +
            // ". More details please refer to http://jsr311.java.net/nonav/releases/1.1/spec/spec.html");
        }
        return pick;
    }
}


/*
 * $#
 * FOS Weka
 *
 * Copyright (C) 2013 Feedzai SA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #$
 */
package com.feedzai.fos.impl.weka.config;

import com.feedzai.fos.api.*;
import com.feedzai.fos.common.validation.NotNull;
import com.feedzai.fos.impl.weka.utils.pool.GenericObjectPoolConfig;
import com.google.common.collect.ImmutableList;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationConverter;
import org.apache.commons.configuration.MapConfiguration;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Represents header that defines the schema of the machine learning model.
 *
 * @author Marco Jorge (marco.jorge@gmail.com)
 */
public class WekaModelConfig {
    /**
     * The property name of the classIndex.
     *
 The classIndex is an integer that indicates the position of the classified instance fields (0 based).
     */
    public static final String CLASS_INDEX = "classIndex";

    /**
     * The property name of the model file.
     *
 The model file is the serialized Classifier that will be used for classification.
     */
    public static final String MODEL_FILE = "model";

    /**
     * The property name of a boolean that indicates if the classifier is thread safe..
     *
 If the model is thread safe then @{WekaThreadSafeScorerPassthrough} will be used, else @{WekaThreadSafeScorerPool} will be used.
     */
    public static final String IS_CLASSIFIER_THREAD_SAFE = "isClassifierThreadSafe";

    /**
     * The property name that defines the weka classifier class (FQCN).
     */
    public static final String CLASSIFIER_IMPL = "classifierimpl";

    /**
     * The property name of the ID of the model.
     */
    public static final String ID = "id";

    /**
     * The property name of the model configuration.
     */
    public static final String CLASSIFIER_CONFIG = "config";

    /**
     * The property name of the format the classifier is in (e.g. PMML, binary, etc).
     */
    public static final String CLASSIFIER_FORMAT = "format";

    private ModelConfig modelConfig;
    private WekaManagerConfig wekaManagerConfig;
    private int classIndex;
    private File model;
    private UUID id;
    private transient File header;
    private transient boolean dirty = true;
    private boolean classifierThreadSafe;
    private Configuration configuration;

    /**
     * The {@link com.feedzai.fos.api.ModelDescriptor} of the model.
     */
    private ModelDescriptor modelDescriptor;

    /**
     * Creates a new model from the given ModelConfig and WekaManagerConfig.
     *

     * From the ModelConfig.properties the parameters MODEL_FILE, ID and CLASS_INDEX are looked up.
     * If the CLASS_INDEX doesn't exist int he ModelConfig, the default value is used from WekaManagerConfig.
     *
     * @param modelConfig       the configuration with MODEL_FILE, ID and CLASS_INDEX
     * @param wekaManagerConfig the configuration with the default CLASS_INDEX
     */
    public WekaModelConfig(ModelConfig modelConfig, WekaManagerConfig wekaManagerConfig) throws FOSException {
        checkNotNull(modelConfig, "Model configuration cannot be null");
        checkNotNull(wekaManagerConfig, "Manager configuration cannot be null");

        this.modelConfig = modelConfig;
        this.wekaManagerConfig = wekaManagerConfig;

        parseModelConfig();
    }

    private void parseModelConfig() throws FOSException {
        configuration = new MapConfiguration((Map) modelConfig.getProperties());
        classIndex = this.modelConfig.getIntProperty(CLASS_INDEX, -1);
        if (classIndex < 0) {
            classIndex = this.modelConfig.getAttributes().size() - 1;
        }

        String modelFile = configuration.getString(MODEL_FILE);
        if (modelFile != null) {
            this.model = new File(modelFile);
        }

        String formatValue = configuration.getString(CLASSIFIER_FORMAT);
        if (formatValue != null) {
            ModelDescriptor.Format format = ModelDescriptor.Format.valueOf(formatValue);
            this.modelDescriptor = new ModelDescriptor(format, modelFile);
        }

        classifierThreadSafe = configuration.getBoolean(IS_CLASSIFIER_THREAD_SAFE, false /* defaults to Pool implementation*/);

        String uuid = configuration.getString(ID);
        if (uuid != null) {
            this.id = UUID.fromString(uuid);
        }
    }

    /**
     * Returns the pool configuration of the scorer.
     *
     * @return a map from configuration key to configuration value
     */
    @NotNull
    public Map<Object, Object> getPoolConfiguration() {
        return ConfigurationConverter.getMap(configuration.subset(GenericObjectPoolConfig.class.getName()));
    }

    /**
     * Returns true, if and only if the underlying classifier implementation is thread safe.
     *
     * @return true if the implementation is thread safe
     */
    public boolean isClassifierThreadSafe() {
        return classifierThreadSafe;
    }

    /**
     * Gets a boolean indicating if this configuration has changed since the last save.
     *
     * @return true if non persisted changes have been mande
     */
    public boolean isDirty() {
        return dirty;
    }

    /**
     * Sets or clears the indicator if this configuration has been changed since the last save.
     *
     * @param dirty true if changes have been persisted to disk, false if changes have been made
     */
    public void setDirty(boolean dirty) {
        this.dirty = dirty;
    }

    /**
     * Gets the header file of the model.
     *
     * @return the header file
     */
    @NotNull
    public File getHeader() {
        return header;
    }

    /**
     * Sets the header file of the model.
     *
     * @param header the header file
     */
    public void setHeader(File header) {
        this.dirty = true;
        this.header = header;
    }

    /**
     * Gets the ID of the model.
     *
     * @return the ID of the model
     */
    @NotNull
    public UUID getId() {
        return id;
    }

    /**
     * Sets the ID of the model.
     *
     * @param id the ID
     */
    public void setId(UUID id) {
        this.dirty = true;
        this.id = id;

        this.modelConfig.setProperty(ID, id.toString());
    }

    /**
     * Gets the index of the class attribute.
     *
     * @return index value od the class attribute.
     */
    public int getClassIndex() {
        return this.classIndex;
    }
    /**
     * Gets the model file of the serialized classifier.
     *
     * @return the model file
     */
    @NotNull
    public File getModel() {
        return model;
    }

    /**
     * Sets the model file of the serialized classifier.
     *
     * @param model the model file
     */
    public void setModel(File model) {
        this.dirty = true;
        this.model = model;

        this.modelConfig.setProperty(MODEL_FILE, model.getAbsolutePath());
    }

    /**
     * Sets the {@link com.feedzai.fos.api.ModelDescriptor} of the model.
     *
     * @param modelDescriptor The {@link com.feedzai.fos.api.ModelDescriptor} of the model.
     */
    public void setModelDescriptor(ModelDescriptor modelDescriptor) {
        this.modelDescriptor = modelDescriptor;

        setModel(new File(modelDescriptor.getModelFilePath()));

        this.modelConfig.setProperty(CLASSIFIER_FORMAT, modelDescriptor.getFormat().toString());
    }

    /**
     * Retrieves the {@link com.feedzai.fos.api.ModelDescriptor} of the model.
     *
     * @return The {@link com.feedzai.fos.api.ModelDescriptor} of the model.
     */
    public ModelDescriptor getModelDescriptor() {
        return modelDescriptor;
    }

    /**
     * Gets the instance fields of this configuration.
     *
     * @return the list of instance fields of this classifier
     */
    @NotNull
    public List<Attribute> getAttributess() {
        return this.modelConfig.getAttributes();
    }

    /**
     * Updates the underlying ModelConfig using ModelConfig.update.
     *
     * @param modelConfig the model config with the new settings
     */
    public void update(ModelConfig modelConfig) throws FOSException {
        checkNotNull(modelConfig);

        this.dirty = true;

        // The class index should be reset everytime a model config is updated.
        // TODO: Refactor the classIndex to be in ModelConfig.
        this.modelConfig.getProperties().remove(CLASS_INDEX);
        this.modelConfig.update(modelConfig);

        parseModelConfig();
    }

    /**
     * Gets the current and updated model config.
     *
     * @return the underlying model config
     */
    @NotNull
    public ModelConfig getModelConfig() {
        return modelConfig;
    }
}


/*
 * $#
 * FOS Weka
 *
 * Copyright (C) 2013 Feedzai SA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #$
 */
package com.feedzai.fos.impl.weka;

import com.feedzai.fos.api.FOSException;
import com.feedzai.fos.api.Scorer;
import com.feedzai.fos.common.validation.NotNull;
import com.feedzai.fos.impl.weka.config.WekaManagerConfig;
import com.feedzai.fos.impl.weka.config.WekaModelConfig;
import com.feedzai.fos.impl.weka.utils.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import weka.classifiers.Classifier;

import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Implementation of the classification-api that supports multiple simultaneous classifiers (thread safe!).
 *
 * @author Marco Jorge (marco.jorge@feedzai.com)
 */
public class WekaScorer implements Scorer {
    private static final Logger logger = LoggerFactory.getLogger(WekaScorer.class);

    private Map<UUID, WekaThreadSafeScorer> wekaThreadSafeScorers = new HashMap<>();
    private ExecutorService executorService;
    private ReentrantReadWriteLock reloadModelsLock = new ReentrantReadWriteLock(true /* fair */);
    private WekaManagerConfig wekaManagerConfig;

    private WekaThreadSafeScorer getScorer(UUID modelId) throws FOSException {
        WekaThreadSafeScorer wekaThreadSafeScorer = wekaThreadSafeScorers.get(modelId);
        if (wekaThreadSafeScorer == null) {
            logger.error("No model with ID '{}'", modelId);
            throw new FOSException("No model with ID " + modelId);
        }
        return wekaThreadSafeScorer;
    }

    private <T> T getFuture(Future<T> future, UUID modelId) throws FOSException {
        try {
            return future.get();
        } catch (InterruptedException e) {
            logger.error("Could not score on model '{}'", modelId, e);
            throw new FOSException(e);
        } catch (ExecutionException e) {
            logger.error("Could not score on model '{}'", modelId, e);
            throw new FOSException(e);
        }
    }

    /**
     * Creates a new scorer for the models identified int he configuration.
     *
     *
If loading of a model was not possible, this logs a message but continues to load other models and does not throw any exception.
     *
     * @param modelConfigs      the list of models to instantiate
     * @param wekaManagerConfig the global configuration
     */
    public WekaScorer(Map<UUID, WekaModelConfig> modelConfigs, WekaManagerConfig wekaManagerConfig) {
        checkNotNull(modelConfigs, "Model configuration map cannot be null");
        checkNotNull(wekaManagerConfig, "Manager config cannot be null");

        this.wekaManagerConfig = wekaManagerConfig;

        for (Map.Entry<UUID, WekaModelConfig> wekaModelConfigEntry : modelConfigs.entrySet()) {
            try {
                if (wekaModelConfigEntry.getValue().isClassifierThreadSafe()) {
                    wekaThreadSafeScorers.put(wekaModelConfigEntry.getValue().getId(), new WekaThreadSafeScorerPassthrough(wekaModelConfigEntry.getValue(), wekaManagerConfig));
                } else {
                    wekaThreadSafeScorers.put(wekaModelConfigEntry.getValue().getId(), new WekaThreadSafeScorerPool(wekaModelConfigEntry.getValue(), wekaManagerConfig));
                }
            } catch (Exception e) {
                logger.error("Could not load from '{}' (continuing to load others)", wekaModelConfigEntry.getKey(), e);
            }
        }
        this.executorService = Executors.newFixedThreadPool(wekaManagerConfig.getThreadPoolSize());
    }

    @Override
    public void close() {
        try {
            reloadModelsLock.writeLock().lock();

            executorService.shutdown();

            for (WekaThreadSafeScorer wekaThreadSafeScorer : this.wekaThreadSafeScorers.values()) {
                if (wekaThreadSafeScorer != null) {
                    wekaThreadSafeScorer.close();
                }
            }
        } finally {
            reloadModelsLock.writeLock().unlock();
        }
    }

    /**
     * Score the scorable for each model ID identified by modelIds.
     *
     *
 If multiple models are given as parameters, they will be scored in parallel.
     *
     * @param modelIds the list of models to score
     * @param scorable the item to score
     * @return a List of scores with the same order and the received modelIds
     * @throws FOSException when classification was not possible
     */
    @Override
    @NotNull
    public List<double[]> score(List<UUID> modelIds, Object[] scorable) throws FOSException {
        checkNotNull(modelIds, "Models to score cannot be null");
        checkNotNull(scorable, "Instance cannot be null");

        List<double[]> scores = new ArrayList<>(modelIds.size());

        try {
            reloadModelsLock.readLock().lock();

            if (modelIds.size() == 1) {
                // if only one model, then don't parallelize scoring
                WekaThreadSafeScorer wekaThreadSafeScorer = getScorer(modelIds.get(0));
                scores.add(wekaThreadSafeScorer.score(scorable));
            } else {
                Map<UUID, Future<double[]>> futureScores = new HashMap<>(modelIds.size());

                // scatter
                for (UUID modelId : modelIds) {
                    WekaThreadSafeScorer wekaThreadSafeScorer = getScorer(modelId);
                    futureScores.put(modelId, executorService.submit(new AsyncScoringTask(wekaThreadSafeScorer, scorable)));
                }
                // gather
                for (UUID modelId : modelIds) {
                    scores.add(getFuture(futureScores.get(modelId), modelId));
                }
            }
        } finally {
            reloadModelsLock.readLock().unlock();
        }

        return scores;
    }

    /**
     * Score each scorable with the given modelId.
     *
     *
 If multiple scorables are given as parameters, they will be scored in parallel.
     *
     * @param modelId   the id of the model
     * @param scorables an array of instances to score
     * @return a list of scores with the same order as the scorable array
     * @throws FOSException when classification was not possible
     */
    @Override
    @NotNull
    public List<double[]> score(UUID modelId, List<Object[]> scorables) throws FOSException {
        checkNotNull(scorables, "List of scorables cannot be null");

        List<double[]> scores = new ArrayList<>(scorables.size());

        try {
            reloadModelsLock.readLock().lock();

            if (scorables.size() == 1) {
                // if only one model, then don't parallelize scoring
                WekaThreadSafeScorer wekaThreadSafeScorer = getScorer(modelId);
                scores.add(wekaThreadSafeScorer.score(scorables.get(0)));
            } else {
                Map<Object[], Future<double[]>> futureScores = new HashMap<>(scorables.size());

                // scatter
                for (Object[] scorable : scorables) {
                    WekaThreadSafeScorer wekaThreadSafeScorer = wekaThreadSafeScorers.get(modelId);
                    futureScores.put(scorable, executorService.submit(new AsyncScoringTask(wekaThreadSafeScorer, scorable)));
                }

                // gather
                for (Object[] scorable : scorables) {
                    scores.add(getFuture(futureScores.get(scorable), modelId));
                }
            }
        } finally {
            reloadModelsLock.readLock().unlock();
        }

        return scores;
    }

    /**
     * Score a single scorable with the given modelId.
     *
     * @param modelId   the id of the model
     * @param scorable  the instance to score
     * @return a list of scores with the same order as the scorable array
     * @throws FOSException when classification was not possible
     */
    @Override
    @NotNull
    public double[] score(UUID modelId, Object[] scorable) throws FOSException {
        checkNotNull(scorable, "The scorable cannot be null");

        try {
            reloadModelsLock.readLock().lock();

            WekaThreadSafeScorer wekaThreadSafeScorer = getScorer(modelId);
            return wekaThreadSafeScorer.score(scorable);

        } finally {
            reloadModelsLock.readLock().unlock();
        }
    }

    /**
     * Adds the given model to the managed models.
     *
     *
 If the provided model id already exists, then the older model is removed and the new one is instantiated.
     *
     * @param wekaModelConfig the configuration of the new model
     * @throws FOSException when the new model could not be instantiated
     */
    public void addOrUpdate(WekaModelConfig wekaModelConfig) throws FOSException {
        checkNotNull(wekaModelConfig, "Model config cannot be null");

        WekaThreadSafeScorer newWekaThreadSafeScorer = new WekaThreadSafeScorerPool(wekaModelConfig, wekaManagerConfig);
        WekaThreadSafeScorer oldWekaThreadSafeScorer = quickSwitch(wekaModelConfig.getId(), newWekaThreadSafeScorer);

        WekaUtils.closeSilently(oldWekaThreadSafeScorer);
    }

    /**
     * Removes the given model from the managed models.
     *
     *
 If the model does not exist no exception will be thrown.
     *
     * @param modelId the id of the model to remove.
     */
    public void removeModel(UUID modelId) {
        WekaThreadSafeScorer newWekaThreadSafeScorer = null;
        WekaThreadSafeScorer oldWekaThreadSafeScorer = quickSwitch(modelId, newWekaThreadSafeScorer);

        WekaUtils.closeSilently(oldWekaThreadSafeScorer);
    }

    /**
     * Retrieves the {@link Classifier} for the given UUID.
     *
     * @param modelId The UUID of the classifier to retrieve.
     * @return The {@link Classifier} for the given UUID.
     * @throws FOSException If it fails to retrieve the classifier.
     */
    public Classifier getClassifier(UUID modelId) throws FOSException {
        return wekaThreadSafeScorers.get(modelId).getClassifier();
    }

    /**
     * Switches the {@link com.feedzai.fos.impl.weka.WekaScorer} used for the model with the given UUID.
     *
     * @param modelId                 The UUID of the model whose scorer to switch.
     * @param newWekaThreadSafeScorer The score to switch to.
     * @return The previous scorer associated to the given UUID.
     */
    private WekaThreadSafeScorer quickSwitch(UUID modelId, WekaThreadSafeScorer newWekaThreadSafeScorer) {
        try { // quick switch - do not do anything inside for performance reasons!!
            reloadModelsLock.writeLock().lock();
            return wekaThreadSafeScorers.put(modelId, newWekaThreadSafeScorer);
        } finally {
            reloadModelsLock.writeLock().unlock();
        }
    }
}


/*
 * $#
 * FOS Weka
 *
 * Copyright (C) 2013 Feedzai SA
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #$
 */
package weka.classifiers.trees;

import com.feedzai.fos.impl.weka.exception.PMMLConversionException;
import com.feedzai.fos.impl.weka.utils.pmml.PMMLConsumer;
import org.dmg.pmml.*;
import org.jpmml.model.JAXBUtil;
import weka.classifiers.Classifier;
import weka.classifiers.RandomForestUtils;
import weka.classifiers.meta.Bagging;
import weka.core.*;
import weka.core.Attribute;

import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.util.List;

import static com.feedzai.fos.impl.weka.utils.pmml.PMMLConversionCommons.*;

/**
 * A consumer that converts PMML to a {@link weka.classifiers.trees.RandomForest} instance.
 *
 * @author Ricardo Ferreira (ricardo.ferreira@feedzai.com)
 * @since 1.0.4
 */
public class RandomForestPMMLConsumer implements PMMLConsumer<RandomForest> {

    @Override
    public RandomForest consume(String pmmlString) throws PMMLConversionException {
        PMML pmml = null;
        try (ByteArrayInputStream bais = new ByteArrayInputStream(pmmlString.getBytes())){
            pmml = JAXBUtil.unmarshalPMML(new StreamSource(bais));
        } catch (Exception e) {
            throw new PMMLConversionException("Failed to unmarshal PMML from string. Make sure it is a valid PMML.", e);
        }
        return consume(pmml);
    }

    @Override
    public RandomForest consume(File file) throws PMMLConversionException {
        PMML pmml = null;
        try (FileInputStream fis = new FileInputStream(file)) {
            pmml = JAXBUtil.unmarshalPMML(new StreamSource(fis));
        } catch (Exception e) {
            throw new PMMLConversionException("Failed to unmarshal PMML file '" + file + "'. Make sure the file is a valid PMML.", e);
        }
        return consume(pmml);
    }

    @Override
    public RandomForest consume(PMML pmml) throws PMMLConversionException {
        MiningModel miningModel = getMiningModel(pmml);
        List<Segment> segments = miningModel.getSegmentation().getSegments();

        int m_numTrees = segments.size();

        RandomForest randomForest = new RandomForest();
        randomForest.m_bagger = new Bagging();
        randomForest.m_bagger.setNumIterations(m_numTrees);
        randomForest.m_bagger.setClassifier(new RandomTree());

        try {
            RandomForestUtils.setupBaggingClassifiers(randomForest.m_bagger);
        } catch (Exception e) {
            throw new PMMLConversionException("Failed to initialize bagging classifiers.", e);
        }

        Instances instances = buildInstances(pmml.getDataDictionary());

        Classifier[] baggingClassifiers = RandomForestUtils.getBaggingClassifiers(randomForest.m_bagger);

        for (int i = 0; i < baggingClassifiers.length; i++) {
            RandomTree root = (RandomTree) baggingClassifiers[i];
            buildRandomTree(root, instances, (TreeModel) segments.get(i).getModel());
        }

        return randomForest;
    }

    /**
     * Builds a new {@link weka.classifiers.trees.RandomTree Weka RandomTree} from the given {@link org.dmg.pmml.TreeModel PMML TreeModel}.
     *
     * @param root      The {@link weka.classifiers.trees.RandomTree Weka RandomTree} which is to be built.
     * @param instances The {@link weka.core.Instances} with the tree's attributes.
     * @param treeModel The {@link org.dmg.pmml.TreeModel PMML TreeModel} which is to be converted to a {@link weka.classifiers.trees.RandomTree Weka RandomTree}.
     * @return The same {@code root} instance.
     */
    private static RandomTree buildRandomTree(RandomTree root, Instances instances, TreeModel treeModel) {
        Instances treeInstances = new Instances(instances);
        treeInstances.setClassIndex(getClassIndex(instances, treeModel));

        root.m_Info = treeInstances;
        root.m_Tree = buildRandomTreeNode(root, treeModel.getNode());

        return root;
    }

    /**
     * Builds a {@link weka.classifiers.trees.RandomTree.Tree Weka RandomTree} node
     * represented by the given {@link org.dmg.pmml.Node PMML node}.
     *
     * @param tree     The {@link weka.classifiers.trees.RandomTree Weka RandomTree} which the returned tree node is part of.
     * @param pmmlNode The {@link org.dmg.pmml.PMML PMML node} to be converted to a {@link weka.classifiers.trees.RandomTree.Tree Weka RandomTree} node.
     * @return A new {@link weka.classifiers.trees.RandomTree.Tree Weka RandomTree} node.
     */
    private static RandomTree.Tree buildRandomTreeNode(RandomTree tree, Node pmmlNode) {
        RandomTree.Tree treeNode = tree.new Tree();
        //Set the class distribution.
        treeNode.m_ClassDistribution = getClassDistribution(pmmlNode);

        Instances instances = tree.m_Info;

        boolean isLeaf = pmmlNode.getNodes().size() == 0;

        if (!isLeaf) {
            List<Node> children = pmmlNode.getNodes();

            String attributeName = ((SimplePredicate) children.get(0).getPredicate()).getField().getValue();
            Attribute attribute = instances.attribute(attributeName);

            treeNode.m_Attribute = attribute.index();

            if (attribute.isNumeric()) {
                /*
                  If the node is numeric, get its two child nodes and covert them into a new RandomTree.Tree node.
                  For example, consider the following PPML Node:

                     <Node id="2">
                       <SimplePredicate field="petal_length" operator="lessThan" value="5.05"/>
                       <Node id="3" score="Iris-setosa">
                         <SimplePredicate field="petal_length" operator="lessThan" value="2.95"/>
                       </Node>
                       <Node id="4" score="Iris-versicolor">
                         <SimplePredicate field="petal_length" operator="greaterOrEqual" value="2.95"/>
                       </Node>
                     </Node>

                  We'll grab the two child nodes and their value will be the split point of the current RandomTree.Tree node.
                */
                assert children.size() == 2 : "Numeric attributes must have exactly 2 children";

                Node left = children.get(0);
                Node right = children.get(1);

                Predicate leftPredicate = left.getPredicate();
                Predicate rightPredicate = right.getPredicate();

                assert leftPredicate instanceof SimplePredicate && leftPredicate.getClass().equals(rightPredicate.getClass()) : "Numeric attribute's nodes must have the same simple predicate.";

                double splitPoint = Double.valueOf(((SimplePredicate) leftPredicate).getValue());

                treeNode.m_SplitPoint = splitPoint;
                treeNode.m_Successors = new RandomTree.Tree[]{buildRandomTreeNode(tree, left), buildRandomTreeNode(tree, right)};
                treeNode.m_Prop = new double[]{getNodeTrainingProportion(left), getNodeTrainingProportion(right)};
            } else if (attribute.isNominal()) {

                treeNode.m_Successors = new RandomTree.Tree[children.size()];
                treeNode.m_Prop = new double[treeNode.m_Successors.length];

                for (int i = 0; i < children.size(); i++) {
                    Node child = children.get(i);

                    SimplePredicate predicate = (SimplePredicate) child.getPredicate();
                    int valueIndex = attribute.indexOfValue(predicate.getValue());

                    treeNode.m_Successors[valueIndex] = buildRandomTreeNode(tree, child);
                    treeNode.m_Prop[valueIndex] = getNodeTrainingProportion(child);
                }
            } else {
                throw new RuntimeException("Attribute type not supported: " + attribute);
            }
        }

        return treeNode;
    }
}