Spf4jJMXBeanMapping.java

/*
 * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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 Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Additionally licensed with:
 *
 * 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.spf4j.jmx.mappers;

import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import com.sun.jmx.mbeanserver.MXBeanMapping;
import com.sun.jmx.mbeanserver.MXBeanMappingFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Map;
import java.io.InvalidObjectException;
import java.io.NotSerializableException;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.Set;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import javax.management.openmbean.ArrayType;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import static javax.management.openmbean.SimpleType.STRING;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;
import org.spf4j.base.Throwables;
import org.spf4j.jmx.JMXBeanMapping;
import org.spf4j.jmx.JMXBeanMappingSupplier;

/**
 * Mapping class to improve open type mappings for collections and avro objects.
 * @author Zoltan Farkas
 */
@SuppressWarnings({"unchecked", "checkstyle:visibilitymodifier"})
abstract class Spf4jJMXBeanMapping implements JMXBeanMapping {

  private static final String KEY = "key";
  private static final String VALUE = "value";
  private static final String[] MAP_INDEX_NAMES = {KEY};
  private static final String[] MAP_ITEM_NAMES = {KEY, VALUE};
  private static final Type[] STR_STR_TYPES = {String.class, String.class};


  protected OpenType<?> openType;
  protected Class<?> mappedTypeClass;

  Spf4jJMXBeanMapping() {
    this(null, null);
  }

  Spf4jJMXBeanMapping(final OpenType<?> openType, final Class<?> mappedTypeClass) {
    this.openType = openType;
    this.mappedTypeClass = mappedTypeClass;
  }


  @SuppressFBWarnings("LEST_LOST_EXCEPTION_STACK_TRACE")
  public static JMXBeanMapping defaultHandler(final Type javaType, final JMXBeanMappingSupplier mappings)
          throws NotSerializableException {
    //falling back to MXBeanMappingFactory.DEFAULT
    try {
      MXBeanMapping mapping = MXBeanMappingFactory.DEFAULT.mappingForType(javaType, new MXBeanMappingFactory() {
        @Override
        public MXBeanMapping mappingForType(final Type t, final MXBeanMappingFactory f) throws OpenDataException {
          JMXBeanMapping m;
          try {
            m = mappings.get(t);
          } catch (NotSerializableException ex) {
            OpenDataException tex = new OpenDataException(t + " is not seriablizable ");
            tex.initCause(ex);
            throw tex;
          }
          if (m != null) {
            return MXBeanMappings.convert(m);
          }
          return MXBeanMappingFactory.DEFAULT.mappingForType(t, f);
        }
      });
     return MXBeanMappings.convert(mapping);
    } catch (OpenDataException ex) {
      NotSerializableException nsex = Throwables.first(ex, NotSerializableException.class);
      if (nsex != null) {
        throw nsex;
      } else {
        throw new IllegalArgumentException("No type mapping for " + javaType, ex);
      }
    }
  }

  // Return the mapped open type
  @Override
  public OpenType<?> getOpenType() {
    return openType;
  }


  @Override
  public Class<?> getMappedType() {
    return mappedTypeClass;
  }

  /**
   * Basic Types - Classes that do not require data conversion
   *              including primitive types and all SimpleType
   *
   *   Mapped open type: SimpleType for corresponding basic type
   *
   * Data Mapping:
   *   T <-> T (no conversion)
   **/
   static class BasicMXBeanType extends Spf4jJMXBeanMapping {

    BasicMXBeanType(final Class<?> c, final OpenType<?> openType) {
      super(openType, c);
    }

    @Override
    public Type getJavaType() {
      return getMappedType();
    }

    @Override
    public Object toOpenValue(final Object data) {
      return data;
    }

    @Override
    public Object fromOpenValue(final Object data) {
      return data;
    }
  }

  /**
   * Enum subclasses
   *   Mapped open type - String
   *
   * Data Mapping:
   *   Enum <-> enum's name
   */
   static class EnumMXBeanType extends Spf4jJMXBeanMapping {

