Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
package org.openmrs.module.htmlformentry.element;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.Concept;
import org.openmrs.ConceptAnswer;
import org.openmrs.Location;
import org.openmrs.LocationTag;
import org.openmrs.Patient;
import org.openmrs.PersonAttribute;
import org.openmrs.PersonAttributeType;
import org.openmrs.api.context.Context;
import org.openmrs.module.htmlformentry.BadFormDesignException;
import org.openmrs.module.htmlformentry.FormEntryContext;
import org.openmrs.module.htmlformentry.FormEntryContext.Mode;
import org.openmrs.module.htmlformentry.FormEntrySession;
import org.openmrs.module.htmlformentry.FormSubmissionError;
import org.openmrs.module.htmlformentry.HtmlFormEntryUtil;
import org.openmrs.module.htmlformentry.action.FormSubmissionControllerAction;
import org.openmrs.module.htmlformentry.comparator.OptionComparator;
import org.openmrs.module.htmlformentry.widget.DropdownWidget;
import org.openmrs.module.htmlformentry.widget.ErrorWidget;
import org.openmrs.module.htmlformentry.widget.Option;
import org.openmrs.module.htmlformentry.widget.TextFieldWidget;
import org.openmrs.module.htmlformentry.widget.Widget;
import org.springframework.util.StringUtils;

