/*
* Copyright 2010 Outerthought bvba
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lilyproject.process.test;
import java.math.BigDecimal;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.google.common.collect.Lists;
import org.apache.http.HttpStatus;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SchemaRestTest extends AbstractRestTest {
@Test
public void testGeneralErrors() throws Exception {
// Perform request to non-existing resource class
ResponseAndContent response = get("/foobar");
assertStatus(HttpStatus.SC_NOT_FOUND, response);
// Submit invalid json
String body = json("{ f [ }");
response = post("/schema/fieldType", body);
assertStatus(HttpStatus.SC_BAD_REQUEST, response);
}
@Test
public void testFieldTypes() throws Exception {
// Create field type using POST
String body = json("{action: 'create', fieldType: {name: 'n$field1', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } } }");
ResponseAndContent response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
JsonNode json = readJson(response);
// verify location header
assertEquals(buildUri("/schema/fieldTypeById/" + json.get("id").getValueAsText()), response.getLocationRef().toString());
// verify name
String prefix = json.get("namespaces").get("org.lilyproject.resttest").getTextValue();
assertEquals(prefix + "$field1", json.get("name").getTextValue());
// Create field type using POST on the name-based resource
body = json("{action: 'create', fieldType: {name: 'n$field1a', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/fieldType", body);
assertStatus(HttpStatus.SC_CREATED, response);
assertEquals(buildUri("/schema/fieldType/n$field1a?ns.n=org.lilyproject.resttest"), response.getLocationRef().toString());
// Create field type using PUT
body = json("{name: 'n$field2', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/fieldType/n$field2?ns.n=org.lilyproject.resttest", body);
String fieldType2Id = readJson(response).get("id").getValueAsText();
assertStatus(HttpStatus.SC_CREATED, response);
assertEquals(buildUri("/schema/fieldType/n$field2?ns.n=org.lilyproject.resttest"), response.getLocationRef().toString());
// Update a field type - by name : change field2 to field3
body = json("{name: 'n$field3', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/fieldType/n$field2?ns.n=org.lilyproject.resttest", body);
assertStatus(HttpStatus.SC_MOVED_PERMANENTLY, response);
assertEquals(buildUri("/schema/fieldType/n$field3?ns.n=org.lilyproject.resttest"), response.getLocationRef().toString());
response = get("/schema/fieldType/n$field2?ns.n=org.lilyproject.resttest");
assertStatus(HttpStatus.SC_NOT_FOUND, response);
response = get("/schema/fieldType/n$field3?ns.n=org.lilyproject.resttest");
assertStatus(HttpStatus.SC_OK, response);
// Update a field type - by ID : change field3 to field4
body = json("{name: 'n$field4', valueType: 'STRING', "
+ "scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/fieldTypeById/" + fieldType2Id, body);
assertStatus(HttpStatus.SC_OK, response);
// Test updating immutable properties
body = json("{name: 'n$field4', valueType: 'INTEGER', "
+ "scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/fieldTypeById/" + fieldType2Id, body);
assertStatus(HttpStatus.SC_CONFLICT, response);
body = json("{name: 'n$field4', valueType: 'STRING', "
+ "scope: 'non_versioned', namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/fieldTypeById/" + fieldType2Id, body);
assertStatus(HttpStatus.SC_CONFLICT, response);
body = json("{name: 'n$field4', valueType: 'LIST<STRING>', "
+ "scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/fieldTypeById/" + fieldType2Id, body);
assertStatus(HttpStatus.SC_CONFLICT, response);
body = json("{name: 'n$field4', valueType: 'PATH<STRING>', "
+ "scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/fieldType/n$field4?ns.n=org.lilyproject.resttest", body);
assertStatus(HttpStatus.SC_CONFLICT, response);
// Get list of field types
response = get("/schema/fieldType");
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertTrue(json.get("results").size() > 0);
response = get("/schema/fieldTypeById");
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertTrue(json.get("results").size() > 0);
}
@Test
public void testRecordTypes() throws Exception {
// Create some field types
List<String> fieldTypeIds = new ArrayList<String>();
for (int i = 1; i < 4; i++) {
String body = json("{action: 'create', fieldType: {name: 'n$rt_field" + i +
"', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } } }");
ResponseAndContent response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
JsonNode json = readJson(response);
fieldTypeIds.add(json.get("id").getValueAsText());
}
// Create a record type using POST
String body = json("{action: 'create', recordType: {name: 'n$recordType1', fields: [ {name: 'n$rt_field1'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } } }");
ResponseAndContent response = post("/schema/recordTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
JsonNode json = readJson(response);
// verify location header
assertEquals(buildUri("/schema/recordTypeById/" + json.get("id").getValueAsText()),
response.getLocationRef().toString());
// verify name
String prefix = json.get("namespaces").get("org.lilyproject.resttest").getTextValue();
assertEquals(prefix + "$recordType1", json.get("name").getTextValue());
// verify the field
assertEquals(fieldTypeIds.get(0), json.get("fields").get(0).get("id").getTextValue());
assertFalse(json.get("fields").get(0).get("mandatory").getBooleanValue());
// verify version
assertEquals(1L, json.get("version").getLongValue());
// Create a record type using POST on the name-based resource
body = json("{action: 'create', recordType: {name: 'n$recordType1a', fields: [ {name: 'n$rt_field1'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/recordType", body);
assertStatus(HttpStatus.SC_CREATED, response);
assertEquals(
buildUri("/schema/recordType/n$recordType1a?ns.n=org.lilyproject.resttest"),
response.getLocationRef().toString());
// Create a record type using PUT
body = json("{name: 'n$recordType2', fields: [ {name: 'n$rt_field1'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/recordType/n$recordType2?ns.n=org.lilyproject.resttest", body);
assertStatus(HttpStatus.SC_CREATED, response);
assertEquals(buildUri("/schema/recordType/n$recordType2?ns.n=org.lilyproject.resttest"), response.getLocationRef().toString());
json = readJson(response);
String secondRtId = json.get("id").getValueAsText();
// Update a record type - by ID
body = json("{name: 'n$recordType2', " +
"fields: [ {name: 'n$rt_field1', mandatory: true}, " +
" {name : 'n$rt_field2', mandatory: true} ], namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/recordTypeById/" + secondRtId, body);
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertEquals(secondRtId, json.get("id").getTextValue());
assertEquals(2L, json.get("version").getLongValue());
assertEquals(2, json.get("fields").size());
assertTrue(json.get("fields").get(0).get("mandatory").getBooleanValue());
assertTrue(json.get("fields").get(1).get("mandatory").getBooleanValue());
// Rename a record type via the name-based resource
body = json("{name: 'n$recordType3', " +
"fields: [ {name: 'n$rt_field1', mandatory: true}, " +
" {name : 'n$rt_field2', mandatory: true} ], namespaces: { 'org.lilyproject.resttest': 'n' } }");
response = put("/schema/recordType/n$recordType2?ns.n=org.lilyproject.resttest", body);
assertStatus(HttpStatus.SC_MOVED_PERMANENTLY, response);
assertEquals(buildUri("/schema/recordType/n$recordType3?ns.n=org.lilyproject.resttest"), response.getLocationRef().toString());
// Get list of record types
response = get("/schema/recordType");
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertTrue(json.get("results").size() > 0);
response = get("/schema/recordTypeById");
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertTrue(json.get("results").size() > 0);
//
// Test supertypes
//
// Create two supertype record types
body = json("{action: 'create', recordType: {name: 'n$supertype1', fields: [ {name: 'n$rt_field2'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/recordTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
String supertype1Id = readJson(response).get("id").getTextValue();
body = json("{action: 'create', recordType: {name: 'n$supertype2', fields: [ {name: 'n$rt_field3'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/recordTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
String supertype2Id = readJson(response).get("id").getTextValue();
// Create a record type using the supertypes
body = json("{action: 'create', recordType: {name: 'n$subtype', fields: [ {name: 'n$rt_field1'} ]," +
"supertypes: [{name: 'n$supertype1', version: 1}, { id: '" + supertype2Id + "' } ], " +
"namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/recordTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
response = getUri(response.getLocationRef().toString());
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertEquals(2, json.get("supertypes").size());
// Update to remove one of the supertypes
body = json("{name: 'n$subtype', fields: [ {name: 'n$rt_field1'} ]," +
"supertypes: [{name: 'n$supertype1', version: 1}], " +
"namespaces: { 'org.lilyproject.resttest': 'n' } }");
String subtypeUri = "/schema/recordType/n$subtype?ns.n=org.lilyproject.resttest";
response = put(subtypeUri, body);
assertStatus(HttpStatus.SC_OK, response);
response = get(subtypeUri);
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertEquals(1, json.get("supertypes").size());
// Update supertype1 with refreshSubtypes=true
body = json("{name: 'n$supertype1', fields: [ {name: 'n$rt_field1'}, {name: 'n$rt_field2'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } }");
String supertype1Uri = "/schema/recordType/n$supertype1?ns.n=org.lilyproject.resttest&refreshSubtypes=true";
response = put(supertype1Uri, body);
assertStatus(HttpStatus.SC_OK, response);
// Check subtype got updated (because refresSubtypes=true)
response = get(subtypeUri);
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertEquals(2, json.get("supertypes").get(0).get("version").getIntValue());
assertEquals(3, json.get("version").getIntValue());
}
/**
* Tests reading and writing each type of field value.
*/
@Test
public void testTypes() throws Exception {
String[] types = {"STRING", "INTEGER", "LONG", "DOUBLE", "DECIMAL", "BOOLEAN", "URI", "DATETIME", "DATE", "LINK"};
for (String type : types) {
String body = json("{action: 'create', fieldType: {name: 'n$f" + type +
"', valueType: '" + type + "', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } } }");
ResponseAndContent response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
}
String body = json("{action: 'create', recordType: {name: 'n$types', fields: [" +
" {name: 'n$fSTRING'}, {name: 'n$fINTEGER'}, {name: 'n$fLONG'}, {name: 'n$fDOUBLE'}" +
" , {name: 'n$fDECIMAL'}, {name: 'n$fBOOLEAN'}, {name: 'n$fURI'}, {name: 'n$fDATETIME'}, " +
" {name: 'n$fDATE'}, {name: 'n$fLINK'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } } }");
ResponseAndContent response = post("/schema/recordTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
body = json("{ action: 'create', record: { type: 'n$types', fields: { " +
"'n$fSTRING' : 'a string'," +
"'n$fINTEGER' : 55," +
"'n$fLONG' : " + Long.MAX_VALUE + "," +
"'n$fDOUBLE' : 33.26," +
"'n$fDECIMAL' : 7.7777777777777777777777777," +
"'n$fBOOLEAN' : true," +
"'n$fURI' : 'http://www.lilyproject.org/'," +
"'n$fDATETIME' : '2010-08-28T21:32:49Z'," +
"'n$fDATE' : '2010-08-28'," +
"'n$fLINK' : 'USER.foobar.!*,arg1=val1,+arg2,-arg3'" +
"}, namespaces : { 'org.lilyproject.resttest': 'n' } } }");
response = post("/record", body);
assertStatus(HttpStatus.SC_CREATED, response);
response = getUri(response.getLocationRef().toString());
assertStatus(HttpStatus.SC_OK, response);
JsonNode json = readJson(response);
ObjectNode fieldsNode = (ObjectNode)json.get("fields");
String prefix = json.get("namespaces").get("org.lilyproject.resttest").getTextValue();
assertEquals("a string", fieldsNode.get(prefix + "$fSTRING").getTextValue());
assertEquals(55, fieldsNode.get(prefix + "$fINTEGER").getIntValue());
assertEquals(Long.MAX_VALUE, fieldsNode.get(prefix + "$fLONG").getLongValue());
assertEquals(33.26, fieldsNode.get(prefix + "$fDOUBLE").getDoubleValue(), 0.0001);
assertEquals(new BigDecimal("7.7777777777777777777777777"), fieldsNode.get(prefix + "$fDECIMAL").getDecimalValue());
assertTrue(fieldsNode.get(prefix + "$fBOOLEAN").getBooleanValue());
assertEquals(new URI("http://www.lilyproject.org/"), new URI(fieldsNode.get(prefix + "$fURI").getTextValue()));
assertEquals(new DateTime("2010-08-28T21:32:49Z"), new DateTime(fieldsNode.get(prefix + "$fDATETIME").getTextValue()));
assertEquals(new LocalDate("2010-08-28"), new LocalDate(fieldsNode.get(prefix + "$fDATE").getTextValue()));
assertEquals("USER.foobar.!*,arg1=val1,+arg2,-arg3", fieldsNode.get(prefix + "$fLINK").getTextValue());
}
@Test
public void testMultiValueAndHierarchical() throws Exception {
// Multi-value field
String body = json("{action: 'create', fieldType: {name: 'n$multiValue', " +
"valueType: 'LIST<STRING>', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } } }");
ResponseAndContent response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
// Hierarchical field
body = json("{action: 'create', fieldType: {name: 'n$hierarchical', " +
"valueType: 'PATH<STRING>', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
// Multi-value + hierarchical field
body = json("{action: 'create', fieldType: {name: 'n$multiValueHierarchical', " +
"valueType: 'LIST<PATH<STRING>>', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
// Combine them into a record type
body = json("{action: 'create', recordType: {name: 'n$mvAndHier', " +
"fields: [ {name: 'n$multiValue'}, {name: 'n$hierarchical'}, {name: 'n$multiValueHierarchical'} ]," +
"namespaces: { 'org.lilyproject.resttest': 'n' } } }");
response = post("/schema/recordTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
// Create a record
body = json("{ type: 'n$mvAndHier', " +
"fields: { 'n$multiValue' : ['val1', 'val2', 'val3']," +
" 'n$hierarchical' : ['part1', 'part2', 'part3'], " +
" 'n$multiValueHierarchical' : [['partA', 'partB'],['partC','partD']]" +
" }, namespaces : { 'org.lilyproject.resttest': 'n' } }");
response = put("/record/USER.multiValueHierarchical", body);
assertStatus(HttpStatus.SC_CREATED, response);
// Read the record
response = get("/record/USER.multiValueHierarchical");
assertStatus(HttpStatus.SC_OK, response);
JsonNode json = readJson(response);
JsonNode fieldsNode = json.get("fields");
String prefix = json.get("namespaces").get("org.lilyproject.resttest").getTextValue();
ArrayNode mv = (ArrayNode)fieldsNode.get(prefix + "$multiValue");
assertEquals("val1", mv.get(0).getTextValue());
assertEquals("val2", mv.get(1).getTextValue());
assertEquals("val3", mv.get(2).getTextValue());
ArrayNode hier = (ArrayNode)fieldsNode.get(prefix + "$hierarchical");
assertEquals("part1", hier.get(0).getTextValue());
assertEquals("part2", hier.get(1).getTextValue());
assertEquals("part3", hier.get(2).getTextValue());
ArrayNode mvAndHier = (ArrayNode)fieldsNode.get(prefix + "$multiValueHierarchical");
assertEquals(2, mvAndHier.size());
ArrayNode mv1 = (ArrayNode)mvAndHier.get(0);
assertEquals("partA", mv1.get(0).getTextValue());
assertEquals("partB", mv1.get(1).getTextValue());
ArrayNode mv2 = (ArrayNode)mvAndHier.get(1);
assertEquals("partC", mv2.get(0).getTextValue());
assertEquals("partD", mv2.get(1).getTextValue());
}
@Test
public void testVersionCollection() throws Exception {
// Create some field types
String body = json("{action: 'create', fieldType: {name: 'p$name', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'p' } } }");
ResponseAndContent response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
body = json("{action: 'create', fieldType: {name: 'p$price', valueType: 'DOUBLE', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'p' } } }");
response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
body = json("{action: 'create', fieldType: {name: 'p$colour', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.resttest': 'p' } } }");
response = post("/schema/fieldTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
// Create a record type
body = json("{action: 'create', recordType: {name: 'p$product', fields: [ {name: 'p$name'}, " +
"{name: 'p$price'}, {name: 'p$colour'} ], namespaces: { 'org.lilyproject.resttest': 'p' } } }");
response = post("/schema/recordTypeById", body);
assertStatus(HttpStatus.SC_CREATED, response);
// Create a record with some versions
body = json("{ type: 'p$product', fields: { 'p$name' : 'Product 1' }, namespaces : { 'org.lilyproject.resttest': 'p' } }");
response = put("/record/USER.product1", body);
assertStatus(HttpStatus.SC_CREATED, response);
body = json("{ fields: { 'p$name' : 'Product 1', 'p$price': 5.5 }, namespaces : { 'org.lilyproject.resttest': 'p' } }");
response = put("/record/USER.product1", body);
assertStatus(HttpStatus.SC_OK, response);
body = json("{ fields: { 'p$name' : 'Product 1', 'p$price': 5.5, 'p$colour': 'red' }, namespaces : { 'org.lilyproject.resttest': 'p' } }");
response = put("/record/USER.product1", body);
assertStatus(HttpStatus.SC_OK, response);
// Get list of versions
response = get("/record/USER.product1/version");
assertStatus(HttpStatus.SC_OK, response);
JsonNode json = readJson(response);
assertEquals(3, json.get("results").size());
JsonNode results = json.get("results");
assertEquals(1, results.get(0).get("fields").size());
assertEquals(2, results.get(1).get("fields").size());
assertEquals(3, results.get(2).get("fields").size());
response = get("/record/USER.product1/version?max-results=1");
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertEquals(1, json.get("results").size());
results = json.get("results");
assertEquals(1, results.get(0).get("version").getLongValue());
response = get("/record/USER.product1/version?start-index=2&max-results=1");
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
assertEquals(1, json.get("results").size());
results = json.get("results");
assertEquals(2, results.get(0).get("version").getLongValue());
// Retrieve only one field
response = get("/record/USER.product1/version?fields=n$name&ns.n=org.lilyproject.resttest");
assertStatus(HttpStatus.SC_OK, response);
json = readJson(response);
results = json.get("results");
assertEquals(1, results.get(0).get("fields").size());
assertEquals(1, results.get(1).get("fields").size());
assertEquals(1, results.get(2).get("fields").size());
}
@Test
public void testTables() throws Exception {
ResponseAndContent getResponse = get("/table");
assertStatus(HttpStatus.SC_OK, getResponse);
List<String> tableNames = getTableNameList(readJson(getResponse));
assertEquals(Lists.newArrayList("record"), tableNames);
ResponseAndContent postResponse = post("/table", "{\"name\": \"resttesttable\"}");
assertStatus(HttpStatus.SC_OK, postResponse);
getResponse = get("/table");
tableNames = getTableNameList(readJson(getResponse));
Collections.sort(tableNames);
assertEquals(Lists.newArrayList("record", "resttesttable"), tableNames);
ResponseAndContent deleteResponse = delete("/table/resttesttable");
assertStatus(HttpStatus.SC_OK, deleteResponse);
getResponse = get("/table");
tableNames = getTableNameList(readJson(getResponse));
assertEquals(Lists.newArrayList("record"), tableNames);
deleteResponse = delete("/table/resttesttable");
assertStatus(HttpStatus.SC_NOT_FOUND, deleteResponse);
}
private List<String> getTableNameList(JsonNode json) {
if (!json.isArray()) {
throw new RuntimeException("Supplied JSON is not an array: " + json.toString());
}
List<String> stringList = Lists.newArrayList();
for (int i = 0; i < json.size(); i++) {
stringList.add(json.get(i).get("name").asText());
}
return stringList;
}
}