    private final Class enumClass;

    EnumMXBeanType(final Class<?> c) {
      super(STRING, String.class);
      this.enumClass = c;
    }

    @Override
    public Type getJavaType() {
      return enumClass;
    }

    @Override
    public Object toOpenValue(final Object data) {
      return ((Enum) data).name();
    }

    @Override
    public Object fromOpenValue(final Object data)
            throws InvalidObjectException {

      try {
        return Enum.valueOf(enumClass, (String) data);
      } catch (IllegalArgumentException e) {
        // missing enum constants
        final InvalidObjectException ioe
                = new InvalidObjectException("Enum constant named "
                        + (String) data + " is missing");
        ioe.initCause(e);
        throw ioe;
      }
    }
  }

  /**
   * Array E[]
   *   Mapped open type - Array with element of OpenType for E
   *
   * Data Mapping:
   *   E[] <-> openTypeData(E)[]
   */
   static class ArrayMXBeanType extends Spf4jJMXBeanMapping {

    private final Class<?> arrayClass;
    protected JMXBeanMapping componentType;
    protected JMXBeanMapping baseElementType;

    ArrayMXBeanType(final Class<?> c, final JMXBeanMappingSupplier mappings)
            throws NotSerializableException {
      this.arrayClass = c;
      this.componentType = mappings.get(c.getComponentType());

      StringBuilder className = new StringBuilder();
      Class<?> et = c;
      int dim;
      for (dim = 0; et.isArray(); dim++) {
        className.append('[');
        et = et.getComponentType();
      }
      baseElementType = mappings.get(et);
      if (et.isPrimitive()) {
        className = new StringBuilder(c.getName());
      } else {
        className.append('L').append(baseElementType.getMappedType().getTypeName()).append(';');
      }
      try {
        mappedTypeClass = Class.forName(className.toString());
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Cannot obtain array class " + className, e);
      }
      try {
        openType = new ArrayType<>(dim, baseElementType.getOpenType());
      } catch (OpenDataException ex) {
        throw new IllegalArgumentException("Unsupported type " + c, ex);
      }
    }

    protected ArrayMXBeanType() {
      arrayClass = null;
    }

    @Override
    public Type getJavaType() {
      return arrayClass;
    }

    @Override
    @SuppressFBWarnings("URV_INHERITED_METHOD_WITH_RELATED_TYPES")
    public Object toOpenValue(final Object data) throws OpenDataException {
      if (baseElementType.isSimpleType()) {
        return data;
      }
      final Object[] array = (Object[]) data;
      final Object[] openArray = (Object[]) Array.newInstance(componentType.getMappedType(),
              array.length);
      int i = 0;
      for (Object o : array) {
        if (o == null) {
          openArray[i] = null;
        } else {
          openArray[i] = componentType.toOpenValue(o);
        }
        i++;
      }
      return openArray;
    }

    @Override
    @SuppressFBWarnings("URV_INHERITED_METHOD_WITH_RELATED_TYPES")
    public Object fromOpenValue(final Object data)
            throws InvalidObjectException {
      if (baseElementType.isSimpleType()) {
        return data;
      }

      final Object[] openArray = (Object[]) data;
      final Object[] array = (Object[]) Array.newInstance((Class) componentType.getJavaType(),
              openArray.length);
      int i = 0;
      for (Object o : openArray) {
        if (o == null) {
          array[i] = null;
        } else {
          array[i] = componentType.fromOpenValue(o);
        }
        i++;
      }
      return array;
    }

  }

  static class GenericArrayMXBeanType extends ArrayMXBeanType {

    private final GenericArrayType gtype;

