package org.gcube.data.access.storagehub.handlers.items; import java.lang.reflect.Field; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Singleton; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import org.apache.commons.io.IOUtils; import org.apache.jackrabbit.util.Text; import org.gcube.common.storagehub.model.annotations.Attribute; import org.gcube.common.storagehub.model.annotations.AttributeRootNode; import org.gcube.common.storagehub.model.annotations.ListNodes; import org.gcube.common.storagehub.model.annotations.MapAttribute; import org.gcube.common.storagehub.model.annotations.NodeAttribute; import org.gcube.common.storagehub.model.exceptions.BackendGenericError; import org.gcube.common.storagehub.model.items.ExternalFolder; import org.gcube.common.storagehub.model.items.Item; import org.gcube.common.storagehub.model.items.RootItem; import org.gcube.common.storagehub.model.items.SharedFolder; import org.gcube.common.storagehub.model.items.TrashItem; import org.gcube.common.storagehub.model.items.nodes.Content; import org.gcube.common.storagehub.model.messages.Message; import org.gcube.data.access.storagehub.Constants; import org.gcube.data.access.storagehub.handlers.ClassHandler; import org.reflections.Configuration; import org.reflections.Reflections; import org.reflections.util.ConfigurationBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton @SuppressWarnings("rawtypes") public class Node2ItemConverter { private static final Logger logger = LoggerFactory.getLogger(Node2ItemConverter.class); private static HashMap, Map> typeToSubtypeMap = new HashMap<>(); public T getFilteredItem(Node node, List excludes, Class nodeTypeToInclude) throws RepositoryException, BackendGenericError{ @SuppressWarnings("unchecked") Class classToHandle = (Class)ClassHandler.instance().get(node.getPrimaryNodeType().getName()); if (classToHandle==null) return null; if (nodeTypeToInclude!=null && !(nodeTypeToInclude.isAssignableFrom(classToHandle))) return null; else return retrieveItem(node, excludes, classToHandle); } public T getItem(String nodeIdentifier, Session session, List excludes) throws RepositoryException, BackendGenericError{ Node node = session.getNodeByIdentifier(nodeIdentifier); return getItem(node, excludes); } public T getItem(Node node, List excludes) throws RepositoryException, BackendGenericError{ @SuppressWarnings("unchecked") Class classToHandle = (Class)ClassHandler.instance().get(node.getPrimaryNodeType().getName()); if (classToHandle==null) return null; T item = retrieveItem(node, excludes, classToHandle); return item; } public Content getContent(Node node) throws RepositoryException, BackendGenericError{ Content content = new Content(); setGenericFields(node, Content.class, content); return content; } public Message getMessageItem(Node node) throws RepositoryException{ Message msg = new Message(); try { Node attachmentNode = node.getNode(Constants.ATTACHMENTNODE_NAME); msg.setWithAttachments(attachmentNode.hasNodes()); }catch (Throwable e) { msg.setWithAttachments(false); } setRooItemCommonFields(node, Collections.emptyList(), Message.class, msg); return msg; } private T retrieveItem(Node node, List excludes, Class classToHandle) throws RepositoryException, BackendGenericError{ T item; try { item = classToHandle.newInstance(); }catch (Exception e) { throw new BackendGenericError(e); } try { item.setShared(SharedFolder.class.isInstance(item) || hasTypedParent(node, SharedFolder.class)); }catch (Exception e) { item.setShared(false); } try { item.setTrashed(TrashItem.class.isInstance(item) || hasTypedParent(node, TrashItem.class)); }catch (Exception e) { item.setTrashed(false); } try { item.setExternalManaged(hasTypedParent(node, ExternalFolder.class)); }catch (Exception e) { item.setExternalManaged(false); } item.setLocked(node.isLocked()); setRooItemCommonFields(node, excludes, classToHandle, item); return item; } private boolean hasTypedParent(Node node, Class parentType) throws BackendGenericError, RepositoryException{ if(node==null) return false; return checkNodeType(node, parentType) || hasTypedParent(node.getParent(), parentType); } private void setRooItemCommonFields(Node node, List excludes, Class classToHandle, T instance) throws RepositoryException{ try{ instance.setParentId(node.getParent().getIdentifier()); instance.setParentPath(node.getParent().getPath()); }catch (Throwable e) { logger.trace("Root node doesn't have a parent"); } instance.setRelatedNode(node); instance.setId(node.getIdentifier()); instance.setName(Text.unescapeIllegalJcrChars(node.getName())); instance.setPath(Text.unescapeIllegalJcrChars(node.getPath())); instance.setPrimaryType(node.getPrimaryNodeType().getName()); setGenericFields(node, classToHandle, instance); } private void setGenericFields(Node node, Class classToHandle, T instance){ for (Field field : retrieveAllFields(classToHandle)){ if (field.isAnnotationPresent(Attribute.class)){ Attribute attribute = field.getAnnotation(Attribute.class); field.setAccessible(true); try{ Class returnType = field.getType(); field.set(instance, getPropertyValue(returnType, node.getProperty(attribute.value()))); logger.trace("retrieve item - added field {}",field.getName()); }catch(PathNotFoundException e){ logger.trace("the current node dosn't contain {} property",attribute.value()); } catch (Exception e ) { logger.debug("error setting value for property {} ",attribute.value()); } } else if (field.isAnnotationPresent(NodeAttribute.class)){ String fieldNodeName = field.getAnnotation(NodeAttribute.class).value(); //for now it excludes only first level node logger.trace("retrieving field node "+field.getName()); field.setAccessible(true); try{ Node fieldNode = node.getNode(fieldNodeName); logger.trace("looking in node {} searched with {}",fieldNode.getName(),fieldNodeName); field.set(instance, iterateNodeAttributeFields(field.getType(), fieldNode)); }catch(PathNotFoundException e){ logger.trace("the current node dosn't contain {} node",fieldNodeName); } catch (Exception e ) { logger.debug("error setting value",e); } } } } private T iterateNodeAttributeFields(Class clazz, Node node) throws Exception{ T obj = clazz.newInstance(); for (Field field : retrieveAllFields(clazz)){ if (field.isAnnotationPresent(Attribute.class)){ Attribute attribute = field.getAnnotation(Attribute.class); field.setAccessible(true); try{ Class returnType = field.getType(); field.set(obj, getPropertyValue(returnType, node.getProperty(attribute.value()))); }catch(PathNotFoundException e){ logger.trace("the current node dosn't contain {} property",attribute.value()); } catch (Exception e ) { logger.debug("error setting value {}",e.getMessage()); } } else if (field.isAnnotationPresent(MapAttribute.class)){ logger.trace("found field {} of type annotated as MapAttribute in class {} and node name {}", field.getName(), clazz.getName(), node.getName()); field.setAccessible(true); String exclude = field.getAnnotation(MapAttribute.class).excludeStartWith(); Map mapToset = new HashMap(); PropertyIterator iterator = node.getProperties(); if (iterator!=null) { while (iterator.hasNext()){ Property prop = iterator.nextProperty(); if (!exclude.isEmpty() && prop.getName().startsWith(exclude)) continue; try{ logger.trace("adding {} in the map",prop.getName()); mapToset.put(prop.getName(), getPropertyValue(prop)); }catch(PathNotFoundException e){ logger.warn("the property {} is not mapped",prop.getName()); } catch (Exception e ) { logger.debug("error setting value {}",e.getMessage()); } } } field.set(obj, mapToset); } else if (field.isAnnotationPresent(ListNodes.class)){ logger.trace("found field {} of type annotated as ListNodes in class {} on node {}", field.getName(), clazz.getName(), node.getName()); field.setAccessible(true); String exclude = field.getAnnotation(ListNodes.class).excludeTypeStartWith(); String include = field.getAnnotation(ListNodes.class).includeTypeStartWith(); Class listType = field.getAnnotation(ListNodes.class).listClass(); Map subTypesMap = Collections.emptyMap(); if (!typeToSubtypeMap.containsKey(listType)) { Configuration config = new ConfigurationBuilder().forPackages(listType.getPackage().getName()); Reflections reflections = new Reflections(config); Set subTypes = reflections.getSubTypesOf(listType); if (subTypes.size()>0) { subTypesMap = new HashMap<>(); for (Class subtype: subTypes) if (subtype.isAnnotationPresent(AttributeRootNode.class)) { AttributeRootNode attributeRootNode = (AttributeRootNode)subtype.getAnnotation(AttributeRootNode.class); subTypesMap.put(attributeRootNode.value(), subtype); } } else logger.trace("no subtypes found for {}",listType.getName()); typeToSubtypeMap.put(listType, subTypesMap); } else { logger.info("subtypes already found in cache"); subTypesMap = typeToSubtypeMap.get(listType); } List toSetList = new ArrayList<>(); NodeIterator iterator = node.getNodes(); while (iterator.hasNext()){ Node currentNode = iterator.nextNode(); String primaryType = currentNode.getPrimaryNodeType().getName(); logger.trace("the current node {} has a list",currentNode.getName()); if (!include.isEmpty() && !primaryType.startsWith(include)) continue; if (!exclude.isEmpty() && primaryType.startsWith(exclude)) continue; if (subTypesMap.containsKey(primaryType)) toSetList.add(iterateNodeAttributeFields(subTypesMap.get(primaryType), currentNode)); else toSetList.add(iterateNodeAttributeFields(listType, currentNode)); } if (toSetList.size()!=0) field.set(obj, toSetList); } } return obj; } @SuppressWarnings({ "unchecked" }) private Object getPropertyValue(Class returnType, Property prop) throws Exception{ if (returnType.equals(String.class)) return prop.getString(); if (returnType.isEnum()) return Enum.valueOf(returnType, prop.getString()); if (returnType.equals(Calendar.class)) return prop.getDate(); if (returnType.equals(URL.class)) return new URI(prop.getString()).toURL(); if (returnType.equals(Boolean.class) || returnType.equals(boolean.class)) return prop.getBoolean(); if (returnType.equals(Long.class) || returnType.equals(long.class)) return prop.getLong(); if (returnType.equals(Integer.class) || returnType.equals(int.class)) return prop.getLong(); if (returnType.isArray()) { if (prop.getType()==PropertyType.BINARY) { byte[] bytes = IOUtils.toByteArray(prop.getBinary().getStream()); return bytes; } else { Object[] ret= getArrayValue(prop); return Arrays.copyOf(ret, ret.length, returnType); } } throw new Exception(String.format("class %s not recognized",returnType.getName())); } private static Set retrieveAllFields(Class clazz){ Set fields = new HashSet(); Class currentClass = clazz; do{ List fieldsFound = Arrays.asList(currentClass.getDeclaredFields()); fields.addAll(fieldsFound); }while ((currentClass =currentClass.getSuperclass())!=null); return fields; } private Object getPropertyValue(Property prop) throws Exception{ if (prop.isMultiple()){ Object[] values = new Object[prop.getValues().length]; int i = 0; for (Value value : prop.getValues()) values[i++] = getSingleValue(value); return values; } else return getSingleValue(prop.getValue()); } private Object getSingleValue(Value value) throws Exception{ switch (value.getType()) { case PropertyType.DATE: return value.getDate(); case PropertyType.BOOLEAN: return value.getBoolean(); case PropertyType.LONG: return value.getDate(); default: return value.getString(); } } private Object[] getArrayValue(Property prop) throws Exception{ Object[] values = new Object[prop.getValues().length]; int i = 0; for (Value value : prop.getValues()) values[i++] = getSingleValue(value); return values; } //Checks if a node is a subtype of classToCompare public boolean checkNodeType(Node node, Class classToCompare) throws BackendGenericError{ try { logger.trace("class from nodetype is {} and class to compare is {}",ClassHandler.instance().get(node.getPrimaryNodeType().getName()), classToCompare); return classToCompare.isAssignableFrom(ClassHandler.instance().get(node.getPrimaryNodeType().getName())); //(node.isNodeType(ClassHandler.instance().getNodeType(classToCompare))); }catch (Throwable e) { throw new BackendGenericError(e); } } }