SnmpTools.java

package eu.linuxengineering.snmp;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;

import eu.javaexperience.collection.map.SmallMap;
import eu.javaexperience.interfaces.simple.SimpleGet;
import eu.javaexperience.interfaces.simple.getBy.GetBy1;
import eu.javaexperience.interfaces.simple.getBy.GetBy2;
import eu.javaexperience.interfaces.simple.getBy.GetBy3;
import eu.javaexperience.log.JavaExperienceLoggingFacility;
import eu.javaexperience.log.LogLevel;
import eu.javaexperience.log.Loggable;
import eu.javaexperience.log.Logger;
import eu.javaexperience.log.LoggingTools;
import eu.javaexperience.reflect.CastTo;
import eu.javaexperience.reflect.Mirror;
import eu.javaexperience.reflect.PrimitiveTools;
import eu.javaexperience.semantic.references.MayNull;
import eu.javaexperience.reflect.Mirror.BelongTo;
import eu.javaexperience.reflect.Mirror.ClassData;
import eu.javaexperience.reflect.Mirror.MethodSelector;
import eu.javaexperience.reflect.Mirror.Select;
import eu.javaexperience.reflect.Mirror.Visibility;
import eu.javaexperience.text.Format;
import eu.javaexperience.text.StringTools;
import eu.linuxengineering.snmp.annotations.SnmpAlternativeElementsCount;
import eu.linuxengineering.snmp.annotations.SnmpAlternativeNodeType;
import eu.linuxengineering.snmp.annotations.SnmpExtraInformation;
import eu.linuxengineering.snmp.annotations.SnmpIndex;
import eu.linuxengineering.snmp.annotations.SnmpNodeDetails;
import eu.linuxengineering.snmp.annotations.SnmpSubnode;
import eu.linuxengineering.snmp.nodes.SnmpAllReflectInformation;
import eu.linuxengineering.snmp.nodes.SnmpNodeType;
import eu.linuxengineering.snmp.nodes.SnmpReflectAltType;
import eu.linuxengineering.snmp.nodes.SnmpReflectFields;
import net.sf.snmpadaptor4j.api.AttributeAccessor;
import net.sf.snmpadaptor4j.object.SnmpDataType;
import net.sf.snmpadaptor4j.object.SnmpOid;

public class SnmpTools
{
	private SnmpTools(){}
	
	protected static final Logger LOG = JavaExperienceLoggingFacility.getLogger(new Loggable("SnmpTools"));
	
	public static SnmpDispatchNode ensurePath
	(
		SnmpDispatchNode from,
		int[] path
	)
	{
		for(int i=0;i<path.length;++i)
		{
			SnmpNode node = from.subNodes.get(path[i]);
			if(null == node)
			{
				SnmpDispatchNode tmp = new SnmpDispatchNode();
				from.addEntry(path[i], tmp);
				node = tmp;
			}
			
			if(node instanceof SnmpDispatchNode)
			{
				from = (SnmpDispatchNode) node;
			}
			else
			{
				throw new RuntimeException
				(
					"A non SnmpDispatchNode node already present in path `"+Arrays.toString(path)+" at "+i+" "+node
				);
			}
		}
		return from;
	}
	
	/**
	 * Returns the node already presen on the requested path
	 * returns null when nothing present on path and adds the node
	 * return the node found ont the path and node will not be added
	 * */
	public static SnmpNode tryAddToPath
	(
		SnmpDispatchNode from,
		int[] path,
		SnmpNode node
	)
	{
		SnmpDispatchNode to = ensurePath(from, Arrays.copyOf(path, path.length-1));
		Integer tar = path[path.length-1];
		
		SnmpNode ret = null;
		ret = to.subNodes.get(tar);
		if(null != ret && ret != node)
		{
			return ret;
		}
		
		to.addEntry(tar, node);
		
		return null;
	}

	
	public static void addToPath
	(
		SnmpDispatchNode from,
		int[] path,
		SnmpNode node,
		boolean forceOverride
	)
	{
		SnmpDispatchNode to = ensurePath(from, Arrays.copyOf(path, path.length-1));
		Integer tar = path[path.length-1];
		if(to.subNodes.containsKey(tar) && !forceOverride)
		{
			throw new RuntimeException("Node at path "+Arrays.toString(path)+" already exists: "+to.subNodes.get(tar));
		}
		to.addEntry(tar, node);
	}

