From 03e154db25948342a858d157a59ec778d0e290c8 Mon Sep 17 00:00:00 2001 From: Timm Fitschen <t.fitschen@indiscale.com> Date: Thu, 16 Jul 2020 00:29:00 +0000 Subject: [PATCH] EHN: SELECT QUERY for properties of referenced entities. --- CHANGELOG.md | 3 + caosdb-webui | 2 +- .../transaction/RetrieveFullEntity.java | 122 ++++++++++++- .../java/caosdb/server/entity/Entity.java | 8 +- .../caosdb/server/entity/EntityInterface.java | 4 + .../server/entity/container/Container.java | 16 +- .../entity/container/PropertyContainer.java | 31 +++- .../container/TransactionContainer.java | 9 - .../server/entity/wrapper/EntityWrapper.java | 8 + .../entity/xml/DomainToElementStrategy.java | 17 +- .../entity/xml/EntityToElementStrategy.java | 144 ++++++++++----- .../entity/xml/ParentToElementStrategy.java | 21 ++- .../server/entity/xml/SetFieldStrategy.java | 54 +++++- .../server/jobs/extension/JobException.java | 42 ----- .../jobs/extension/SQLiteTransaction.java | 172 ------------------ src/main/java/caosdb/server/query/Query.java | 4 +- .../transaction/RetrieveFullEntityTest.java | 79 ++++++++ .../caosdb/server/entity/SelectionTest.java | 69 ++++++- .../container/PropertyContainerTest.java | 80 ++++++++ .../xml/PropertyToElementStrategyTest.java | 105 +++++++++++ 20 files changed, 684 insertions(+), 306 deletions(-) delete mode 100644 src/main/java/caosdb/server/jobs/extension/JobException.java delete mode 100644 src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java create mode 100644 src/test/java/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java create mode 100644 src/test/java/caosdb/server/entity/container/PropertyContainerTest.java create mode 100644 src/test/java/caosdb/server/entity/xml/PropertyToElementStrategyTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ce0c27..7bebc70c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for deeply nested selectors in SELECT queries. - One-time Authentication Tokens for login without credentials and login with particular permissions and roles for the course of the session. - `Entity/names` resource for retrieving all known entity names. @@ -23,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [README.md](misc/move_files/README.md). - LDAP server may now be given and may be different from LDAP domain. See `misc/pam_authentication/ldap.conf` +- #47 - Sub-properties can now be queried, such as in `SELECT window.width FROM house`. + ### Changed diff --git a/caosdb-webui b/caosdb-webui index 13658264..66026626 160000 --- a/caosdb-webui +++ b/caosdb-webui @@ -1 +1 @@ -Subproject commit 136582641fb1b675d9630b4eacea54fbf7765eea +Subproject commit 66026626089e2b514538510a1a6744868f46b661 diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java index 7ac8c159..a1817767 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,12 +25,32 @@ package caosdb.server.database.backend.transaction; import caosdb.server.database.BackendTransaction; +import caosdb.server.datatype.ReferenceDatatype; +import caosdb.server.datatype.ReferenceValue; import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Message; import caosdb.server.entity.RetrieveEntity; import caosdb.server.entity.Role; import caosdb.server.entity.container.Container; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.query.Query; +import caosdb.server.query.Query.Selection; import caosdb.server.utils.EntityStatus; +import java.util.LinkedList; +import java.util.List; +/** + * Retrieve the full entity from the backend - with all parents, properties, file properties and so + * on. + * + * <p>TODO: This class should rather be called FullEntityRetrieval or FullEntityRetrieveTransaction. + * + * <p>When the entity which is to be retrieved has a defined list of {@link Query.Selection} which + * select properties from referenced entities, the referenced entities are retrieved as well. + * Otherwise, only the referenced id is retrieved and the entity stays rather flat. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ public class RetrieveFullEntity extends BackendTransaction { private final Container<? extends EntityInterface> container; @@ -49,22 +71,106 @@ public class RetrieveFullEntity extends BackendTransaction { @Override public void execute() { - for (final EntityInterface e : this.container) { + retrieveFullEntitiesInContainer(this.container); + } + + /** + * Retrieve the entities in the container. + * + * @param container + */ + public void retrieveFullEntitiesInContainer(Container<? extends EntityInterface> container) { + for (final EntityInterface e : container) { if (e.hasId() && e.getId() > 0 && e.getEntityStatus() == EntityStatus.QUALIFIED) { + retrieveFullEntity(e, e.getSelections()); + } + } + } - execute(new RetrieveSparseEntity(e)); - if (e.getEntityStatus() == EntityStatus.VALID) { - if (e.getRole() == Role.QueryTemplate) { - execute(new RetrieveQueryTemplateDefinition(e)); - } - execute(new RetrieveParents(e)); + /** + * Retrieve a single full entity. + * + * <p>If the selections are not empty, retrieve the referenced entities matching the 'selections' + * as well. + * + * <p>This method is called recursively during the retrieval of the referenced entities. + * + * @param e The entity. + * @param selections + */ + public void retrieveFullEntity(EntityInterface e, List<Selection> selections) { + execute(new RetrieveSparseEntity(e)); + + if (e.getEntityStatus() == EntityStatus.VALID) { + if (e.getRole() == Role.QueryTemplate) { + execute(new RetrieveQueryTemplateDefinition(e)); + } + execute(new RetrieveParents(e)); + execute(new RetrieveProperties(e)); - execute(new RetrieveProperties(e)); + // recursion! retrieveSubEntities calls retrieveFull sometimes, but with reduced selectors. + if (selections != null && !selections.isEmpty()) { + retrieveSubEntities(e, selections); + } + } + } + + /** + * Retrieve the Entities which match the selections and are referenced by the Entity 'e'. + * + * @param e + * @param selections + */ + public void retrieveSubEntities(EntityInterface e, List<Selection> selections) { + for (final Selection s : selections) { + if (s.getSubselection() != null) { + + String propertyName = s.getSelector(); + + // Find matching (i.e. referencing) Properties + for (Property p : e.getProperties()) { + // get reference properties by name. + if (propertyName.equalsIgnoreCase(p.getName()) + && p.getDatatype() instanceof ReferenceDatatype) { + if (p.getValue() != null) { + try { + p.parseValue(); + } catch (Message m) { + p.addError(m); + } + + ReferenceValue value = (ReferenceValue) p.getValue(); + RetrieveEntity ref = new RetrieveEntity(value.getId()); + // recursion! (Only for the matching selections) + retrieveFullEntity(ref, getSubSelects(selections, propertyName)); + value.setEntity(ref); + } + } } } } } + /** + * Return all non-null subselects of those selections which match the given select String. + * + * <p>Effectively, this reduces the depth of the selections by one (and drops non-matching + * selections). + * + * @param selections + * @param select + * @return A new list of Selections. + */ + public List<Selection> getSubSelects(List<Selection> selections, String select) { + List<Selection> result = new LinkedList<>(); + for (Selection s : selections) { + if (s.getSelector().equalsIgnoreCase(select) && s.getSubselection() != null) { + result.add(s.getSubselection()); + } + } + return result; + } + public Container<? extends EntityInterface> getContainer() { return container; } diff --git a/src/main/java/caosdb/server/entity/Entity.java b/src/main/java/caosdb/server/entity/Entity.java index 5c997d7f..7fed351c 100644 --- a/src/main/java/caosdb/server/entity/Entity.java +++ b/src/main/java/caosdb/server/entity/Entity.java @@ -521,6 +521,7 @@ public class Entity extends AbstractObservable implements EntityInterface { } } + // Strategy to convert this Entity to an XML element. private ToElementStrategy toElementStrategy = null; @Override @@ -535,7 +536,12 @@ public class Entity extends AbstractObservable implements EntityInterface { @Override public final void addToElement(final Element element) { - getToElementStrategy().addToElement(this, element, new SetFieldStrategy(getSelections())); + addToElement(element, new SetFieldStrategy(getSelections())); + } + + @Override + public void addToElement(Element element, SetFieldStrategy strategy) { + getToElementStrategy().addToElement(this, element, strategy); } /** diff --git a/src/main/java/caosdb/server/entity/EntityInterface.java b/src/main/java/caosdb/server/entity/EntityInterface.java index 21ed5cd8..20aa57a5 100644 --- a/src/main/java/caosdb/server/entity/EntityInterface.java +++ b/src/main/java/caosdb/server/entity/EntityInterface.java @@ -31,6 +31,7 @@ import caosdb.server.entity.container.PropertyContainer; import caosdb.server.entity.wrapper.Domain; import caosdb.server.entity.wrapper.Parent; import caosdb.server.entity.wrapper.Property; +import caosdb.server.entity.xml.SetFieldStrategy; import caosdb.server.entity.xml.ToElementable; import caosdb.server.jobs.JobTarget; import caosdb.server.permissions.EntityACL; @@ -40,6 +41,7 @@ import caosdb.unit.Unit; import java.util.List; import org.apache.shiro.authz.Permission; import org.apache.shiro.subject.Subject; +import org.jdom2.Element; public interface EntityInterface extends JobTarget, Observable, ToElementable, WriteEntity, TransactionEntity { @@ -182,4 +184,6 @@ public interface EntityInterface public abstract String getQueryTemplateDefinition(); public abstract void setQueryTemplateDefinition(String query); + + public abstract void addToElement(Element element, SetFieldStrategy strategy); } diff --git a/src/main/java/caosdb/server/entity/container/Container.java b/src/main/java/caosdb/server/entity/container/Container.java index 9ab0415d..de20b6a3 100644 --- a/src/main/java/caosdb/server/entity/container/Container.java +++ b/src/main/java/caosdb/server/entity/container/Container.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -27,5 +29,17 @@ import java.util.ArrayList; public class Container<T extends EntityInterface> extends ArrayList<T> { - private static final long serialVersionUID = 3476714088253567549L; + private static final long serialVersionUID = 8519435849678175750L; + + /** + * Return the entity with the matching id, if it can be found inside this Container, else null. + */ + public T getEntityById(final Integer id) { + for (final T e : this) { + if (e.hasId() && e.getId().equals(id)) { + return e; + } + } + return null; + } } diff --git a/src/main/java/caosdb/server/entity/container/PropertyContainer.java b/src/main/java/caosdb/server/entity/container/PropertyContainer.java index a80e2bfc..80cf8161 100644 --- a/src/main/java/caosdb/server/entity/container/PropertyContainer.java +++ b/src/main/java/caosdb/server/entity/container/PropertyContainer.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -50,6 +52,7 @@ public class PropertyContainer extends Container<Property> { this.s = new PropertyToElementStrategy(); } + /** Sort the properties by their pidx (Property Index). */ public void sort() { Collections.sort( this, @@ -64,14 +67,32 @@ public class PropertyContainer extends Container<Property> { }); } - public Element addToElement(final Element element, final SetFieldStrategy setFieldStrategy) { + /** + * Add a single property to the element using the given setFieldStrategy. + * + * @param property + * @param element + * @param setFieldStrategy + */ + public void addToElement( + EntityInterface property, Element element, SetFieldStrategy setFieldStrategy) { + if (setFieldStrategy.isToBeSet(property.getName())) { + SetFieldStrategy strategy = setFieldStrategy.forProperty(property.getName()); + this.s.addToElement(property, element, strategy); + } + } + + /** + * Add all properties to the element using the given setFieldStrategy. + * + * @param element + * @param setFieldStrategy + */ + public void addToElement(final Element element, final SetFieldStrategy setFieldStrategy) { sort(); for (final EntityInterface property : this) { - if (setFieldStrategy.isToBeSet(property.getName())) { - this.s.addToElement(property, element, setFieldStrategy.forProperty(property.getName())); - } + addToElement(property, element, setFieldStrategy); } - return element; } @Override diff --git a/src/main/java/caosdb/server/entity/container/TransactionContainer.java b/src/main/java/caosdb/server/entity/container/TransactionContainer.java index f0ac55fa..f3a8a0f3 100644 --- a/src/main/java/caosdb/server/entity/container/TransactionContainer.java +++ b/src/main/java/caosdb/server/entity/container/TransactionContainer.java @@ -145,15 +145,6 @@ public class TransactionContainer extends Container<EntityInterface> } } - public EntityInterface getEntityById(final Integer id) { - for (final EntityInterface e : this) { - if (e.hasId() && e.getId().equals(id)) { - return e; - } - } - return null; - } - public Subject getOwner() { return this.owner; } diff --git a/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java b/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java index 4d419bbf..056f342c 100644 --- a/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java +++ b/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -33,6 +35,7 @@ import caosdb.server.entity.Role; import caosdb.server.entity.StatementStatus; import caosdb.server.entity.container.ParentContainer; import caosdb.server.entity.container.PropertyContainer; +import caosdb.server.entity.xml.SetFieldStrategy; import caosdb.server.entity.xml.ToElementStrategy; import caosdb.server.entity.xml.ToElementable; import caosdb.server.permissions.EntityACL; @@ -551,4 +554,9 @@ public class EntityWrapper implements EntityInterface { public boolean hasPermission(Subject subject, Permission permission) { return this.entity.hasPermission(subject, permission); } + + @Override + public void addToElement(Element element, SetFieldStrategy strategy) { + this.entity.addToElement(element, strategy); + } } diff --git a/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java b/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java index 2559cfd1..099b5104 100644 --- a/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -25,11 +27,22 @@ package caosdb.server.entity.xml; import caosdb.server.entity.EntityInterface; import org.jdom2.Element; -public class DomainToElementStrategy implements ToElementStrategy { +/** + * Generates a JDOM (XML) representation of an entity with role "Domain". + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public class DomainToElementStrategy extends EntityToElementStrategy { + + public DomainToElementStrategy() { + super("Domain"); + } @Override public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - return EntityToElementStrategy.sparseEntityToElement("Domain", entity, setFieldStrategy); + Element element = new Element(tagName); + sparseEntityToElement(element, entity, setFieldStrategy); + return element; } @Override diff --git a/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java b/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java index 19bfe80c..ae69b940 100644 --- a/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,27 +24,62 @@ */ package caosdb.server.entity.xml; +import caosdb.server.datatype.ReferenceValue; import caosdb.server.entity.EntityInterface; import caosdb.server.entity.Message; import caosdb.server.utils.EntityStatus; import caosdb.server.utils.TransactionLogMessage; -import java.util.Comparator; import org.apache.shiro.SecurityUtils; -import org.jdom2.Content; -import org.jdom2.Content.CType; +import org.jdom2.Attribute; import org.jdom2.Element; +/** + * Base class for the generation of a JDOM (XML) representation for entities. + * + * <p>Record and RecordType entities use this class only. Properties, Parents, Files and other + * entities have specialized sub classes. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ public class EntityToElementStrategy implements ToElementStrategy { - private final String tagName; + protected final String tagName; public EntityToElementStrategy(final String tagName) { this.tagName = tagName; } - public static Element sparseEntityToElement( - final String tagName, final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - final Element element = new Element(tagName); + /** + * Set the data type of this entity as a JDOM {@link Attribute} of the given element. + * + * <p>If the data type has a name, the name is used, otherwise the id is used. + * + * @param entity + * @param element + */ + public void setDatatype(EntityInterface entity, Element element) { + if (entity.getDatatype().getName() != null) { + element.setAttribute("datatype", entity.getDatatype().getName()); + } else { + element.setAttribute("datatype", entity.getDatatype().getId().toString()); + } + } + + /** + * Set all properties of the entity that are considered to be part of the sparse entity, e.g. + * name, description, etc, as {@link Attribute} of the given element. + * + * <p>The setFieldStrategy decides which attributes are set if present and which are omitted in + * any case. + * + * @param element + * @param entity + * @param setFieldStrategy + */ + public void sparseEntityToElement( + final Element element, + final EntityInterface entity, + final SetFieldStrategy setFieldStrategy) { if (entity.getEntityACL() != null) { element.addContent(entity.getEntityACL().getPermissionsFor(SecurityUtils.getSubject())); @@ -60,11 +97,7 @@ public class EntityToElementStrategy implements ToElementStrategy { element.setAttribute("description", entity.getDescription()); } if (setFieldStrategy.isToBeSet("datatype") && entity.hasDatatype()) { - if (entity.getDatatype().getName() != null) { - element.setAttribute("datatype", entity.getDatatype().getName()); - } else { - element.setAttribute("datatype", entity.getDatatype().getId().toString()); - } + setDatatype(entity, element); } if (setFieldStrategy.isToBeSet("message") && entity.hasMessages()) { for (final ToElementable m : entity.getMessages()) { @@ -76,55 +109,78 @@ public class EntityToElementStrategy implements ToElementStrategy { q.setText(entity.getQueryTemplateDefinition()); element.addContent(q); } + } - return element; + /** + * Set the value of the entity. + * + * <p>The setFieldStrategy decides if the value is to be set at all. + * + * <p>If the value is a reference, the setFieldStrategy decides whether the referenced entity is + * added as a deep Element tree (as a whole, so to speak) or just the ID of the referenced entity. + * + * @param entity + * @param element + * @param setFieldStrategy + */ + public void setValue(EntityInterface entity, Element element, SetFieldStrategy setFieldStrategy) { + if (entity.hasValue()) { + try { + entity.parseValue(); + } catch (final Message | NullPointerException e) { + // Ignore. Parsing the value failed. But that does not concern us here, because this is the + // case when a write transaction failed. The error for that has already been handled by the + // CheckValueParsable job. + } + + if (entity.getValue() instanceof ReferenceValue + && setFieldStrategy.isToBeSet("_referenced")) { + // Append the complete entity. This needs to be done when we are + // processing SELECT Queries. + EntityInterface ref = ((ReferenceValue) entity.getValue()).getEntity(); + if (ref != null) { + if (entity.hasDatatype()) { + setDatatype(entity, element); + } + ref.addToElement(element, setFieldStrategy); + // the referenced entity has been appended. Return here to suppress + // adding the reference id as well. + return; + } + } + + if (setFieldStrategy.isToBeSet("value")) { + if (entity.hasDatatype()) { + setDatatype(entity, element); + } + entity.getValue().addToElement(element); + } + } } @Override public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - final Element element = sparseEntityToElement(this.tagName, entity, setFieldStrategy); + final Element element = new Element(tagName); + + // always have the values at the beginning of the children + setValue(entity, element, setFieldStrategy); - if (setFieldStrategy.isToBeSet("importance") && entity.hasStatementStatus()) { + sparseEntityToElement(element, entity, setFieldStrategy); + + if (entity.hasStatementStatus() && setFieldStrategy.isToBeSet("importance")) { element.setAttribute("importance", entity.getStatementStatus().toString()); } - if (setFieldStrategy.isToBeSet("parent") && entity.hasParents()) { + if (entity.hasParents() && setFieldStrategy.isToBeSet("parent")) { entity.getParents().addToElement(element); } if (entity.hasProperties()) { entity.getProperties().addToElement(element, setFieldStrategy); } - if (setFieldStrategy.isToBeSet("history") && entity.hasTransactionLogMessages()) { + if (entity.hasTransactionLogMessages() && setFieldStrategy.isToBeSet("history")) { for (final TransactionLogMessage t : entity.getTransactionLogMessages()) { t.xmlAppendTo(element); } } - if (setFieldStrategy.isToBeSet("value") && entity.hasValue()) { - if (entity.hasDatatype()) { - try { - - entity.getDatatype().parseValue(entity.getValue()).addToElement(element); - } catch (final Message e) { - // - } - } else { - entity.getValue().addToElement(element); - } - // put value at first position - element.sortContent( - new Comparator<Content>() { - - @Override - public int compare(final Content o1, final Content o2) { - if (o1.getCType() == CType.CDATA || o1.getCType() == CType.Text) { - return -1; - } - if (o2.getCType() == CType.CDATA || o2.getCType() == CType.Text) { - return 1; - } - return 0; - } - }); - } return element; } diff --git a/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java b/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java index 0d79951f..2498e00b 100644 --- a/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -27,17 +29,26 @@ import caosdb.server.entity.wrapper.Parent; import caosdb.server.utils.EntityStatus; import org.jdom2.Element; -public class ParentToElementStrategy implements ToElementStrategy { +/** + * Generates a JDOM (XML) representation of an entity's parent. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public class ParentToElementStrategy extends EntityToElementStrategy { + + public ParentToElementStrategy() { + super("Parent"); + } @Override public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - final Element e = - EntityToElementStrategy.sparseEntityToElement("Parent", entity, setFieldStrategy); + final Element element = new Element(this.tagName); + sparseEntityToElement(element, entity, setFieldStrategy); final Parent parent = (Parent) entity; if (parent.getAffiliation() != null) { - e.setAttribute("affiliation", parent.getAffiliation().toString()); + element.setAttribute("affiliation", parent.getAffiliation().toString()); } - return e; + return element; } @Override diff --git a/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java b/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java index 72996599..3e7e735e 100644 --- a/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,20 +25,34 @@ package caosdb.server.entity.xml; import caosdb.server.entity.EntityInterface; +import caosdb.server.query.Query; import caosdb.server.query.Query.Selection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; +/** + * A class which decides whether the properties, parents, name, etc. of an entity are to be included + * into the serialization or not. + * + * <p>The decision is based on a list of {@link Query.Selection} or smart defaults. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ public class SetFieldStrategy { private final List<Selection> selections = new LinkedList<Selection>(); private HashMap<String, Boolean> cache = null; + + /** + * The default is: Any field should be included into the serialization, unless it is a referenced + * entity. + */ private static final SetFieldStrategy defaultSelections = new SetFieldStrategy(null) { @Override public boolean isToBeSet(final String field) { - return true; + return field == null || !field.equalsIgnoreCase("_referenced"); } }; @@ -68,8 +84,9 @@ public class SetFieldStrategy { return forProperty(property.getName()); } + /** Return the strategy for a property. */ public SetFieldStrategy forProperty(final String name) { - // if property is to be omitted. + // if property is to be omitted: always-false-strategy if (!isToBeSet(name)) { return new SetFieldStrategy() { @Override @@ -85,6 +102,23 @@ public class SetFieldStrategy { subselections.add(s.getSubselection()); } } + if (subselections.isEmpty() && this.isToBeSet("_referenced")) { + + /** + * If the super selection decided that the referenced entity is to be included into the + * serialization, while it doesn't specify the subselects, every field is to be included. + * + * <p>This is the case when the selections are deeply nested but only the very last segments + * are actually used, e.g ["a.b.c.d1", "a.b.c.d2"]. + */ + return new SetFieldStrategy() { + // Always-true-strategy + @Override + public boolean isToBeSet(String field) { + return true; + } + }; + } return new SetFieldStrategy(subselections); } @@ -94,13 +128,23 @@ public class SetFieldStrategy { return defaultSelections.isToBeSet(field); } + // There must be a least one selection defined, a null field won't match anything. + if (field == null) { + return false; + } + if (this.cache == null) { this.cache = new HashMap<String, Boolean>(); - // fill cache + // always include the id and the name this.cache.put("id", true); + this.cache.put("name", true); + + // ... and the referenced entity. + this.cache.put("_referenced", true); for (final Selection selection : this.selections) { - if (!selection.getSelector().equals("id")) { - this.cache.put("name", true); + if (selection.getSelector().equals("value")) { + // if the value is present, the data type is needed as well + this.cache.put("datatype", true); } this.cache.put(selection.getSelector().toLowerCase(), true); } diff --git a/src/main/java/caosdb/server/jobs/extension/JobException.java b/src/main/java/caosdb/server/jobs/extension/JobException.java deleted file mode 100644 index e41475bb..00000000 --- a/src/main/java/caosdb/server/jobs/extension/JobException.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package caosdb.server.jobs.extension; - -import caosdb.server.jobs.Job; - -public class JobException extends RuntimeException { - - /** */ - private static final long serialVersionUID = -2802983933079999366L; - - private final Job job; - - public JobException(final Job job, final Exception e) { - super(e); - this.job = job; - } - - public Job getJob() { - return this.job; - } -} diff --git a/src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java b/src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java deleted file mode 100644 index 4882f0af..00000000 --- a/src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package caosdb.server.jobs.extension; - -import caosdb.server.CaosDBException; -import caosdb.server.entity.FileProperties; -import caosdb.server.entity.Message; -import caosdb.server.jobs.EntityJob; -import caosdb.server.utils.FileUtils; -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; - -public class SQLiteTransaction extends EntityJob { - private static HashMap<String, ReentrantLock> lockedTables = new HashMap<String, ReentrantLock>(); - - @SuppressWarnings("unlikely-arg-type") - @Override - public final void run() { - Connection con = null; - try { - if (getEntity().hasFileProperties()) { - if (getEntity().getFileProperties().retrieveFromFileSystem() != null) { - - if (getEntity().hasMessage("execute")) { - try { - if (!lockedTables.containsKey( - getEntity().getFileProperties().getFile().getCanonicalPath())) { - synchronized (lockedTables) { - lockedTables.put( - getEntity().getFileProperties().getFile().getCanonicalPath(), - new ReentrantLock()); - } - } - - lockedTables.get(getEntity().getFileProperties().getFile().getCanonicalPath()).lock(); - try { - con = - getConnection( - getEntity() - .getFileProperties() - .retrieveFromFileSystem() - .getCanonicalPath()); - final Statement stmt = con.createStatement(); - - // - final List<Message> msgs = getEntity().getMessages("execute"); - Collections.sort(msgs); - for (final Message m : msgs) { - getEntity().removeMessage(m); - stmt.execute(m.getBody()); - final ResultSet rs = stmt.getResultSet(); - final int updateCount = stmt.getUpdateCount(); - if (rs != null) { - final ResultSetMetaData rsmd = rs.getMetaData(); - final int columns = rsmd.getColumnCount(); - final StringBuilder s = new StringBuilder(); - if (columns > 0) { - s.append("|"); - for (int i = 1; i <= columns; i++) { - s.append(rsmd.getColumnName(i)); - s.append("|"); - } - s.append("\n"); - while (rs.next()) { - s.append("|"); - for (int i = 1; i <= columns; i++) { - s.append(rs.getString(i)); - s.append("|"); - } - s.append("\n"); - } - } else { - s.append(""); - } - final Message mret = new Message("result", m.getCode(), null, s.toString()); - getEntity().addMessage(mret); - - } else if (updateCount != -1) { - final Message mret = - new Message( - "result", - m.getCode(), - null, - Integer.toString(updateCount) + " row(s) affected by the query."); - getEntity().addMessage(mret); - } - } - } finally { - lockedTables - .get(getEntity().getFileProperties().getFile().getCanonicalPath()) - .unlock(); - } - synchronized ( - lockedTables.get(getEntity().getFileProperties().getFile().getCanonicalPath())) { - if (!lockedTables - .get(getEntity().getFileProperties().getFile().getCanonicalPath()) - .hasQueuedThreads()) { - lockedTables.remove( - lockedTables.get( - getEntity().getFileProperties().getFile().getCanonicalPath())); - } - } - } finally { - if (con != null && !con.isClosed()) { - con.close(); - } - resetSizeAndChecksum(getEntity().getFileProperties()); - } - } - } - } - } catch (final NoSuchAlgorithmException e) { - throw new JobException(this, e); - } catch (final IOException e) { - throw new JobException(this, e); - } catch (final CaosDBException e) { - throw new JobException(this, e); - } catch (final ClassNotFoundException e) { - throw new JobException(this, e); - } catch (final SQLException e) { - throw new JobException(this, e); - } - } - - private static void resetSizeAndChecksum(final FileProperties f) - throws NoSuchAlgorithmException, IOException { - f.setSize(f.getFile().length()); - f.setChecksum(FileUtils.getChecksum(f.getFile())); - } - - private static Connection getConnection(final String database) - throws ClassNotFoundException, SQLException { - if (!driverLoaded) { - Class.forName("org.sqlite.JDBC"); - driverLoaded = true; - } - final Connection connection = DriverManager.getConnection("jdbc:sqlite:" + database); - return connection; - } - - private static boolean driverLoaded = false; -} diff --git a/src/main/java/caosdb/server/query/Query.java b/src/main/java/caosdb/server/query/Query.java index 1f858269..4eeaafc0 100644 --- a/src/main/java/caosdb/server/query/Query.java +++ b/src/main/java/caosdb/server/query/Query.java @@ -66,18 +66,20 @@ import org.jdom2.Element; public class Query implements QueryInterface, ToElementable, TransactionInterface { + /** Class which represents the selection of (sub)properties. */ public static class Selection { private final String selector; private Selection subselection = null; public Selection setSubSelection(final Selection sub) { if (this.subselection != null) { - throw new UnsupportedOperationException("subselection is immutble!"); + throw new UnsupportedOperationException("SubSelection is immutable!"); } this.subselection = sub; return this; } + /** No parsing, just sets the selector string. */ public Selection(final String selector) { this.selector = selector.trim(); } diff --git a/src/test/java/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java b/src/test/java/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java new file mode 100644 index 00000000..78c2ae82 --- /dev/null +++ b/src/test/java/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java @@ -0,0 +1,79 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.database.backend.transaction; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import caosdb.server.datatype.ReferenceValue; +import caosdb.server.entity.Entity; +import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.entity.xml.PropertyToElementStrategyTest; +import caosdb.server.query.Query.Selection; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +public class RetrieveFullEntityTest { + + @Test + public void testRetrieveSubEntities() { + RetrieveFullEntity r = + new RetrieveFullEntity(0) { + + /** Mock-up */ + @Override + public void retrieveFullEntity(EntityInterface e, List<Selection> selections) { + // The id of the referenced window + assertEquals(1234, (int) e.getId()); + + // The level of selectors has been reduced by 1 + assertEquals("description", selections.get(0).getSelector()); + + e.setDescription("A heart-shaped window."); + }; + }; + + Property window = new Property(2345); + window.setName("Window"); + window.setDatatype("Window"); + window.setValue(new ReferenceValue(1234)); + + Entity house = new Entity(); + house.addProperty(window); + ReferenceValue value = (ReferenceValue) house.getProperties().getEntityById(2345).getValue(); + assertEquals(1234, (int) value.getId()); + assertNull(value.getEntity()); + + List<Selection> selections = new ArrayList<>(); + selections.add(PropertyToElementStrategyTest.parse("window.description")); + + r.retrieveSubEntities(house, selections); + + assertEquals(1234, (int) value.getId()); + assertNotNull(value.getEntity()); + assertEquals("A heart-shaped window.", value.getEntity().getDescription()); + } +} diff --git a/src/test/java/caosdb/server/entity/SelectionTest.java b/src/test/java/caosdb/server/entity/SelectionTest.java index cc961b9a..068a87c4 100644 --- a/src/test/java/caosdb/server/entity/SelectionTest.java +++ b/src/test/java/caosdb/server/entity/SelectionTest.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,13 +24,26 @@ */ package caosdb.server.entity; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import caosdb.server.CaosDBServer; import caosdb.server.entity.xml.SetFieldStrategy; +import caosdb.server.query.Query; import caosdb.server.query.Query.Selection; +import java.io.IOException; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; public class SelectionTest { + @BeforeClass + public static void initServerProperties() throws IOException { + CaosDBServer.initServerProperties(); + } + @Test public void testEmpty1() { final SetFieldStrategy setFieldStrategy = new SetFieldStrategy(); @@ -59,7 +74,7 @@ public class SelectionTest { final Selection selection = new Selection("id"); final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection); - Assert.assertFalse(setFieldStrategy.isToBeSet("name")); + Assert.assertTrue(setFieldStrategy.isToBeSet("name")); } @Test @@ -262,16 +277,50 @@ public class SelectionTest { .addSelection(new Selection("blabla").setSubSelection(new Selection("value"))) .addSelection(new Selection("blabla").setSubSelection(new Selection("description"))); - Assert.assertTrue(setFieldStrategy.isToBeSet("blabla")); - Assert.assertTrue(setFieldStrategy.isToBeSet("id")); - Assert.assertTrue(setFieldStrategy.isToBeSet("name")); - Assert.assertFalse(setFieldStrategy.isToBeSet("bleb")); + assertTrue(setFieldStrategy.isToBeSet("blabla")); + assertTrue(setFieldStrategy.isToBeSet("id")); + assertTrue(setFieldStrategy.isToBeSet("name")); + assertFalse(setFieldStrategy.isToBeSet("bleb")); final SetFieldStrategy forProperty = setFieldStrategy.forProperty("blabla"); - Assert.assertTrue(forProperty.isToBeSet("id")); - Assert.assertTrue(forProperty.isToBeSet("name")); - Assert.assertTrue(forProperty.isToBeSet("description")); - Assert.assertTrue(forProperty.isToBeSet("value")); - Assert.assertFalse(forProperty.isToBeSet("blub")); + assertTrue(forProperty.isToBeSet("id")); + assertTrue(forProperty.isToBeSet("name")); + assertTrue(forProperty.isToBeSet("description")); + assertTrue(forProperty.isToBeSet("value")); + assertFalse(forProperty.isToBeSet("blub")); + } + + @Test + public void testSubSubSelection() { + String query_str = "SELECT property.subproperty.subsubproperty FROM ENTITY"; + Query query = new Query(query_str); + query.parse(); + assertEquals(query.getSelections().size(), 1); + + Selection s = query.getSelections().get(0); + assertEquals(s.toString(), "property.subproperty.subsubproperty"); + assertEquals(s.getSubselection().toString(), "subproperty.subsubproperty"); + assertEquals(s.getSubselection().getSubselection().toString(), "subsubproperty"); + } + + @Test + public void testSubSubProperty() { + Selection s = + new Selection("property") + .setSubSelection( + new Selection("subproperty").setSubSelection(new Selection("subsubproperty"))); + + assertEquals(s.toString(), "property.subproperty.subsubproperty"); + + SetFieldStrategy setFieldStrategy = new SetFieldStrategy().addSelection(s); + assertTrue(setFieldStrategy.isToBeSet("property")); + assertFalse(setFieldStrategy.forProperty("property").isToBeSet("sadf")); + // assertFalse(setFieldStrategy.forProperty("property").isToBeSet("name")); + assertTrue(setFieldStrategy.forProperty("property").isToBeSet("subproperty")); + assertTrue( + setFieldStrategy + .forProperty("property") + .forProperty("subproperty") + .isToBeSet("subsubproperty")); } } diff --git a/src/test/java/caosdb/server/entity/container/PropertyContainerTest.java b/src/test/java/caosdb/server/entity/container/PropertyContainerTest.java new file mode 100644 index 00000000..0678e830 --- /dev/null +++ b/src/test/java/caosdb/server/entity/container/PropertyContainerTest.java @@ -0,0 +1,80 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.entity.container; + +import static org.junit.Assert.assertEquals; + +import caosdb.server.datatype.GenericValue; +import caosdb.server.datatype.ReferenceValue; +import caosdb.server.entity.Entity; +import caosdb.server.entity.Role; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.entity.xml.PropertyToElementStrategyTest; +import caosdb.server.entity.xml.SetFieldStrategy; +import org.jdom2.Element; +import org.junit.BeforeClass; +import org.junit.Test; + +public class PropertyContainerTest { + + public static Entity house = null; + public static Property houseHeight = null; + public static Entity window = null; + public static Property windowHeight = null; + public static Entity houseOwner = null; + public static Property windowProperty = null; + + @BeforeClass + public static void setup() { + window = new Entity(1234); + windowHeight = new Property(new Entity("window.height", Role.Property)); + window.addProperty(windowHeight); + windowHeight.setValue(new GenericValue("windowHeight")); + + houseOwner = new Entity("The Queen", Role.Record); + + house = new Entity("Buckingham Palace", Role.Record); + houseHeight = new Property(new Entity("height", Role.Property)); + houseHeight.setValue(new GenericValue("houseHeight")); + house.addProperty(houseHeight); + windowProperty = new Property(2345); + windowProperty.setName("window"); + windowProperty.setValue(new ReferenceValue(window.getId())); + house.addProperty(windowProperty); + + house.addProperty(new Property()); + house.addProperty(new Property(houseHeight)); + } + + @Test + public void test() { + PropertyContainer container = new PropertyContainer(new Entity()); + Element element = new Element("Record"); + SetFieldStrategy setFieldStrategy = + new SetFieldStrategy().addSelection(PropertyToElementStrategyTest.parse("window.height")); + + container.addToElement(windowProperty, element, setFieldStrategy); + + assertEquals(1, element.getChildren().size()); + } +} diff --git a/src/test/java/caosdb/server/entity/xml/PropertyToElementStrategyTest.java b/src/test/java/caosdb/server/entity/xml/PropertyToElementStrategyTest.java new file mode 100644 index 00000000..7e0feec8 --- /dev/null +++ b/src/test/java/caosdb/server/entity/xml/PropertyToElementStrategyTest.java @@ -0,0 +1,105 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.entity.xml; + +import static org.junit.Assert.assertEquals; + +import caosdb.server.datatype.GenericValue; +import caosdb.server.datatype.ReferenceValue; +import caosdb.server.entity.Entity; +import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Role; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.query.Query.Selection; +import org.jdom2.Element; +import org.junit.Before; +import org.junit.Test; + +public class PropertyToElementStrategyTest { + + public static Entity house = null; + public static Property houseHeight = null; + public static Entity window = null; + public static Property windowHeight = null; + public static Entity houseOwner = null; + public static Property windowProperty = null; + + /** + * Create a nested selection out of the dot-separated parts of <code>select</code>. + * + * <p>The returned Selection has nested subselections, so that each subselection corresponds to + * the next part and the remainder of the initial <code>select</code> String. + */ + public static Selection parse(String select) { + String[] split = select.split("\\."); + Selection result = new Selection(split[0]); + Selection next = result; + + for (int i = 1; i < split.length; i++) { + next.setSubSelection(new Selection(split[i])); + next = next.getSubselection(); + } + return result; + } + + @Before + public void setup() { + window = new Entity(1234, Role.Record); + windowHeight = new Property(new Entity("height", Role.Property)); + window.addProperty(windowHeight); + windowHeight.setValue(new GenericValue("windowHeight")); + + houseOwner = new Entity("The Queen", Role.Record); + + house = new Entity("Buckingham Palace", Role.Record); + houseHeight = new Property(new Entity("height", Role.Property)); + houseHeight.setValue(new GenericValue("houseHeight")); + house.addProperty(houseHeight); + windowProperty = new Property(2345); + windowProperty.setName("window"); + windowProperty.setValue(new ReferenceValue(window.getId())); + house.addProperty(windowProperty); + + house.addProperty(new Property()); + house.addProperty(new Property(houseHeight)); + } + + @Test + public void test() { + PropertyToElementStrategy strategy = new PropertyToElementStrategy(); + SetFieldStrategy setFieldStrategy = new SetFieldStrategy().addSelection(parse("height")); + EntityInterface property = windowProperty; + ((ReferenceValue) property.getValue()).setEntity(window); + Element element = strategy.toElement(property, setFieldStrategy); + assertEquals("Property", element.getName()); + assertEquals("2345", element.getAttributeValue("id")); + assertEquals("window", element.getAttributeValue("name")); + assertEquals(1, element.getChildren().size()); + assertEquals("Record", element.getChildren().get(0).getName()); + + Element recordElement = element.getChild("Record"); + assertEquals("1234", recordElement.getAttributeValue("id")); + assertEquals(1, recordElement.getChildren().size()); + assertEquals("windowHeight", recordElement.getChild("Property").getText()); + } +} -- GitLab