    GenericArrayMXBeanType(final GenericArrayType gat, final JMXBeanMappingSupplier mappings)
            throws NotSerializableException {
      this.gtype = gat;
      this.componentType = mappings.get(gat.getGenericComponentType());

      StringBuilder className = new StringBuilder();
      Type elementType = gat;
      int dim;
      for (dim = 0; elementType instanceof GenericArrayType; dim++) {
        className.append('[');
        GenericArrayType et = (GenericArrayType) elementType;
        elementType = et.getGenericComponentType();
      }
      baseElementType = mappings.get(elementType);
      if (elementType instanceof Class && ((Class) elementType).isPrimitive()) {
        className = new StringBuilder(gat.toString());
      } else {
        className.append('L').append(baseElementType.getMappedType().getTypeName()).append(';');
      }
      try {
        mappedTypeClass = Class.forName(className.toString());
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Cannot obtain array class " + className, e);
      }
      try {
        openType = new ArrayType<>(dim, baseElementType.getOpenType());
      } catch (OpenDataException ex) {
        throw new IllegalArgumentException(ex);
      }
    }

    @Override
    public Type getJavaType() {
      return gtype;
    }

  }

  /**
   * List<E>
   *   Mapped open type - Array with element of OpenType for E
   *
   * Data Mapping:
   *   List<E> <-> openTypeData(E)[]
   */
  static class ListMXBeanType extends Spf4jJMXBeanMapping {

    private final ParameterizedType javaType;
    private final JMXBeanMapping paramMapping;
    private final Class<?> rawType;

    ListMXBeanType(final ParameterizedType pt, final JMXBeanMappingSupplier mappings)
            throws NotSerializableException {
      this.javaType = pt;
      this.rawType = (Class) pt.getRawType();
      final Type[] argTypes = pt.getActualTypeArguments();
      assert (argTypes.length == 1);
      Type argType = argTypes[0];

      TypeToken<?> tt = TypeToken.of(argType);
      final Class<?> et = tt.getRawType();
      if (et.isArray()) {
        throw new IllegalArgumentException("Element Type for " + pt
                + " not supported");
      }
      paramMapping = mappings.get(argType);
      String cname = "[L" + paramMapping.getMappedType().getName() + ";";
      try {
        mappedTypeClass = Class.forName(cname);
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Array class not found " + cname, e);
      }
      try {
        openType = new ArrayType<>(1, paramMapping.getOpenType());
      } catch (OpenDataException ex) {
        throw new IllegalArgumentException("Invalid arg " + pt, ex);
      }
    }

    @Override
    public Type getJavaType() {
      return javaType;
    }

    @Override
    public Object toOpenValue(final Object data) throws OpenDataException {
      if (data instanceof Collection) {
        final Collection<Object> list = (Collection<Object>) data;
        final Object[] openArray = (Object[]) Array.newInstance(paramMapping.getMappedType(),
                list.size());
        int i = 0;
        for (Object o : list) {
          openArray[i++] = paramMapping.toOpenValue(o);
        }
        return openArray;
      } else {
        final Iterable<Object> list = (Iterable<Object>) data;
        List result = new ArrayList(16);
        for (Object o : list) {
          result.add(paramMapping.toOpenValue(o));
        }
        return result.toArray((Object[]) Array.newInstance(paramMapping.getMappedType(),
                result.size()));

      }
    }

    @Override
    public Object fromOpenValue(final Object data)
            throws InvalidObjectException {

      final Object[] openArray = (Object[]) data;
      Collection<Object> result;
      if (rawType.isInterface()) {
        if (Set.class == rawType) {
          result = new HashSet<>(openArray.length);
        } else if (Deque.class == rawType) {
          result = new ArrayDeque<>(openArray.length);
        } else {
          result = new ArrayList<>(openArray.length);
        }
      } else {
        try {
          result = (Collection) rawType.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
          InvalidObjectException iox = new InvalidObjectException("Cannot instantiate " + rawType);
          iox.addSuppressed(ex);
          throw iox;
        }
      }
      for (Object o : openArray) {
        result.add(paramMapping.fromOpenValue(o));
      }
      return result;
    }
  }

