Adding a New Field to an Entity

Introduction

This guide explains how to add a new plain field to the SORMAS data schema. It does not explain how to add list fields, new sections, or concepts to SORMAS.

Important: This is the first version of this guide. Please get in contact if anything is not clear or if you have suggestions on how to improve the guide, the source code, or the underlying architecture.

Example use cases

  • A symptom is needed for a specific disease (e.g. headache)

  • A field with additional epidemiological details on a case (e.g. contact with a special type of animal)

Preparation (!)

  1. Make sure the field is not already in the system. SORMAS has a lot of data fields and many of them are only used for a few diseases and hidden for other ones. The best way to make sure is to open the data dictionary and go through the existing fields of all related data sections:

  2. Clearly define the field:

    • Name and description

    • Field type: plain text, pre-defined values (enum), date, time, number

    • Example values

    • Who is supposed to enter the field?

    • Who is supposed to read the field?

    • Is it a required field?

  3. Make sure that your development setup is ready and functioning.

Adding the field to the SORMAS API

The SORMAS API is the heart of the data schema. Accordingly, this is where you have to get started.

  1. Identify the class where the field needs to be added. For most scenarios, you will only need to have a look at CaseDataDto.java and all the fields used in there, e.g. SymptomsDto.

  2. Add the field as a private member of the class with a get- and set method. In addition, a static final String is to be used as a constant to identify the field.

  3. Add needed annotations that configure the field's usage for different countries and diseases, validation (e.g. Size), and whether the field is required.

  4. If the field has pre-defined values add an enum in the package of the class. Have a look at one of the existing enums for reference.

  5. Add the caption to captions.properties and description to description.properties in the project resources. For enums add all values to enum.properties.

    Symptoms.soreThroat = Sore throat/pharyngitis
  6. When you make additions/changes on keys in captions.properties, strings.properties, or validations.properties you have to run I18nConstantGenerator (run as ... Java Application) to update the corresponding Constants classes.

  7. Very important: We have now officially made a change to the API which likely means that old versions are no longer fully compatible. When data with the new field is sent to a mobile device with an old version, it will not know about the field, and the data is lost on the device. When the data is sent back to the server the empty field may override any existing data and it's now also lost on the server itself. To avoid this the following has to be done:

    • Open the InfoProvider.getMinimumRequiredVersion method.

    • Set the version to the current development version (without the -SNAPSHOT). You can find the current version in the maven pom.xml configuration file.

Adding the field to the SORMAS backend

The SORMAS backend is responsible for persisting all data into the server's database and making this data accessible. Accordingly, it's necessary to extend the persistence logic with the new field.

  1. Identify the entity class that matches the API class where the field was added (e.g. Case.java).

  2. Add the field as a private member of the entity class with a get- and set method.

  3. Add the correct JPA annotation to the get-method (see other fields for examples).

    @Enumerated(EnumType.STRING) public SymptomState getSoreThroat() { return soreThroat; }

In the backend, we only add not null constraints to basic metadata fields like id, uuid, changedate and similar, NOT to content data fields (e.g. first name) that are marked as required in the API. Checks on whether those fields are filled are done in the SORMAS backend, not in the database.

In addition to this, the sormas_schema.sql file in sormas-base/sql has to be extended:

  1. Scroll to the bottom and add a new schema_version block. It starts with a comment that contains the date and a short info on the changes and the GitHub issue id and ends with an "INSERT INTO schema_version..." where the version has to be incremented.

    -- 2019-02-20 Additional signs and symptoms #938 INSERT INTO schema_version (version_number, comment) VALUES (131, 'Additional signs and symptoms #938');
  2. Within this block add a new column to the table that matches the entity where the new field was added in sormas-backend. You can scroll up to see examples of this for all the different field types. Note that the column name is all lowercase.

    ALTER TABLE symptoms ADD COLUMN sorethroat varchar(255);
  3. Make sure to also add the column to the corresponding history table in the database.

  4. Update default values if needed.

  5. Try to execute the SQL on your system!

Now we need to make sure data in the new field is exchanged between the backend entity classes and the API data transfer objects.

  1. Identify the *FacadeEjb class for the entity (e.g. CaseFacadeEjb).

  2. Extend the toDto and fromDto/fillOrBuildEntity methods to exchange data between the API class and the backend entity class that is persisted.

    target.setSoreThroat(source.getSoreThroat());

