/** * 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.schema; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.common.StructrTest; import org.structr.common.error.FrameworkException; import org.structr.core.app.StructrApp; import org.structr.core.entity.AbstractUser; import org.structr.core.entity.Relation; import org.structr.core.entity.Relation.Cardinality; import org.structr.core.entity.SchemaNode; import org.structr.core.entity.SchemaRelationshipNode; import org.structr.core.entity.SchemaView; import org.structr.core.graph.NodeAttribute; import org.structr.core.graph.Tx; import org.structr.schema.export.StructrSchema; import org.structr.schema.json.InvalidSchemaException; import org.structr.schema.json.JsonObjectType; import org.structr.schema.json.JsonProperty; import org.structr.schema.json.JsonReferenceProperty; import org.structr.schema.json.JsonReferenceType; import org.structr.schema.json.JsonSchema; import org.structr.schema.json.JsonSchema.Cascade; import org.structr.schema.json.JsonType; /** * * */ public class SchemaTest extends StructrTest { private static final Logger logger = LoggerFactory.getLogger(SchemaTest.class.getName()); @Test public void test00SimpleProperties() { try { final JsonSchema sourceSchema = StructrSchema.createFromDatabase(app); // a customer final JsonType customer = sourceSchema.addType("Customer"); customer.addStringProperty("name", "public", "ui").setRequired(true).setUnique(true); customer.addStringProperty("street", "public", "ui"); customer.addStringProperty("city", "public", "ui"); customer.addDateProperty("birthday", "public", "ui"); customer.addEnumProperty("status", "public", "ui").setEnums("active", "retired", "none").setDefaultValue("active"); customer.addIntegerProperty("count", "public", "ui").setMinimum(1).setMaximum(10, true).setDefaultValue("5"); customer.addNumberProperty("number", "public", "ui").setMinimum(2.0, true).setMaximum(5.0, true).setDefaultValue("3.0"); customer.addLongProperty("loong", "public", "ui").setMinimum(20, true).setMaximum(50); customer.addBooleanProperty("isCustomer", "public", "ui"); customer.addFunctionProperty("displayName", "public", "ui").setReadFunction("concat(this.name, '.', this.id)"); customer.addStringProperty("description", "public", "ui").setContentType("text/plain").setFormat("multi-line"); customer.addStringArrayProperty("stringArray", "public", "ui"); customer.addIntegerArrayProperty("intArray", "public", "ui").setMinimum(0, true).setMaximum(100, true); customer.addLongArrayProperty("longArray", "public", "ui").setMinimum(1, true).setMaximum(101, true); customer.addDoubleArrayProperty("doubleArray", "public", "ui").setMinimum(2.0, true).setMaximum(102.0, true); customer.addBooleanArrayProperty("booleanArray", "public", "ui"); final String schema = sourceSchema.toString(); final Map<String, Object> map = new GsonBuilder().create().fromJson(schema, Map.class); mapPathValue(map, "definitions.Customer.type", "object"); mapPathValue(map, "definitions.Customer.required.0", "name"); mapPathValue(map, "definitions.Customer.properties.booleanArray.type", "array"); mapPathValue(map, "definitions.Customer.properties.booleanArray.items.type", "boolean"); mapPathValue(map, "definitions.Customer.properties.city.unique", null); mapPathValue(map, "definitions.Customer.properties.count.type", "integer"); mapPathValue(map, "definitions.Customer.properties.count.minimum", 1.0); mapPathValue(map, "definitions.Customer.properties.count.maximum", 10.0); mapPathValue(map, "definitions.Customer.properties.count.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.doubleArray.type", "array"); mapPathValue(map, "definitions.Customer.properties.doubleArray.items.type", "number"); mapPathValue(map, "definitions.Customer.properties.doubleArray.items.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.doubleArray.items.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.doubleArray.items.maximum", 102.0); mapPathValue(map, "definitions.Customer.properties.doubleArray.items.minimum", 2.0); mapPathValue(map, "definitions.Customer.properties.number.type", "number"); mapPathValue(map, "definitions.Customer.properties.number.minimum", 2.0); mapPathValue(map, "definitions.Customer.properties.number.maximum", 5.0); mapPathValue(map, "definitions.Customer.properties.number.exclusiveMinimum", true); mapPathValue(map, "definitions.Customer.properties.number.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.longArray.type", "array"); mapPathValue(map, "definitions.Customer.properties.longArray.items.type", "long"); mapPathValue(map, "definitions.Customer.properties.longArray.items.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.longArray.items.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.longArray.items.maximum", 101.0); mapPathValue(map, "definitions.Customer.properties.longArray.items.minimum", 1.0); mapPathValue(map, "definitions.Customer.properties.loong.type", "long"); mapPathValue(map, "definitions.Customer.properties.loong.minimum", 20.0); mapPathValue(map, "definitions.Customer.properties.loong.maximum", 50.0); mapPathValue(map, "definitions.Customer.properties.loong.exclusiveMinimum", true); mapPathValue(map, "definitions.Customer.properties.intArray.type", "array"); mapPathValue(map, "definitions.Customer.properties.intArray.items.type", "integer"); mapPathValue(map, "definitions.Customer.properties.intArray.items.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.intArray.items.exclusiveMaximum", true); mapPathValue(map, "definitions.Customer.properties.intArray.items.maximum", 100.0); mapPathValue(map, "definitions.Customer.properties.intArray.items.minimum", 0.0); mapPathValue(map, "definitions.Customer.properties.isCustomer.type", "boolean"); mapPathValue(map, "definitions.Customer.properties.description.type", "string"); mapPathValue(map, "definitions.Customer.properties.description.contentType", "text/plain"); mapPathValue(map, "definitions.Customer.properties.description.format", "multi-line"); mapPathValue(map, "definitions.Customer.properties.displayName.type", "function"); mapPathValue(map, "definitions.Customer.properties.displayName.readFunction", "concat(this.name, '.', this.id)"); mapPathValue(map, "definitions.Customer.properties.name.type", "string"); mapPathValue(map, "definitions.Customer.properties.name.unique", true); mapPathValue(map, "definitions.Customer.properties.street.type", "string"); mapPathValue(map, "definitions.Customer.properties.status.type", "string"); mapPathValue(map, "definitions.Customer.properties.status.enum.0", "active"); mapPathValue(map, "definitions.Customer.properties.status.enum.1", "none"); mapPathValue(map, "definitions.Customer.properties.status.enum.2", "retired"); mapPathValue(map, "definitions.Customer.properties.stringArray.type", "array"); mapPathValue(map, "definitions.Customer.properties.stringArray.items.type", "string"); mapPathValue(map, "definitions.Customer.views.public.0", "birthday"); mapPathValue(map, "definitions.Customer.views.public.1", "booleanArray"); mapPathValue(map, "definitions.Customer.views.public.2", "city"); mapPathValue(map, "definitions.Customer.views.public.3", "count"); mapPathValue(map, "definitions.Customer.views.public.4", "description"); mapPathValue(map, "definitions.Customer.views.public.5", "displayName"); mapPathValue(map, "definitions.Customer.views.public.6", "doubleArray"); mapPathValue(map, "definitions.Customer.views.public.7", "intArray"); mapPathValue(map, "definitions.Customer.views.public.8", "isCustomer"); mapPathValue(map, "definitions.Customer.views.public.9", "longArray"); mapPathValue(map, "definitions.Customer.views.public.10", "loong"); mapPathValue(map, "definitions.Customer.views.public.11", "name"); mapPathValue(map, "definitions.Customer.views.public.12", "number"); mapPathValue(map, "definitions.Customer.views.public.13", "status"); mapPathValue(map, "definitions.Customer.views.public.14", "street"); mapPathValue(map, "definitions.Customer.views.ui.0", "birthday"); mapPathValue(map, "definitions.Customer.views.ui.1", "booleanArray"); mapPathValue(map, "definitions.Customer.views.ui.2", "city"); mapPathValue(map, "definitions.Customer.views.ui.3", "count"); mapPathValue(map, "definitions.Customer.views.ui.4", "description"); mapPathValue(map, "definitions.Customer.views.ui.5", "displayName"); mapPathValue(map, "definitions.Customer.views.ui.6", "doubleArray"); mapPathValue(map, "definitions.Customer.views.ui.7", "intArray"); mapPathValue(map, "definitions.Customer.views.ui.8", "isCustomer"); mapPathValue(map, "definitions.Customer.views.ui.9", "longArray"); mapPathValue(map, "definitions.Customer.views.ui.10", "loong"); mapPathValue(map, "definitions.Customer.views.ui.11", "name"); mapPathValue(map, "definitions.Customer.views.ui.12", "number"); mapPathValue(map, "definitions.Customer.views.ui.13", "status"); mapPathValue(map, "definitions.Customer.views.ui.14", "street"); // advanced: test schema roundtrip compareSchemaRoundtrip(sourceSchema); } catch (Throwable t) { t.printStackTrace(); fail("Unexpected exception."); } } @Test public void test01Inheritance() { // we need to wait for the schema service to be initialized here.. :( try { Thread.sleep(1000); } catch (Throwable t) {} try { final JsonSchema sourceSchema = StructrSchema.createFromDatabase(app); final JsonType contact = sourceSchema.addType("Contact").setExtends(StructrApp.getSchemaId(AbstractUser.class)); final JsonType customer = sourceSchema.addType("Customer").setExtends(contact); final String schema = sourceSchema.toString(); final Map<String, Object> map = new GsonBuilder().create().fromJson(schema, Map.class); mapPathValue(map, "definitions.Contact.type", "object"); mapPathValue(map, "definitions.Contact.$extends", "https://structr.org/v1.1/definitions/AbstractUser"); mapPathValue(map, "definitions.Customer.type", "object"); mapPathValue(map, "definitions.Customer.$extends", "#/definitions/Contact"); // advanced: test schema roundtrip compareSchemaRoundtrip(sourceSchema); } catch (Exception t) { logger.warn("", t); fail("Unexpected exception."); } } @Test public void test02SimpleSymmetricReferences() { // we need to wait for the schema service to be initialized here.. :( try { Thread.sleep(1000); } catch (Throwable t) {} try { final JsonSchema sourceSchema = StructrSchema.createFromDatabase(app); final JsonObjectType project = sourceSchema.addType("Project"); final JsonObjectType task = sourceSchema.addType("Task"); // create relation final JsonReferenceType rel = project.relate(task, "has", Cardinality.OneToMany, "project", "tasks"); rel.setName("ProjectTasks"); final String schema = sourceSchema.toString(); System.out.println(schema); // test map paths final Map<String, Object> map = new GsonBuilder().create().fromJson(schema, Map.class); mapPathValue(map, "definitions.Project.type", "object"); mapPathValue(map, "definitions.Project.properties.tasks.$link", "#/definitions/ProjectTasks"); mapPathValue(map, "definitions.Project.properties.tasks.items.$ref", "#/definitions/Task"); mapPathValue(map, "definitions.Project.properties.tasks.type", "array"); mapPathValue(map, "definitions.ProjectTasks.$source", "#/definitions/Project"); mapPathValue(map, "definitions.ProjectTasks.$target", "#/definitions/Task"); mapPathValue(map, "definitions.ProjectTasks.cardinality", "OneToMany"); mapPathValue(map, "definitions.ProjectTasks.rel", "has"); mapPathValue(map, "definitions.ProjectTasks.sourceName", "project"); mapPathValue(map, "definitions.ProjectTasks.targetName", "tasks"); mapPathValue(map, "definitions.ProjectTasks.type", "object"); mapPathValue(map, "definitions.Task.type", "object"); mapPathValue(map, "definitions.Task.properties.project.$link", "#/definitions/ProjectTasks"); mapPathValue(map, "definitions.Task.properties.project.$ref", "#/definitions/Project"); mapPathValue(map, "definitions.Task.properties.project.type", "object"); // test compareSchemaRoundtrip(sourceSchema); } catch (FrameworkException | InvalidSchemaException |URISyntaxException ex) { logger.warn("", ex); fail("Unexpected exception."); } } @Test public void test03SchemaBuilder() { // we need to wait for the schema service to be initialized here.. :( try { Thread.sleep(1000); } catch (Throwable t) {} try { final JsonSchema sourceSchema = StructrSchema.createFromDatabase(app); final String instanceId = app.getInstanceId(); final JsonObjectType task = sourceSchema.addType("Task"); final JsonProperty title = task.addStringProperty("title", "public", "ui").setRequired(true); final JsonProperty desc = task.addStringProperty("description", "public", "ui").setRequired(true); task.addDateProperty("description", "public", "ui").setDatePattern("dd.MM.yyyy").setRequired(true); // test function property task.addFunctionProperty("displayName", "public", "ui").setReadFunction("this.name"); task.addFunctionProperty("javascript", "public", "ui").setReadFunction("{ var x = 'test'; return x; }").setContentType("application/x-structr-javascript"); // a project final JsonObjectType project = sourceSchema.addType("Project"); project.addStringProperty("name", "public", "ui").setRequired(true); final JsonReferenceType projectTasks = project.relate(task, "HAS", Cardinality.OneToMany, "project", "tasks"); projectTasks.setCascadingCreate(Cascade.targetToSource); project.getViewPropertyNames("public").add("tasks"); task.getViewPropertyNames("public").add("project"); // test enums project.addEnumProperty("status", "ui").setEnums("active", "planned", "finished"); // a worker final JsonObjectType worker = sourceSchema.addType("Worker"); final JsonReferenceType workerTasks = worker.relate(task, "HAS", Cardinality.OneToMany, "worker", "tasks"); workerTasks.setCascadingDelete(Cascade.sourceToTarget); // reference Worker -> Task final JsonReferenceProperty workerProperty = workerTasks.getSourceProperty(); final JsonReferenceProperty tasksProperty = workerTasks.getTargetProperty(); tasksProperty.setName("renamedTasks"); worker.addReferenceProperty("taskNames", tasksProperty, "public", "ui").setProperties("name"); worker.addReferenceProperty("taskInfos", tasksProperty, "public", "ui").setProperties("id", "name"); worker.addReferenceProperty("taskErrors", tasksProperty, "public", "ui"); task.addReferenceProperty("workerName", workerProperty, "public", "ui").setProperties("name"); task.addReferenceProperty("workerNotion", workerProperty, "public", "ui"); // test date properties.. project.addDateProperty("startDate", "public", "ui"); // methods project.addMethod("onCreate", "set(this, 'name', 'wurst')", "comment for wurst"); // test URIs assertEquals("Invalid schema URI", "https://structr.org/schema/" + instanceId + "/#", sourceSchema.getId().toString()); assertEquals("Invalid schema URI", "https://structr.org/schema/" + instanceId + "/definitions/Task", task.getId().toString()); assertEquals("Invalid schema URI", "https://structr.org/schema/" + instanceId + "/definitions/Task/properties/title", title.getId().toString()); assertEquals("Invalid schema URI", "https://structr.org/schema/" + instanceId + "/definitions/Task/properties/description", desc.getId().toString()); assertEquals("Invalid schema URI", "https://structr.org/schema/" + instanceId + "/definitions/Worker/properties/renamedTasks", tasksProperty.getId().toString()); compareSchemaRoundtrip(sourceSchema); } catch (Exception ex) { logger.warn("", ex); fail("Unexpected exception."); } } @Test public void test04ManualSchemaRelatedPropertyNameCreation() { try { try (final Tx tx = app.tx()) { final SchemaNode source = app.create(SchemaNode.class, "Source"); final SchemaNode target = app.create(SchemaNode.class, "Target"); app.create(SchemaRelationshipNode.class, new NodeAttribute(SchemaRelationshipNode.relationshipType, "link"), new NodeAttribute(SchemaRelationshipNode.sourceNode, source), new NodeAttribute(SchemaRelationshipNode.targetNode, target), new NodeAttribute(SchemaRelationshipNode.sourceMultiplicity, "1"), new NodeAttribute(SchemaRelationshipNode.targetMultiplicity, "*") ); tx.success(); } checkSchemaString(StructrSchema.createFromDatabase(app).toString()); } catch (FrameworkException | URISyntaxException t) { logger.warn("", t); } } @Test public void test05SchemaRelatedPropertyNameCreationWithPresets() { try { // create test case final JsonSchema schema = StructrSchema.newInstance(URI.create(app.getInstanceId())); final JsonObjectType source = schema.addType("Source"); final JsonObjectType target = schema.addType("Target"); source.relate(target, "link", Relation.Cardinality.OneToMany, "sourceLink", "linkTargets"); checkSchemaString(schema.toString()); } catch (FrameworkException | URISyntaxException t) { logger.warn("", t); } } @Test public void test06SchemaRelatedPropertyNameCreationWithoutPresets() { try { // create test case final JsonSchema schema = StructrSchema.newInstance(URI.create(app.getInstanceId())); final JsonObjectType source = schema.addType("Source"); final JsonObjectType target = schema.addType("Target"); source.relate(target, "link", Relation.Cardinality.OneToMany); checkSchemaString(schema.toString()); } catch (FrameworkException | URISyntaxException t) { logger.warn("", t); } } @Test public void test00DeleteSchemaRelationshipInView() { SchemaRelationshipNode rel = null; try (final Tx tx = app.tx()) { // create source and target node final SchemaNode fooNode = app.create(SchemaNode.class, "Foo"); final SchemaNode barNode = app.create(SchemaNode.class, "Bar"); // create relationship rel = app.create(SchemaRelationshipNode.class, new NodeAttribute<>(SchemaRelationshipNode.sourceNode, fooNode), new NodeAttribute<>(SchemaRelationshipNode.targetNode, barNode), new NodeAttribute<>(SchemaRelationshipNode.relationshipType, "narf") ); // create "public" view that contains the related property app.create(SchemaView.class, new NodeAttribute<>(SchemaView.name, "public"), new NodeAttribute<>(SchemaView.schemaNode, fooNode), new NodeAttribute<>(SchemaView.nonGraphProperties, "type, id, narfBars") ); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); fail("Unexpected exception"); } try (final Tx tx = app.tx()) { app.delete(rel); tx.success(); } catch (Throwable t) { // deletion of relationship should not fail logger.warn("", t); fail("Unexpected exception"); } } // ----- private methods ----- private void checkSchemaString(final String source) { System.out.println("########################################## checking"); System.out.println(source); final Gson gson = new GsonBuilder().create(); final Map<String, Object> map = gson.fromJson(source, Map.class); assertNotNull("Invalid schema serialization", map); final Map<String, Object> defs = (Map)map.get("definitions"); assertNotNull("Invalid schema serialization", defs); final Map<String, Object> src = (Map)defs.get("Source"); assertNotNull("Invalid schema serialization", src); final Map<String, Object> srcp = (Map)src.get("properties"); assertNotNull("Invalid schema serialization", srcp); final Map<String, Object> tgt = (Map)defs.get("Target"); assertNotNull("Invalid schema serialization", tgt); final Map<String, Object> tgtp = (Map)tgt.get("properties"); assertNotNull("Invalid schema serialization", tgtp); final Map<String, Object> lnk = (Map)defs.get("SourcelinkTarget"); assertNotNull("Invalid schema serialization", lnk); // check related property names assertTrue("Invalid schema serialization result", srcp.containsKey("linkTargets")); assertTrue("Invalid schema serialization result", tgtp.containsKey("sourceLink")); assertEquals("Invalid schema serialization result", "sourceLink", lnk.get("sourceName")); assertEquals("Invalid schema serialization result", "linkTargets", lnk.get("targetName")); } private void mapPathValue(final Map<String, Object> map, final String mapPath, final Object value) { final String[] parts = mapPath.split("[\\.]+"); Object current = map; for (int i=0; i<parts.length; i++) { final String part = parts[i]; if (StringUtils.isNumeric(part)) { int index = Integer.valueOf(part); if (current instanceof List) { current = ((List)current).get(index); } } else { if (current instanceof Map) { current = ((Map)current).get(part); } } } assertEquals("Invalid map path result for " + mapPath, value, current); } private void compareSchemaRoundtrip(final JsonSchema sourceSchema) throws FrameworkException, InvalidSchemaException, URISyntaxException { final String source = sourceSchema.toString(); System.out.println("##################### source"); System.out.println(source); final JsonSchema targetSchema = StructrSchema.createFromSource(sourceSchema.toString()); final String target = targetSchema.toString(); System.out.println("##################### target"); System.out.println(target); assertEquals("Invalid schema (de)serialization roundtrip result", source, target); StructrSchema.replaceDatabaseSchema(app, targetSchema); final JsonSchema replacedSchema = StructrSchema.createFromDatabase(app); final String replaced = replacedSchema.toString(); System.out.println("##################### replaced"); System.out.println(replaced); assertEquals("Invalid schema replacement result", source, replaced); } }