  /**
   * Map<K,V>
   *   Mapped open type - TabularType with row type:
   *                        CompositeType:
   *                          "key"   of openDataType(K)
   *                          "value" of openDataType(V)
   *                        "key" is the index name
   *
   * Data Mapping:
   *   Map<K,V> <-> TabularData
   */
   static class MapMXBeanType extends Spf4jJMXBeanMapping {

    private final Type javaType;
    private final Class<?> rawType;
    private final JMXBeanMapping keyType;
    private final JMXBeanMapping valueType;

    @SuppressFBWarnings({ "CLI_CONSTANT_LIST_INDEX", "ITC_INHERITANCE_TYPE_CHECKING" })
    MapMXBeanType(final Type pt, final JMXBeanMappingSupplier mappings)
      throws  NotSerializableException {
      try {
        this.javaType = pt;

        final Type[] argTypes;
        if (pt instanceof ParameterizedType) {
          argTypes = ((ParameterizedType) pt).getActualTypeArguments();
          rawType = (Class) ((ParameterizedType) pt).getRawType();
        } else if (pt instanceof Class && Properties.class.isAssignableFrom((Class) pt)) {
          argTypes = STR_STR_TYPES;
          rawType = (Class) pt;
        } else {
          throw new IllegalArgumentException("Unsupported type " + pt);
        }
        assert (argTypes.length == 2);
        this.keyType = mappings.get(argTypes[0]);
        if (this.keyType == null) {
          throw new IllegalArgumentException("Key of " + pt + " cannot be converted to open type");
        }
        this.valueType = mappings.get(argTypes[1]);
        if (this.valueType == null) {
          throw new IllegalArgumentException("Value of " + pt + " cannot be converted to open type");
        }

        // FIXME: generate typeName for generic
        String typeName = pt.getTypeName();
        final OpenType<?>[] mapItemTypes = new OpenType<?>[]{
          keyType.getOpenType(),
          valueType.getOpenType()
        };
        final CompositeType rowType
                = new CompositeType(typeName,
                        typeName,
                        MAP_ITEM_NAMES,
                        MAP_ITEM_NAMES,
                        mapItemTypes);

        openType = new TabularType(typeName, typeName, rowType, MAP_INDEX_NAMES);
        mappedTypeClass = TabularData.class;
      } catch (OpenDataException ex) {
        throw new IllegalArgumentException("Unsupported type " + pt, ex);
      }
    }

    @Override
    public Type getJavaType() {
      return javaType;
    }

    @Override
    public Object toOpenValue(final Object data) throws OpenDataException {
      final Map<Object, Object> map = (Map<Object, Object>) data;
      final TabularType tabularType = (TabularType) openType;
      final TabularData table = new TabularDataSupport(tabularType);
      final CompositeType rowType = tabularType.getRowType();

      for (Map.Entry<Object, Object> entry : map.entrySet()) {
        final Object key = keyType.toOpenValue(entry.getKey());
        final Object value = valueType.toOpenValue(entry.getValue());
        final CompositeData row
                = new CompositeDataSupport(rowType,
                        MAP_ITEM_NAMES,
                        new Object[]{key, value});
        table.put(row);
      }
      return table;
    }

    @Override
    public Object fromOpenValue(final Object data)
            throws InvalidObjectException {

      final TabularData td = (TabularData) data;
      Map<Object, Object> result;
      if (rawType.isInterface()) {
        result = Maps.newHashMapWithExpectedSize(td.values().size());
      } else {
        try {
          result = (Map) rawType.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
          InvalidObjectException iox = new InvalidObjectException("Cannot instantiate " + rawType);
          iox.addSuppressed(ex);
          throw iox;
        }
      }
      for (CompositeData row : (Collection<CompositeData>) td.values()) {
        Object key = keyType.fromOpenValue(row.get(KEY));
        Object value = valueType.fromOpenValue(row.get(VALUE));
        result.put(key, value);
      }
      return result;
    }
  }

}