/**
* Holds the widget and submission logic for the {@code <personAttribute>} tag.
*
* <p>Supports three PersonAttributeType formats:
* <ul>
* <li>{@code java.lang.String} – plain text input</li>
* <li>{@code org.openmrs.Concept} – dropdown whose answer concepts are resolved in priority order:
* (1) explicit {@code answerConceptIds} tag attribute (comma-separated IDs or UUIDs),
* (2) {@code PersonAttributeType.foreignKey} pointing to a concept set (set members used)
* or a question concept (concept answers used),
* (3) {@link BadFormDesignException} if neither is configured</li>
* <li>{@code org.openmrs.Location} – dropdown of locations, optionally filtered by {@code tags}</li>
* </ul>
*
* <p>When multiple non-voided attributes of the requested type exist for the patient, the most
* recently created one is used and a warning is logged.
*/
public class PersonAttributeSubmissionElement implements HtmlGeneratorElement, FormSubmissionControllerAction {

protected final Log log = LogFactory.getLog(getClass());

/** The resolved attribute type. */
private final PersonAttributeType attributeType;

/** The widget used to render and submit the attribute value. */
private Widget valueWidget;

/** The error widget (rendered in ENTER / EDIT modes only). */
private ErrorWidget errorWidget;

/** The existing non-voided attribute (most recent if there are several), or null. */
private PersonAttribute existingAttribute;

public PersonAttributeSubmissionElement(FormEntryContext context, Map<String, String> parameters)
throws BadFormDesignException {

// ---- 1. Resolve the PersonAttributeType ----------------------------------------
String attributeTypeUuid = parameters.get("attributeType");
if (!StringUtils.hasText(attributeTypeUuid)) {
throw new BadFormDesignException("<personAttribute> tag requires an \"attributeType\" attribute");
}
attributeType = Context.getPersonService().getPersonAttributeTypeByUuid(attributeTypeUuid);
if (attributeType == null) {
throw new BadFormDesignException(
"<personAttribute> tag: PersonAttributeType not found for uuid=\"" + attributeTypeUuid + "\"");
}

// ---- 2. Find existing attribute on the patient ---------------------------------
Patient patient = context.getExistingPatient();
if (patient != null) {
List<PersonAttribute> matching = new ArrayList<PersonAttribute>();
for (PersonAttribute attr : patient.getActiveAttributes()) {
if (attr.getAttributeType().equals(attributeType)) {
matching.add(attr);
}
}
if (matching.size() > 1) {
log.warn("Patient " + patient.getPatientId() + " has " + matching.size()
+ " non-voided PersonAttributes of type \"" + attributeType.getName()
+ "\"; using most recent.");
matching.sort(Comparator.comparing(PersonAttribute::getDateCreated).reversed());
}
existingAttribute = matching.isEmpty() ? null : matching.get(0);
}

// ---- 3. Build widget by format (all modes, including VIEW) --------------------
errorWidget = new ErrorWidget();

if (String.class.getName().equals(attributeType.getFormat())) {
valueWidget = buildStringWidget(context);

} else if (Concept.class.getName().equals(attributeType.getFormat())) {
valueWidget = buildConceptWidget(context, parameters);

} else if (Location.class.getName().equals(attributeType.getFormat())) {
valueWidget = buildLocationWidget(context, parameters);

} else {
throw new BadFormDesignException("<personAttribute> tag: unsupported attribute format \"" + attributeType.getFormat()
+ "\" for type \"" + attributeType.getName() + "\". Supported formats: "
+ String.class.getName() + ", " + Concept.class.getName() + ", " + Location.class.getName());
}

context.registerWidget(valueWidget);
context.registerErrorWidget(valueWidget, errorWidget);
}

// ---------------------------------------------------------------------------------
// Widget builders
// ---------------------------------------------------------------------------------

private Widget buildStringWidget(FormEntryContext context) {
TextFieldWidget w = new TextFieldWidget();
if (existingAttribute != null) {
w.setInitialValue(existingAttribute.getValue());
}
return w;
}

private Widget buildConceptWidget(FormEntryContext context, Map<String, String> parameters)
throws BadFormDesignException {

DropdownWidget w = new DropdownWidget();
w.addOption(new Option());

for (Concept concept : resolveAnswerConcepts(parameters)) {
w.addOption(new Option(concept.getName(Context.getLocale(), false).getName(), concept.getConceptId().toString(), false));
}

if (existingAttribute != null && StringUtils.hasText(existingAttribute.getValue())) {
w.setInitialValue(existingAttribute.getValue());
}

return w;
}

private List<Concept> resolveAnswerConcepts(Map<String, String> parameters) throws BadFormDesignException {
String answerConceptIds = parameters.get("answerConceptIds");
if (StringUtils.hasText(answerConceptIds)) {
return resolveConceptsFromIdList(answerConceptIds);
}
if (attributeType.getForeignKey() != null) {
return resolveConceptsFromForeignKey(attributeType.getForeignKey());
}
throw new BadFormDesignException(
"<personAttribute> tag: PersonAttributeType \"" + attributeType.getName()
+ "\" (format=" + Concept.class.getName() + ") requires either an \"answerConceptIds\" "
+ "tag attribute or a foreignKey on the attribute type pointing to a concept set or question concept");
}

private List<Concept> resolveConceptsFromIdList(String answerConceptIds) throws BadFormDesignException {
List<Concept> concepts = new ArrayList<>();
for (String idOrUuid : answerConceptIds.split(",")) {
String trimmed = idOrUuid.trim();
if (trimmed.isEmpty()) {
continue;
}
Concept concept = HtmlFormEntryUtil.getConcept(trimmed);
if (concept == null) {
throw new BadFormDesignException(
"<personAttribute> tag: cannot find concept for answerConceptIds value \"" + trimmed + "\"");
}
concepts.add(concept);
}
return concepts;
}

private List<Concept> resolveConceptsFromForeignKey(Integer foreignKey) throws BadFormDesignException {
Concept fk = Context.getConceptService().getConcept(foreignKey);
if (fk == null) {
throw new BadFormDesignException(
"<personAttribute> tag: cannot find concept for foreignKey=" + foreignKey
+ " on PersonAttributeType \"" + attributeType.getName() + "\"");
}
if (Boolean.TRUE.equals(fk.getSet())) {
return fk.getSetMembers(false);
}
return fk.getAnswers(false).stream()
.map(ConceptAnswer::getAnswerConcept)
.collect(Collectors.toList());
}

private Widget buildLocationWidget(FormEntryContext context, Map<String, String> parameters)
throws BadFormDesignException {

DropdownWidget w = new DropdownWidget();

// blank / choose prompt
w.addOption(new Option(Context.getMessageSourceService().getMessage("htmlformentry.chooseALocation"), "", false));

// Build location list
List<Location> locations;
String tagsParam = parameters.get("tags");
if (StringUtils.hasText(tagsParam)) {
List<LocationTag> locationTags = new ArrayList<LocationTag>();
for (String tagName : tagsParam.split(",")) {
String trimmed = tagName.trim();
if (trimmed.isEmpty()) {
continue;
}
LocationTag tag = HtmlFormEntryUtil.getLocationTag(trimmed);
if (tag == null) {
throw new BadFormDesignException(
"<personAttribute> tag: cannot find location tag \"" + trimmed + "\"");
}
locationTags.add(tag);
}
locations = Context.getLocationService().getLocationsHavingAnyTag(locationTags);
} else {
locations = Context.getLocationService().getAllLocations(false);
}

// Build and sort options
List<Option> locationOptions = new ArrayList<Option>();
for (Location location : locations) {
locationOptions.add(new Option(HtmlFormEntryUtil.format(location), location.getId().toString(), false));
}
Collections.sort(locationOptions, new OptionComparator());

for (Option option : locationOptions) {
w.addOption(option);
}

if (existingAttribute != null && StringUtils.hasText(existingAttribute.getValue())) {
w.setInitialValue(existingAttribute.getValue());
}

return w;
}

// ---------------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------------

/**
* Converts the raw submitted string from the widget into the canonical storage value for
* the attribute. For Concept and Location types, we look up the actual typed object and call
* its {@link org.openmrs.Attributable#serialize()} method so that core controls the format.
*/
private String toStorageValue(String rawValue) {
if (!StringUtils.hasText(rawValue)) {
return rawValue;
}
String format = attributeType.getFormat();
if (Concept.class.getName().equals(format)) {
Concept concept = Context.getConceptService().getConcept(Integer.parseInt(rawValue));
return concept != null ? concept.serialize() : rawValue;
} else if (Location.class.getName().equals(format)) {
Location location = Context.getLocationService().getLocation(Integer.parseInt(rawValue));
return location != null ? location.serialize() : rawValue;
}
return rawValue;
}

// ---------------------------------------------------------------------------------
// HtmlGeneratorElement
// ---------------------------------------------------------------------------------

@Override
public String generateHtml(FormEntryContext context) {
StringBuilder sb = new StringBuilder();
sb.append(valueWidget.generateHtml(context));
if (context.getMode() != Mode.VIEW) {
sb.append(errorWidget.generateHtml(context));
}
return sb.toString();
}

// ---------------------------------------------------------------------------------
// FormSubmissionControllerAction
// ---------------------------------------------------------------------------------

@Override
public Collection<FormSubmissionError> validateSubmission(FormEntryContext context, HttpServletRequest submission) {
// No current validation
return Collections.emptyList();
}

@Override
public void handleSubmission(FormEntrySession session, HttpServletRequest submission) {
FormEntryContext context = session.getContext();
Patient patient = session.getPatient();
if (patient == null) {
log.warn("<personAttribute> handleSubmission: patient is null, skipping");
return;
}

Object rawValue = valueWidget.getValue(context, submission);
String value = (rawValue == null) ? null : rawValue.toString().trim();

if (existingAttribute != null) {
if (!StringUtils.hasText(value)) {
existingAttribute.setVoided(true);
existingAttribute.setVoidedBy(Context.getAuthenticatedUser());
existingAttribute.setVoidReason("Cleared via htmlformentry form submission");
existingAttribute.setDateVoided(new Date());
} else {
existingAttribute.setValue(toStorageValue(value));
existingAttribute.setChangedBy(Context.getAuthenticatedUser());
existingAttribute.setDateChanged(new Date());
}
} else {
if (StringUtils.hasText(value)) {
PersonAttribute newAttribute = new PersonAttribute();
newAttribute.setAttributeType(attributeType);
newAttribute.setValue(toStorageValue(value));
patient.addAttribute(newAttribute);
}
}

// Signal that the patient needs to be persisted
session.getSubmissionActions().setPatientUpdateRequired(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.openmrs.module.htmlformentry.handler;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.openmrs.Concept;
import org.openmrs.LocationTag;
import org.openmrs.PersonAttributeType;
import org.openmrs.module.htmlformentry.BadFormDesignException;
import org.openmrs.module.htmlformentry.FormEntryContext;
import org.openmrs.module.htmlformentry.FormEntrySession;
import org.openmrs.module.htmlformentry.element.PersonAttributeSubmissionElement;
import org.w3c.dom.Node;

/**
* Handles the {@code <personAttribute>} tag, which allows viewing, entering, and editing a person
* attribute of type String, Concept, or Location.
*
* <p>Supported attributes:
* <ul>
* <li>{@code attributeType} (required) – UUID of the {@link PersonAttributeType}</li>
* <li>{@code answerConceptIds} (required for Concept-format types) – comma-separated list of
* concept IDs or UUIDs shown in the dropdown</li>
* <li>{@code tags} (optional, Location-format types only) – comma-separated location tag names
* used to filter the location dropdown</li>
* </ul>
*/
public class PersonAttributeTagHandler extends AbstractTagHandler {

@Override
protected List<AttributeDescriptor> createAttributeDescriptors() {
List<AttributeDescriptor> descriptors = new ArrayList<AttributeDescriptor>();
descriptors.add(new AttributeDescriptor("attributeType", PersonAttributeType.class));
descriptors.add(new AttributeDescriptor("answerConceptIds", Concept.class));
descriptors.add(new AttributeDescriptor("tags", LocationTag.class));
return Collections.unmodifiableList(descriptors);
}

@Override
public boolean doStartTag(FormEntrySession session, PrintWriter out, Node parent, Node node)
throws BadFormDesignException {
FormEntryContext context = session.getContext();
PersonAttributeSubmissionElement element = new PersonAttributeSubmissionElement(context, getAttributes(node));
session.getSubmissionController().addAction(element);
out.print(element.generateHtml(context));
return false; // no body to process
}

@Override
public void doEndTag(FormEntrySession session, PrintWriter out, Node parent, Node node)
throws BadFormDesignException {
// nothing needed
}
}
3 changes: 3 additions & 0 deletions api/src/main/resources/moduleApplicationContext.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@
<entry key="appointments">
<bean class="org.openmrs.module.htmlformentry.handler.AppointmentsTagHandler"/>
</entry>
<entry key="personAttribute">
<bean class="org.openmrs.module.htmlformentry.handler.PersonAttributeTagHandler"/>
</entry>
</map>
</property>
</bean>
Expand Down
Loading