/** * Actually submits the selected translation * * @param userName The content of the User Name Field * @param userEmail The content of the User E-Mail address Field */ public void submitTranslation(String userName, String userEmail, boolean contribute) { if (this.isValidEmailAddress(userEmail)) { this.ui.alert(InternationalisationUtils.getI18NString(I18N_COMMON_INVALID_EMAIL)); } else { String subject = "FrontlineSMS translation: " + this.languageBundle.getLanguageName(); String textContent = "Sent translation: " + this.languageBundle.getLanguageName() + " (" + this.languageBundle.getLanguageCode() + ").\n" + userName + (contribute ? " would" : " wouldn't") + " like to appear as a contributor for this translation."; try { FrontlineUtils.sendToFrontlineSupport( userName, userEmail, subject, textContent, InternationalisationUtils.getLanguageDirectory() + File.separator + languageBundle.getFilename()); this.removeDialog(); this.ui.infoMessage(InternationalisationUtils.getI18NString(I18N_TRANSLATION_SENT)); } catch (EmailException e) { this.ui.alert(InternationalisationUtils.getI18NString(I18N_UNABLE_SEND_TRANSLATION)); } } }
/* * RemindersFactory * @author Dale Zak * * see {@link "http://www.frontlinesms.net"} for more details. * copyright owned by Kiwanja.net */ public final class RemindersFactory { private static Logger LOG = FrontlineUtils.getLogger(RemindersFactory.class); /* * Get list of Reminder class implementations * (To add a new Reminder classes to the project, append a new row to the file * /resources/META-INF/services/net.frontlinesms.plugins.reminders.data.domain.Reminder * with the full package and class name of the new implementing Reminder class) */ public static List<Reminder> getReminderClasses() { if (reminderClasses == null) { reminderClasses = new ArrayList<Reminder>(); for (Reminder reminder : ServiceLoader.load(Reminder.class)) { LOG.debug("Reminder Discovered: " + reminder); reminderClasses.add(reminder); } } return reminderClasses; } private static List<Reminder> reminderClasses = null; /* * Create a Reminder according to it's occurrence */ public static Reminder createReminder( long startdate, long enddate, Type type, String recipients, String subject, String content, String occurrence) { for (Reminder reminderClass : getReminderClasses()) { if (reminderClass.getOccurrence().equalsIgnoreCase(occurrence)) { try { Reminder newReminder = reminderClass.getClass().newInstance(); newReminder.setStartDate(startdate); newReminder.setEndDate(enddate); newReminder.setType(type); newReminder.setRecipients(recipients); newReminder.setSubject(subject); newReminder.setContent(content); LOG.debug("Reminder Created: " + newReminder); return newReminder; } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } LOG.debug("Unable to find class for occurrence: " + occurrence); return null; } }
/* * WeekdaysReminder * @author Dale Zak * * see {@link "http://www.frontlinesms.net"} for more details. * copyright owned by Kiwanja.net */ @Entity @DiscriminatorValue(value = "weekdays") public class WeekdaysReminder extends Reminder { private static final Logger LOG = FrontlineUtils.getLogger(WeekdaysReminder.class); public WeekdaysReminder() {} public WeekdaysReminder( long startdate, long enddate, Type type, String recipients, String subject, String content) { super(startdate, enddate, type, recipients, subject, content); } @Override public String getOccurrenceLabel() { return InternationalisationUtils.getI18nString(RemindersConstants.WEEKDAYS); } @Override public String getOccurrence() { return "weekdays"; } public static boolean isSatisfiedBy(String occurrence) { return "weekdays".equalsIgnoreCase(occurrence); } public long getPeriod() { return 1000 * 60 * 60 * 24; } public void run() { LOG.debug("run: " + this); Calendar now = Calendar.getInstance(); Calendar start = this.getStartCalendar(); Calendar end = this.getEndCalendar(); if (now.after(end)) { this.setStatus(Status.SENT); this.stopReminder(); this.refreshReminder(); } else if ((now.equals(start) || now.after(start)) && now.get(Calendar.MINUTE) == start.get(Calendar.MINUTE) && now.get(Calendar.HOUR_OF_DAY) == start.get(Calendar.HOUR_OF_DAY) && Arrays.asList(WEEKDAYS).contains(now.get(Calendar.DAY_OF_WEEK))) { this.sendReminder(); } } private static final int[] WEEKDAYS = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY }; }
/** Base UI used for FrontlineSMS. */ @SuppressWarnings("serial") public abstract class FrontlineUI extends ExtendedThinlet implements ThinletUiEventHandler { // > UI DEFINITION FILES /** Thinlet UI layout File: alert popup box */ protected static final String UI_FILE_ALERT = "/ui/core/util/dgAlert.xml"; /** Thinlet UI layout File: info popup box */ protected static final String UI_FILE_INFO = "/ui/core/util/dgInfo.xml"; // > UI COMPONENTS /** Component of {@link #UI_FILE_ALERT} which contains the message to display */ private static final String COMPONENT_ALERT_MESSAGE = "alertMessage"; /** Component of {@link #UI_FILE_INFO} which contains the message to display */ private static final String COMPONENT_INFO_MESSAGE = "infoMessage"; // > INSTANCE PROPERTIES /** Logging object */ protected final Logger log = FrontlineUtils.getLogger(this.getClass()); /** * The language bundle currently in use. N.B. THIS CAN BE NULL, IN WHICH CASE THE DEFAULT BUNDLE * SHOULD BE USED TODO handle and document this better */ public static LanguageBundle currentResourceBundle; /** * Frame launcher that this UI instance is displayed within. We need to keep a handle on it so * that we can dispose of it when we quit or change UI modes. */ protected FrameLauncher frameLauncher; /** * Gets the icon for a specific language bundle * * @param languageBundle * @return the flag image for the language bundle, or <code>null</code> if none could be found. */ public Image getFlagIcon(LanguageBundle languageBundle) { return getFlagIcon(languageBundle.getCountry()); } /** * Gets the icon for a specific country * * @param languageBundle * @return the flag image for the language bundle, or <code>null</code> if none could be found. */ public Image getFlagIcon(String country) { String flagFile = country != null ? "/icons/flags/" + country + ".png" : null; return country == null ? null : getIcon(flagFile); } /** * Loads a Thinlet UI descriptor from an XML file. If there are any problems loading the file, * this will log Throwables thrown and allow the program to continue running. * * <p>{@link #loadComponentFromFile(String, Object)} should always be used by external handlers in * preference to this. * * @param filename path of the UI XML file to load from * @return thinlet component loaded from the file */ public Object loadComponentFromFile(String filename) { return loadComponentFromFile(filename, this); } /** * Loads a Thinlet UI descriptor from an XML file and sets the provided event handler. If there * are any problems loading the file, this will log Throwables thrown and allow the program to * continue running. * * @param filename path of the UI XML file to load from * @param thinletEventHandler event handler for the UI component * @return thinlet component loaded from the file */ public Object loadComponentFromFile(String filename, ThinletUiEventHandler thinletEventHandler) { log.trace("ENTER"); try { log.debug("Filename [" + filename + "]"); log.trace("EXIT"); return parse(filename, thinletEventHandler); } catch (Throwable t) { log.error("Error parsing file [" + filename + "]", t); log.trace("EXIT"); throw new RuntimeException(t); } } /** * This method opens a fileChooser. * * @param textFieldToBeSet The text field whose value should be sert to the chosen file */ public void showFileChooser(Object textFieldToBeSet) { FileChooser.showFileChooser(this, textFieldToBeSet); } /** * Popup an alert to the user with the supplied message. * * @param alertMessage */ public void alert(String alertMessage) { Object alertDialog = loadComponentFromFile(UI_FILE_ALERT); setText(find(alertDialog, COMPONENT_ALERT_MESSAGE), alertMessage); add(alertDialog); } /** * Popup an info message to the user with the supplied message. * * @param infoMessage */ public void infoMessage(String infoMessage) { Object infoDialog = loadComponentFromFile(UI_FILE_INFO); setText(find(infoDialog, COMPONENT_INFO_MESSAGE), infoMessage); add(infoDialog); } /** * Removes the supplied dialog from the application. * * @param dialog */ public void removeDialog(Object dialog) { remove(dialog); } /** * Opens a link in the system browser * * @param url the url to open * @see FrontlineUtils#openExternalBrowser(String) */ public void openBrowser(String url) { FrontlineUtils.openExternalBrowser(url); } /** * Opens a page of the help manual * * @param page The name of the help manual page, including file extension. */ public void showHelpPage(String page) { FrontlineUtils.openHelpPageInBrowser(page); } /** Shows an error dialog informing the user that an unhandled error has occurred. */ @Override protected void handleException(Throwable throwable) { log.error("Unhandled exception from thinlet.", throwable); ErrorUtils.showErrorDialog( "Unexpected error", "There was an unexpected error.", throwable, false); } }
/** * Opens a page of the help manual * * @param page The name of the help manual page, including file extension. */ public void showHelpPage(String page) { FrontlineUtils.openHelpPageInBrowser(page); }
/** * Opens a link in the system browser * * @param url the url to open * @see FrontlineUtils#openExternalBrowser(String) */ public void openBrowser(String url) { FrontlineUtils.openExternalBrowser(url); }
/** * Base properties wrapper class. * * @author Alex */ class BasePropertySet { // > STATIC CONSTANTS /** Logging object for this instance. */ public static final Logger LOG = FrontlineUtils.getLogger(BasePropertySet.class); // > INSTANCE PROPERTIES /** Map from property key to value */ private Map<String, String> properties; // > CONSTRUCTORS // > ACCESSORS /** * @param propertyKey The key for the property to get * @return value from {@link #properties} */ String getProperty(String propertyKey) { return this.properties.get(propertyKey); } Map<String, String> getProperties() { return properties; } /** * Set the properties. This should be done exactly once. * * @param properties value for {@link #properties} */ protected void setProperties(Map<String, String> properties) { assert (this.properties == null) : "Properties already set. Cannot be changed."; assert (properties != null) : "Cannot set properties to null."; this.properties = properties; } // > INSTANCE HELPER METHODS // > STATIC FACTORIES // > STATIC HELPER METHODS /** * Load properties from an {@link InputStream}. * * @param inputStream The input stream to load the properties from. * @return The loaded properties * @throws IOException If there was a problem loading the properties from the supplied {@link * InputStream}. */ static void load(Map<String, String> map, InputStream inputStream) throws IOException { if (inputStream == null) throw new NullPointerException("The supplied input stream was null."); BufferedReader in = null; try { in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); String line; while ((line = in.readLine()) != null) { line = line.trim(); if (line.length() == 0 || line.charAt(0) == '#') { // This is a comment, so we should remember it to write back later } else { int splitChar = line.indexOf('='); if (splitChar <= 0) { // there's no "key=value" pair on this line, but it does have text on it. That's // not strictly legal, so we'll log a warning and carry on. LOG.warn("Bad line in properties file: '" + line + "'"); } else { String key = line.substring(0, splitChar); if (map.containsKey(key)) { // This key has already been read from the language file. Ignore the new value. LOG.warn("Duplicate key in properties file: ''"); } else { String value = line.substring(splitChar + 1); map.put(key, value); } } } } LOG.trace("EXIT"); } finally { // Close all streams if (in != null) try { in.close(); } catch (IOException ex) { // nothing we can do except log the exception LOG.warn("Exception thrown while closing stream 'fis'.", ex); } } } static HashMap<String, String> load(InputStream inputStream) throws IOException { HashMap<String, String> map = new HashMap<String, String>(); load(map, inputStream); return map; } }
public class IncomingFormMatcher implements EventObserver { private static Logger LOG = FrontlineUtils.getLogger(MedicFormResponse.class); private Levenshtein levenshtein; private JaroWinkler jaroWinkler; private FormDao vanillaFormDao; private MedicFormDao formDao; private MedicFormResponseDao formResponseDao; private MedicFormFieldResponseDao formFieldResponseDao; private PatientDao patientDao; private CommunityHealthWorkerDao chwDao; private SimpleDateFormat shortFormatter; private DateFormat longFormatter; public IncomingFormMatcher(ApplicationContext appCon) { vanillaFormDao = (FormDao) appCon.getBean("formDao"); formDao = (MedicFormDao) appCon.getBean("MedicFormDao"); formResponseDao = (MedicFormResponseDao) appCon.getBean("MedicFormResponseDao"); formFieldResponseDao = (MedicFormFieldResponseDao) appCon.getBean("MedicFormFieldResponseDao"); patientDao = (PatientDao) appCon.getBean("PatientDao"); chwDao = (CommunityHealthWorkerDao) appCon.getBean("CHWDao"); ((EventBus) appCon.getBean("eventBus")).registerObserver(this); // create the test harness ExtendedThinlet thinlet = new ExtendedThinlet(); Object panel = thinlet.createPanel("mainPanel"); thinlet.setWeight(panel, 1, 1); Object button = thinlet.createButton("Click me to test form handling"); thinlet.setAction(button, "testHandler", null, this); thinlet.add(panel, button); thinlet.add(panel); // set up the date formatter String dateString = InternationalisationUtils.getI18NString(FrontlineSMSConstants.DATEFORMAT_YMD); dateString = dateString.toLowerCase(); dateString = dateString.replace("mm", "MM"); dateString = dateString.replace("yyyy", "yy"); shortFormatter = new SimpleDateFormat(dateString); longFormatter = InternationalisationUtils.getDateFormat(); // FrameLauncher f = new FrameLauncher("Test form handling",thinlet,200,100,null) // { public void windowClosing(WindowEvent e){ dispose(); }}; } public void testHandler() { List<ResponseValue> responses = new ArrayList<ResponseValue>(); responses.add(new ResponseValue("Frankie Tenday")); responses.add(new ResponseValue("16/06/1964")); responses.add(new ResponseValue("First answer")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); responses.add(new ResponseValue("false")); responses.add(new ResponseValue("true")); FormResponse fr = new FormResponse("2099707079", vanillaFormDao.getFromId(10L), responses); handleFormResponse(fr); } public boolean isMedicForm(Form f) { return formDao.getMedicFormForForm(f) != null; } /** * When a form is submitted, this method attempts to pair that form with the patient that is its * subject. If there is more than one possibility, the form is posted to the changelog with a list * of suggested patients. If there is only one real possibility, but that possibility does not * match exactly, it is also posted to the changelog with a snippet about what did not match and * what did. * * @param formResponse */ public void handleFormResponse(FormResponse formResponse) { // if the form submitted is not a medic form, then do nothing if (!isMedicForm(formResponse.getParentForm())) { return; } // get the medic form equivalent of the form submitted MedicForm mForm = formDao.getMedicFormForForm(formResponse.getParentForm()); CommunityHealthWorker submitter = chwDao.getCommunityHealthWorkerByPhoneNumber(formResponse.getSubmitter()); MedicFormResponse mfr = new MedicFormResponse(formResponse, mForm, submitter, null); mfr.setSubject(getFinalCandidate(mfr)); formResponseDao.saveMedicFormResponse(mfr); } /** * Returns a float from 1.0 - 0.0 that measures the similarity between 2 strings (mainly names) * using the jaro-winkler method. The higher the number, the greater the similarity * * @param patientName string 1 (generally the name of the patient) * @param responseName string 2 (generally the name typed into the form * @return a float from 1.0 - 0.0 */ private float getNameDistance(String patientName, String responseName) { if (jaroWinkler == null) { jaroWinkler = new JaroWinkler(); } return jaroWinkler.getSimilarity(patientName, responseName); } /** * Returns the edit distance between 2 strings, as implemented by Levenshtein * * @param stringOne * @param stringTwo * @return a value from 0.0 -1.0. The greater the similarity, the higher the number */ public float getEditDistance(String stringOne, String stringTwo) { if (levenshtein == null) { levenshtein = new Levenshtein(); } return levenshtein.getSimilarity(stringOne, stringTwo); } public List<Candidate> getCandidatesForResponse(MedicFormResponse response) { Log.info("Attempting to map response"); // get the CHW that submitted the form CommunityHealthWorker chw = (CommunityHealthWorker) response.getSubmitter(); // get the list of patients that the CHW cares for ArrayList<Patient> patients = (ArrayList<Patient>) patientDao.getPatientsForCHW(chw); ArrayList<Candidate> candidates = new ArrayList<Candidate>(); // iterate through all fields on the form, seeing if they are mapped to patient identifying // fields // e.g. Birthdate, Name, and Patient ID for (Patient patient : patients) { candidates.add(new Candidate(patient)); } List<MedicFormFieldResponse> responses = response.getResponses(); try { responses.get(0).getDateSubmitted(); } catch (Exception e) { responses = formFieldResponseDao.getResponsesForFormResponse(response); } for (MedicFormFieldResponse fieldResponse : responses) { // if it is mapped to a namefield, score it as a name if (fieldResponse.getField().getMapping() == PatientFieldMapping.NAMEFIELD) { for (Candidate c : candidates) { c.setNameScore( getNameDistance(c.getName().toLowerCase(), fieldResponse.getValue().toLowerCase())); } // if it is mapped to an id field, score it as an ID } else if (fieldResponse.getField().getMapping() == PatientFieldMapping.IDFIELD) { for (Candidate c : candidates) { c.setIdScore(getEditDistance(c.getStringID(), fieldResponse.getValue())); } // if it is mapped as a bday field, score it as a bday } else if (fieldResponse.getField().getMapping() == PatientFieldMapping.BIRTHDATEFIELD) { for (Candidate c : candidates) { if (fieldResponse.getValue().length() <= 8) { c.setBirthdateScore( getEditDistance( shortFormatter.format(c.getPatient().getBirthdate()), fieldResponse.getValue())); } else { c.setBirthdateScore( getEditDistance( longFormatter.format(c.getPatient().getBirthdate()), fieldResponse.getValue())); } } } } Collections.sort(candidates); return candidates.subList(0, 5); } public float getConfidence(Patient subject, MedicFormResponse mfr) { List<MedicFormFieldResponse> responses = formFieldResponseDao.getResponsesForFormResponse(mfr); float result = 0.0F; float total = 0.0F; for (MedicFormFieldResponse fieldResponse : responses) { // if it is mapped to a namefield, score it as a name if (fieldResponse.getField().getMapping() == PatientFieldMapping.NAMEFIELD) { result += getNameDistance(subject.getName(), fieldResponse.getValue()); total += 1.0F; // if it is mapped to an id field, score it as an ID } else if (fieldResponse.getField().getMapping() == PatientFieldMapping.IDFIELD) { result += getEditDistance(subject.getStringID(), fieldResponse.getValue()); total += 1.0F; // if it is mapped as a bday field, score it as a bday } else if (fieldResponse.getField().getMapping() == PatientFieldMapping.BIRTHDATEFIELD) { if (fieldResponse.getValue().length() <= 8) { result += getEditDistance( shortFormatter.format(subject.getBirthdate()), fieldResponse.getValue()); } else { result += getEditDistance( longFormatter.format(subject.getBirthdate()), fieldResponse.getValue()); } total += 1.0F; } } return (result / total) * 100; } /** * Returns the 'final candidate', i.e. the most likely candidate for the subject of the supplied * form response. This is determined by fetching all candidates, and selecting the ones that are * over 97% confidence. If there is only one over 97% confidence, then that candidate is returned. * Otherwise, this method returns null * * @param response * @return */ public Person getFinalCandidate(MedicFormResponse response) { List<Candidate> candidates = getCandidatesForResponse(response); List<Candidate> finalCandidates = new ArrayList<Candidate>(); for (Candidate c : candidates) { if (c.getAverageScore() >= 97F) { finalCandidates.add(c); } } if (finalCandidates.size() == 1) { return finalCandidates.get(0).getPatient(); } return null; } public void notify(FrontlineEventNotification notification) { if (notification instanceof EntitySavedNotification<?>) { EntitySavedNotification<?> sNotification = (EntitySavedNotification<?>) notification; if (sNotification.getDatabaseEntity() instanceof FormResponse) { handleFormResponse((FormResponse) sNotification.getDatabaseEntity()); } } } }