Revisited moderation code

This commit is contained in:
Luca Frosini 2021-11-19 16:49:43 +01:00
parent 3333d15b87
commit e769f7ef2b
2 changed files with 292 additions and 243 deletions

View File

@ -28,6 +28,7 @@ import org.gcube.gcat.api.CMItemStatus;
import org.gcube.gcat.api.CMItemVisibility;
import org.gcube.gcat.api.GCatConstants;
import org.gcube.gcat.api.Role;
import org.gcube.gcat.api.interfaces.Moderated;
import org.gcube.gcat.oldutils.Validator;
import org.gcube.gcat.profile.MetadataUtility;
import org.gcube.gcat.social.PortalUser;
@ -40,7 +41,7 @@ import org.slf4j.LoggerFactory;
/**
* @author Luca Frosini (ISTI - CNR)
*/
public class CKANPackage extends CKAN {
public class CKANPackage extends CKAN implements Moderated {
private static final Logger logger = LoggerFactory.getLogger(CKANPackage.class);
/*
@ -121,7 +122,6 @@ public class CKANPackage extends CKAN {
protected final CKANInstance ckanInstance;
protected final Set<String> supportedOrganizations;
protected String moderationMessage;
protected ZulipStream zulipStream;
public CKANPackage() {
@ -198,7 +198,6 @@ public class CKANPackage extends CKAN {
return jsonNode;
}
/**
* @param json The json to check
* @param allowPartialInfo used for patch method which provide only partial information (i.e. the info to patch)
@ -302,49 +301,7 @@ public class CKANPackage extends CKAN {
}
}
public int count() {
Map<String,String> parameters = new HashMap<>();
if(uriInfo != null) {
MultivaluedMap<String,String> queryParameters = uriInfo.getQueryParameters();
parameters = checkListParameters(queryParameters, parameters);
}
int limit = 1;
parameters.put(ROWS_KEY, String.valueOf(limit));
int offset = 0;
parameters.put(START_KEY, String.valueOf(offset * limit));
if(!parameters.containsKey(GCatConstants.Q_KEY)) {
String filter = getFilterForOrganizations();
parameters.put(GCatConstants.Q_KEY, filter);
}
sendGetRequest(LIST, parameters);
int count = result.get(GCatConstants.COUNT_KEY).asInt();
return count;
}
protected CMItemStatus getRequestedCMItemStatus() {
CMItemStatus cmItemStatus = null;
try {
MultivaluedMap<String,String> queryParameters = uriInfo.getQueryParameters();
if(queryParameters.containsKey(GCatConstants.CM_ITEM_STATUS_QUERY_PARAMETER)) {
String cmItemStatusString = queryParameters.getFirst(GCatConstants.CM_ITEM_STATUS_QUERY_PARAMETER);
cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString);
}
}catch (Exception e) {
cmItemStatus = null;
}
return cmItemStatus;
}
@Override
public String list(int limit, int offset) {
protected Map<String,String> getListCountParameters(int limit, int offset) {
Map<String,String> parameters = new HashMap<>();
if(limit <= 0) {
// According to CKAN documentation
@ -369,62 +326,49 @@ public class CKANPackage extends CKAN {
parameters.put(GCatConstants.Q_KEY, filter);
}
if(isModerationEnabled()) {
String q = parameters.get(GCatConstants.Q_KEY);
parameters = addModerationStatusFilter(parameters);
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("(");
stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY);
stringBuffer.append(":");
return parameters;
}
CMItemStatus cmItemStatus = getRequestedCMItemStatus();
public String list(Map<String,String> parameters) {
sendGetRequest(LIST, parameters);
if(!ckanUser.getPortalUser().isCatalogueModerator()) {
ArrayNode results = (ArrayNode) result.get(RESULTS_KEY);
switch (ckanUser.getRole()) {
case ADMIN:
break;
case EDITOR:
if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) {
q = String.format("%s AND %s:%s", q, AUTHOR_EMAIL_KEY, ckanUser.getPortalUser().getEMail());
}else{
cmItemStatus = CMItemStatus.APPROVED;
}
break;
case MEMBER:
if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) {
throw new ForbiddenException("You are only authorized to list " + CMItemStatus.APPROVED.getValue() + " items");
}
cmItemStatus = CMItemStatus.APPROVED;
break;
default:
break;
ArrayNode arrayNode = mapper.createArrayNode();
for(JsonNode node : results) {
try {
String name = node.get(NAME_KEY).asText();
arrayNode.add(name);
} catch(Exception e) {
try {
logger.error("Unable to get the ID of {}. the result will not be included in the result",
mapper.writeValueAsString(node));
} catch(Exception ex) {
logger.error("", ex);
}
}
if(cmItemStatus!=null) {
stringBuffer.append(cmItemStatus.getValue());
if(cmItemStatus == CMItemStatus.APPROVED) {
stringBuffer.append(" OR (*:* -");
stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY);
stringBuffer.append(":[* TO *])");
}
stringBuffer.append(")");
q = String.format("%s AND %s", q, stringBuffer.toString());
parameters.put(GCatConstants.Q_KEY, q);
}
parameters.put(INCLUDE_PRIVATE_KEY, String.valueOf(true));
}
return getAsString(arrayNode);
}
@Override
public String list(int limit, int offset) {
Map<String,String> parameters = getListCountParameters(limit, offset);
return list(parameters);
}
public int count() {
Map<String,String> parameters = getListCountParameters(1, 0);
sendGetRequest(LIST, parameters);
int count = result.get(GCatConstants.COUNT_KEY).asInt();
return count;
}
protected Set<String> checkOrganizationFilter(String q) {
Matcher m = ORGANIZATION_REGEX_PATTERN.matcher(q);
@ -498,30 +442,6 @@ public class CKANPackage extends CKAN {
return parameters;
}
public String list(Map<String,String> parameters) {
sendGetRequest(LIST, parameters);
ArrayNode results = (ArrayNode) result.get(RESULTS_KEY);
ArrayNode arrayNode = mapper.createArrayNode();
for(JsonNode node : results) {
try {
String name = node.get(NAME_KEY).asText();
arrayNode.add(name);
} catch(Exception e) {
try {
logger.error("Unable to get the ID of {}. the result will not be included in the result",
mapper.writeValueAsString(node));
} catch(Exception ex) {
logger.error("", ex);
}
}
}
return getAsString(arrayNode);
}
protected void rollbackManagedResources() {
for(CKANResource ckanResource : managedResources) {
try {
@ -619,35 +539,6 @@ public class CKANPackage extends CKAN {
}
}
protected CMItemStatus getCMItemStatus() throws Exception {
String cmItemStatusString = CMItemStatus.APPROVED.getValue();
boolean found = false;
if(result.has(EXTRAS_KEY)) {
ArrayNode extras = (ArrayNode) result.get(EXTRAS_KEY);
for(JsonNode extra : extras) {
if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY).asText().compareTo(GCatConstants.SYSTEM_CM_ITEM_STATUS) == 0) {
cmItemStatusString = extra.get(EXTRAS_VALUE_KEY).asText();
found = true;
break;
}
}
}
CMItemStatus cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString);
if(!found) {
// The item was published before activating the moderation.
// The item is considered as approved and the item representation must be updated
setToApproved(result);
String ret = sendPostRequest(ITEM_PATCH, getAsString(result));
result = mapper.readTree(ret);
result = cleanResult(result);
}
return cmItemStatus;
}
protected boolean isItemCreator() {
return result.get(AUTHOR_EMAIL_KEY).asText().compareTo(ckanUser.getPortalUser().getEMail())==0;
}
@ -660,29 +551,7 @@ public class CKANPackage extends CKAN {
result = mapper.readTree(ret);
result = cleanResult(result);
if(isModerationEnabled()) {
CMItemStatus cmItemStatus = getCMItemStatus();
if(cmItemStatus == CMItemStatus.APPROVED) {
return getAsString(result);
}
PortalUser portalUser = ckanUser.getPortalUser();
if(isItemCreator()) {
// The author is entitled to read its own items independently from the status
return getAsString(result);
}
if(ckanUser.getRole() == Role.ADMIN || portalUser.isCatalogueModerator()) {
// Catalogue-Admin and Catalogue-Moderator are entitled to read items with any statues
return getAsString(result);
}
throw new ForbiddenException("You are not entitled to read a " + cmItemStatus.getValue() + " item");
}
checkModerationRead();
return getAsString(result);
@ -694,23 +563,6 @@ public class CKANPackage extends CKAN {
}
protected void setToPending(JsonNode jsonNode) {
addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_STATUS, CMItemStatus.PENDING.getValue());
CMItemVisibility cmItemVisibility = CMItemVisibility.PUBLIC;
if(jsonNode.has(PRIVATE_KEY)) {
boolean privatePackage = jsonNode.get(PRIVATE_KEY).asBoolean();
if(privatePackage) {
cmItemVisibility = CMItemVisibility.RESTRICTED;
}
}
addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_VISIBILITY, cmItemVisibility.getValue());
((ObjectNode) jsonNode).put(PRIVATE_KEY, true);
((ObjectNode) jsonNode).put(SEARCHABLE_KEY, false);
}
// see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.package_create
@Override
public String create(String json) {
@ -730,7 +582,8 @@ public class CKANPackage extends CKAN {
JsonNode jsonNode = validateJson(json);
if(isModerationEnabled()) {
boolean moderationEnabled = isModerationEnabled();
if(moderationEnabled) {
setToPending(jsonNode);
}
@ -758,7 +611,7 @@ public class CKANPackage extends CKAN {
sendSocialPost(title, catalogueItemURL);
}
if(isModerationEnabled()) {
if(moderationEnabled) {
createNewStream();
postItemCreatedToStream();
}
@ -917,9 +770,7 @@ public class CKANPackage extends CKAN {
@Override
protected void delete() {
if(isModerationEnabled()) {
// TODO
}
checkModerationDelete();
super.delete();
}
@ -935,8 +786,14 @@ public class CKANPackage extends CKAN {
throw e;
}
}
setApiKey(CKANUtility.getSysAdminAPI());
read();
if(ckanUser.getRole()!=Role.ADMIN && !isItemCreator()) {
throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s and item creator are entitled to purge an the item");
}
if(result.has(RESOURCES_KEY)) {
itemID = result.get(ID_KEY).asText();
ArrayNode arrayNode = (ArrayNode) result.get(RESOURCES_KEY);
@ -958,12 +815,59 @@ public class CKANPackage extends CKAN {
}
/*
* ----------------------------------------------------------------------------------------
* --------------------------------------------------------------------------------------------------------
* Moderation Related functions
* ----------------------------------------------------------------------------------------
* --------------------------------------------------------------------------------------------------------
*
*/
protected CMItemStatus getCMItemStatus() {
String cmItemStatusString = CMItemStatus.APPROVED.getValue();
boolean found = false;
if(result.has(EXTRAS_KEY)) {
ArrayNode extras = (ArrayNode) result.get(EXTRAS_KEY);
for(JsonNode extra : extras) {
if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY).asText().compareTo(GCatConstants.SYSTEM_CM_ITEM_STATUS) == 0) {
cmItemStatusString = extra.get(EXTRAS_VALUE_KEY).asText();
found = true;
break;
}
}
}
CMItemStatus cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString);
if(!found) {
// The item was published before activating the moderation.
// The item is considered as approved and the item representation must be updated
setToApproved(result);
String ret = sendPostRequest(ITEM_PATCH, getAsString(result));
try {
result = mapper.readTree(ret);
}catch (Exception e) {
new InternalServerErrorException(e);
}
result = cleanResult(result);
}
return cmItemStatus;
}
protected CMItemStatus getRequestedCMItemStatus() {
CMItemStatus cmItemStatus = null;
try {
MultivaluedMap<String,String> queryParameters = uriInfo.getQueryParameters();
if(queryParameters.containsKey(GCatConstants.CM_ITEM_STATUS_QUERY_PARAMETER)) {
String cmItemStatusString = queryParameters.getFirst(GCatConstants.CM_ITEM_STATUS_QUERY_PARAMETER);
cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString);
}
}catch (Exception e) {
cmItemStatus = null;
}
return cmItemStatus;
}
protected boolean isModerationEnabled() {
boolean moderationEnabled = ckanInstance.isModerationEnabled();
if(moderationEnabled && zulipStream==null) {
@ -972,36 +876,102 @@ public class CKANPackage extends CKAN {
return moderationEnabled;
}
// private String getItemName() {
// String itemName = result.get(name).asText();
// return itemName;
// }
protected Map<String,String> addModerationStatusFilter(Map<String,String> parameters){
if(isModerationEnabled()) {
String q = parameters.get(GCatConstants.Q_KEY);
private void createNewStream() throws Exception {
zulipStream.setItemCoordinates(itemID, name);
zulipStream.setCKANUser(ckanUser);
zulipStream.create();
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("(");
stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY);
stringBuffer.append(":");
CMItemStatus cmItemStatus = getRequestedCMItemStatus();
if(!ckanUser.getPortalUser().isCatalogueModerator()) {
switch (ckanUser.getRole()) {
case ADMIN:
break;
case EDITOR:
if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) {
q = String.format("%s AND %s:%s", q, AUTHOR_EMAIL_KEY, ckanUser.getPortalUser().getEMail());
}else{
cmItemStatus = CMItemStatus.APPROVED;
}
break;
case MEMBER:
if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) {
throw new ForbiddenException("You are only authorized to list " + CMItemStatus.APPROVED.getValue() + " items");
}
cmItemStatus = CMItemStatus.APPROVED;
break;
default:
break;
}
}
if(cmItemStatus!=null) {
stringBuffer.append(cmItemStatus.getValue());
if(cmItemStatus == CMItemStatus.APPROVED) {
stringBuffer.append(" OR (*:* -");
stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY);
stringBuffer.append(":[* TO *])");
}
stringBuffer.append(")");
q = String.format("%s AND %s", q, stringBuffer.toString());
parameters.put(GCatConstants.Q_KEY, q);
}
parameters.put(INCLUDE_PRIVATE_KEY, String.valueOf(true));
}
return parameters;
}
private void postItemCreatedToStream() {
zulipStream.postItemCreatedToStream();
protected void checkModerationRead() {
if(isModerationEnabled()) {
CMItemStatus cmItemStatus = getCMItemStatus();
if(cmItemStatus == CMItemStatus.APPROVED) {
return;
}
PortalUser portalUser = ckanUser.getPortalUser();
if(isItemCreator()) {
// The author is entitled to read its own items independently from the status
return;
}
if(ckanUser.getRole() == Role.ADMIN || portalUser.isCatalogueModerator()) {
// Catalogue-Admin and Catalogue-Moderator are entitled to read items with any statues
return;
}
throw new ForbiddenException("You are not entitled to read the item");
}
}
protected JsonNode checkModerationUpdate(JsonNode jsonNode) throws Exception{
PortalUser portalUser = ckanUser.getPortalUser();
protected JsonNode checkModerationUpdate(JsonNode jsonNode) {
if(isModerationEnabled()) {
CMItemStatus cmItemStatus = getCMItemStatus();
boolean setToPending = true;
PortalUser portalUser = ckanUser.getPortalUser();
switch (cmItemStatus) {
case APPROVED:
if(ckanUser.getRole() != Role.ADMIN) {
throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s are entitled to update an " + cmItemStatus.getValue() + " item");
if(ckanUser.getRole() != Role.ADMIN && !isItemCreator()) {
throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s and item creator are entitled to update an " + cmItemStatus.getValue() + " item");
}
if(ckanUser.getRole() == Role.ADMIN) {
setToApproved(jsonNode);
setToPending = false;
}
setToApproved(jsonNode);
setToPending = false;
break;
case PENDING:
@ -1017,7 +987,7 @@ public class CKANPackage extends CKAN {
if(isItemCreator()) {
break;
}
if(ckanUser.getRole() == Role.ADMIN || portalUser.isCatalogueModerator()) {
if(portalUser.isCatalogueModerator()) {
break;
}
throw new ForbiddenException("You are not entitled to update a " + cmItemStatus.getValue() + " item");
@ -1035,17 +1005,70 @@ public class CKANPackage extends CKAN {
return jsonNode;
}
protected void checkModerationDelete() {
if(isModerationEnabled()) {
read();
public void setModerationMessage(String moderationMessage) {
this.moderationMessage = moderationMessage;
PortalUser portalUser = ckanUser.getPortalUser();
if(ckanUser.getRole() == Role.ADMIN) {
// Ad Admin can delete any item independently from the status
return;
}
CMItemStatus cmItemStatus = getCMItemStatus();
switch (cmItemStatus) {
case APPROVED:
if(isItemCreator()) {
break;
}
throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s and item creator are entitled to delete an " + cmItemStatus.getValue() + " item");
case REJECTED:
if(isItemCreator()) {
break;
}
if(portalUser.isCatalogueModerator()) {
break;
}
throw new ForbiddenException("You are not entitled to delete a " + cmItemStatus.getValue() + " item");
case PENDING:
if(isItemCreator()) {
break;
}
if(portalUser.isCatalogueModerator()) {
break;
}
throw new ForbiddenException("You are not entitled to update a " + cmItemStatus.getValue() + " item");
default:
break;
}
}
}
protected void setToRejected(JsonNode jsonNode) {
addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_STATUS, CMItemStatus.REJECTED.getValue());
}
protected void setToPending(JsonNode jsonNode) {
addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_STATUS, CMItemStatus.PENDING.getValue());
CMItemVisibility cmItemVisibility = CMItemVisibility.PUBLIC;
if(jsonNode.has(PRIVATE_KEY)) {
boolean privatePackage = jsonNode.get(PRIVATE_KEY).asBoolean();
if(privatePackage) {
cmItemVisibility = CMItemVisibility.RESTRICTED;
}
}
addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_VISIBILITY, cmItemVisibility.getValue());
((ObjectNode) jsonNode).put(PRIVATE_KEY, true);
((ObjectNode) jsonNode).put(SEARCHABLE_KEY, false);
}
protected void setToApproved(JsonNode jsonNode) {
ArrayNode extras = (ArrayNode) jsonNode.get(EXTRAS_KEY);
@ -1078,11 +1101,22 @@ public class CKANPackage extends CKAN {
((ObjectNode) jsonNode).put(SEARCHABLE_KEY, true);
}
public String approve() {
private void createNewStream() throws Exception {
zulipStream.setItemCoordinates(itemID, name);
zulipStream.setCKANUser(ckanUser);
zulipStream.create();
}
private void postItemCreatedToStream() {
zulipStream.postItemCreatedToStream();
}
@Override
public String approve(String moderatorMessage) {
try {
if(isModerationEnabled()) {
String ret = read();
PortalUser portalUser = ckanUser.getPortalUser();
CMItemStatus cmItemStatus = getCMItemStatus();
switch (cmItemStatus) {
case APPROVED:
@ -1093,7 +1127,7 @@ public class CKANPackage extends CKAN {
throw new MethodNotSupportedException("You can't approve a rejected item. The item must be updated first. The update will set the item in pending, than it can be approved/rejected.");
case PENDING:
if(ckanUser.getRole() != Role.ADMIN) {
if(!portalUser.isCatalogueModerator()) {
throw new MethodNotSupportedException("Only catalogue moderator can approve a pending item.");
}
setToApproved(result);
@ -1110,15 +1144,13 @@ public class CKANPackage extends CKAN {
ZulipStream zulipStream = new ZulipStream();
zulipStream.setItemCoordinates(itemID, name);
zulipStream.postItemCreatedToStream();
if(moderationMessage!=null && moderationMessage.compareTo("")!=0) {
zulipStream.postMessageToStream(CMItemStatus.PENDING, moderationMessage);
if(moderatorMessage!=null && moderatorMessage.compareTo("")!=0) {
zulipStream.postMessageToStream(CMItemStatus.PENDING, moderatorMessage);
}
return getAsString(result);
}
throw new MethodNotSupportedException("The approve operation is available only in moderation mode");
}catch(WebApplicationException e) {
throw e;
} catch(Exception e) {
@ -1126,11 +1158,12 @@ public class CKANPackage extends CKAN {
}
}
public String reject() {
@Override
public String reject(String moderatorMessage) {
try {
if(isModerationEnabled()) {
String ret = read();
PortalUser portalUser = ckanUser.getPortalUser();
CMItemStatus cmItemStatus = getCMItemStatus();
switch (cmItemStatus) {
case APPROVED:
@ -1141,7 +1174,7 @@ public class CKANPackage extends CKAN {
break;
case PENDING:
if(ckanUser.getRole() != Role.ADMIN) {
if(!portalUser.isCatalogueModerator()) {
throw new MethodNotSupportedException("Only catalogue moderator can reject a pending item.");
}
setToRejected(result);
@ -1158,15 +1191,13 @@ public class CKANPackage extends CKAN {
ZulipStream zulipStream = new ZulipStream();
zulipStream.setItemCoordinates(itemID, name);
zulipStream.postItemRejectedToStream();
if(moderationMessage!=null && moderationMessage.compareTo("")!=0) {
zulipStream.postMessageToStream(CMItemStatus.REJECTED, moderationMessage);
if(moderatorMessage!=null && moderatorMessage.compareTo("")!=0) {
zulipStream.postMessageToStream(CMItemStatus.REJECTED, moderatorMessage);
}
return getAsString(result);
}
throw new MethodNotSupportedException("The reject operation is available only in moderation mode");
}catch(WebApplicationException e) {
throw e;
} catch(Exception e) {
@ -1174,10 +1205,11 @@ public class CKANPackage extends CKAN {
}
}
public void message() {
@Override
public void message(String message) {
try {
if(isModerationEnabled()) {
if(moderationMessage==null || moderationMessage.compareTo("")==0) {
if(message==null || message.compareTo("")==0) {
return;
}
@ -1193,10 +1225,8 @@ public class CKANPackage extends CKAN {
ZulipStream zulipStream = new ZulipStream();
zulipStream.setItemCoordinates(itemID, name);
CMItemStatus cmItemStatus = getCMItemStatus();
zulipStream.postMessageToStream(cmItemStatus, moderationMessage);
zulipStream.postMessageToStream(cmItemStatus, message);
}
throw new MethodNotSupportedException("The message operation is available only in moderation mode");
}catch(WebApplicationException e) {
@ -1206,5 +1236,4 @@ public class CKANPackage extends CKAN {
}
}
}

View File

@ -29,6 +29,8 @@ public class Item extends REST<CKANPackage> implements org.gcube.gcat.api.interf
public static final String ITEM_ID_PARAMETER = "ITEM_ID";
protected String moderationMessage;
public Item() {
super(ITEMS, ITEM_ID_PARAMETER, CKANPackage.class);
}
@ -127,4 +129,22 @@ public class Item extends REST<CKANPackage> implements org.gcube.gcat.api.interf
return delete(name, new Boolean(purge));
}
@Override
public String approve(String moderatorMessage) {
// TODO Auto-generated method stub
return null;
}
@Override
public String reject(String moderatorMessage) {
// TODO Auto-generated method stub
return null;
}
@Override
public void message(String message) {
// TODO Auto-generated method stub
}
}