Commit 9da00e07 authored by Stefan Funk's avatar Stefan Funk 🐧
Browse files

Merge branch 'release/10.3.0.43-TG' into main

parents 72cbe2f6 2f879a60
......@@ -5,7 +5,7 @@
<parent>
<groupId>info.textgrid.middleware</groupId>
<artifactId>crud</artifactId>
<version>10.3.0.40-TG</version>
<version>10.3.0.43-TG</version>
</parent>
<artifactId>crud-base</artifactId>
<name>DARIAHDE :: CrudService :: Base</name>
......
......@@ -41,6 +41,8 @@ import info.textgrid.utils.httpclient.TGHttpResponse;
**
* CHANGELOG
*
* 2020-11-04 - Funk - Add synchronized blocks in locking methods.
*
* 2016-06-29 - Funk - Added some finals. Fixed bug if only gotten 1 URI from the NOID and
* (possibly) no "/n" in the result.
*
......@@ -244,36 +246,38 @@ public class TGCrudServiceIdentifierNoidImpl extends CrudServiceIdentifierAbs {
// If key is contained, check locking time and user.
else {
// Compute locking duration (in millis), parse locking value first.
Long lockingValue = Long.parseLong(resultMap.get(revisionKey)
.substring(resultMap.get(revisionKey).lastIndexOf(USER_TIMESTAMP_SEPARATON_CHAR) + 1));
Long lockingDuration = System.currentTimeMillis() - lockingValue;
// Some locking logging :-D
String lockingDurationString = TGCrudServiceUtilities.getDuration(lockingDuration);
long stillLocked = this.conf.getIDaUTOMAGICuNLOCKINGtIME() - lockingDuration;
String stillLockedString = TGCrudServiceUtilities.getDuration(stillLocked);
// Do log.
String message = "Locked since " + lockingDurationString + " by " + theUser;
if (stillLocked > 0) {
message += ", automagically unlocked in " + stillLockedString;
} else {
message += ", automagically unlocking NOW!";
}
message += " [" + lockingDuration + ">" + this.conf.getIDaUTOMAGICuNLOCKINGtIME() + "]";
TGCrudServiceUtilities.serviceLog(CrudService.DEBUG, meth, message);
// Check if user is the one who locked this URI in the first place or URI has been locked
// longer than permitted without re-locking. If so, re-lock with bind+set.
if (resultMap.get(revisionKey).startsWith(theUser)
|| lockingDuration > this.conf.getIDaUTOMAGICuNLOCKINGtIME()) {
response = noidBind(theUri, NOID_BIND_SET, theUser, revisionKey);
}
synchronized (this) {
// Compute locking duration (in millis), parse locking value first.
Long lockingValue = Long.parseLong(resultMap.get(revisionKey).substring(
resultMap.get(revisionKey).lastIndexOf(USER_TIMESTAMP_SEPARATON_CHAR) + 1));
Long lockingDuration = System.currentTimeMillis() - lockingValue;
// Some locking logging :-D
String lockingDurationString = TGCrudServiceUtilities.getDuration(lockingDuration);
long stillLocked = this.conf.getIDaUTOMAGICuNLOCKINGtIME() - lockingDuration;
String stillLockedString = TGCrudServiceUtilities.getDuration(stillLocked);
// Do log.
String message = "Locked since " + lockingDurationString + " by " + theUser;
if (stillLocked > 0) {
message += ", automagically unlocked in " + stillLockedString;
} else {
message += ", automagically unlocking NOW!";
}
message += " [" + lockingDuration + ">" + this.conf.getIDaUTOMAGICuNLOCKINGtIME() + "]";
TGCrudServiceUtilities.serviceLog(CrudService.DEBUG, meth, message);
// Check if user is the one who locked this URI in the first place or URI has been locked
// longer than permitted without re-locking. If so, re-lock with bind+set.
if (resultMap.get(revisionKey).startsWith(theUser)
|| lockingDuration > this.conf.getIDaUTOMAGICuNLOCKINGtIME()) {
response = noidBind(theUri, NOID_BIND_SET, theUser, revisionKey);
}
// Otherwise no locking is allowed!
else {
return false;
// Otherwise no locking is allowed!
else {
return false;
}
}
}
......@@ -405,19 +409,21 @@ public class TGCrudServiceIdentifierNoidImpl extends CrudServiceIdentifierAbs {
}
// If the key is not contained (means the URI is not yet locked), just do lock using bind+new!
else {
response = noidBindInternal(theUri, NOID_BIND_NEW, revisionKey, TRUE);
synchronized (this) {
response = noidBindInternal(theUri, NOID_BIND_NEW, revisionKey, TRUE);
// Check status code.
int statusCode = response.getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
String resultBody = IOUtils.readStringFromStream(response.getBuffEntity().getContent());
// Parse result.
if (resultBody.startsWith(ERROR_TEXT)) {
return false;
// Check status code.
int statusCode = response.getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
String resultBody = IOUtils.readStringFromStream(response.getBuffEntity().getContent());
// Parse result.
if (resultBody.startsWith(ERROR_TEXT)) {
return false;
}
} else {
throw TGCrudServiceExceptions.ioFault(NOID_ERROR + ": " + statusCode);
}
} else {
throw TGCrudServiceExceptions.ioFault(NOID_ERROR + ": " + statusCode);
}
}
......
/**
* This software is copyright (c) 2020 by
* This software is copyright (c) 2021 by
*
* TextGrid Consortium (https://textgrid.de)
*
......@@ -36,6 +36,7 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
......@@ -57,9 +58,11 @@ import info.textgrid.middleware.common.TextGridMimetypes;
*
**
* CHANGELOG
*
* 2021-06-21 - Funk - Add filenames for REST metadata responses.
*
* 2020-03-04 - Funk - Remove special header.
*
* 2020-11-04 - Funk - Add internal locking for new revision creation.
*
* 2018-08-31 - Funk - Fixed exception mapping for RESTful HTML response messages.
*
* 2018-05-22 - Funk - Added abstract initMessageProducer class.
......@@ -258,6 +261,8 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
// Create URI.
URI uri = null;
TgCrudCheckAccessResponse checkAccessResponse = null;
boolean internalLockingSucceeded = false;
try {
init(methodInfo);
......@@ -287,24 +292,14 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
// Check general CREATE access permission using the session ID and the project ID, get back
// some additional data from the RBAC.
TgCrudCheckAccessResponse checkAccessResponse =
(TgCrudCheckAccessResponse) aaiImplementation.checkAccessGetInfo(sessionId, logParameter,
projectId, CrudServiceAai.OPERATION_CREATE);
checkAccessResponse = (TgCrudCheckAccessResponse) aaiImplementation
.checkAccessGetInfo(sessionId, logParameter, projectId, CrudServiceAai.OPERATION_CREATE);
// Set revision to initial state.
int revision = CrudServiceIdentifier.INITIAL_REVISION;
// Check if a new revision shall be created.
if (createRevision) {
// Do internal locking here! The same revision can be created simultaneously otherwise (see
// #33975)!
boolean internalLockingSucceeded = identifierImplementation.lockInternal(uri);
if (!internalLockingSucceeded) {
UpdateConflictFault fault = new UpdateConflictFault(PROCESS_CONFLICT + ": " + uri);
throw TGCrudServiceExceptions.ioFault(fault, NEW_REVISION_CREATION_DENIED);
}
if (baseUri == null || baseUri.toString().equals("")) {
throw TGCrudServiceExceptions.ioFault("No URI given");
}
......@@ -340,7 +335,24 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
uri = URI.create(
baseUri.toASCIIString() + CrudServiceIdentifier.REVISION_SEPARATION_CHAR + revision);
log(INFO, methodInfo, "Revision URI to be used: " + uri.toASCIIString());
log(INFO, methodInfo, "Revision URI to be used (" + (createRevision ? "NEW" : "INITIAL")
+ " revision): " + uri.toASCIIString());
// Do internal locking here if revision is not equal 0 (initial revision)! So the same
// revision cannot be created simultaneously (see #33975)!
if (revision != CrudServiceIdentifier.INITIAL_REVISION) {
synchronized (this) {
internalLockingSucceeded = identifierImplementation.lockInternal(uri);
log(INFO, methodInfo, "Internal locking fur URI " + uri + " "
+ (internalLockingSucceeded ? "COMPLETE" : "FAILED"));
if (!internalLockingSucceeded) {
UpdateConflictFault fault = new UpdateConflictFault(PROCESS_CONFLICT + ": " + uri);
throw TGCrudServiceExceptions.ioFault(fault, NEW_REVISION_CREATION_DENIED);
}
}
}
// Check if the URI is already existing in the security implementation.
if (aaiImplementation.resourceExists(sessionId, logParameter, uri.toASCIIString())) {
......@@ -659,6 +671,13 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
}
// Release data stream hold to delete the cached data file.
finally {
synchronized (CrudService.class) {
// If internal locking succeeded, release internal lock.
if (internalLockingSucceeded && checkAccessResponse != null) {
identifierImplementation.unlockInternal(uri);
}
}
CrudServiceHoldSlipStream.releaseAndClose(tgObjectData);
log(DEBUG, methodInfo, STREAM_RELEASE_COMPLETE);
......@@ -698,8 +717,14 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
this.create(sessionId, logParameter, baseUri, createRevision, projectId, metadata,
tgObjectData);
// Get filename from metadata.
String filename = TGCrudServiceUtilities.getDataFilenameFromMetadata(metadata.value)
+ conf.getMETADATAfILEsUFFIX();
// Build a response.
ResponseBuilder rBuilder = Response.ok(metadata.value);
ResponseBuilder rBuilder =
Response.ok(metadata.value).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
// Set last modified date for client caching reasons, and location.
Date lastModified = new Date(metadata.value.getObject().getGeneric().getGenerated()
......@@ -782,8 +807,13 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
this.createMetadata(sessionId, logParameter, baseUri, projectId, externalReference,
tgObjectMetadata);
// Get filename from metadata.
String filename = TGCrudServiceUtilities.getDataFilenameFromMetadata(tgObjectMetadata.value);
// Build a response.
ResponseBuilder rBuilder = Response.ok(tgObjectMetadata.value);
ResponseBuilder rBuilder =
Response.ok(tgObjectMetadata.value).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
// Set last modified date for client caching reasons, and location.
Date lastModified = new Date(tgObjectMetadata.value.getObject().getGeneric().getGenerated()
......@@ -877,6 +907,10 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
// internal locking for a clean behaviour.
synchronized (CrudService.class) {
internalLockingSucceeded = identifierImplementation.lockInternal(uri);
log(INFO, methodInfo, "Internal locking fur URI " + uri + " "
+ (internalLockingSucceeded ? "COMPLETE" : "FAILED"));
if (!internalLockingSucceeded) {
IoFault fault = new IoFault(PROCESS_CONFLICT + ": " + uri);
throw TGCrudServiceExceptions.ioFault(fault, DELETION_DENIED);
......@@ -1444,9 +1478,14 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
try {
this.read(sessionId, logParameter, baseUri, metadata, data);
// Get filename from metadata.
String filename = TGCrudServiceUtilities.getDataFilenameFromMetadata(metadata.value);
// Build a response.
ResponseBuilder rBuilder = Response.ok(data.value.getInputStream(),
metadata.value.getObject().getGeneric().getProvided().getFormat());
String mimetype = metadata.value.getObject().getGeneric().getProvided().getFormat();
ResponseBuilder rBuilder = Response.ok(data.value.getInputStream(), mimetype)
.header(HttpHeaders.CONTENT_TYPE, MediaType.valueOf(mimetype))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
// Set last modified date for client caching reasons.
Date lastModified = new Date(metadata.value.getObject().getGeneric().getGenerated()
......@@ -1479,8 +1518,14 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
Holder<MetadataContainerType> metadata =
new Holder<MetadataContainerType>(this.readMetadata(sessionId, logParameter, baseUri));
// Get filename from metadata.
String filename = TGCrudServiceUtilities.getDataFilenameFromMetadata(metadata.value)
+ conf.getMETADATAfILEsUFFIX();
// Build a response.
ResponseBuilder rBuilder = Response.ok(metadata.value);
ResponseBuilder rBuilder =
Response.ok(metadata.value).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
// Set last modified date for client caching reasons.
Date lastModified = new Date(metadata.value.getObject().getGeneric().getGenerated()
......@@ -1701,6 +1746,10 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
// Only if user locking succeeded or already in place, we do lock internally, too.
internalLockingSucceeded = identifierImplementation.lockInternal(uri);
log(INFO, methodInfo, "Internal locking fur URI " + uri + " "
+ (internalLockingSucceeded ? "COMPLETE" : "FAILED"));
if (!internalLockingSucceeded) {
UpdateConflictFault fault = new UpdateConflictFault(PROCESS_CONFLICT + ": " + uri);
throw TGCrudServiceExceptions.updateConflictFault(fault, UPDATE_DENIED);
......@@ -2039,8 +2088,14 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
try {
this.update(sessionId, logParameter, metadata, tgObjectData);
// Get filename from metadata.
String filename = TGCrudServiceUtilities.getDataFilenameFromMetadata(metadata.value)
+ conf.getMETADATAfILEsUFFIX();
// Build a response.
ResponseBuilder rBuilder = Response.ok(metadata.value);
ResponseBuilder rBuilder =
Response.ok(metadata.value).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
// Set last modified date for client caching reasons, and location.
Date lastModified = new Date(metadata.value.getObject().getGeneric().getGenerated()
......@@ -2164,6 +2219,10 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
// Only if user locking succeeded or already in place, we do lock internally, too.
internalLockingSucceeded = identifierImplementation.lockInternal(uri);
log(INFO, methodInfo, "Internal locking fur URI " + uri + " "
+ (internalLockingSucceeded ? "COMPLETE" : "FAILED"));
if (!internalLockingSucceeded) {
UpdateConflictFault fault = new UpdateConflictFault(PROCESS_CONFLICT + ": " + uri);
throw TGCrudServiceExceptions.updateConflictFault(fault, UPDATE_DENIED);
......@@ -2417,8 +2476,14 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
try {
this.updateMetadata(sessionId, logParameter, metadata);
// Get filename from metadata.
String filename = TGCrudServiceUtilities.getDataFilenameFromMetadata(metadata.value)
+ conf.getMETADATAfILEsUFFIX();
// Build a response.
ResponseBuilder rBuilder = Response.ok(metadata.value);
ResponseBuilder rBuilder =
Response.ok(metadata.value).header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"");
// Set last modified date for client caching reasons, and location.
Date lastModified = new Date(metadata.value.getObject().getGeneric().getGenerated()
......@@ -2651,6 +2716,10 @@ public class TGCrudServiceImpl extends CrudService implements TGCrudService, TGC
// so we only need internal locking.
synchronized (CrudService.class) {
internalLockingSucceeded = identifierImplementation.lockInternal(uri);
log(INFO, methodInfo, "Internal locking fur URI " + uri + " "
+ (internalLockingSucceeded ? "COMPLETE" : "FAILED"));
if (!internalLockingSucceeded) {
IoFault fault = new IoFault(PROCESS_CONFLICT + ": " + uri);
throw TGCrudServiceExceptions.ioFault(fault, MOVATION_DENIED);
......
/*******************************************************************************
* This software is copyright (c) 2018 by
/**
* This software is copyright (c) 2021 by
*
* TextGrid Consortium (http://www.textgrid.de)
* TextGrid Consortium (https://textgrid.de)
*
* DAASI International GmbH (http://www.daasi.de)
* DAASI International GmbH (https://daasi.de)
*
* This is free software. You can redistribute it and/or modify it under the terms described in the
* GNU Lesser General Public License v3 of which you should have received a copy. Otherwise you can
......@@ -11,11 +11,11 @@
*
* http://www.gnu.org/licenses/lgpl-3.0.txt
*
* @copyright TextGrid Consortium (http://www.textgrid.de)
* @copyright DAASI International GmbH (http://www.daasi.de)
* @copyright TextGrid Consortium (https://textgrid.de)
* @copyright DAASI International GmbH (https://daasi.de)
* @license GNU Lesser General Public License v3 (http://www.gnu.org/licenses/lgpl-3.0.txt)
* @author Stefan E. Funk (stefan.e.funk@daasi.de)
******************************************************************************/
*/
package info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice;
......@@ -39,6 +39,8 @@ import org.apache.jena.rdf.model.Selector;
import org.apache.jena.rdf.model.SimpleSelector;
import org.apache.jena.rdf.model.Statement;
import org.w3._1999._02._22_rdf_syntax_ns_.RdfType;
import info.textgrid.middleware.common.LTPUtils;
import info.textgrid.middleware.common.TextGridMimetypes;
import info.textgrid.namespaces.metadata.core._2010.EditionType;
import info.textgrid.namespaces.metadata.core._2010.GeneratedType;
import info.textgrid.namespaces.metadata.core._2010.GeneratedType.Pid;
......@@ -49,28 +51,32 @@ import info.textgrid.namespaces.metadata.core._2010.MetadataContainerType;
import info.textgrid.namespaces.metadata.core._2010.ObjectType;
import info.textgrid.namespaces.metadata.core._2010.RelationType;
/*******************************************************************************
/**
* TODOLOG
*
* TODO Generalise the class loading of the storage classes with the DHCrudServiceUtilities class!
*
*******************************************************************************
**
* CHANGELOG
*
* 2015-03-09 Funk Slightly changed start method log and version log.
* 2021-06-21 - Funk - Add method for getting filename from URI and format.
*
* 2021-06-21 - Funk - Add method for getting filename from URI and format.
*
******************************************************************************/
* 2015-03-09 - Funk - Slightly changed start method log and version log.
*/
/*******************************************************************************
/**
* <p>
* The TGCrudServiceUtilities class provides some utilities, needed by all the TGCrudService
* classes.
* </p>
*
* @author Stefan E. Funk, DAASI International GmbH
* @version 2018-03-09
* @author Stefan E. Funk, SUB Göttingen
* @version 2021-06-21
* @since 2010-06-24
******************************************************************************/
*/
public class TGCrudServiceUtilities extends CrudServiceUtilities {
......@@ -647,6 +653,8 @@ public class TGCrudServiceUtilities extends CrudServiceUtilities {
+ CrudService.URI_SEPARATOR + theSessionId.substring(theSessionId.length() - 3) : "");
}
// **
// PRIVATE METHODS
// **
......@@ -690,4 +698,68 @@ public class TGCrudServiceUtilities extends CrudServiceUtilities {
}
}
/**
* <p>
* Build a fine filename from metadata:
*
* <ul>
* <li>edition: [title].[baseURI].[revision].edition<br/>
* Reise_nach_dem_Mittelpunkt_der_Erde.wr7j.0.edition</li>
* <li>collection: [title].[baseURI].[revision].collection<br/>
* example: Reise_nach_dem_Mittelpunkt_der_Erde.wr7j.0.collection</li>
* <li>aggregation: [title].[baseURI].[revision].aggregation<br/>
* example: Reise_nach_dem_Mittelpunkt_der_Erde.wr7j.0.aggregation</li>
* <li>work: [title].[baseURI].[revision].work<br/>
* example: Reise_nach_dem_Mittelpunkt_der_Erde.wr7g.0.work</li>
* <li>data: [title].[baseUri].[revision].[fileExtension]<br/>
* example: Graphic_Recording_1.24g6b.0.jpg</li>
* </ul>
*
* Metadata filename is always data filename plus ".meta"! Has to be appended from calling method!
* </p>
*
* <p>
* TODO Put this method in common, it's used in TGCrudClientUtils, too!
* </p>
*
*
* @param theURI
* @param theMimetype
* @return The filename for the given URI.
*/
public static String getDataFilenameFromMetadata(MetadataContainerType theMetadata) {
String result;
// Get suffix from format or file extension, if applicable.
String mimetype = theMetadata.getObject().getGeneric().getProvided().getFormat();
String formatSuffix;
switch (mimetype) {
case TextGridMimetypes.EDITION:
formatSuffix = ".edition";
break;
case TextGridMimetypes.COLLECTION:
formatSuffix = ".collection";
break;
case TextGridMimetypes.AGGREGATION:
formatSuffix = ".aggregation";
break;
case TextGridMimetypes.WORK:
formatSuffix = ".work";
break;
default:
formatSuffix = LTPUtils.getFileExtension(mimetype);
}
// Get more information.
String title = theMetadata.getObject().getGeneric().getProvided().getTitle().get(0);
String uri = theMetadata.getObject().getGeneric().getGenerated().getTextgridUri().getValue()
.toASCIIString();
// Gather everything.
result = title.replaceAll("\\W+", "_") + "." + uri.replace("textgrid:", "") + formatSuffix;
return result.trim();
}
}
......@@ -5,7 +5,7 @@
<parent>
<groupId>info.textgrid.middleware</groupId>
<artifactId>crud</artifactId>
<version>10.3.0.40-TG</version>
<version>10.3.0.43-TG</version>
</parent>
<groupId>info.textgrid.middleware</groupId>
<artifactId>crud-common</artifactId>
......
......@@ -5,7 +5,7 @@
<parent>
<groupId>info.textgrid.middleware</groupId>
<artifactId>crud</artifactId>
<version>10.3.0.40-TG</version>
<version>10.3.0.43-TG</version>
</parent>
<artifactId>crudclient-online</artifactId>
<name>DARIAHDE :: CrudClient :: Online Tests</name>
......
......@@ -5,7 +5,7 @@
<parent>
<groupId>info.textgrid.middleware</groupId>
<artifactId>crud</artifactId>
<version>10.3.0.40-TG</version>
<version>10.3.0.43-TG</version>
</parent>
<artifactId>dhcrud-api</artifactId>
<name>DARIAHDE :: DHCrudService :: API</name>
......
......@@ -5,7 +5,7 @@
<parent>
<groupId>info.textgrid.middleware</groupId>
<artifactId>crud</artifactId>
<version>10.3.0.40-TG</version>
<version>10.3.0.43-TG</version>
</parent>
<artifactId>dhcrud-base</artifactId>
<name>DARIAHDE :: DHCrudService :: Base</name>
......
......@@ -5,7 +5,7 @@
<parent>
<groupId>info.textgrid.middleware</groupId>
<artifactId>crud</artifactId>
<version>10.3.0.40-TG</version>
<version>10.3.0.43-TG</version>
</parent>
<artifactId>dhcrud-webapp-public</artifactId>
<name>DARIAHDE :: DHCrudService :: Public Web Application</name>
......