Now we need to make sure data in the new field is exported by the detailed export.

  1. Identify corresponding *ExportDto (e.g. CaseExportDto)

  2. Add the field as a private member of the dto class with a get- and set-method.

  3. Add the @Order annotation on the getter method of the new field

    @Order(33) public SymptomState getSoreThroat() { return soreThroat; }

    NOTE: The @Order numbers should be unique so please increase the order of the getters below if there are any.

  4. Initialize the new field in the constructor

  5. Add the new field in the selection list in the getExportList method of the *FacadeEJB

    cq.multiselect( ..., caseRoot.get(Case.SORE_THROAT), ... )

    NOTE: Make sure the order of the fields in the selection list corresponds the order of arguments in the constructor of *ExportDto class

Adding the field to the SORMAS UI

The SORMAS UI is a web application that is used by supervisors, laboratory users, national instances, and others. Here we have to extend the form where the field is supposed to be shown and edited. Note that the web application uses the same form for read and write mode, so the field only needs to be added once.

  1. Identify the Form class where the new field is supposed to be shown. Examples of this are SymptomsForm.class, CaseDataForm.class or EpiDataForm.class.

  2. Add the new field to the HTML layout definition in the top of the form class. The forms use column layouts based on the Bootstrap CSS library.

    LayoutUtil.fluidRowLocs(SymptomsDto.TEMPERATURE, SymptomsDto.TEMPERATURE_SOURCE) +
  3. Go to the addFields method of the form and add the field. You can add it without defining a UI field type - this will use a default UI field type based on the type of the data field (see SormasFieldGroupFieldFactory):

    addFields(EpiDataDto.WATER_BODY, EpiDataDto.WATER_BODY_DETAILS, EpiDataDto.WATER_SOURCE);

    Or you can define the type of UI field that should be used and provide additional initialization for the field:

    ComboBox region = addField(CaseDataDto.REGION, ComboBox.class); region.addItems(FacadeProvider.getRegionFacade().getAllAsReference());
  4. The FieldHelper class provides methods to conditionally make the field visible, required or read-only, based on the values of other fields.

  5. Finally have a try in the web application to check if everything is working as expected.

Adding the field to the SORMAS Android app

The SORMAS Android app synchronizes with the server using the SORMAS ReST interface. The app does have it's own database to persist all data of the user for offline usage. Thus it's necessary to extend the entity classes used by the app.

  1. Identify the entity class in the Sormas-app backend sub-package.

  2. Add the field as a private member of the entity class with a get- and set-method.

  3. Add the correct JPA or ORM-lite annotation to the private member (see other fields for examples). Note: In the future, this may be replaced by using Android Room.

  4. Identify the *DtoHelper class for the entity (e.g. CaseDtoHelper).

  5. Extend the fillInnerFromAdo and fillInnerFromDto methods to exchange data between the API class and the app entity class that is persisted.

SORMAS allows users to upgrade from old app versions. Thus it's necessary to add the needed SQL to the onUpgrade method in the DatabaseHelper class.

  1. Increment the DATABASE_VERSION variable in the DatabaseHelper class.

  2. Go to the end of the onUpgrade method and add a new case block that defines how to upgrade to the new version.

  3. Execute the needed SQL using the DAO (database access object) of the entity class. You can mostly use the same SQL used for adding the field to the SORMAS backend. The column name has to match the field name in the entity class (not all lowercase).

    getDao(Symptoms.class).executeRaw("ALTER TABLE symptoms ADD COLUMN soreThroat varchar(255);");

The SORMAS app has separate fragments used for read and edit activities. Each fragment is split into the xml layout file and the Java class containing its logic.

  1. Identify the edit fragment layout XML file where the field needs to be added. E.g. /res/layout/fragment_symptoms_edit_layout.xml

  2. Add the field to the layout. See the existing fields for reference. Our custom Android components automatically add captions and descriptions to the field.

    <de.symeda.sormas.app.component.controls.ControlSwitchField android:id="@+id/symptoms_soreThroat" app:enumClass="@{symptomStateClass}" app:slim="true" app:value="@={data.soreThroat}" style="@style/ControlSingleColumnStyle" />

    Note that this comes with automatic data-binding. Use @={...} for edit fields and @{...} for read fields.

  3. Identify the edit fragment java class. E.g. SymptomsEditFragment.java

  4. Add needed field initializations to the existing onAfterLayoutBinding method. If necessary you can prepare any data in the prepareFragmentData method, while the fragment is still loading. Since there are many use cases please have a look at the existing classes.

  5. Do the same for the read fragment and possibly also create a fragment.

  6. Finally have a try in the app to check if everything is working as expected. Make sure data entered on the Android device is properly synchronized and also appears on the server.

Now you are done :-)