/**
* The contents of this file are subject to the OpenMRS Public License
* Version 1.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://license.openmrs.org
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* Copyright (C) OpenMRS, LLC. All Rights Reserved.
*/
package org.openmrs.web.controller;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.collections.FactoryUtils;
import org.apache.commons.collections.ListUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.Concept;
import org.openmrs.ConceptAnswer;
import org.openmrs.ConceptComplex;
import org.openmrs.ConceptDescription;
import org.openmrs.ConceptMap;
import org.openmrs.ConceptName;
import org.openmrs.ConceptNameTag;
import org.openmrs.ConceptNumeric;
import org.openmrs.ConceptSet;
import org.openmrs.Form;
import org.openmrs.api.APIException;
import org.openmrs.api.ConceptService;
import org.openmrs.api.ConceptsLockedException;
import org.openmrs.api.context.Context;
import org.openmrs.propertyeditor.ConceptAnswersEditor;
import org.openmrs.propertyeditor.ConceptClassEditor;
import org.openmrs.propertyeditor.ConceptDatatypeEditor;
import org.openmrs.propertyeditor.ConceptSetsEditor;
import org.openmrs.propertyeditor.ConceptSourceEditor;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.web.WebConstants;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.view.RedirectView;
/**
* This is the controlling class for hte conceptForm.jsp page. It initBinder and formBackingObject
* are called before page load. After submission, formBackingObject (because we're not a session
* form), processFormSubmission, and onSubmit methods are called
*
* @see org.openmrs.Concept
*/
public class ConceptFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log log = LogFactory.getLog(getClass());
/**
* Allows for other Objects to be used as values in input tags. Normally, only strings and lists
* are expected
*
* @see org.springframework.web.servlet.mvc.BaseCommandController#initBinder(javax.servlet.http.HttpServletRequest,
* org.springframework.web.bind.ServletRequestDataBinder)
*/
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
super.initBinder(request, binder);
ConceptFormBackingObject commandObject = (ConceptFormBackingObject) binder.getTarget();
NumberFormat nf = NumberFormat.getInstance(Context.getLocale());
binder.registerCustomEditor(java.lang.Integer.class, new CustomNumberEditor(java.lang.Integer.class, nf, true));
binder.registerCustomEditor(java.lang.Double.class, new CustomNumberEditor(java.lang.Double.class, nf, true));
binder.registerCustomEditor(java.util.Date.class, new CustomDateEditor(SimpleDateFormat.getDateInstance(
SimpleDateFormat.SHORT, Context.getLocale()), true));
binder.registerCustomEditor(org.openmrs.ConceptClass.class, new ConceptClassEditor());
binder.registerCustomEditor(org.openmrs.ConceptDatatype.class, new ConceptDatatypeEditor());
binder.registerCustomEditor(java.util.Collection.class, "concept.conceptSets", new ConceptSetsEditor(commandObject.getConcept().getConceptSets()));
binder.registerCustomEditor(java.util.Collection.class, "concept.answers", new ConceptAnswersEditor(commandObject.getConcept().getAnswers(true)));
binder.registerCustomEditor(org.openmrs.ConceptSource.class, new ConceptSourceEditor());
}
/**
* @see org.springframework.web.servlet.mvc.AbstractFormController#processFormSubmission(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse, java.lang.Object,
* org.springframework.validation.BindException)
*/
protected ModelAndView processFormSubmission(HttpServletRequest request, HttpServletResponse response, Object object,
BindException errors) throws Exception {
Concept concept = ((ConceptFormBackingObject) object).getConcept();
ConceptService cs = Context.getConceptService();
// check to see if they clicked next/previous concept:
String jumpAction = request.getParameter("jumpAction");
if (jumpAction != null) {
Concept newConcept = null;
if ("previous".equals(jumpAction))
newConcept = cs.getPrevConcept(concept);
else if ("next".equals(jumpAction))
newConcept = cs.getNextConcept(concept);
if (newConcept != null)
return new ModelAndView(new RedirectView(getSuccessView() + "?conceptId=" + newConcept.getConceptId()));
else
return new ModelAndView(new RedirectView(getSuccessView()));
}
return super.processFormSubmission(request, response, object, errors);
}
/**
* The onSubmit function receives the form/command object that was modified by the input form
* and saves it to the db
*
* @see org.springframework.web.servlet.mvc.SimpleFormController#onSubmit(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse, java.lang.Object,
* org.springframework.validation.BindException)
* @should display numeric values from table
* @should copy numeric values into numeric concepts
*/
protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object obj,
BindException errors) throws Exception {
HttpSession httpSession = request.getSession();
ConceptService cs = Context.getConceptService();
if (Context.isAuthenticated()) {
ConceptFormBackingObject conceptBackingObject = (ConceptFormBackingObject) obj;
MessageSourceAccessor msa = getMessageSourceAccessor();
String action = request.getParameter("action");
if (action.equals(msa.getMessage("Concept.delete", "Delete Concept"))) {
Concept concept = conceptBackingObject.getConcept();
try {
cs.purgeConcept(concept);
httpSession.setAttribute(WebConstants.OPENMRS_MSG_ATTR, "Concept.deleted");
return new ModelAndView(new RedirectView("index.htm"));
}
catch (ConceptsLockedException cle) {
log.error("Tried to delete concept while concepts were locked", cle);
httpSession.setAttribute(WebConstants.OPENMRS_ERROR_ATTR, "Concept.concepts.locked");
}
catch (DataIntegrityViolationException e) {
log.error("Unable to delete a concept because it is in use: " + concept, e);
httpSession.setAttribute(WebConstants.OPENMRS_ERROR_ATTR, "Concept.cannot.delete");
}
catch (Exception e) {
log.error("Unable to delete concept because an error occurred: " + concept, e);
httpSession.setAttribute(WebConstants.OPENMRS_ERROR_ATTR, "Concept.cannot.delete");
}
// return to the edit screen because an error was thrown
return new ModelAndView(new RedirectView(getSuccessView() + "?conceptId=" + concept.getConceptId()));
} else {
Concept concept = conceptBackingObject.getConceptFromFormData();
try {
cs.saveConcept(concept);
httpSession.setAttribute(WebConstants.OPENMRS_MSG_ATTR, "Concept.saved");
return new ModelAndView(new RedirectView(getSuccessView() + "?conceptId=" + concept.getConceptId()));
}
catch (ConceptsLockedException cle) {
log.error("Tried to save concept while concepts were locked", cle);
httpSession.setAttribute(WebConstants.OPENMRS_ERROR_ATTR, "Concept.concepts.locked");
errors.reject("concept", "Concept.concepts.locked");
}
catch (APIException e) {
log.error("Error while trying to save concept", e);
httpSession.setAttribute(WebConstants.OPENMRS_ERROR_ATTR, "Concept.cannot.save");
errors.reject("concept", "Concept.cannot.save");
}
// return to the edit form because an error was thrown
return showForm(request, response, errors);
}
}
return new ModelAndView(new RedirectView(getFormView()));
}
/**
* This is called prior to displaying a form for the first time. It tells Spring the
* form/command object to load into the request
*
* @see org.springframework.web.servlet.mvc.AbstractFormController#formBackingObject(javax.servlet.http.HttpServletRequest)
*/
protected ConceptFormBackingObject formBackingObject(HttpServletRequest request) throws ServletException {
String conceptId = request.getParameter("conceptId");
if (conceptId == null) {
return new ConceptFormBackingObject(new Concept());
} else {
ConceptService cs = Context.getConceptService();
Concept concept = cs.getConcept(Integer.valueOf(conceptId));
return new ConceptFormBackingObject(concept);
}
}
/**
* Called prior to form display. Allows for data to be put in the request to be used in the view
*
* @see org.springframework.web.servlet.mvc.SimpleFormController#referenceData(javax.servlet.http.HttpServletRequest)
*/
protected Map<String, Object> referenceData(HttpServletRequest request) throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
ConceptService cs = Context.getConceptService();
String defaultVerbose = "false";
if (Context.isAuthenticated())
defaultVerbose = Context.getAuthenticatedUser().getUserProperty(OpenmrsConstants.USER_PROPERTY_SHOW_VERBOSE);
map.put("defaultVerbose", defaultVerbose.equals("true") ? true : false);
map.put("tags", cs.getAllConceptNameTags());
//get complete class and datatype lists
map.put("classes", cs.getAllConceptClasses());
map.put("datatypes", cs.getAllConceptDatatypes());
String conceptId = request.getParameter("conceptId");
boolean dataTypeReadOnly = false;
if (conceptId != null) {
Concept concept = cs.getConcept(Integer.valueOf(conceptId));
dataTypeReadOnly = cs.hasAnyObservation(concept);
}
map.put("dataTypeReadOnly", dataTypeReadOnly);
//get complex handlers
map.put("handlers", Context.getObsService().getHandlers());
// make spring locale available to jsp
map.put("locale", Context.getLocale()); // should be same string format as conceptNamesByLocale map keys
return map;
}
/**
* Class that represents all data on this form
*/
public class ConceptFormBackingObject {
public Concept concept = null;
public List<Locale> locales = null;
public Map<Locale, ConceptName> namesByLocale = new HashMap<Locale, ConceptName>();
public Map<Locale, ConceptName> shortNamesByLocale = new HashMap<Locale, ConceptName>();
public Map<Locale, List<ConceptName>> synonymsByLocale = new HashMap<Locale, List<ConceptName>>();
public Map<Locale, ConceptDescription> descriptionsByLocale = new HashMap<Locale, ConceptDescription>();
public List<ConceptMap> mappings; // a "lazy list" version of the concept.getMappings() list
public Double hiAbsolute;
public Double lowAbsolute;
public Double lowCritical;
public Double hiCritical;
public Double lowNormal;
public Double hiNormal;
public boolean precise = false;
public String units;
public String handlerKey;
/**
* Default constructor must take in a Concept object to create itself
*
* @param concept The concept for this page
*/
@SuppressWarnings("unchecked")
public ConceptFormBackingObject(Concept concept) {
this.concept = concept;
this.locales = Context.getAdministrationService().getAllowedLocales();
for (Locale locale : locales) {
namesByLocale.put(locale, concept.getPreferredName(locale));
shortNamesByLocale.put(locale, concept.getShortNameInLocale(locale));
synonymsByLocale.put(locale, (List<ConceptName>) concept.getSynonyms(locale));
descriptionsByLocale.put(locale, concept.getDescription(locale, true));
// put in default values so the binding doesn't fail
if (namesByLocale.get(locale) == null)
namesByLocale.put(locale, new ConceptName(null, locale));
if (shortNamesByLocale.get(locale) == null)
shortNamesByLocale.put(locale, new ConceptName(null, locale));
if (descriptionsByLocale.get(locale) == null)
descriptionsByLocale.put(locale, new ConceptDescription(null, locale));
synonymsByLocale.put(locale, ListUtils.lazyList(synonymsByLocale.get(locale), FactoryUtils
.instantiateFactory(ConceptName.class)));
}
// turn the list objects into lazy lists
mappings = ListUtils.lazyList(new Vector(concept.getConceptMappings()), FactoryUtils
.instantiateFactory(ConceptMap.class));
if (concept.isNumeric()) {
ConceptNumeric cn = (ConceptNumeric) concept;
this.hiAbsolute = cn.getHiAbsolute();
this.lowAbsolute = cn.getLowAbsolute();
this.lowCritical = cn.getLowCritical();
this.hiCritical = cn.getHiCritical();
this.lowNormal = cn.getLowNormal();
this.hiNormal = cn.getHiNormal();
this.precise = cn.getPrecise();
this.units = cn.getUnits();
} else if (concept.isComplex()) {
ConceptComplex complex = (ConceptComplex) concept;
this.handlerKey = complex.getHandler();
}
}
/**
* This method takes all the form data from the input boxes and puts it onto the concept
* object so that it can be saved to the database
*
* @return the concept to be saved to the database
*/
public Concept getConceptFromFormData() {
ConceptNameTag preferredTag = Context.getConceptService().getConceptNameTagByName(ConceptNameTag.PREFERRED);
ConceptNameTag shortTag = Context.getConceptService().getConceptNameTagByName(ConceptNameTag.SHORT);
ConceptNameTag synonymTag = Context.getConceptService().getConceptNameTagByName(ConceptNameTag.SYNONYM);
// add all the new names/descriptions to the concept
for (Locale locale : locales) {
ConceptName preferredNameInLocale = namesByLocale.get(locale);
if (StringUtils.hasLength(preferredNameInLocale.getName())
&& !concept.getNames().contains(preferredNameInLocale)) {
preferredNameInLocale.addTag(preferredTag);
concept.addName(preferredNameInLocale);
}
ConceptName shortNameInLocale = shortNamesByLocale.get(locale);
if (StringUtils.hasLength(shortNameInLocale.getName()) && !concept.getNames().contains(shortNameInLocale)
&& !concept.hasName(shortNameInLocale.getName(), locale)) {
shortNameInLocale.addTag(shortTag);
concept.addName(shortNameInLocale);
}
for (ConceptName synonym : synonymsByLocale.get(locale)) {
if (synonym != null && synonym.getName() != null && !concept.getNames().contains(synonym)
&& !concept.hasName(synonym.getName(), locale)) {
synonym.addTag(synonymTag);
synonym.setLocale(locale);
concept.addName(synonym);
}
}
ConceptDescription descInLocale = descriptionsByLocale.get(locale);
if (StringUtils.hasLength(descInLocale.getDescription())
&& !concept.getDescriptions().contains(descInLocale)) {
concept.addDescription(descInLocale);
}
}
// add in all the mappings
for (ConceptMap map : mappings) {
if (map != null) {
if (map.getSourceCode() == null) {
// because of the _mappings[x].sourceCode input name in the jsp, the sourceCode will be empty for
// deleted mappings. remove those from the concept object now.
concept.removeConceptMapping(map);
} else if (!concept.getConceptMappings().contains(map)) {
// assumes null sources also don't get here
concept.addConceptMapping(map);
}
}
}
// if the user unchecked the concept sets box, erase past saved sets
if (!concept.isSet()) {
if (concept.getConceptSets() != null) {
concept.getConceptSets().clear();
}
}
// if the user changed the datatype to be non "Coded", erase past saved datatypes
if (!concept.getDatatype().isCoded()) {
if (concept.getAnswers(true) != null) {
concept.getAnswers(true).clear();
}
}
// add in subobject specific code
if (concept.getDatatype().getName().equals("Numeric")) {
ConceptNumeric cn;
if (concept instanceof ConceptNumeric)
cn = (ConceptNumeric) concept;
else {
cn = new ConceptNumeric(concept);
}
cn.setHiAbsolute(hiAbsolute);
cn.setLowAbsolute(lowAbsolute);
cn.setHiCritical(hiCritical);
cn.setLowCritical(lowCritical);
cn.setHiNormal(hiNormal);
cn.setLowNormal(lowNormal);
cn.setPrecise(precise);
cn.setUnits(units);
concept = cn;
} else if (concept.getDatatype().getName().equals("Complex")) {
ConceptComplex complexConcept;
if (concept instanceof ConceptComplex)
complexConcept = (ConceptComplex) concept;
else {
complexConcept = new ConceptComplex(concept);
}
complexConcept.setHandler(handlerKey);
concept = complexConcept;
}
return concept;
}
/**
* @return the concept
*/
public Concept getConcept() {
return concept;
}
/**
* @param concept the concept to set
*/
public void setConcept(Concept concept) {
this.concept = concept;
}
/**
* @return the locales
*/
public List<Locale> getLocales() {
return locales;
}
/**
* @param locales the locales to set
*/
public void setLocales(List<Locale> locales) {
this.locales = locales;
}
/**
* @return the namesByLocale
*/
public Map<Locale, ConceptName> getNamesByLocale() {
return namesByLocale;
}
/**
* @param namesByLocale the namesByLocale to set
*/
public void setNamesByLocale(Map<Locale, ConceptName> namesByLocale) {
this.namesByLocale = namesByLocale;
}
/**
* @return the shortNamesByLocale
*/
public Map<Locale, ConceptName> getShortNamesByLocale() {
return shortNamesByLocale;
}
/**
* @param shortNamesByLocale the shortNamesByLocale to set
*/
public void setShortNamesByLocale(Map<Locale, ConceptName> shortNamesByLocale) {
this.shortNamesByLocale = shortNamesByLocale;
}
/**
* @return the descriptionsByLocale
*/
public Map<Locale, ConceptDescription> getDescriptionsByLocale() {
return descriptionsByLocale;
}
/**
* @param descriptionsByLocale the descriptionsByLocale to set
*/
public void setDescriptionsByLocale(Map<Locale, ConceptDescription> descriptionsByLocale) {
this.descriptionsByLocale = descriptionsByLocale;
}
/**
* @return the mappings
*/
public List<ConceptMap> getMappings() {
return mappings;
}
/**
* @param mappings the mappings to set
*/
public void setMappings(List<ConceptMap> mappings) {
this.mappings = mappings;
}
/**
* @return the synonymsByLocale
*/
public Map<Locale, List<ConceptName>> getSynonymsByLocale() {
return synonymsByLocale;
}
/**
* @param synonymsByLocale the synonymsByLocale to set
*/
public void setSynonymsByLocale(Map<Locale, List<ConceptName>> synonymsByLocale) {
this.synonymsByLocale = synonymsByLocale;
}
/**
* @return the hiAbsolute
*/
public Double getHiAbsolute() {
return hiAbsolute;
}
/**
* @param hiAbsolute the hiAbsolute to set
*/
public void setHiAbsolute(Double hiAbsolute) {
this.hiAbsolute = hiAbsolute;
}
/**
* @return the lowAbsolute
*/
public Double getLowAbsolute() {
return lowAbsolute;
}
/**
* @param lowAbsolute the lowAbsolute to set
*/
public void setLowAbsolute(Double lowAbsolute) {
this.lowAbsolute = lowAbsolute;
}
/**
* @return the lowCritical
*/
public Double getLowCritical() {
return lowCritical;
}
/**
* @param lowCritical the lowCritical to set
*/
public void setLowCritical(Double lowCritical) {
this.lowCritical = lowCritical;
}
/**
* @return the hiCritical
*/
public Double getHiCritical() {
return hiCritical;
}
/**
* @param hiCritical the hiCritical to set
*/
public void setHiCritical(Double hiCritical) {
this.hiCritical = hiCritical;
}
/**
* @return the lowNormal
*/
public Double getLowNormal() {
return lowNormal;
}
/**
* @param lowNormal the lowNormal to set
*/
public void setLowNormal(Double lowNormal) {
this.lowNormal = lowNormal;
}
/**
* @return the hiNormal
*/
public Double getHiNormal() {
return hiNormal;
}
/**
* @param hiNormal the hiNormal to set
*/
public void setHiNormal(Double hiNormal) {
this.hiNormal = hiNormal;
}
/**
* @return the precise
*/
public boolean isPrecise() {
return precise;
}
/**
* @param precise the precise to set
*/
public void setPrecise(boolean precise) {
this.precise = precise;
}
/**
* @return the units
*/
public String getUnits() {
return units;
}
/**
* @param units the units to set
*/
public void setUnits(String units) {
this.units = units;
}
/**
* @return the handlerKey
*/
public String getHandlerKey() {
return handlerKey;
}
/**
* @param handlerKey the handlerKey to set
*/
public void setHandlerKey(String handlerKey) {
this.handlerKey = handlerKey;
}
/**
* Get the forms that this concept is declared to be used in
*
* @return
*/
public List<Form> getFormsInUse() {
return Context.getFormService().getFormsContainingConcept(concept);
}
/**
* Get the other concept questions that this concept is declared as an answer for
*
* @return
*/
public List<Concept> getQuestionsAnswered() {
return Context.getConceptService().getConceptsByAnswer(concept);
}
/**
* Get the sets that this concept is declared to be a child member of
*
* @return
*/
public List<ConceptSet> getContainedInSets() {
return Context.getConceptService().getSetsContainingConcept(concept);
}
/**
* Get the answers for this concept with decoded names. The keys to this map are the
* conceptIds or the conceptIds^drugId if applicable
*
* @return
*/
public Map<String, String> getConceptAnswers() {
Map<String, String> conceptAnswers = new TreeMap<String, String>();
// get concept answers with locale decoded names
for (ConceptAnswer answer : concept.getAnswers(true)) {
log.debug("getting answers");
String key = answer.getAnswerConcept().getConceptId().toString();
ConceptName cn = answer.getAnswerConcept().getName(Context.getLocale());
String name = "";
if (cn != null)
name = cn.toString();
if (answer.getAnswerDrug() != null) {
// if this answer is a drug, append the drug id information
key = key + "^" + answer.getAnswerDrug().getDrugId();
name = answer.getAnswerDrug().getFullName(Context.getLocale());
}
if (answer.getAnswerConcept().isRetired())
name = "<span class='retired'>" + name + "</span>";
conceptAnswers.put(key, name);
}
return conceptAnswers;
}
}
}