While working on a MAF application for one of our customers, we had a scenario which involved multiple users working on the same device. The data that one user has persisted offline, must not be viewable or editable by other users which are working on the same device.
This particular scenario is not supported in AMPA (A-Team Mobile Persistance Accelerator) by default. The framework is not multi-user-aware. It only works in the context of one user.

This article describes the solution that we implemented to solve this particular issue. To achieve it, we made changes to the SQLite objects that AMPA generates and extended functionality from the framework, but without changing the core source code of AMPA.

If you don’t know what AMPA is, read the tutorial at:
http://docs.oracle.com/cd/E53569_01/tutorials/tut_jdev_maf_persistance/tut_jdev_maf_persistance.html

AMPA is open-source and can be found on GitHub:
https://github.com/oracle/mobile-persistence

 

SQLite database & Model changes

After completing the AMPA wizard, an SQLite script is generated for us. This script is executed at the initial installation of the application and it is used to create the structure of the database, the tables and so on.
This file can be found in the ApplicationController project, under src/META-INF/<name_of_your_project>.sql.

From the SQLite database’s point of view, the natural solution is to add a new column for each table which stores data relevant to the user. This new column holds the username.
For better understanding, consider the following scenario: A table is used to store the projects on which a user is working on. It is possible that the same project is assigned to two or more users; put it in another way: two or more users can work on the same project.
This means that the username column has to form a primary key constraint together with the initial primary key field of the project so that the same project can be assigned to multiple users.

For our Projects table, we add something like this:
[dt_code]CREATE TABLE PROJECTS
(
PROJECT_DEF VARCHAR NOT NULL,
USERNAME VARCHAR NOT NULL,
PROJECT_NAME VARCHAR ,
— Other attributes follow —
CONSTRAINT PROJECTS_PK PRIMARY KEY(PROJECT_DEF, USERNAME)
);[/dt_code]

 

We need to create this username attribute in the Entity that AMPA created for us with its corresponding accessor methods.

public class Projects extends Entity {
  //….
  private String username;

  public void setUsername(String username) {
    this.username = username;
  }

  public String getUsername() {
    return username;
  }
  //…
}

 

Now, if we want to retrieve the project for a specific user, we would do a query with a where clause which specifies that the username is equal to the username that we are logged-in.
Something similar to:
[dt_code]select * from projects where username=“max_mustermann“;[/dt_code]

But AMPA handles the SQLite interaction for us, so let’s see how we achieve the same result using AMPA.
For each entity that you want to work with – in this case, Projects – AMPA creates a Service class, to interact with the database and the webservice. This class always extends from EntityCRUDService<T> (T – entity type, in this case, Projects).

EntityCRUDService contains a method called executeLocalFindAll() which is executed each time projects are retrieved from the database. This looks like a good place to hook and filter the results to only return projects which belong to the current username.

Override the method in the ProjectsService class with code similar to the following:

@Override
protected List<Projects> executeLocalFindAll() {
  DBPersistenceManager pm = getLocalPersistenceManager();

  //find() method supports sending a list with criteria to filter the query on.
  //Add the username to the list criteria.
  ArrayList<String> searchAttrs = new ArrayList<String>();
  searchAttrs.add("username");
  //Username is retrieved from memory or some other place.
  String userName = SecurityUtils.getCurrentUsername();
  List<Projects> tempProjects = pm.find(Projects.class, userName, searchAttrs);

  //find() method uses a LIKE operator, which means that there could be a case
  //where a username string contains another. e.g: maxm and maxmustermann
  //To avoid this, we do a filtering on the Java objects, to only retain the
  //ones which fully-match the username, not only partially.

  tempProjects = filterProjectsByUsername(tempProjects, userName);
  return tempProjects;
}

filterProjectsByUsername() is not shown here but it is pretty clear what it does. It only returns projects which have the username property the same as the one we send as parameter.

AMPA WebService mappings

So far, we have discussed how we can store the user-specific data on the mobile device. But we need to do some changes in the way AMPA handles the retrieval of the data from the webservice also.
In AMPA, each entity – like Projects – has a corresponding WebService call to retrieve them from the server.
Each entity which is retrieved from the server and persisted in the database, must have a one-to-one mapping between attributes which are persisted and attributes which are retrieved from the server.

These mappings between attributes in Java entity, column names and fields which come from the server, is done in the persistence-mapping.xml. This file is automatically generated by the AMPA wizard and for most of the cases you don’t need to touch it. However, we need to do some changes in there to fully support our scenario.
The file is found in the ApplicationController project, under src/META-INF/persistence-mapping.xml.