	public static AttributeAccessor createReadOnlyAttributeAccessor(SnmpOid id, SnmpDataType type, SimpleGet<?> source)
	{
		return new AttributeAccessor()
		{
			@Override
			public void setValue(Object value) throws Exception {}
			
			@Override
			public boolean isWritable()
			{
				return false;
			}
			
			@Override
			public boolean isReadable()
			{
				return true;
			}
			
			@Override
			public Object getValue() throws Exception
			{
				return source.get();
			}
			
			@Override
			public SnmpDataType getSnmpDataType()
			{
				return type;
			}
			
			@Override
			public SnmpOid getOid()
			{
				return id;
			}
			
			@Override
			public Class<?> getJmxDataType()
			{
				return null;
			}
		};
	}

	public static SnmpFinalNode addRoAccessorToPath(SnmpDispatchNode root, int[] id, SnmpDataType type, SimpleGet<Object> getter, boolean forceOverride)
	{
		SnmpOid oid = SnmpOid.newInstance(id);
		SnmpFinalNode ret = new SnmpFinalNode(createReadOnlyAttributeAccessor(oid, type, getter));
		addToPath(root, id, ret, forceOverride);
		return ret;
	}
	
	public static SnmpFinalNode createFinalNode(SnmpOid oid, SnmpDataType type, SimpleGet<?> getter)
	{
		return new SnmpFinalNode(createReadOnlyAttributeAccessor(oid, type, getter));
	}
	
	public static SnmpNode addSnmpObjectToPath(SnmpMibDispatch root, int[] path, Object node, boolean forceOverride)
	{
		return addSnmpObjectToPath(root, path, node, forceOverride, null);
	}
	
	public static SnmpNode addSnmpObjectToPath(SnmpMibDispatch root, int[] path, Object node, boolean forceOverride, ReflectionExtraDataProvider redp)
	{
		SnmpNode add = wrapSnmpObject(null, null, null, redp, node); 
		addToPath(root, path, add, forceOverride);
		return add;
	}
	
	public static final MethodSelector SELECT_ALL_PUBLIC_INSTANCE_METHOD = new MethodSelector
	(
		true,
		Visibility.Public,
		BelongTo.Instance,
		Select.IsNot,
		Select.All,
		Select.All,
		Select.All,
		Select.All
	);
	
	public static SnmpDataType recogniseSnmpDataType(Class cls)
	{
		cls = PrimitiveTools.toObjectClassType(cls, cls);
		
		if(String.class == cls)
		{
			return SnmpDataType.octetString;
		}
		
		if
		(
				Boolean.class == cls
			||
				Byte.class == cls
			||
				Character.class == cls
			||
				Short.class == cls
			||
				Integer.class == cls
		)
		{
			return SnmpDataType.integer32;
		}
		
		if(Double.class == cls || Float.class == cls)
		{
			return SnmpDataType.octetString;
		}
		
		if
		(
			Long.class == cls
		)
		{
			return SnmpDataType.counter64;
		}
		
		if(SnmpOid.class == cls)
		{
			return SnmpDataType.objectIdentifier;
		}
		
		return null;
	}
	
