diff --git a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/EntitySynchronizer.java b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/EntitySynchronizer.java index 61c473c6caa3e074b7e1141afab74d071297875c..13a2b02a00a161d0aeba5bf3d5c9c1d42963f9fe 100644 --- a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/EntitySynchronizer.java +++ b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/EntitySynchronizer.java @@ -36,6 +36,7 @@ import java.util.Set; import org.apache.commons.codec.binary.Hex; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.time.DateUtils; import org.apache.log4j.Logger; import org.w3c.dom.Document; @@ -66,6 +67,7 @@ import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.Res import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.ResourceListParserData.ProjectWithConnections; import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.ResourceListParserData.SampleWithConnections; import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.datasourceconnector.DataSourceConnector; +import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.datasourceconnector.IDataSourceConnector; import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.translator.INameTranslator; import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.translator.PrefixBasedNameTranslator; import ch.ethz.sis.openbis.generic.shared.entitygraph.EntityGraph; @@ -75,6 +77,7 @@ import ch.systemsx.cisd.common.concurrent.ParallelizedExecutor; import ch.systemsx.cisd.common.exceptions.Status; import ch.systemsx.cisd.common.filesystem.FileUtilities; import ch.systemsx.cisd.common.logging.Log4jSimpleLogger; +import ch.systemsx.cisd.common.spring.HttpInvokerUtils; import ch.systemsx.cisd.etlserver.registrator.api.v1.impl.ConversionUtils; import ch.systemsx.cisd.openbis.dss.generic.shared.DataSetDirectoryProvider; import ch.systemsx.cisd.openbis.dss.generic.shared.DataSetProcessingContext; @@ -84,6 +87,11 @@ import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService; import ch.systemsx.cisd.openbis.dss.generic.shared.IShareIdManager; import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider; import ch.systemsx.cisd.openbis.dss.generic.shared.utils.SegmentedStoreUtils; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.impl.EncapsulatedCommonServer; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.impl.MasterDataRegistrationException; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.impl.MasterDataRegistrationTransactionWrapper; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.impl.MasterDataTransactionErrors; +import ch.systemsx.cisd.openbis.generic.shared.ICommonServer; import ch.systemsx.cisd.openbis.generic.shared.basic.TechId; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment; @@ -145,6 +153,8 @@ public class EntitySynchronizer private final Set<String> blackListedDataSetCodes; + private final MasterDataRegistrationTransactionWrapper masterDataRegistrationTransaction; + public EntitySynchronizer(IEncapsulatedOpenBISService service, String dataStoreCode, File storeRoot, Date lastSyncTimestamp, Set<String> dataSetsCodesToRetry, Set<String> blackListedDataSetCodes, DataSetProcessingContext context, SyncConfig config, Logger operationLog) @@ -158,17 +168,20 @@ public class EntitySynchronizer this.context = context; this.config = config; this.operationLog = operationLog; + this.masterDataRegistrationTransaction = getMasterDataRegistrationTransactionWrapper(); } public Date syncronizeEntities() throws Exception { - // operationLog.info("register master data"); - // registerMasterData(); + DataSourceConnector dataSourceConnector = new DataSourceConnector(config.getDataSourceURI(), config.getAuthenticationCredentials()); + return syncronizeEntities(dataSourceConnector); + } + public Date syncronizeEntities(IDataSourceConnector dataSourceConnector) throws Exception + { // retrieve the document from the data source operationLog.info("Retrieving the resource list.."); - DataSourceConnector connector = new DataSourceConnector(config.getDataSourceURI(), config.getAuthenticationCredentials()); - Document doc = connector.getResourceListAsXMLDoc(Arrays.asList(ArrayUtils.EMPTY_STRING_ARRAY)); + Document doc = dataSourceConnector.getResourceListAsXMLDoc(Arrays.asList(ArrayUtils.EMPTY_STRING_ARRAY)); // Parse the resource list: This sends back all projects, // experiments, samples and data sets contained in the XML together with their last modification date to be used for filtering @@ -180,11 +193,15 @@ public class EntitySynchronizer nameTranslator = new PrefixBasedNameTranslator(dataSourcePrefix); } - ResourceListParser parser = ResourceListParser.create(nameTranslator, dataStoreCode); // , lastSyncTimestamp + ResourceListParser parser = ResourceListParser.create(nameTranslator, dataStoreCode, masterDataRegistrationTransaction); // , + // lastSyncTimestamp ResourceListParserData data = parser.parseResourceListDocument(doc); processDeletions(data); + operationLog.info("registering master data"); + // registerMasterData(); + AtomicEntityOperationDetailsBuilder builder = new AtomicEntityOperationDetailsBuilder(); for (String spaceCode : data.getHarvesterSpaceList()) @@ -416,11 +433,28 @@ public class EntitySynchronizer private void registerMasterData() { - // EncapsulatedCommonServer encapsulatedServer = EncapsulatedCommonServer.create("http://localhost:8888/openbis/openbis", "admin", "a"); - // MasterDataRegistrationService service = new MasterDataRegistrationService(encapsulatedServer); - // IMasterDataRegistrationTransaction transaction = service.transaction(); - // transaction.getOrCreateNewDataSetType("test dataset type"); - // service.commit(); + masterDataRegistrationTransaction.execute(); + MasterDataTransactionErrors transactionErrors = masterDataRegistrationTransaction.getTransactionErrors(); + if (false == transactionErrors.getErrors().isEmpty()) + { + MasterDataRegistrationException masterDataRegistrationException = + new MasterDataRegistrationException("Master data synchronization finished with errors:", + Collections + .<MasterDataTransactionErrors> singletonList(transactionErrors)); + operationLog.info("Master data synchronizatio finished with errors"); + masterDataRegistrationException.logErrors(new Log4jSimpleLogger(operationLog)); + } + } + + private MasterDataRegistrationTransactionWrapper getMasterDataRegistrationTransactionWrapper() + { + ICommonServer commonService = + HttpInvokerUtils.createServiceStub(ICommonServer.class, ServiceProvider.getConfigProvider().getOpenBisServerUrl() + + "/openbis/rmi-common", + 5 * DateUtils.MILLIS_PER_MINUTE); + EncapsulatedCommonServer encapsulatedServer = + EncapsulatedCommonServer.create(commonService, ServiceProvider.getOpenBISService().getSessionToken()); + return new MasterDataRegistrationTransactionWrapper(encapsulatedServer); } private void processDeletions(ResourceListParserData data) throws NoSuchAlgorithmException, UnsupportedEncodingException diff --git a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/MasterDataParser.java b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/MasterDataParser.java new file mode 100644 index 0000000000000000000000000000000000000000..88f422af2f5c31ebb54808d8340386d0cfdd8f7f --- /dev/null +++ b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/MasterDataParser.java @@ -0,0 +1,192 @@ +/* + * Copyright 2016 ETH Zuerich, SIS + * + * 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 ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer; + +import java.util.HashMap; +import java.util.Map; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.DataType; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.IMasterDataRegistrationTransaction; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.IPropertyAssignment; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.IPropertyType; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.ISampleType; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.IVocabulary; + +/** + * + * + * @author Ganime Betul Akin + */ +public class MasterDataParser +{ + private final IMasterDataRegistrationTransaction masterDataRegistrationTransaction; + + private Map<String, IPropertyType> propertyTypeMap = new HashMap<String, IPropertyType>(); + + private Map<String, IVocabulary> vocabularyMap = new HashMap<String, IVocabulary>(); + + /** + * @param masterDataRegistrationTransaction + */ + public MasterDataParser(IMasterDataRegistrationTransaction masterDataRegistrationTransaction) + { + this.masterDataRegistrationTransaction = masterDataRegistrationTransaction; + } + + public void parseMasterData(Document doc, XPath xpath, String uri) throws XPathExpressionException + { + XPathExpression expr = + xpath.compile("//s:url/s:loc[normalize-space(.)='" + uri + "']//following-sibling::*[local-name() = 'masterData'][1]"); + Node xdNode = (Node) expr.evaluate(doc, XPathConstants.NODE); + if (xdNode == null) + { + throw new XPathExpressionException("The master data resurce list should contain 1 master data element"); + } + Element docElement = (Element) xdNode; + + parseVocabularies(docElement.getElementsByTagName("vocabularies")); + parsePropertyTypes(docElement.getElementsByTagName("propertyTypes")); + parseSampleTypes(docElement.getElementsByTagName("sampleTypes")); + } + + private void parseVocabularies(NodeList vocabulariesNode) + { + if (vocabulariesNode.getLength() == 1) + { + Element vocabsElement = (Element) vocabulariesNode.item(0); + NodeList vocabNodes = vocabsElement.getElementsByTagName("vocabulary"); + for (int i = 0; i < vocabNodes.getLength(); i++) + { + Element vocabElement = (Element) vocabNodes.item(i); + String code = vocabElement.getAttributes().getNamedItem("code").getTextContent(); + if (code.startsWith("$")) + continue; + //TODO complete other attributes + IVocabulary newVocabulary = masterDataRegistrationTransaction.getOrCreateNewVocabulary(code); + vocabularyMap.put(code, newVocabulary); + parseVocabularyTerms(vocabElement, newVocabulary); + } + } + } + + private void parseVocabularyTerms(Element vocabElement, IVocabulary newVocabulary) + { + NodeList termNodes = vocabElement.getElementsByTagName("term"); + for (int i = 0; i < termNodes.getLength(); i++) + { + Element termElement = (Element) termNodes.item(i); + // TODO set other attributes + String code = termElement.getAttributes().getNamedItem("code").getTextContent(); + newVocabulary.addTerm(masterDataRegistrationTransaction.createNewVocabularyTerm(code)); + } + } + + private void parseSampleTypes(NodeList sampleTypesNode) + { + if (sampleTypesNode.getLength() == 1) + { + Element sampleTypesElement = (Element) sampleTypesNode.item(0); + NodeList sampleTypeNodes = sampleTypesElement.getElementsByTagName("sampleType"); + for (int i = 0; i < sampleTypeNodes.getLength(); i++) + { + Element sampleTypeElement = (Element) sampleTypeNodes.item(i); + String code = sampleTypeElement.getAttributes().getNamedItem("code").getTextContent(); + ISampleType newSampleType = masterDataRegistrationTransaction.getOrCreateNewSampleType(code); + newSampleType.setGeneratedCodePrefix("S"); + + handlePropertyAssignments(newSampleType, sampleTypeElement.getElementsByTagName("propertyAssignments")); + } + } + } + + private void handlePropertyAssignments(ISampleType newSampleType, NodeList propertyAssignmentsNode) + { + if (propertyAssignmentsNode.getLength() == 1) + { + Element propertyAssignmentsElement = (Element) propertyAssignmentsNode.item(0); + NodeList propertyAssignmentNodes = propertyAssignmentsElement.getElementsByTagName("propertyAssigment"); + for (int i = 0; i < propertyAssignmentNodes.getLength(); i++) + { + Element propertyAssignmentElement = (Element) propertyAssignmentNodes.item(i); + // TODO set other attributes + String property_type_code = propertyAssignmentElement.getAttributes().getNamedItem("property_type_code").getTextContent(); + String data_type_code = propertyAssignmentElement.getAttributes().getNamedItem("data_type_code").getTextContent(); + if (property_type_code.startsWith("$")) + continue; + boolean mandatory = Boolean.valueOf(propertyAssignmentElement.getAttributes().getNamedItem("mandatory").getTextContent()); + int ordinal = Integer.valueOf(propertyAssignmentElement.getAttributes().getNamedItem("ordinal").getTextContent()); + + if (propertyTypeMap.get(property_type_code) != null) + { + IPropertyAssignment assignment = + masterDataRegistrationTransaction.assignPropertyType(newSampleType, propertyTypeMap.get(property_type_code)); + assignment.setMandatory(mandatory); + } + } + } + } + + private void parsePropertyTypes(NodeList propertyTypesNode) + { + if (propertyTypesNode.getLength() == 1) + { + Element propertyTypesElement = (Element) propertyTypesNode.item(0); + NodeList propertyTypeNodes = propertyTypesElement.getElementsByTagName("propertyType"); + for (int i = 0; i < propertyTypeNodes.getLength(); i++) + { + Element propertyTypeElement = (Element) propertyTypeNodes.item(i); + String code = propertyTypeElement.getAttributes().getNamedItem("code").getTextContent(); + // TODO handle internal properties + if (code.startsWith("$")) + continue; + String label = propertyTypeElement.getAttributes().getNamedItem("label").getTextContent(); + String dataType = propertyTypeElement.getAttributes().getNamedItem("dataType").getTextContent(); + String description = propertyTypeElement.getAttributes().getNamedItem("description").getTextContent(); + boolean internalNamespace = Boolean.valueOf(propertyTypeElement.getAttributes().getNamedItem("internalNamespace").getTextContent()); + boolean managedInternally = Boolean.valueOf(propertyTypeElement.getAttributes().getNamedItem("managedInternally").getTextContent()); + String vocabulary = null; + Node namedItem = propertyTypeElement.getAttributes().getNamedItem("vocabulary"); + if (namedItem != null) + { + vocabulary = namedItem.getTextContent(); + } + + IPropertyType newPropertyType = masterDataRegistrationTransaction.getOrCreateNewPropertyType(code, DataType.valueOf(dataType)); + propertyTypeMap.put(code, newPropertyType); + newPropertyType.setInternalNamespace(internalNamespace); + newPropertyType.setManagedInternally(managedInternally); + newPropertyType.setLabel(label); + newPropertyType.setDescription(description); + if (vocabulary != null) + { + newPropertyType.setVocabulary(vocabularyMap.get(vocabulary)); + } + // TODO handle the case for property types that are of data type material + } + } + } +} diff --git a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/ResourceListParser.java b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/ResourceListParser.java index 7c31428637a87aeb30405c1b5045b18c0419deeb..728403185c5091505d53cc24e490efdb8a0e9ff2 100644 --- a/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/ResourceListParser.java +++ b/datastore_server/source/java/ch/ethz/sis/openbis/generic/server/dss/plugins/harvester/synchronizer/ResourceListParser.java @@ -49,6 +49,7 @@ import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.Res import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.ResourceListParserData.SampleWithConnections; import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.translator.DefaultNameTranslator; import ch.ethz.sis.openbis.generic.server.dss.plugins.harvester.synchronizer.translator.INameTranslator; +import ch.systemsx.cisd.openbis.generic.server.jython.api.v1.IMasterDataRegistrationTransaction; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityProperty; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty; @@ -80,30 +81,35 @@ public class ResourceListParser private final String dataStoreCode; + private final IMasterDataRegistrationTransaction masterDataRegistrationTransaction; + public INameTranslator getNameTranslator() { return nameTranslator; } - private ResourceListParser(INameTranslator nameTranslator, String dataStoreCode) + private ResourceListParser(INameTranslator nameTranslator, String dataStoreCode, + IMasterDataRegistrationTransaction masterDataRegistrationTransaction) { this.data = new ResourceListParserData(); this.nameTranslator = nameTranslator; this.dataStoreCode = dataStoreCode; + this.masterDataRegistrationTransaction = masterDataRegistrationTransaction; } - public static ResourceListParser create(INameTranslator nameTranslator, String dataStoreCode) + public static ResourceListParser create(INameTranslator nameTranslator, String dataStoreCode, + IMasterDataRegistrationTransaction masterDataRegistrationTransaction) { if (nameTranslator == null) { - return create(dataStoreCode); + return create(dataStoreCode, masterDataRegistrationTransaction); } - return new ResourceListParser(nameTranslator, dataStoreCode); + return new ResourceListParser(nameTranslator, dataStoreCode, masterDataRegistrationTransaction); } - public static ResourceListParser create(String dataStoreCode) + public static ResourceListParser create(String dataStoreCode, IMasterDataRegistrationTransaction masterDataRegistrationTransaction) { - return create(new DefaultNameTranslator(), dataStoreCode); + return create(new DefaultNameTranslator(), dataStoreCode, masterDataRegistrationTransaction); } public ResourceListParserData parseResourceListDocument(Document doc) throws XPathExpressionException @@ -152,19 +158,16 @@ public class ResourceListParser private Date getResourceListTimestamp(Document doc, XPath xpath) throws XPathExpressionException { - XPathExpression expr = xpath.compile("/s:urlset/rs:md"); - Object result = expr.evaluate(doc, XPathConstants.NODESET); - NodeList nodes = (NodeList) result; - for (int i = 0; i < nodes.getLength(); i++) + XPathExpression expr = xpath.compile("*[name() = 'urlset']/*[name() = 'rs:md']"); + Node mdNode = (Node) expr.evaluate(doc, XPathConstants.NODE); + String timestamp = mdNode.getAttributes().getNamedItem("at").getTextContent(); + try { - String capability = nodes.item(i).getAttributes().getNamedItem("capability").getTextContent(); - if (capability.equals("resourcelist")) - { - String timestamp = nodes.item(i).getAttributes().getNamedItem("at").getTextContent(); - return convertFromW3CDate(timestamp); - } + return convertFromW3CDate(timestamp); + } catch (ParseException e) + { + throw new XPathExpressionException("Last modification date cannot be parsed:" + timestamp); } - throw new XPathExpressionException("The master data resurce list should send a timestamp."); } private List<String> getResourceLocations(Document doc, XPath xpath) throws XPathExpressionException @@ -176,14 +179,12 @@ public class ResourceListParser List<String> list = new ArrayList<String>(); for (int i = 0; i < nodes.getLength(); i++) { - // System.out.print(nodes.item(i).getNodeName() + ":" + nodes.item(i).getAttributes().getNamedItem("href")); String uri = nodes.item(i).getTextContent(); - // if (uri.endsWith("MASTER_DATA/MASTER_DATA/M")) - // { - // // parseMasterData(doc, xpath, uri); - // } - // else - if (uri.endsWith("/M")) + if (uri.endsWith("MASTER_DATA/MASTER_DATA/M")) + { + parseMasterData(doc, xpath, uri); + } + else if (uri.endsWith("/M")) { list.add(uri); } @@ -193,27 +194,8 @@ public class ResourceListParser private void parseMasterData(Document doc, XPath xpath, String uri) throws XPathExpressionException { - XPathExpression expr = - xpath.compile("//s:url/s:loc[normalize-space(.)='" + uri + "']//following-sibling::*[local-name() = 'masterData'][1]"); - Node xdNode = (Node) expr.evaluate(doc, XPathConstants.NODE); - if (xdNode == null) - { - throw new XPathExpressionException("The master data resurce list should contain 1 master data element"); - } - Element docElement = (Element) xdNode; - NodeList sampleTypesNode = docElement.getElementsByTagName("sampleTypes"); - if (sampleTypesNode.getLength() == 1) - { - Element sampleTypesElement = (Element) sampleTypesNode.item(0); - NodeList sampleTypeNodes = sampleTypesElement.getElementsByTagName("sampleType"); - for (int i = 0; i < sampleTypeNodes.getLength(); i++) - { - Element sampleTypeElement = (Element) sampleTypeNodes.item(i); - // TODO proper error handling needed below in case the XML is not correct and item 0 does not exist - String code = sampleTypeElement.getElementsByTagName("code").item(0).getTextContent(); - - } - } + MasterDataParser mdParser = new MasterDataParser(masterDataRegistrationTransaction); + mdParser.parseMasterData(doc, xpath, uri); } private void parseUriMetaData(Document doc, XPath xpath, String uri) throws XPathExpressionException @@ -255,25 +237,22 @@ public class ResourceListParser } String lastModDataStr = lastModNode.getTextContent().trim(); - // TODO data source servlet that generates the XML on the data source side MUST use the same format - return convertFromW3CDate(lastModDataStr); - } - - private Date convertFromW3CDate(String lastModDataStr) throws XPathExpressionException - { - DateFormat df1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); - df1.setTimeZone(TimeZone.getTimeZone("GMT")); - // DateFormat df1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); try { - return df1.parse(lastModDataStr); + return convertFromW3CDate(lastModDataStr); } catch (ParseException e) { - e.printStackTrace(); - throw new XPathExpressionException("Lastmod date cannot be parsed"); + throw new XPathExpressionException("Last modification date cannot be parsed:" + lastModDataStr); } } + private Date convertFromW3CDate(String lastModDataStr) throws ParseException + { + DateFormat df1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + df1.setTimeZone(TimeZone.getTimeZone("GMT")); + return df1.parse(lastModDataStr); + } + private Node extractXdNode(Document doc, XPath xpath, String uri) throws XPathExpressionException { // alternative expression: //s:url/s:loc[normalize-space(.)='" + uri + "']/../x:xd");