/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr 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 General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.rest.resource; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.Predicate; import org.structr.api.graph.Node; import org.structr.api.util.Iterables; import org.structr.common.PagingHelper; import org.structr.common.SecurityContext; import org.structr.common.error.FrameworkException; import org.structr.core.GraphObject; import org.structr.core.GraphObjectMap; import org.structr.core.Result; import org.structr.core.app.App; import org.structr.core.app.Query; import org.structr.core.app.StructrApp; import org.structr.core.entity.AbstractNode; import org.structr.core.entity.OtherNodeTypeRelationFilter; import org.structr.core.entity.Relation; import org.structr.core.graph.NodeFactory; import org.structr.core.graph.NodeInterface; import org.structr.core.notion.Notion; import org.structr.core.property.ArrayProperty; import org.structr.core.property.BooleanProperty; import org.structr.core.property.DateProperty; import org.structr.core.property.DoubleProperty; import org.structr.core.property.EndNode; import org.structr.core.property.GenericProperty; import org.structr.core.property.IntProperty; import org.structr.core.property.LongProperty; import org.structr.core.property.PropertyKey; import org.structr.core.property.RelationProperty; import org.structr.core.property.StartNode; import org.structr.core.property.StringProperty; import org.structr.rest.RestMethodResult; import org.structr.rest.exception.IllegalPathException; import org.structr.rest.exception.NotFoundException; //~--- classes ---------------------------------------------------------------- /** * * */ public class StaticRelationshipResource extends SortableResource { private static final Logger logger = LoggerFactory.getLogger(StaticRelationshipResource.class.getName()); //~--- fields --------------------------------------------------------- TypeResource typeResource = null; TypedIdResource typedIdResource = null; PropertyKey propertyKey = null; //~--- constructors ---------------------------------------------------constructors public StaticRelationshipResource(final SecurityContext securityContext, final TypedIdResource typedIdResource, final TypeResource typeResource) { this.securityContext = securityContext; this.typedIdResource = typedIdResource; this.typeResource = typeResource; this.propertyKey = findPropertyKey(typedIdResource, typeResource); } //~--- methods -------------------------------------------------------- @Override public Result doGet(final PropertyKey sortKey, final boolean sortDescending, final int pageSize, final int page, final String offsetId) throws FrameworkException { // ok, source node exists, fetch it final GraphObject sourceEntity = typedIdResource.getEntity(); if (sourceEntity != null) { // first try: look through existing relations if (propertyKey == null) { if (sourceEntity instanceof NodeInterface) { if (!typeResource.isNode) { final NodeInterface source = (NodeInterface) sourceEntity; final Node sourceNode = source.getNode(); final Class relationshipType = typeResource.entityClass; final Relation relation = AbstractNode.getRelationshipForType(relationshipType); final Class destNodeType = relation.getOtherType(typedIdResource.getEntityClass()); final Set partialResult = new LinkedHashSet<>(typeResource.doGet(sortKey, sortDescending, NodeFactory.DEFAULT_PAGE_SIZE, NodeFactory.DEFAULT_PAGE, null).getResults()); // filter list according to end node type final Set<GraphObject> set = Iterables.toSet(Iterables.filter(new OtherNodeTypeRelationFilter(securityContext, sourceNode, destNodeType), source.getRelationships(relationshipType))); // intersect partial result with result list set.retainAll(partialResult); final List<GraphObject> finalResult = new LinkedList<>(set); // sort after merge applyDefaultSorting(finalResult, sortKey, sortDescending); // return result return new Result(PagingHelper.subList(finalResult, pageSize, page, offsetId), finalResult.size(), isCollectionResource(), isPrimitiveArray()); } else { // what here? throw new NotFoundException("Cannot access relationship collection " + typeResource.getRawType()); } } } else { Query query = typeResource.query; if (query == null) { query = StructrApp.getInstance(securityContext).nodeQuery(); } // use search context from type resource typeResource.collectSearchAttributes(query); final Predicate<GraphObject> predicate = query.toPredicate(); final Object value = sourceEntity.getProperty(propertyKey, predicate); if (value != null) { if (value instanceof Iterable) { final Set<Object> propertyResults = new LinkedHashSet<>(); Iterator<Object> iter = ((Iterable<Object>) value).iterator(); boolean iterableContainsGraphObject = false; while (iter.hasNext()) { Object obj = iter.next(); propertyResults.add(obj); if (obj != null && !iterableContainsGraphObject) { if (obj instanceof GraphObject) { iterableContainsGraphObject = true; } } } int rawResultCount = propertyResults.size(); if (!iterableContainsGraphObject) { GraphObjectMap gObject = new GraphObjectMap(); gObject.setProperty(new ArrayProperty(this.typeResource.rawType, Object.class), propertyResults.toArray()); Result r = new Result(gObject, true); r.setRawResultCount(rawResultCount); return r; } final List<GraphObject> finalResult = new LinkedList<>(); propertyResults.forEach( v -> finalResult.add((GraphObject) v) ); applyDefaultSorting(finalResult, sortKey, sortDescending); // return result Result r = new Result(PagingHelper.subList(finalResult, pageSize, page, offsetId), finalResult.size(), isCollectionResource(), isPrimitiveArray()); r.setRawResultCount(rawResultCount); return r; } else if (value instanceof GraphObject) { return new Result((GraphObject) value, isPrimitiveArray()); } else if (value != null) { GraphObjectMap gObject = new GraphObjectMap(); PropertyKey key; String keyName = this.typeResource.rawType; int resultCount = 1; //FIXME: Dynamically resolve all property types and their result count if (value instanceof String) { key = new StringProperty(keyName); } else if (value instanceof Integer) { key = new IntProperty(keyName); } else if (value instanceof Long) { key = new LongProperty(keyName); } else if (value instanceof Double) { key = new DoubleProperty(keyName); } else if (value instanceof Boolean) { key = new BooleanProperty(keyName); } else if (value instanceof Date) { key = new DateProperty(keyName); } else if (value instanceof String[]) { key = new ArrayProperty(keyName, String.class); resultCount = ((String[]) value).length; } else { key = new GenericProperty(keyName); } gObject.setProperty(key, value); Result r = new Result(gObject, true); r.setRawResultCount(resultCount); return r; } else { logger.info("Found object {}, but will not return as it is no graph object or iterable", value); } } // check propertyKey to return the right variant of empty result if (!(propertyKey instanceof StartNode || propertyKey instanceof EndNode)) { return new Result(Collections.EMPTY_LIST, 1, false, true); } } } return new Result(Collections.EMPTY_LIST, 0, false, true); } @Override public RestMethodResult doPut(final Map<String, Object> propertySet) throws FrameworkException { final List<? extends GraphObject> results = typedIdResource.doGet(null, false, NodeFactory.DEFAULT_PAGE_SIZE, NodeFactory.DEFAULT_PAGE, null).getResults(); final App app = StructrApp.getInstance(securityContext); if (results != null) { // fetch static relationship definition if (propertyKey != null && propertyKey instanceof RelationProperty) { final GraphObject sourceEntity = typedIdResource.getEntity(); if (sourceEntity != null) { if (propertyKey.isReadOnly()) { logger.info("Read-only property on {}: {}", new Object[]{sourceEntity.getClass(), typeResource.getRawType()}); return new RestMethodResult(HttpServletResponse.SC_FORBIDDEN); } final List<GraphObject> nodes = new LinkedList<>(); // Now add new relationships for any new id: This should be the rest of the property set for (final Object obj : propertySet.values()) { nodes.add(app.getNodeById(obj.toString())); } // set property on source node sourceEntity.setProperty(propertyKey, nodes); } } } return new RestMethodResult(HttpServletResponse.SC_OK); } @Override public RestMethodResult doPost(final Map<String, Object> propertySet) throws FrameworkException { final GraphObject sourceNode = typedIdResource.getEntity(); RestMethodResult result = null; if (sourceNode != null && propertyKey != null && propertyKey instanceof RelationProperty) { final RelationProperty relationProperty = (RelationProperty) propertyKey; final Class sourceNodeType = sourceNode.getClass(); NodeInterface newNode = null; if (propertyKey.isReadOnly()) { logger.info("Read-only property on {}: {}", new Object[]{sourceNodeType, typeResource.getRawType()}); return null; } // fetch notion final Notion notion = relationProperty.getNotion(); final PropertyKey primaryPropertyKey = notion.getPrimaryPropertyKey(); // apply notion if the property set contains the ID property as the only element if (primaryPropertyKey != null && propertySet.containsKey(primaryPropertyKey.jsonName()) && propertySet.size() == 1) { // FIXME: what happens here? } else { // the notion can not deserialize objects with a single key, or the POSTed propertySet did not contain a key to deserialize, // so we create a new node from the POSTed properties and link the source node to it. (this is the "old" implementation) newNode = typeResource.createNode(propertySet); if (newNode != null) { relationProperty.addSingleElement(securityContext, sourceNode, newNode); } } if (newNode != null) { result = new RestMethodResult(HttpServletResponse.SC_CREATED); result.addHeader("Location", buildLocationHeader(newNode)); return result; } } else { final Class entityType = typedIdResource.getTypeResource().getEntityClass(); final String methodName = typeResource.getRawType(); try { final String source = SchemaMethodResource.findMethodSource(entityType, methodName); result = SchemaMethodResource.invoke(securityContext, typedIdResource.getEntity(), source, propertySet, methodName); } catch (IllegalPathException ex) { // try direct invocation of the schema method on the node type try { result = SchemaMethodResource.wrapInResult(typedIdResource.getEntity().invokeMethod(methodName, propertySet, true)); } catch (Throwable t) { logger.warn("Unable to execute {}.{}: {}", entityType.getSimpleName(), methodName, t.getMessage()); } } } if (result == null) { throw new IllegalPathException("Illegal path"); } else { return result; } } @Override public boolean checkAndConfigure(final String part, final SecurityContext securityContext, final HttpServletRequest request) { return false; } @Override public Resource tryCombineWith(final Resource next) throws FrameworkException { if (next instanceof TypeResource) { throw new IllegalPathException("Unable to resolve URL path, no type resource expected here"); } return super.tryCombineWith(next); } @Override public Class getEntityClass() { Class type = typeResource.getEntityClass(); if (type == null && propertyKey != null) { return propertyKey.relatedType(); } return type; } @Override public String getUriPart() { return typedIdResource.getUriPart().concat("/").concat(typeResource.getUriPart()); } public TypedIdResource getTypedIdConstraint() { return typedIdResource; } public TypeResource getTypeConstraint() { return typeResource; } @Override public boolean isCollectionResource() { return true; } @Override public String getResourceSignature() { return typedIdResource.getResourceSignature().concat("/").concat(typeResource.getResourceSignature()); } }