	protected static SnmpNode createReflectNode
	(
		String name,
		String description,
		SnmpNodeType type,
		SimpleGet<Integer> itemsCount,
		ReflectionExtraDataProvider redp,
		Object source,
		Object connection,
		Object target
	)
	{
		SnmpRequestSensitiveDispatchCollection ret = new SnmpRequestSensitiveDispatchCollection();
		if(!StringTools.isNullOrTrimEmpty(name))
		{
			ret.addNodeFactory(1, (oid)->createFinalNode(oid, SnmpDataType.octetString, ()->name));
		}
		
		if(!StringTools.isNullOrTrimEmpty(description))
		{
			ret.addNodeFactory(2, (oid)->createFinalNode(oid, SnmpDataType.octetString, ()->description));
		}
		
		ret.addNodeFactory(3, (oid)->createFinalNode(oid, SnmpDataType.octetString, ()->type.name()));
		ret.addNodeFactory(4, (oid)->createFinalNode(oid, SnmpDataType.integer32, itemsCount));
		
		if(null != redp)
		{
			Object info = redp.getBy(source, connection, target);
			if(null != info)
			{
				ret.addNodeFactory(10, (oid)->wrapSnmpObject(oid, null, null, null, info));
			}
		}
		
		if(connection instanceof Method)
		{
			Method m = (Method) connection;
			//11 annotation the target itself
			SnmpExtraInformation[] infos = m.getAnnotationsByType(SnmpExtraInformation.class);
			if(null != infos && infos.length > 0)
			{
				Map<Integer, String> add = new SmallMap<>();
				for(SnmpExtraInformation info:infos)
				{
					add.put(info.index(), info.data());
				}
				ret.addNodeFactory(11, (oid)->wrapSnmpObject(oid, null, null, null, add));
			}
		}
		
		if(target instanceof SnmpReflectExtraData)
		{
			Map<Integer, String> tar = ((SnmpReflectExtraData) target).getExtraData();
			ret.addNodeFactory(12, (oid)->wrapMapSource(tar, null, null, ()->tar, null));
		}
		else if(target instanceof GetBy1)
		{
			try
			{
				GetBy1<Object, SnmpOid> get = ((GetBy1<Object, SnmpOid>) target);
				if(get instanceof SnmpReflectExtraData)
				{
					ret.addNodeFactory(12, (oid)->
					{
						Map<Integer, String> map = ((SnmpReflectExtraData) get.getBy(oid)).getExtraData();
						return wrapMapSource(map, null, null, ()->map, null);
					});
				}
			}
			catch(Exception e)
			{
				LoggingTools.tryLogFormatException(LOG, LogLevel.ERROR, e, "Error while getting SnmpReflectExtraData from `%s`: ", target);
			}
		}
		
		return ret;
	}
	
	/**
	 * Well, looks like there's a bug or somethin' in the SNMP agent server
	 * because can't accept SnmpOid object marked with the type of
	 * {@link SnmpDataType#objectIdentifier} even when providing with strings.
	 * So now i propagate them as an octetString 'til this situation gonna be
	 * resolved. 
	 * */
	public static GetBy1<SnmpNode, SnmpOid> wrapToSnmpObjectCreator(Object o)
	{
		return wrapToSnmpObjectCreator(null, null, null, o);
	}
	
	public static GetBy1<SnmpNode, SnmpOid> wrapToSnmpObjectCreator(Object o, ReflectionExtraDataProvider redp)
	{
		return wrapToSnmpObjectCreator(null, null, redp, o);
	}
	
	public static GetBy1<SnmpNode, SnmpOid> wrapToSnmpObjectCreator(Object source, Object connection, ReflectionExtraDataProvider redp, Object o)
	{
		if(o instanceof SnmpRelativeOid)
		{
			return (oid)->createFinalNode(oid, SnmpDataType.octetString, ()->((SnmpRelativeOid)o).resolve(oid).toString());
		}
		else if(String.class == o.getClass() || PrimitiveTools.isPrimitiveTypeObject(PrimitiveTools.translatePrimitiveToObjectType(o.getClass())))
		{
			return (oid)->createFinalNode(oid, recogniseSnmpDataType(o.getClass()), ()->o);
		}
		else if(o instanceof Collection)
		{
			return (oid)->wrapCollectionSource
			(
				source,
				connection,
				redp,
				()->(Collection)o,
				null
			);
		}
		else if(o instanceof Map)
		{
			return (oid)->wrapMapSource(source, connection, redp, ()->(Map)o, null);
		}
		
		return (oid)->wrapSnmpObject(oid, source, connection, redp, o);
	}
	
