Java tutorial
/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/ */ package org.phenotips.data.internal.controller; import org.phenotips.data.DictionaryPatientData; import org.phenotips.data.Patient; import org.phenotips.data.PatientData; import org.phenotips.data.PatientDataController; import org.phenotips.data.VocabularyProperty; import org.phenotips.data.internal.AbstractPhenoTipsVocabularyProperty; import org.xwiki.component.util.DefaultParameterizedType; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.ObjectPropertyReference; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.inject.Provider; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.BaseObject; import com.xpn.xwiki.objects.BaseProperty; /** * Base class for handling data in different types of objects (String, List, etc) and preserving the object type. Has * custom functions for dealing with conversion to booleans, and vocabulary codes to human readable labels. * * @param <T> the type of data being managed by this component, usually {@code String}, but other types are possible, * even more complex types * @version $Id: 9568611e1805262465c066bd27ac6419dccb8266 $ * @since 1.0RC1 */ public abstract class AbstractComplexController<T> implements PatientDataController<T> { /** Logging helper object. */ @Inject private Logger logger; @Inject private Provider<XWikiContext> contextProvider; @Override @SuppressWarnings("unchecked") public PatientData<T> load(Patient patient) { try { XWikiDocument doc = patient.getXDocument(); BaseObject data = doc.getXObject(getXClassReference()); if (data == null) { return null; } Map<String, T> result = new LinkedHashMap<>(); for (String propertyName : getProperties()) { BaseProperty<ObjectPropertyReference> field = (BaseProperty<ObjectPropertyReference>) data .getField(propertyName); if (field != null) { Object propertyValue = field.getValue(); /* If the controller only works with codes, store the Vocabulary Instances rather than Strings */ if (getCodeFields().contains(propertyName) && isCodeFieldsOnly()) { List<VocabularyProperty> propertyValuesList = new LinkedList<>(); List<String> terms = (List<String>) propertyValue; for (String termId : terms) { propertyValuesList.add(new QuickVocabularyProperty(termId)); } propertyValue = propertyValuesList; } result.put(propertyName, (T) propertyValue); } } return new DictionaryPatientData<>(getName(), result); } catch (Exception e) { this.logger.error(ERROR_MESSAGE_LOAD_FAILED, e.getMessage()); } return null; } @Override public void writeJSON(Patient patient, JSONObject json, Collection<String> selectedFieldNames) { PatientData<T> data = patient.getData(getName()); String jsonPropertyName = getJsonPropertyName(); if (data == null || data.size() == 0) { return; } Iterator<Map.Entry<String, T>> iterator = data.dictionaryIterator(); JSONObject container = json.optJSONObject(jsonPropertyName); while (iterator.hasNext()) { Map.Entry<String, T> item = iterator.next(); String itemKey = item.getKey(); Object formattedValue = format(itemKey, item.getValue()); if (selectedFieldNames == null || selectedFieldNames.contains(getControllingFieldName(item.getKey()))) { if (container == null) { // put() is placed here because we want to create the property iff at least one field is set/enabled json.put(jsonPropertyName, new JSONObject()); container = json.optJSONObject(jsonPropertyName); } container.put(itemKey, formattedValue); } } } /** * @return name of controlling field which is responsible for export fields grouping */ protected String getControllingFieldName(String field) { return field; } /** * @return list of fields which should be resolved to booleans */ protected abstract List<String> getBooleanFields(); /** * @return list of fields which contain HPO codes, and therefore additional data can be obtained, such as human * readable name */ protected abstract List<String> getCodeFields(); /** * In case all fields are code fields, then the controller can store data in memory as vocabulary objects rather * than strings. * * @return true if all fields contain HPO codes */ protected boolean isCodeFieldsOnly() { Type type = this.getClass().getGenericSuperclass(); if (!(type instanceof ParameterizedType)) { return false; } ParameterizedType t = (ParameterizedType) type; return new DefaultParameterizedType(null, List.class, VocabularyProperty.class) .equals(t.getActualTypeArguments()[0]); } /** * Checks if a the value needs to be formatted and then calls the appropriate function. * * @param key the key under which the value will be stored in JSON * @param value the value which possibly needs to be formatted * @return the formatted object or the original value */ @SuppressWarnings("unchecked") private Object format(String key, Object value) { if (value == null || "Unknown".equals(value)) { return JSONObject.NULL; } if (getBooleanFields().contains(key)) { return booleanConvert(value.toString()); } else if (getCodeFields().contains(key)) { return codeToHumanReadable((List<T>) value); } else { return value; } } /** For converting JSON into internal representation. */ private Object inverseFormat(String key, Object value) { if (value != null && !value.equals(null)) { try { if (this.getBooleanFields().contains(key)) { return value; } else if (this.getCodeFields().contains(key)) { LinkedList<VocabularyProperty> terms = new LinkedList<>(); for (Object termJson : (JSONArray) value) { VocabularyProperty term = new QuickVocabularyProperty((JSONObject) termJson); terms.add(term); } return terms; } else if (value instanceof JSONArray) { List<Object> list = new LinkedList<>(); for (Object o : (JSONArray) value) { list.add(o); } return list; } else { return value.toString(); } } catch (Exception ex) { // improper format } } return null; } private Boolean booleanConvert(String integerValue) { if (StringUtils.equals("0", integerValue)) { return false; } else if (StringUtils.equals("1", integerValue)) { return true; } else { return null; } } /** * For different types to ones that can be saved by XWiki. * * @return the converted value if `value` is convertible, original `value` otherwise */ private Object saveFormat(Object value) { if (value == null) { return null; } if (value instanceof Boolean) { return (Boolean) value ? 1 : 0; } else if (NumberUtils.isCreatable(String.valueOf(value))) { try { return Integer.valueOf(value.toString()); } catch (Exception ex) { return value; } } return value; } private JSONArray codeToHumanReadable(List<T> codes) { JSONArray labeledList = new JSONArray(); for (T code : codes) { QuickVocabularyProperty term; if (code instanceof QuickVocabularyProperty) { term = (QuickVocabularyProperty) code; } else { term = new QuickVocabularyProperty(code.toString()); } labeledList.put(term.toJSON()); } return labeledList; } /** * Check whether the property is present in the patient data to be updated. * * @return true if present, false otherwise. */ private Boolean isInKeySet(PatientData<T> data, String property) { Iterator<String> keys = data.keyIterator(); while (keys.hasNext()) { if (keys.next().equals(property)) { return true; } } return false; } @Override public void writeJSON(Patient patient, JSONObject json) { writeJSON(patient, json, null); } @Override public void save(Patient patient) { BaseObject dataHolder = patient.getXDocument().getXObject(getXClassReference()); PatientData<T> data = patient.getData(this.getName()); if (dataHolder == null || data == null) { return; } XWikiContext context = this.contextProvider.get(); for (String propertyName : getProperties()) { Object propertyValue = data.get(propertyName); if (isInKeySet(data, propertyName)) { if (this.getCodeFields().contains(propertyName) && this.isCodeFieldsOnly()) { @SuppressWarnings("unchecked") List<VocabularyProperty> terms = (List<VocabularyProperty>) propertyValue; List<String> listToStore = new LinkedList<>(); for (VocabularyProperty term : terms) { String name = StringUtils.isNotBlank(term.getId()) ? term.getId() : term.getName(); listToStore.add(name); } dataHolder.set(propertyName, listToStore, context); } else { dataHolder.set(propertyName, this.saveFormat(propertyValue), context); } } } } @Override public PatientData<T> readJSON(JSONObject json) { Map<String, T> result = new LinkedHashMap<>(); JSONObject container = json.optJSONObject(getJsonPropertyName()); if (container == null) { return null; } for (String propertyName : getProperties()) { if (container.has(propertyName)) { @SuppressWarnings("unchecked") T value = (T) this.inverseFormat(propertyName, container.get(propertyName)); result.put(propertyName, value); } } return new DictionaryPatientData<>(getName(), result); } protected abstract List<String> getProperties(); protected abstract String getJsonPropertyName(); /** * The XClass used for storing data managed by this controller. By default, data is stored in the main * {@code PhenoTips.PatientClass} object that defines the patient record. Override this method if a different type * of XObject is used. * * @return a local reference (without the wiki reference) pointing to the XDocument containing the target XClass * @since 1.2RC1 */ protected EntityReference getXClassReference() { return Patient.CLASS_REFERENCE; } /** * There exists no class currently that would be able to covert a vocabulary code into a human readable format given * only a code string. Considering that there is a need for such functionality, there are 3 options: copy the code * that performs the function needed into the controller, create a class extending * {@link org.phenotips.data.internal.AbstractPhenoTipsVocabularyProperty} in a separate file, or create such class * here. Given the fact the the {@link org.phenotips.data.internal.AbstractPhenoTipsVocabularyProperty} is abstract * only by having a protected constructor, which fully satisfies the needed functionality, it makes the most sense * to put {@link QuickVocabularyProperty} here. */ protected static final class QuickVocabularyProperty extends AbstractPhenoTipsVocabularyProperty { public QuickVocabularyProperty(String id) { super(id); } public QuickVocabularyProperty(JSONObject json) { super(json); } } }