org.powertac.logtool.common.DomainObjectReader.java Source code

Java tutorial

Introduction

Here is the source code for org.powertac.logtool.common.DomainObjectReader.java

Source

/*
 * Copyright (c) 2012-2013 by the original author
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.powertac.logtool.common;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

import org.apache.log4j.Logger;
import org.joda.time.Instant;
import org.powertac.common.TimeService;
import org.powertac.common.msg.BalanceReport;
import org.powertac.common.state.Domain;
import org.powertac.du.DefaultBroker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ReflectionUtils;

/**
 * Processor for state log entries; creates domain object instances,
 * stores them in repositories as well as in a master repo indexed by
 * id value.
 * 
 * @author John Collins
 */
@Service
public class DomainObjectReader {
    static private Logger log = Logger.getLogger(DomainObjectReader.class.getName());

    @Autowired
    private TimeService timeService;

    HashMap<Long, Object> idMap;
    HashMap<Class<?>, Class<?>> ifImplementors;
    HashMap<String, Class<?>> substitutes;
    HashSet<String> ignores;
    HashSet<Class> noIdTypes;

    // listeners
    HashMap<Class<?>, ArrayList<NewObjectListener>> newObjectListeners;

    /**
     * Default constructor
     */
    public DomainObjectReader() {
        super();
        idMap = new HashMap<Long, Object>();

        // Set up the interface defaults
        ifImplementors = new HashMap<Class<?>, Class<?>>();
        ifImplementors.put(List.class, ArrayList.class);

        // set up substitute list to handle inner classes in a reasonable way
        substitutes = new HashMap<String, Class<?>>();
        substitutes.put("org.powertac.du.DefaultBrokerService$LocalBroker", DefaultBroker.class);

        // set up the ignore list
        ignores = new HashSet<String>();
        ignores.add("org.powertac.common.Tariff");
        //ignores.add("org.powertac.genco.Genco");
        ignores.add("org.powertac.common.Rate$ProbeCharge");
        ignores.add("org.powertac.common.msg.SimPause");
        ignores.add("org.powertac.common.msg.SimResume");
        ignores.add("org.powertac.common.msg.PauseRequest");
        ignores.add("org.powertac.common.msg.PauseRelease");
        ignores.add("org.powertac.common.RandomSeed");
        ignores.add("org.powertac.factoredcustomer.DefaultUtilityOptimizer$DummyTariffSubscription");

        // set up the no-id list
        noIdTypes = new HashSet<Class>();
        noIdTypes.add(TimeService.class);
        noIdTypes.add(BalanceReport.class);

        // set up listener list
        newObjectListeners = new HashMap<Class<?>, ArrayList<NewObjectListener>>();
    }

    /**
     * Registers a NewObjectListener. The listener will be called with
     * each newly-created object of the given type. If type is null, then
     * the listener will be called for each new object. 
     */
    public void registerNewObjectListener(NewObjectListener listener, Class<?> type) {
        ArrayList<NewObjectListener> list = newObjectListeners.get(type);
        if (null == list) {
            list = new ArrayList<NewObjectListener>();
            newObjectListeners.put(type, list);
        }
        list.add(listener);
    }