	protected static SnmpNode wrapCollectionSource
	(
		Object source,
		Object connection,
		ReflectionExtraDataProvider redp,
		SimpleGet<Collection> src,
		SnmpReflectFields info
	)
	{
		return new SnmpProxyNode()
		{
			protected Collection prev;
			
			public synchronized void beforeAccess()
			{
				try
				{
					Collection now = src.get();
				
					if(null == original || !Mirror.equals(prev, now))
					{
						SnmpRequestSensitiveDispatchCollection ret = new SnmpRequestSensitiveDispatchCollection();
						//SnmpNodeDetails info = m.getAnnotation(SnmpNodeDetails.class);
						
						int[] items = new int[]{0};
						if(null != info)
						{
							ret.addNodeFactory
							(
								0,
								createReflectNode
								(
									null == info?null:info.getName(),
									null == info?null:info.getDescription(),
									SnmpNodeType.ENUMERATION,
									()->items[0],
									redp,
									source,
									connection,
									src
								)
							);
						}
						
						int i = 1;
						for(Object o:now)
						{
							if(null != o)
							{
								ret.addNodeFactory(i, wrapToSnmpObjectCreator(source, connection, redp, o));
								++items[0];
							}
							++i;
						}
						
						prev = now;
						original = ret;
					}
				}
				catch(Exception e)
				{
					Mirror.propagateAnyway(e);
				}
			}
			
			@Override
			public String toString()
			{
				return "SnmpTools.wrapCollectionSource(src: `"+src+"`, info: `"+info+"`)";
			}
		};
	}
	
	protected static SnmpNode wrapMapSource
	(
		Object source,
		Object connection,
		ReflectionExtraDataProvider redp,
		SimpleGet<Map> src,
		SnmpReflectFields info
	)
	{
		return new SnmpProxyNode()
		{
			protected Map<Object, Object> prev;
			
			public synchronized void beforeAccess()
			{
				try
				{
					Map<Object, Object> now = src.get();
				
					if(null == original || !Mirror.equals(prev, now))
					{
						SnmpRequestSensitiveDispatchCollection ret = new SnmpRequestSensitiveDispatchCollection();
						
						int[] items = new int[]{0};
						
						if(null != info)
						{
							ret.addNodeFactory
							(
								0,
								createReflectNode
								(
									null == info?null:info.getName(),
									null == info?null:info.getDescription(),
									SnmpNodeType.ENUMERATION,
									()->items[0],
									redp,
									source,
									connection,
									src
								)
							);
						}
						
						if(null != now)
						{
							for(Entry<Object, Object> kv:now.entrySet())
							{
								Object v = kv.getValue();
								if(null != v)
								{
									Integer index = (Integer) CastTo.Int.cast(kv.getKey());
									if(null != index)
									{
										ret.addNodeFactory(index, wrapToSnmpObjectCreator(source, connection, redp, v));
										++items[0];
									}
								}
							}
						}
						
						prev = now;
						original = ret;
					}
				}
				catch(Exception e)
				{
					Mirror.propagateAnyway(e);
				}
			}
			
			@Override
			public String toString()
			{
				return "SnmpTools.wrapMapSource(src: `"+src+"`, info: `"+info+"`)";
			}
		};
	}
	
	public static SnmpNode wrapSnmpObject(Object node, ReflectionExtraDataProvider redp)
	{
		return wrapSnmpObject(null, null, null, redp, node);
	}
	
