org.apache.s4.edsl.AppBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.s4.edsl.AppBuilder.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.s4.edsl;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.s4.base.Event;
import org.apache.s4.core.App;
import org.apache.s4.core.ProcessingElement;
import org.apache.s4.core.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

/**
 * Implementation of the S4 embedded domain-specific language (EDSL).
 * 
 * <p>
 * To write an app extend this class and define the application graph using a chain of methods as follows:
 * 
 * <pre>
 *    final public class MyApp extends BuilderS4DSL {
 * 
 *     protected void onInit() {
 * 
 *         pe("Consumer").type(ConsumerPE.class).asSingleton().
 *         pe("Producer").type(ProducerPE.class).timer().withPeriod(1, TimeUnit.MILLISECONDS).asSingleton().
 *         emit(SomeEvent.class).withKey("someKey").to("Consumer").
 *         build()
 *    }
 * </pre>
 * 
 * <p>
 * A few things to notice:
 * <ul>
 * <li>Applications must extend class {@link BuilderS4DSL}
 * <li>The graph definition is implemented in the {@link App#onInit} method which is called by the container when the
 * application is loaded.
 * <li>PEs are defined using strings because they need to be referenced by other parts of the graph. By doing this, we
 * can create the whole application in a single chain of methods.
 * <li>To assign target streams to PE fields additional information may need to be provided using the {@code onField}
 * grammar token when there is an ambiguity. This will happen when a PE has more than one targetStream field with the
 * same {@link Event} type. Use the construct {@code emit(SomeEvent.class).onField("streamFieldName")}. If the PE
 * doesn't have a field named {@code "streamField"} whose stream parameter type is {@code someEvent)} then the parser
 * will fail to build the app.
 * <li>To configure a PE, set property values by chaining any number of {@code prop(name, value)} methods. The name
 * should match a PE field and the value will be parsed using the type of that field.
 * </ul>
 * <p>
 * Grammar:
 * 
 * <pre>
 *  (pe , type , prop* , (fireOn , afterInterval? , afterNumEvents?)? , (timer, withPeriod)? ,
 *  (cache, size , expires? )? , asSingleton? , (emit, onField?,
 *  (withKey|withKeyFinder)?, to )*  )+ , build
 * </pre>
 * 
 * <p>
 * See the <a href="http://code.google.com/p/diezel">Diezel</a> project for details.
 * 
 */
public class AppBuilder extends App {

    protected App app = this;

    static final Logger logger = LoggerFactory.getLogger(AppBuilder.class);

    private Multimap<ProcessingElement, StreamBuilder<? extends Event>> pe2stream = LinkedListMultimap.create();
    Set<StreamBuilder<? extends Event>> streamBuilders = Sets.newHashSet();

    /* Variables used to hold values from state to state. */
    ProcessingElement processingElement;
    String peName;
    Class<? extends Event> triggerEventType;
    long triggerInterval = 0;
    TimeUnit triggerTimeUnit;
    int cacheSize;
    StreamBuilder<? extends Event> streamBuilder;
    String propertyName, propertyValue;

    public static AppBuilder getAppBuilder() {
        return new BuilderS4DSL();
    }

    void addProperty(String name, String value) {
        propertyName = name;
        propertyValue = value;
        setField();
    }

    void addPe2Stream(ProcessingElement pe, StreamBuilder<? extends Event> st) {
        pe2stream.put(pe, st);
    }

    App buildApp() {

        /* Stream to PE writing. */
        for (StreamBuilder<? extends Event> sb : streamBuilders) {
            for (String peName : sb.pes) {
                ProcessingElement pe = getPE(peName);
                sb.stream.setPEs(pe);
            }
        }

        /* PE to Stream wiring. */
        Map<ProcessingElement, Collection<StreamBuilder<? extends Event>>> pe2streamMap = pe2stream.asMap();
        for (Map.Entry<ProcessingElement, Collection<StreamBuilder<? extends Event>>> entry : pe2streamMap
                .entrySet()) {
            ProcessingElement pe = entry.getKey();
            Collection<StreamBuilder<? extends Event>> streams = entry.getValue();

            if (pe != null && streams != null) {
                try {
                    setStreamField(pe, streams);
                } catch (Exception e) {
                    logger.error("Unable to build app.", e);
                    return null;
                }
            }
        }

        return this;
    }

    /**
     * @param peName
     *            the peName to set
     */
    protected void setPeName(String peName) {
        this.peName = peName;
    }

    /*
     * Cannot create an abstract class in Diezel so for now, I just implement the abstract methods here. They need to be
     * overloaded by the app developer.
     */
    @Override
    protected void onStart() {
    }

    @Override
    protected void onInit() {
    }

    @Override
    protected void onClose() {
    }