    /**
     * Converts a line from the log to an object.
     * Each line is of the form<br>
     * &nbsp;&nbsp;<code>ms:class::id::method{::arg}*</code>
     * 
     * Note that some objects cannot be resolved in the order they appear
     * in a logfile, because they have forward dependencies. This means
     * that a failure to resolve an object does not necessarily mean it's bogus,
     * but could mean that it could be resolved at a later time, typically
     * within one or a very few input lines. 
     * @throws MissingDomainObject 
     */
    public Object readObject(String line) throws MissingDomainObject {
        log.debug("readObject(" + line + ")");
        String body = line.substring(line.indexOf(':') + 1);
        String[] tokens = body.split("::");
        Class<?> clazz;
        if (ignores.contains(tokens[0])) {
            //log.info("ignoring " + tokens[0]);
            return null;
        }
        try {
            clazz = Class.forName(tokens[0]);
        } catch (ClassNotFoundException e) {
            Class<?> subst = substitutes.get(tokens[0]);
            if (null == subst) {
                log.warn("class " + tokens[0] + " not found");
                return null;
            } else {
                clazz = subst;
                //log.info("substituting " + clazz.getName() + " for " + tokens[0]);
            }
        }

        long id = -1;
        try {
            id = Long.parseLong(tokens[1]);
        } catch (NumberFormatException nfe) {
            if (clazz == TimeService.class) {
                // normal case - timeService does not have an id
                updateTime(tokens[3]);
                return null;
            } else if (noIdTypes.contains(clazz)) {
                id = 0;
            } else {
                log.debug("Number format exception reading id");
                return null;
            }
        }
        String methodName = tokens[2];
        log.debug("methodName=" + methodName);
        if (methodName.equals("new")) {
            // constructor
            Object newInst = constructInstance(clazz, Arrays.copyOfRange(tokens, 3, tokens.length));
            if (null != newInst) {
                if (!noIdTypes.contains(clazz)) {
                    setId(newInst, id);
                    idMap.put(id, newInst);
                }
                log.debug("Created new instance " + id + " of class " + tokens[0]);
                fireNewObjectEvent(newInst);
            }
            return newInst;
        } else if (methodName.equals("-rr")) {
            // readResolve
            Object newInst = restoreInstance(clazz, Arrays.copyOfRange(tokens, 3, tokens.length));
            if (null != newInst) {
                setId(newInst, id);
                idMap.put(id, newInst);
                log.debug("Restored instance " + id + " of class " + tokens[0]);
                fireNewObjectEvent(newInst);
            }
            return newInst;
        } else {
            // other method calls -- object should already exist
            Object inst = idMap.get(id);
            if (null == inst) {
                log.warn("Cannot find instance for id " + id + " of type " + clazz.getCanonicalName());
                return null;
            }
            Method[] methods = clazz.getMethods();
            ArrayList<Method> candidates = new ArrayList<Method>();
            for (Method method : methods) {
                if (method.getName().equals(methodName)) {
                    candidates.add(method);
                }
            }
            // We now have a list of candidate methods.
            if (0 == candidates.size()) {
                log.error("Cannot find method " + methodName + " for class " + clazz.getName());
                return null;
            }
            if (1 == candidates.size()) {
                // there's one candidate, probably it is the correct one
                if (!tryMethodCall(inst, candidates.get(0), Arrays.copyOfRange(tokens, 3, tokens.length))) {
                    log.error("Failed to invoke method " + methodName + " on instance of " + clazz.getName());
                }
            } else {
                // multiple candidates -- try them until we get success
                boolean success = false;
                for (Method candidate : candidates) {
                    success = tryMethodCall(inst, candidate, Arrays.copyOfRange(tokens, 3, tokens.length));
                    if (success)
                        break;
                }
                if (!success) {
                    log.error("Failed to find viable candidate for " + methodName + " on instance of "
                            + clazz.getName());
                }
            }
        }
        return null;
    }

    public Object getById(long id) {
        return idMap.get(id);
    }

    private void updateTime(String time) {
        Instant value = Instant.parse(time);
        timeService.setCurrentTime(value);
        log.info("time set to " + time);
    }

    private void fireNewObjectEvent(Object thing) {
        ArrayList<NewObjectListener> listeners = newObjectListeners.get(thing.getClass());
        if (null == listeners)
            // try one up the tree to catch local subclasses like the default broker
            listeners = newObjectListeners.get(thing.getClass().getSuperclass());
        if (null != listeners) {
            for (NewObjectListener li : listeners) {
                li.handleNewObject(thing);
            }
        }
        // check for promiscuous listener
        listeners = newObjectListeners.get(null);
        if (null != listeners) {
            for (NewObjectListener li : listeners) {
                li.handleNewObject(thing);
            }
        }
    }

    private Object constructInstance(Class<?> clazz, String[] args) throws MissingDomainObject {
        Constructor<?>[] potentials = clazz.getDeclaredConstructors();
        Constructor<?> target = null;
        Object[] params = null;
        for (Constructor<?> cons : potentials) {
            Type[] types = cons.getGenericParameterTypes();
            if (types.length != args.length)
                // not this one
                continue;
            // correct length of parameter list -
            // now try to resolve the types.
            // If we get a MissingDomainObject exception, keep going.
            try {
                params = resolveArgs(types, args);
            } catch (MissingDomainObject mdo) {
                // ignore
            }
            if (null == params)
                // no match
                continue;
            else {
                target = cons;
                break;
            }
        }
        // if we found one, use it, then update the id value
        if (null != target) {
            Object result = null;
            try {
                target.setAccessible(true);
                result = target.newInstance(params);
            } catch (InvocationTargetException ite) {
                // arg-constructor mismatch
                return restoreInstance(clazz, args);
            } catch (Exception e) {
                log.error("could not construct instance of " + clazz.getName() + ": " + e.toString());
                return null;
            }
            return result;
        } else {
            // otherwise, try to use the readResolve method
            return restoreInstance(clazz, args);
        }
    }