Each entity is described in the XML by the classMappingDescriptor tag. Under this tag, the table, the webservice endpoints and the attributes mapping can be found. We are especially interested in attribute mappings.
The attributes are under <attributeMappings> tag with entries for each attribute, like the following:
[dt_code]<directMapping attributeName=“projectName“ payloadAttributeName=“project_name“ columnName=“PROJECT_NAME“ columnDataType=“VARCHAR“ required=“false“ persisted=“true“ javaType=“java.lang.String“ keyAttribute=“false“/>[/dt_code]

Overriding RestJSONPersistenceManager to persist username on the fly

Now, the issue that we have is that, in order to save the current username in the database for each project, we need to specify an attribute here, as a mapping. But the username attribute is not returned by the webservice call, it only returns the other data about the project. Username is a field which we created manually in our database and Entity.

We have the option to specify on the attribute that it is not needed to be present in the payload. We do this by a required=“false“ attribute. What remains is to find a way to say to the framework that, even if we don’t have the username attribute coming from the webservice response, to save the current username in the database on the fly.

We cannot add a custom attribute on the tag, because it is validated against the schema, but we can do the following trick, see below:
[dt_code]<directMapping attributeName=“username“ payloadAttributeName=“username“ columnName=“USERNAME“ columnDataType=“VARCHAR“ required=“false“ persisted=“true“ javaType=“java.lang.String“ keyAttribute=“true“ xml:ELExpression=“#{MbSecurityContext.username}“/>[/dt_code]

Note the xml:ELExpression=“#{MbSecurityContext.username}“ part.

Basically, we want the expression language to be executed when the attribute is processed. This allows us the evaluate an expression language which will resolve the current username. We will see next what we have to change to achieve this.

By digging through the AMPA code, we notice there is a method in RestJSONPersistenceManager, which is called after the response is received from the WebService and before the entities are stored in the database. This method is called: createBindParamInfoFromPayloadAttribute(). This creates a so called BindParamInfo which containes information about the information that is stored in the database for each attribute.
We need to override this method in our own custom class. For this, we need to create a class which extends from RestJSONPersistenceManager.
Create this class somewhere in the ApplicationController project.
Next, we need to inform the AMPA framework, that we want it to use our new custom class instead of the RestJSONPersistenceManager. We do this, by changing the remotePersistenceManager attribute in the crudServiceClass tag in persistence-mapping.xml:

[dt_code]<crudServiceClass className=“de.virtual7.mobile.model.service.ProjectsService“ autoIncrementPrimaryKey=“false“ localPersistenceManager=“oracle.ateam.sample.mobile.v2.persistence.manager.DBPersistenceManager“ remotePersistenceManager=“de.virtual7.ampa.persistence.manager.CustomRestPersistenceManager“ remoteReadInBackground=“true“ remoteWriteInBackground=“true“ showWebServiceInvocationErrors=“false“ autoQuery=“true“/>[/dt_code]

Now, the code for the method which is being overriden:

@Override
protected BindParamInfo createBindParamInfoFromPayloadAttribute(Class entityClass, AttributeMapping mapping, Object rawValue) {
  if (rawValue == null || "".equals(rawValue.toString().trim()) || rawValue == JSONObject.NULL) {
    //Normally, the super method would return null here. But we need to check if    
    //the attribute mapping contains a mapping that we can process.

    String elExpressionValue = mapping.getAttributeStringValue("xml:ELExpression");
    if (elExpressionValue != null) {
      rawValue = AdfmfJavaUtilities.getELValue(elExpressionValue);
    }
  }
  BindParamInfo bindParamInfo = super.createBindParamInfoFromPayloadAttribute(entityClass, mapping, rawValue);
  return bindParamInfo;
}

 

What we do, is intercept the attribute computation, check if the name of the attribute is our custom one and if it is, compute the expression language. In this way, we assign the value for the username, on the fly.

Wir bleiben für Sie da!

In unserem neusten Blog-Beitrag sprechen wir mit zwei neuen Mitarbeitern, die Mitten in der Corona-Krise bei virtual7 angefangen haben.

Welche Eigenheiten und Herausforderungen einen Jobwechsel in dieser Zeit bedeuten und wie ihr erster Arbeitstag im Home-Office ausgesehen hat, erfahren Sie in diesem Beitrag.