    private <T extends ProcessingElement> void setField() {

        logger.debug("Adding property [{}] to PE of type [{}].", propertyName,
                processingElement.getClass().getName());

        Class<? extends ProcessingElement> type = processingElement.getClass();

        try {
            Field f = type.getDeclaredField(propertyName);
            f.setAccessible(true);
            logger.trace("Type: {}.", f.getType());
            logger.trace("GenericType: {}.", f.getGenericType());

            /* Set the field. */
            if (f.getType().getCanonicalName() == "long") {
                f.setLong(processingElement, Long.parseLong(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "int") {
                f.setInt(processingElement, Integer.parseInt(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "float") {
                f.setFloat(processingElement, Float.parseFloat(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "double") {
                f.setDouble(processingElement, Double.parseDouble(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "short") {
                f.setShort(processingElement, Short.parseShort(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "byte") {
                f.setByte(processingElement, Byte.parseByte(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "boolean") {
                f.setBoolean(processingElement, Boolean.parseBoolean(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "char") {
                f.setChar(processingElement, (char) Byte.parseByte(propertyValue));
                return;
            } else if (f.getType().getCanonicalName() == "java.lang.String") {
                f.set(processingElement, propertyValue);
                return;
            }

            logger.error("Unable to set field named [{}] in PE of type [{}].", propertyName, type);
            throw new IllegalArgumentException();

            // production code should handle these exceptions more gracefully
        } catch (NoSuchFieldException e) {
            logger.error("There is no field named [{}] in PE of type [{}].", propertyName, type);
        } catch (Exception e) {
            logger.error("Couldn't set value for field [{}] in PE of type [{}].", propertyName, type);
        }
    }

    /* Set the stream fields in PE classes. Infer the field by checking the stream parameter type <? extends Event>. */
    private void setStreamField(ProcessingElement pe, Collection<StreamBuilder<? extends Event>> streams)
            throws Exception {

        /*
         * Create a map of the stream fields to the corresponding generic type. We will use this info to assign the
         * streams. If the field type matches the stream type and there is no ambiguity, then the assignment is easy. If
         * more than one field has the same type, then then we need to do more work.
         */
        Field[] fields = pe.getClass().getDeclaredFields();
        Multimap<String, Field> typeMap = LinkedListMultimap.create();
        logger.debug("Analyzing PE [{}].", pe.getClass().getName());
        for (Field field : fields) {
            logger.trace("Field [{}] is of generic type [{}].", field.getName(), field.getGenericType());

            if (field.getType() == Stream[].class) {
                logger.debug("Found stream field: {}", field.getGenericType());

                /* Track what fields have streams with the same event type. */
                String key = field.getGenericType().toString();
                typeMap.put(key, field);
            }
        }

        /* Assign streams to stream fields. */
        Multimap<Field, Stream<? extends Event>> assignment = LinkedListMultimap.create();
        for (StreamBuilder<? extends Event> sm : streams) {

            Stream<? extends Event> stream = sm.stream;
            Class<? extends Event> eventType = sm.type;
            String key = Stream.class.getCanonicalName() + "<" + eventType.getCanonicalName() + ">[]";
            if (typeMap.containsKey(key)) {
                String fieldName;
                Field field;
                Collection<Field> streamFields = typeMap.get(key);
                int numStreamFields = streamFields.size();
                logger.debug("Found [{}] stream fields for type [{}].", numStreamFields, key);

                if (numStreamFields > 1) {

                    /*
                     * There is more than one field that can be used for this stream type. To resolve the ambiguity we
                     * need additional information. The app graph should include the name of the field that should be
                     * used to assign this stream. If the name is missing we bail out.
                     */
                    fieldName = sm.fieldName;

                    /* Bail out. */
                    if (fieldName == null) {
                        String msg = String.format(
                                "There are [%d] stream fields in PE [%s]. To assign stream [%s] you need to provide the field name in the application graph using the method withFiled(). See Javadocs for an example.",
                                numStreamFields, pe.getClass().getName(), stream.getName());
                        logger.error(msg);
                        throw new Exception(msg);
                    }

                    /* Use the provided field name to choose the PE field. */
                    field = pe.getClass().getDeclaredField(fieldName);

                } else {

                    /*
                     * The easy case, no ambiguity, we don't need an explicit field name to be provided. We have the
                     * field that matches the stream type.
                     */
                    Iterator<Field> iter = streamFields.iterator();
                    field = iter.next(); // Note that numStreamFields == 1, the size of this collection is 1.
                    logger.debug("Using field [{}].", field.getName());
                }

                /*
                 * By now, we found the field to use for this stream or we bailed out. We are not ready to finish yet.
                 * There may be more than one stream that needs to be assigned to this field. The stream fields must be
                 * arrays by convention and there may be more than one stream assigned to this fields. For now we create
                 * a multimap from field to streams so we can construct the array in the next step.
                 */
                assignment.put(field, stream);

            } else {

                /* We couldn't find a match. Tell user to fix the EDSL code. */
                String msg = String.format(
                        "There is no stream of type [%s] in PE [%s]. I was unable to assign stream [%s].", key,
                        pe.getClass().getName(), stream.getName());
                logger.error(msg);
                throw new Exception(msg);

            }
        }
        /* Now we construct the array and do the final assignment. */

        Map<Field, Collection<Stream<? extends Event>>> assignmentMap = assignment.asMap();
        for (Map.Entry<Field, Collection<Stream<? extends Event>>> entry : assignmentMap.entrySet()) {
            Field f = entry.getKey();

            int arraySize = entry.getValue().size();
            @SuppressWarnings("unchecked")
            Stream<? extends Event> streamArray[] = (Stream<? extends Event>[]) Array.newInstance(Stream.class,
                    arraySize);
            int i = 0;
            for (Stream<? extends Event> s : entry.getValue()) {
                streamArray[i++] = s;

                f.setAccessible(true);
                f.set(pe, streamArray);
                logger.debug("Assigned [{}] streams to field [{}].", streamArray.length, f.getName());
            }
        }
    }

    void clearPEState() {
        propertyName = null;
        propertyValue = null;
        processingElement = null;
        peName = null;
        triggerEventType = null;
        triggerTimeUnit = null;
        cacheSize = -1;
        streamBuilder = null;
    }

}