    // restores an instance from a readResolve record.
    // Fields are given in the @Domain annotation.
    private Object restoreInstance(Class<?> clazz, String[] args) throws MissingDomainObject {
        Domain domain = clazz.getAnnotation(Domain.class);
        if (domain instanceof Domain) {
            // only do this for @Domain classes
            Object thing = null;
            try {
                Constructor<?> cons = clazz.getDeclaredConstructor();
                cons.setAccessible(true);
                thing = cons.newInstance();
            } catch (Exception e) {
                log.warn("No default constructor for " + clazz.getName() + ": " + e.toString());
                return null;
            }
            String[] fieldNames = domain.fields();
            Field[] fields = new Field[fieldNames.length];
            Class<?>[] types = new Class<?>[fieldNames.length];
            for (int i = 0; i < fieldNames.length; i++) {
                fields[i] = ReflectionUtils.findField(clazz, resolveDoubleCaps(fieldNames[i]));
                if (null == fields[i]) {
                    log.warn("No field in " + clazz.getName() + " named " + fieldNames[i]);
                    types[i] = null;
                } else {
                    types[i] = fields[i].getType();
                }
            }
            Object[] data = resolveArgs(types, args);
            if (null == data) {
                log.error("Could not resolve args for " + clazz.getName());
                return null;
            } else {
                for (int i = 0; i < fields.length; i++) {
                    if (null == fields[i])
                        continue;
                    fields[i].setAccessible(true);
                    try {
                        fields[i].set(thing, data[i]);
                    } catch (Exception e) {
                        log.error("Exception setting field: " + e.toString());
                        return null;
                    }
                }
            }
            return thing;
        }
        return null;
    }

    private String resolveDoubleCaps(String name) {
        // lowercase first char of field name with two initial caps
        if (Character.isUpperCase(name.charAt(0)) && Character.isUpperCase(name.charAt(1))) {
            char[] chars = name.toCharArray();
            chars[0] = Character.toLowerCase(chars[0]);
            return (String.valueOf(chars));
        }
        return name;
    }

    // attempts to call a method by reconstructing its args and invoking it
    private boolean tryMethodCall(Object thing, Method method, String[] args) {
        Type[] argTypes = method.getGenericParameterTypes();
        if (argTypes.length != args.length)
            // bail if arglist lengths do not match
            return false;
        Object[] realArgs;
        if (0 == argTypes.length) {
            // no args
            realArgs = null;
        } else {
            try {
                realArgs = resolveArgs(argTypes, args);
                if (null == realArgs || realArgs.length != args.length) {
                    log.debug("Could not resolve args: method " + method.getName() + ", class = "
                            + thing.getClass().getName() + ", args = " + args);
                    return false;
                }
            } catch (MissingDomainObject mdo) {
                return false;
            }
        }
        try {
            method.invoke(thing, realArgs);
            return true;
        } catch (Exception e) {
            log.error("Exception calling method " + thing.getClass().getName() + "." + method.getName()
                    + " on args " + args);
        }
        return false;
    }

    // attempts to match a set of types with a set of String arguments
    // from the logfile. They match if the strings can be resolved to
    // the corresponding types. 
    private Object[] resolveArgs(Type[] types, String[] args) throws MissingDomainObject {
        // for each type, we attempt to resolve the corresponding arg
        // as an instance of that type.
        Object[] result = new Object[types.length];
        for (int i = 0; i < args.length; i++) {
            result[i] = resolveArg(types[i], args[i]);
        }
        return result;
    }

    private Object resolveArg(Type type, String arg) throws MissingDomainObject {
        // type can be null in a few cases - nothing to be done about it?
        if (null == type) {
            return null;
        }

        // check for non-parameterized types
        if (type instanceof Class) {
            Class<?> clazz = (Class<?>) type;
            if (clazz.isEnum()) {
                return Enum.valueOf((Class<Enum>) type, arg);
            } else {
                return resolveSimpleArg(clazz, arg);
            }
        }

        // check for collection, denoted by leading (
        if (type instanceof ParameterizedType) {
            ParameterizedType ptype = (ParameterizedType) type;
            Class<?> clazz = (Class<?>) ptype.getRawType();
            boolean isCollection = false;
            if (clazz.equals(Collection.class))
                isCollection = true;
            else {
                Class<?>[] ifs = clazz.getInterfaces();
                for (Class<?> ifc : ifs) {
                    if (ifc.equals(Collection.class)) {
                        isCollection = true;
                        break;
                    }
                }
            }
            if (isCollection) {
                // expect arg to start with "("
                log.debug("processing collection " + clazz.getName());
                if (arg.charAt(0) != '(') {
                    log.error("Collection arg " + arg + " does not start with paren");
                    return null;
                }
                // extract element type and resolve recursively
                Type[] tas = ptype.getActualTypeArguments();
                if (1 == tas.length) {
                    Class<?> argClazz = (Class<?>) tas[0];
                    // create an instance of the collection
                    Collection<Object> coll;
                    // resolve interfaces into actual classes
                    if (clazz.isInterface())
                        clazz = ifImplementors.get(clazz);
                    try {
                        coll = (Collection<Object>) clazz.newInstance();
                    } catch (Exception e) {
                        log.error("Exception creating collection: " + e.toString());
                        return null;
                    }
                    // at this point, we can split the string and resolve recursively
                    String body = arg.substring(1, arg.indexOf(')'));
                    String[] items = body.split(",");
                    for (String item : items) {
                        coll.add(resolveSimpleArg(argClazz, item));
                    }
                    return coll;
                }
            }
        }

        // if we get here, no resolution
        log.error("unresolved arg: type = " + type + ", arg = " + arg);
        return null;
    }