	/**
	 * 
	 * nocovered cases:
	 * 	- constant values: int, boolean, double, String (enum as string) 
	 * 	- collection or array
	 * 
	 * covered:
	 * 	- object contains methods returns int, string, collection values 
	 * 
	 * */
	protected static SnmpNode wrapSnmpObject(@MayNull SnmpOid oid, Object source, Object connection, ReflectionExtraDataProvider redp, Object node)
	{
		if(null == node)
		{
			return null;
		}
		
		if(node instanceof SnmpNode)
		{
			return (SnmpNode) node;
		}
		
		else if(node instanceof Collection)
		{
			return wrapCollectionSource(source, connection, redp, ()->(Collection)node, null);
		}
		else if(node instanceof Map)
		{
			return wrapMapSource(source, connection, redp, ()->(Map)node, null);
		}
		else
		{
			SnmpDataType type = recogniseSnmpDataType(node.getClass());
			if(null != type)
			{
				if(null == oid)
				{
					SnmpRequestSensitiveDispatchCollection ret = new SnmpRequestSensitiveDispatchCollection();
					ret.addNodeFactory(1, (foid)->SnmpFinalNode.wrap(foid, type, ()->node));
					return ret;
				}
				else
				{
					return createFinalNode(oid, type, ()->node);
				}
			}
		}
		
		
		return wrapSnmpBean(node, redp);
	}
	
	public static GetBy1<SnmpNode, SnmpOid> createRelativeDemandProxy
	(
		GetBy2<SnmpNode, Object, SnmpOid> nodeCreator,
		SimpleGet<Object> source,
		boolean useCache
	)
	{
		return (f)->new SnmpProxyNode()
		{
			protected Object prev;
			
			public void beforeAccess()
			{
				Object now = null;
				try
				{
					now = source.get();
				}
				catch(Exception e)
				{
					now = Format.getPrintedStackTrace(e);
					//Mirror.propagateAnyway(e);
				}
				
				if(null == original || !useCache || !Mirror.equals(prev, now))
				{
					if(null == now)
					{
						now = "null";
					}

					original = nodeCreator.getBy(now, f);
					prev = now;
				}
			}
			
			@Override
			public String toString()
			{
				return "createRelativeDemandProxy(nodeCreator: `"+nodeCreator+"`, source: `"+source+"`, useCache: `"+useCache+"`)";
			}
		};
	}
	
	/**
	 * Intentionally for internal usage, but you can use for other SNMP related
	 * functions, but don't abuse and use in other facility type.
	 * Because errors written to the "SnmpTools" label. 
	 * */
	@Deprecated
	public static SimpleGet<Object> wrapMethodGetterWithLogging(Method m, Object subject)
	{
		return ()->
		{
			try
			{
				return m.invoke(subject);
			}
			catch(Exception e)
			{
				LoggingTools.tryLogFormatException
				(
					LOG,
					LogLevel.ERROR,
					e,
					"Exception while invoking method `%s` on `%s`",
					m,
					subject
				);
				Mirror.propagateAnyway(e);
				return null;
			}
		};
	}
	
	public static @MayNull SnmpAllReflectInformation tryExtractReflectInformation(Method m)
	{
		SnmpNodeDetails info = m.getAnnotation(SnmpNodeDetails.class);
		SnmpAlternativeNodeType type = m.getAnnotation(SnmpAlternativeNodeType.class);
		SnmpAlternativeElementsCount count = m.getAnnotation(SnmpAlternativeElementsCount.class);
		
		if(null == info && null == type)
		{
			return null;
		}
		
		return new SnmpAllReflectInformation()
		{
			@Override
			public SnmpNodeType getType()
			{
				return null == type? null:type.getType();
			}
			
			@Override
			public int getLength()
			{
				return null == count?-1:count.length();
			}
			
			@Override
			public String getName()
			{
				return null == info?null:info.name();
			}

			@Override
			public String getDescription()
			{
				return null == info?null:info.description();
			}
		};
	}
	
	public static interface ReflectionExtraDataProvider extends GetBy3<Object, Object, Object, Object>{}
	
	public static SnmpNode wrapSnmpBean(Object node)
	{
		return wrapSnmpBean(node, null);
	}
	
