/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/osp/trunk/matrix/tool/src/java/org/theospi/portfolio/matrix/control/ReviewHelperController.java $
* $Id: ReviewHelperController.java 131548 2013-11-14 16:42:13Z dsobiera@indiana.edu $
***********************************************************************************
*
* Copyright (c) 2005, 2006, 2007, 2008, 2009 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.theospi.portfolio.matrix.control;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.content.api.ContentCollection;
import org.sakaiproject.content.api.ContentCollectionEdit;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.LockManager;
import org.sakaiproject.content.api.ResourceEditingHelper;
import org.sakaiproject.email.cover.DigestService;
import org.sakaiproject.email.cover.EmailService;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.event.api.NotificationService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.IdUsedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.metaobj.security.AuthenticationManager;
import org.sakaiproject.metaobj.shared.FormHelper;
import org.sakaiproject.metaobj.shared.mgt.IdManager;
import org.sakaiproject.metaobj.shared.model.Agent;
import org.sakaiproject.metaobj.shared.model.Id;
import org.sakaiproject.metaobj.utils.mvc.intf.Controller;
import org.sakaiproject.site.cover.SiteService;
import org.sakaiproject.tool.api.Placement;
import org.sakaiproject.tool.cover.ToolManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.user.api.UserNotificationPreferencesRegistration;
import org.sakaiproject.user.cover.UserDirectoryService;
import org.sakaiproject.util.ResourceLoader;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
import org.theospi.portfolio.matrix.MatrixManager;
import org.theospi.portfolio.matrix.model.Scaffolding;
import org.theospi.portfolio.matrix.model.ScaffoldingCell;
import org.theospi.portfolio.matrix.model.WizardPage;
import org.theospi.portfolio.matrix.model.WizardPageDefinition;
import org.theospi.portfolio.matrix.util.FormNameGeneratorUtil;
import org.theospi.portfolio.review.ReviewHelper;
import org.theospi.portfolio.review.mgt.ReviewManager;
import org.theospi.portfolio.review.model.Review;
import org.theospi.portfolio.shared.model.Node;
import org.theospi.portfolio.shared.model.ObjectWithWorkflow;
import org.theospi.portfolio.style.mgt.StyleManager;
import org.theospi.portfolio.wizard.mgt.WizardManager;
import org.theospi.portfolio.wizard.model.CompletedWizard;
import org.theospi.portfolio.workflow.mgt.WorkflowManager;
import org.theospi.portfolio.workflow.model.Workflow;
public class ReviewHelperController implements Controller {
private static ResourceLoader myResources = new ResourceLoader("org.theospi.portfolio.matrix.bundle.Messages");
protected final Log LOG = LogFactory.getLog(getClass());
private MatrixManager matrixManager;
private IdManager idManager = null;
private ReviewManager reviewManager;
private WizardManager wizardManager;
private LockManager lockManager;
private ContentHostingService contentHosting;
private StyleManager styleManager;
private AuthenticationManager authManager = null;
private SecurityService securityService;
private WorkflowManager workflowManager;
private UserNotificationPreferencesRegistration matrixPreferencesConfig = null;
private UserNotificationPreferencesRegistration wizardPreferencesConfig = null;
public ModelAndView handleRequest(Object requestModel, Map request, Map session, Map application, Errors errors) {
String strId = null;
String lookupId = null;
String returnView = "return";
String manager = "";
Placement placement = ToolManager.getCurrentPlacement();
if (request.get("process_type_key") != null) {
session.put("process_type_key", request.get("process_type_key"));
session.put(ReviewHelper.REVIEW_TYPE_KEY, request.get(ReviewHelper.REVIEW_TYPE_KEY));
}
String processTypeKey = (String)session.get("process_type_key");
if (processTypeKey != null && !processTypeKey.equals(WizardPage.PROCESS_TYPE_KEY)) {
lookupId = processTypeKey;
returnView = "helperDone";
manager = "org.theospi.portfolio.wizard.mgt.WizardManager";
}
else if (processTypeKey != null) {
lookupId = processTypeKey;
manager = "matrixManager";
}
strId = (String) request.get(lookupId);
if (strId==null) {
strId = (String) session.get(lookupId);
}
//
// If this is the second pass,
// then we are creating a new [feedback | evaluation | reflection] review
//
String secondPass = (String)session.get("secondPass");
if (secondPass != null) {
strId = (String)session.get(lookupId);
String formType = (String)session.get(ResourceEditingHelper.CREATE_SUB_TYPE);
String itemId = (String)session.get(ReviewHelper.REVIEW_ITEM_ID);
String currentReviewId = (String)session.get(ResourceEditingHelper.ATTACHMENT_ID);
Map<String, Object> model = new HashMap<String, Object>();
model.put(lookupId, strId);
String currentSite = placement.getContext();
// check if this is a new review
if ( currentReviewId == null &&
!FormHelper.RETURN_ACTION_CANCEL.equals((String)session.get(FormHelper.RETURN_ACTION_TAG))) {
Review review = getReviewManager().createNew("New Review", currentSite);
review.setDeviceId(formType);
review.setParent(strId);
review.setItemId(itemId);
String strType = (String)session.get(ReviewHelper.REVIEW_TYPE);
// (note: strType may be null if user hits submit twice)
if ( strType != null && ! strType.equals("") )
review.setType(Integer.parseInt(strType));
if (FormHelper.RETURN_ACTION_SAVE.equals((String)session.get(FormHelper.RETURN_ACTION_TAG))
&& session.get(FormHelper.RETURN_REFERENCE_TAG) != null)
{
String artifactId = (String)session.get(FormHelper.RETURN_REFERENCE_TAG);
session.remove(FormHelper.RETURN_REFERENCE_TAG);
Node node = getMatrixManager().getNode(getIdManager().getId(artifactId));
review.setReviewContentNode(node);
review.setReviewContent(node.getId());
getReviewManager().saveReview(review);
}
// Lock review content (reflection, feedback, evaluation)
// (note: reviewContent may be null if user hits submit twice)
if ( review.getReviewContent() != null )
getLockManager().lockObject(review.getReviewContent().getValue(),
strId, "lock all review content", true);
boolean isEval = review.getType() == Review.EVALUATION_TYPE;
boolean isFeedback = review.getType() == Review.FEEDBACK_TYPE;
if (isEval || isFeedback) {
processEvalFeedbackNotifications(lookupId, strId, isEval, isFeedback,
currentSite, placement);
}
}
// otherwise this is an existing review being edited
else if (currentReviewId != null) {
// Lock review content (reflection, feedback, evaluation)
currentReviewId = contentHosting.getUuid( currentReviewId );
getLockManager().lockObject(currentReviewId,
strId, "lock all review content", true);
}
// Clean up session attributes
session.remove(ResourceEditingHelper.CREATE_TYPE);
session.remove(ResourceEditingHelper.CREATE_SUB_TYPE);
session.remove(ReviewHelper.REVIEW_TYPE);
session.remove(ResourceEditingHelper.CREATE_PARENT);
session.remove(ReviewHelper.REVIEW_ITEM_ID);
session.remove(lookupId);
//session.remove("process_type_key");
session.remove("secondPass");
// Check for workflow post process
if (session.get(ReviewHelper.REVIEW_POST_PROCESSOR_WORKFLOWS) != null) {
Set workflows = (Set)session.get(ReviewHelper.REVIEW_POST_PROCESSOR_WORKFLOWS);
List wfList = Arrays.asList(workflows.toArray());
if(!isSuperUser()){
wfList = removeResetWorkflow(wfList);
}
Collections.sort(wfList, Workflow.getComparator());
model.put("workflows", wfList);
model.put("manager", manager);
model.put("obj_id", strId);
return new ModelAndView("postProcessor", model);
}
// Check for workflow post process
if (session.get(ReviewHelper.REVIEW_POST_PROCESSOR_WORKFLOWS) != null &&
FormHelper.RETURN_ACTION_SAVE.equals((String)session.get(FormHelper.RETURN_ACTION_TAG))) {
Set workflows = (Set)session.get(ReviewHelper.REVIEW_POST_PROCESSOR_WORKFLOWS);
List wfList = Arrays.asList(workflows.toArray());
Collections.sort(wfList, Workflow.getComparator());
model.put("workflows", wfList);
model.put("manager", manager);
model.put("obj_id", strId);
session.remove(FormHelper.RETURN_ACTION_TAG);
return new ModelAndView("postProcessor", model);
}
session.remove(FormHelper.RETURN_ACTION_TAG);
return new ModelAndView(returnView, model);
}
//
// This is the first pass,
// so we are presenting the form to create a new [feedback | evaluation | reflection] review
//
Id id = getIdManager().getId(strId);
ObjectWithWorkflow obj = null;
Set<Workflow> evalWorkflows = new HashSet<Workflow>();
if (lookupId.equals(WizardPage.PROCESS_TYPE_KEY)) {
WizardPage page = matrixManager.getWizardPage(id);
if(request.get("scaffoldingId") != null){
//scaffoldingId is used for the reflection form when the default user forms
//are selected for a matrix cell
obj = matrixManager.getScaffolding(idManager.getId((String) request.get("scaffoldingId")));
WizardPageDefinition wpd = page.getPageDefinition();
evalWorkflows = wpd.getEvalWorkflows();
if (evalWorkflows == null || evalWorkflows.size() == 0) {
evalWorkflows = getWorkflowManager().createEvalWorkflows(wpd, ((Scaffolding)obj).getEvaluationDevice());
ScaffoldingCell sc = getMatrixManager().getScaffoldingCellByWizardPageDef(wpd.getId());
sc.getWizardPageDefinition().setEvalWorkflows(evalWorkflows);
getMatrixManager().storeScaffoldingCell(sc);
}
}else{
obj = page.getPageDefinition();
evalWorkflows = obj.getEvalWorkflows();
}
}else {
CompletedWizard cw = wizardManager.getCompletedWizard(id);
obj = cw.getWizard();
evalWorkflows = obj.getEvalWorkflows();
}
String type = (String)session.get(ReviewHelper.REVIEW_TYPE_KEY);
session.remove(ReviewHelper.REVIEW_TYPE_KEY);
int intType = Integer.parseInt(type);
String formTypeId = "";
String formTypeTitleKey = "";
session.remove(ReviewHelper.REVIEW_POST_PROCESSOR_WORKFLOWS);
switch (intType) {
case Review.FEEDBACK_TYPE:
formTypeId = obj.getReviewDevice().getValue();
formTypeTitleKey = "osp.reviewType." + Review.FEEDBACK_TYPE;
break;
case Review.EVALUATION_TYPE:
formTypeId = obj.getEvaluationDevice().getValue();
session.put(ReviewHelper.REVIEW_POST_PROCESSOR_WORKFLOWS,
evalWorkflows);
formTypeTitleKey = "osp.reviewType." + Review.EVALUATION_TYPE;
break;
case Review.REFLECTION_TYPE:
formTypeId = obj.getReflectionDevice().getValue();
formTypeTitleKey = "osp.reviewType." + Review.REFLECTION_TYPE;
break;
case Review.ITEM_LEVEL_EVAL_TYPE:
formTypeId = obj.getItemLevelEvaluationDevice().getValue();
formTypeTitleKey = "osp.reviewType." + Review.ITEM_LEVEL_EVAL_TYPE;
break;
}
String formView = "formCreator";
session.put(ReviewHelper.REVIEW_TYPE, type);
session.put(ResourceEditingHelper.CREATE_TYPE,
ResourceEditingHelper.CREATE_TYPE_FORM);
formView = setupSessionInfo(request, session, formTypeId, formTypeTitleKey, strId, placement);
session.put("page_id", strId);
session.put("secondPass", "true");
return new ModelAndView(formView);
}
/**
* Determine if a notification should be sent out and send it if needed
* @param lookupId
* @param strId
* @param isEval
* @param isFeedback
* @param currentSite
* @param cellPageType
* @param cellPageTypeBig
* @param matrixWizardType
*/
private void processEvalFeedbackNotifications(String lookupId, String strId,
boolean isEval, boolean isFeedback, String currentSite, Placement placement) {
//send out notification emails
boolean sendNotification = true;
boolean skipNotification = false;
String cellPageType = "";
String cellPageTypeBig = "";
String matrixWizardType = "";
String cellPageName = "";
String matrixWizardName = "";
String to = null;
String toUserId = null;
Agent owner = null;
String notificationId = "";
if ("page_id".equals(lookupId)) {
Id pageId = getIdManager().getId(strId);
WizardPage wizPage = getMatrixManager().getWizardPage(pageId);
owner = wizPage.getOwner();
try {
User user = UserDirectoryService.getUser(owner.getId().getValue());
if (user != null) {
to = user.getEmail();
toUserId = user.getId();
}
} catch (UserNotDefinedException e) {
LOG.warn("Unable to find user " + e.getId() + " to get email address for notification");
}
WizardPageDefinition wpd = wizPage.getPageDefinition();
cellPageName = wpd.getTitle();
if (wpd.getType().equals(WizardPageDefinition.WPD_MATRIX_TYPE)) {
cellPageType = myResources.getString("cellType");
cellPageTypeBig = myResources.getString("cellTypeBig");
matrixWizardType = myResources.getString("matrixType");
Scaffolding scaffolding = getMatrixManager().getMatrixByPage(pageId).getScaffolding();
matrixWizardName = scaffolding.getTitle();
if(wpd.isDefaultEvaluationForm()){
sendNotification = !scaffolding.isHideEvaluations();
}else{
sendNotification = !wpd.isHideEvaluations();
}
notificationId = getMatrixPreferencesConfig().getType();
}
else {
//at a wizard page?
cellPageType = myResources.getString("pageType");
cellPageTypeBig = myResources.getString("pageTypeBig");
matrixWizardType = myResources.getString("wizardType");
CompletedWizard cWizard = getWizardManager().getCompletedWizardByPage(pageId);
matrixWizardName = cWizard.getWizard().getName();
notificationId = getWizardPreferencesConfig().getType();
}
}
else {
//At a wizard?
skipNotification = true;
}
String typeStr = myResources.getString("legend_feedback");
String typeIntroStr = myResources.getString("notification_newFeedback");
if (isEval) {
typeStr = myResources.getString("legend_evaluation");
typeIntroStr = myResources.getString("notification_aNewEval");
//TODO only send if hide evals is toggled off
}
//send email if
if (!skipNotification && (isFeedback || (sendNotification && isEval))) {
String siteName;
try {
siteName = SiteService.getSite(currentSite).getTitle();
} catch (IdUnusedException e) {
siteName="<site not found>";
}
String directLink = ServerConfigurationService.getServerUrl() +
"/direct/matrixcell/" + strId + "/" + placement.getId() + "/viewCell.osp";
String[] subjArray = {cellPageName, matrixWizardName, matrixWizardType, typeStr};
String message_subject = myResources.getFormattedMessage("feedbackEvalNotificationSubject", subjArray);
String[] contentArray = {cellPageType, cellPageName, cellPageTypeBig, matrixWizardType, matrixWizardName, siteName, typeIntroStr, directLink};
String content = myResources.getFormattedMessage("feedbackEvalNotificationBody", contentArray);
String from = "postmaster@".concat(ServerConfigurationService.getServerName());
from = ServerConfigurationService.getString("setup.request", from);
//String to = ownerUser.getEmail();
int userPref = getMatrixManager().getNotificationOption(owner.getId().getValue(), notificationId, currentSite);
if (userPref == NotificationService.PREF_DIGEST) {
LOG.debug("processEvalFeedbackNotifications() - Sending digest to " + to);
DigestService.digest(toUserId, message_subject, content);
}
else if (userPref == NotificationService.PREF_IMMEDIATE) {
LOG.debug("processEvalFeedbackNotifications() - Sending message to " + to);
EmailService.send(from, to,
message_subject, content, to, from, null);
}
else {
LOG.debug("processEvalFeedbackNotifications() - Sending nothing to " + to);
}
}
}
private List removeResetWorkflow(List wfList){
List newList = new ArrayList();
for (Iterator iter = wfList.iterator(); iter.hasNext();) {
Workflow wf = (Workflow) iter.next();
if(!wf.getTitle().equals("Return Workflow")){
newList.add(wf);
}
}
return newList;
}
private boolean isSuperUser(){
return (getSecurityService().isSuperUser(authManager.getAgent().getId().getValue())) ? true : false;
}
/**
*
* @param request
* @param session
* @param pageTitle
* @param formTypeId
* @param formTypeTitleKey
* @param ownerEid The eid of the user that owns the object in question (wizard or page)
* @param pageId the id of the page
* @return
*/
protected String setupSessionInfo(Map request, Map<String, Object> session,
String formTypeId, String formTypeTitleKey,
String pageId, Placement placement) {
String retView = "formCreator";
// check if this is a request for a new rewiew (i.e. no current_review_id)
if (request.get("current_review_id") == null) {
session.remove(ResourceEditingHelper.ATTACHMENT_ID);
session.put(ResourceEditingHelper.CREATE_TYPE,
ResourceEditingHelper.CREATE_TYPE_FORM);
session.put(ResourceEditingHelper.CREATE_SUB_TYPE, formTypeId);
String objectId = (String)request.get("objectId");
String objectTitle = (String)request.get("objectTitle");
String itemId = (String)request.get("itemId");
session.put(ReviewHelper.REVIEW_ITEM_ID, itemId);
String formTypeTitle = myResources.getString(formTypeTitleKey);
List contentResourceList = null;
try {
String folderBase = getUserCollection().getId();
String currentSite = placement.getContext();
String rootDisplayName = myResources.getString("portfolioInteraction.displayName");
String rootDescription = myResources.getString("portfolioInteraction.description");
String folderPath = createFolder(folderBase, "portfolio-interaction", rootDisplayName, rootDescription);
folderPath = createFolder(folderPath, currentSite, SiteService.getSiteDisplay(currentSite), null);
folderPath = createFolder(folderPath, objectId, objectTitle, null);
folderPath = createFolder(folderPath, formTypeId, formTypeTitle, null);
contentResourceList = this.getContentHosting().getAllResources(folderPath);
session.put(FormHelper.PARENT_ID_TAG, folderPath);
} catch (TypeException e) {
throw new RuntimeException("Failed to redirect to helper", e);
} catch (IdUnusedException e) {
throw new RuntimeException("Failed to redirect to helper", e);
} catch (PermissionException e) {
throw new RuntimeException("Failed to redirect to helper", e);
}
//CWM OSP-UI-09 - for auto naming
session.put(FormHelper.NEW_FORM_DISPLAY_NAME_TAG, FormNameGeneratorUtil.getFormDisplayName(formTypeTitle, 1, contentResourceList));
}
// Otherwise, editting an existing review
else {
String currentReviewId = (String)request.get("current_review_id");
session.remove(ResourceEditingHelper.CREATE_TYPE);
session.remove(ResourceEditingHelper.CREATE_SUB_TYPE);
session.remove(ResourceEditingHelper.CREATE_PARENT);
session.put(ResourceEditingHelper.CREATE_TYPE,
ResourceEditingHelper.CREATE_TYPE_FORM);
session.put(ResourceEditingHelper.ATTACHMENT_ID, currentReviewId);
// unlock review content for edit
String reviewContentId = contentHosting.getUuid( currentReviewId );
if ( getLockManager().isLocked(reviewContentId) ) {
getLockManager().removeLock(reviewContentId, pageId );
}
retView = "formEditor";
}
session.put(FormHelper.FORM_STYLES,
getStyleManager().createStyleUrlList(getStyleManager().getStyles(getIdManager().getId(pageId))));
return retView;
}
/**
*
* @param base
* @param append
* @param appendDisplay
* @param appendDescription
* @return
*/
protected String createFolder(String base, String append, String appendDisplay, String appendDescription) {
String folder = base + append + "/";
try {
ContentCollectionEdit propFolder = getContentHosting().addCollection(folder);
propFolder.getPropertiesEdit().addProperty(ResourceProperties.PROP_DISPLAY_NAME, appendDisplay);
propFolder.getPropertiesEdit().addProperty(ResourceProperties.PROP_DESCRIPTION, appendDescription);
getContentHosting().commitCollection(propFolder);
return propFolder.getId();
}
catch (IdUsedException e) {
// ignore... it is already there.
}
catch (Exception e) {
throw new RuntimeException(e);
}
return folder;
}
/**
*
* @return
* @throws TypeException
* @throws IdUnusedException
* @throws PermissionException
*/
protected ContentCollection getUserCollection() throws TypeException, IdUnusedException, PermissionException {
User user = UserDirectoryService.getCurrentUser();
String userId = user.getId();
String wsId = SiteService.getUserSiteId(userId);
String wsCollectionId = getContentHosting().getSiteCollection(wsId);
ContentCollection collection = getContentHosting().getCollection(wsCollectionId);
return collection;
}
/**
* @return Returns the idManager.
*/
public IdManager getIdManager() {
return idManager;
}
/**
* @param idManager The idManager to set.
*/
public void setIdManager(IdManager idManager) {
this.idManager = idManager;
}
/**
* @return Returns the matrixManager.
*/
public MatrixManager getMatrixManager() {
return matrixManager;
}
/**
* @param matrixManager The matrixManager to set.
*/
public void setMatrixManager(MatrixManager matrixManager) {
this.matrixManager = matrixManager;
}
/**
* @return Returns the reviewManager.
*/
public ReviewManager getReviewManager() {
return reviewManager;
}
/**
* @param reviewManager The reviewManager to set.
*/
public void setReviewManager(ReviewManager reviewManager) {
this.reviewManager = reviewManager;
}
/**
* @return Returns the wizardManager.
*/
public WizardManager getWizardManager() {
return wizardManager;
}
/**
* @param wizardManager The wizardManager to set.
*/
public void setWizardManager(WizardManager wizardManager) {
this.wizardManager = wizardManager;
}
public LockManager getLockManager() {
return lockManager;
}
public void setLockManager(LockManager lockManager) {
this.lockManager = lockManager;
}
/**
* @return the contentHosting
*/
public ContentHostingService getContentHosting() {
return contentHosting;
}
/**
* @param contentHosting the contentHosting to set
*/
public void setContentHosting(ContentHostingService contentHosting) {
this.contentHosting = contentHosting;
}
public StyleManager getStyleManager() {
return styleManager;
}
public void setStyleManager(StyleManager styleManager) {
this.styleManager = styleManager;
}
public SecurityService getSecurityService() {
return securityService;
}
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
public AuthenticationManager getAuthManager() {
return authManager;
}
public void setAuthManager(AuthenticationManager authManager) {
this.authManager = authManager;
}
public WorkflowManager getWorkflowManager() {
return workflowManager;
}
public void setWorkflowManager(WorkflowManager workflowManager) {
this.workflowManager = workflowManager;
}
public UserNotificationPreferencesRegistration getMatrixPreferencesConfig() {
return matrixPreferencesConfig;
}
public void setMatrixPreferencesConfig(
UserNotificationPreferencesRegistration matrixPreferencesConfig) {
this.matrixPreferencesConfig = matrixPreferencesConfig;
}
public UserNotificationPreferencesRegistration getWizardPreferencesConfig() {
return wizardPreferencesConfig;
}
public void setWizardPreferencesConfig(
UserNotificationPreferencesRegistration wizardPreferencesConfig) {
this.wizardPreferencesConfig = wizardPreferencesConfig;
}
}