    private Object resolveSimpleArg(Class<?> clazz, String arg) throws MissingDomainObject {
        // handle the simplest case first
        if (arg.equals("null"))
            return null;

        if (clazz.getName().startsWith("org.powertac")) {
            Method getId;
            try {
                getId = clazz.getMethod("getId");
                if (getId.getReturnType() == long.class) {
                    // this is a domain type; it may or may not be in the map
                    Long key = Long.parseLong(arg);
                    Object value = idMap.get(key);
                    if (null != value) {
                        return value;
                    } else {
                        // it's a domain object, but we cannot resolve it
                        // -- this should not happen.
                        log.info("Missing domain object " + key);
                        throw new MissingDomainObject("missing object id=" + key);
                    }
                }
            } catch (SecurityException e) {
                log.error("Exception on getId(): " + e.toString());
                return null;
            } catch (NoSuchMethodException e) {
                // normal result of no getId() method
            } catch (NumberFormatException e) {
                // normal result of non-integer id value
            }
        }

        // arg is not an id value - check if it's supposed to be a primitive
        if (clazz.getName().equals("boolean")) {
            boolean value = Boolean.parseBoolean(arg);
            if (value) {
                return true; // resolved as boolean
            } else if (arg.equalsIgnoreCase("false")) {
                return false; // resolved as boolean
            } else
                return null; // does not resolve
        }

        if (clazz.getName().equals("long")) {
            try {
                long value = Long.parseLong(arg);
                return value;
            } catch (NumberFormatException nfe) {
                // not a long
                return null;
            }
        }

        if (clazz.getName().equals("int")) {
            try {
                int value = Integer.parseInt(arg);
                return value;
            } catch (NumberFormatException nfe) {
                // not an int
                return null;
            }
        }

        if (clazz.getName().equals("double") || clazz == Double.class) {
            try {
                double value = Double.parseDouble(arg);
                return value;
            } catch (NumberFormatException nfe) {
                // not a double
                return null;
            }
        }

        if (clazz.getName() == "java.lang.Double") {

        }

        // check for time value
        if (clazz.getName() == "org.joda.time.Instant") {
            try {
                Instant value = Instant.parse(arg);
                return value;
            } catch (IllegalArgumentException iae) {
                // make Instant from Long
                try {
                    Long msec = Long.parseLong(arg);
                    return new Instant(msec);
                } catch (Exception e) {
                    // Long parse failure
                    log.error("could not parse Long " + arg);
                    return null;
                }
            } catch (Exception e) {
                // Instant parse failure
                log.error("could not parse Instant " + arg);
                return null;
            }
        }

        // check for type with String constructor
        try {
            Constructor<?> cons = clazz.getConstructor(String.class);
            return cons.newInstance(arg);
        } catch (NoSuchMethodException e) {
            // normal result of failure - fall through and try something else
        } catch (Exception e) {
            log.error("Exception looking up constructor for " + clazz.getName() + ": " + e.toString());
            return null;
        }
        // no type matched
        return null;
    }

    // Sets the id field of a newly-constructed thing
    private void setId(Object thing, Long id) {
        Class<?> clazz = thing.getClass();
        Method setId;
        try {
            setId = clazz.getMethod("setId", long.class);
            setId.setAccessible(true);
            setId.invoke(thing, (long) id);
        } catch (SecurityException e) {
            log.error("Exception on setId(): " + e.toString());
        } catch (NoSuchMethodException e) {
            // normal result of no setId() method
            ReflectionTestUtils.setField(thing, "id", id);
        } catch (Exception e) {
            log.error("Error setting id value " + e.toString());
        }
    }
}