	public static SnmpNode wrapSnmpBean(Object node, ReflectionExtraDataProvider redp)
	{
		ClassData cd = Mirror.getClassData(node.getClass());
		
		Method[] methods = cd.select(SELECT_ALL_PUBLIC_INSTANCE_METHOD);
		
		SnmpRequestSensitiveDispatchCollection ret = new SnmpRequestSensitiveDispatchCollection();
		
		int[] itemsCount = new int[]{0};
		
		if(node instanceof SnmpReflectFields || node instanceof SnmpReflectExtraData)
		{
			String name = null;
			String detail = null;
			
			if(node instanceof SnmpReflectFields)
			{
				SnmpReflectFields srf = (SnmpReflectFields) node;
				name = srf.getName();
				detail = srf.getDescription();
			}
			
			SnmpNode cre = createReflectNode
			(
				name,
				detail,
				node instanceof SnmpReflectAltType?((SnmpReflectAltType)node).getType():SnmpNodeType.COLLECTION,
				node instanceof SnmpReflectAltType?()->((SnmpReflectAltType)node).getLength():()->itemsCount[0],
				redp,
				null,
				null,
				node
			);
			
			ret.addNodeFactory(0, cre);
		}
		
		for(Method m:methods)
		{
			if(0 != m.getParameters().length)
			{
				continue;
			}
			
			SnmpIndex index = m.getAnnotation(SnmpIndex.class);
			if(null != index)
			{
				Class retType = m.getReturnType();
				
				SnmpDataType type = recogniseSnmpDataType(retType);
				
				SnmpNodeType nodeType = SnmpNodeType.EXACT_VALUE;
				SimpleGet<Integer> nodeItemCount = ()->1;
				GetBy1<SnmpNode, SnmpOid> getter = null;
				
				SimpleGet<Object> invoke = wrapMethodGetterWithLogging(m, node);
				
				SnmpReflectFields info = tryExtractReflectInformation(m);
				
				if(Collection.class.isAssignableFrom(retType))
				{
					ret.addNodeFactory(index.index(), wrapCollectionSource(node, m, redp, (SimpleGet)invoke, info));
					++itemsCount[0];
					continue;
				}
				else if(Map.class.isAssignableFrom(retType))
				{
					ret.addNodeFactory(index.index(), wrapMapSource(node, m, redp, (SimpleGet)invoke, info));
					++itemsCount[0];
					continue;
				}
				else if(SnmpRelativeOid.class.isAssignableFrom(retType))
				{
					getter = (oid) ->
					createRelativeDemandProxy
					(
						(o, foid)->wrapToSnmpObjectCreator(node, m, redp, o).getBy(foid),
						invoke,
						true
					).getBy(oid);
				}
				else if(null == type)
				{
					getter = createRelativeDemandProxy((o, oid)->wrapSnmpObject(oid, node, m, redp, o), invoke, true);
				}
				else
				{
					getter = (oid)->SnmpFinalNode.wrap(oid, type, invoke);
				}
				
				if(null == getter)
				{
					continue;
				}
								
				++itemsCount[0];
				
				boolean subnode = null != m.getAnnotation(SnmpSubnode.class);
				
				if(subnode)
				{
					SnmpRequestSensitiveDispatchCollection add = new SnmpRequestSensitiveDispatchCollection();
					
					add.addNodeFactory
					(
						0,
						createReflectNode
						(
							null == info?null:info.getName(),
							null == info?null:info.getDescription(),
							node instanceof SnmpReflectAltType?((SnmpReflectAltType)node).getType():nodeType,
							node instanceof SnmpReflectAltType?()->((SnmpReflectAltType)node).getLength():nodeItemCount,
							
							redp,
							node,
							m,
							invoke
						)
					);
					
					add.addNodeFactory(1, getter);
					
					ret.addNodeFactory(index.index(), add);
				}
				else
				{
					ret.addNodeFactory(index.index(), getter);
				}
			}
		}
		
		return ret;
	}
}