DEV Community

Chris
Chris

Posted on

Creating REST API with CRNK, Part 2

In Part 1 we created a basic REST application with Spring Framework and CRNK. We had a basic model how our data is representative in the database that external user can GET, POST or PUT.

External facing data

In previous article we only worked with one model, knowing that every information that is externally requested should be given in full. But if you construct an API where the model may hold information that an external user should not see, say you use some data to base filters on, there is a way to hide this. Two models are created, one for the external use and one for the repository. Then a mapper will be created that link between the external model and the internal model.

This is done with implementing JpaModuleConfigurator and overriding configure. The parameter is JpaModuleConfig which let you configure the CRNK application. Here below we add a repository where we bind the two models, external and internal, with a mapper.


package com.test.store.configuration;

import com.test.store.model.ModelMapper;
import com.test.store.model.ModelExternal;
import com.test.store.model.ModelEntity;
import io.crnk.jpa.JpaModuleConfig;
import io.crnk.jpa.JpaRepositoryConfig;
import io.crnk.spring.setup.boot.jpa.JpaModuleConfigurer;
import org.springframework.stereotype.Component;

@Component
public class JpaModuleConfigurator implements JpaModuleConfigurer {

  @Override
  public void configure(JpaModuleConfig jpaModuleConfig) {
       jpaModuleConfig.addRepository(JpaRepositoryConfig.builder(
                                     ModelEntity.class,
                                     ModelExternal.class, new ModelMapper())
                      .build());
    }
}
Enter fullscreen mode Exit fullscreen mode

And for JpaModuleConfigurator to use the mapper it needs to implement JpaMapper where you specify the two models and override method to jump between them.


package com.test.store.model;

import com.test.store.model.ModelExternal;
import com.test.store.model.ModelEntity;
import io.crnk.jpa.mapping.JpaMapper;
import io.crnk.jpa.query.Tuple;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Component
public class ModelMapper implements JpaMapper<ModelEntity, ModelExternal> {

  @PersistenceContext
  private EntityManager entityManager;@Override
  public ModelExternal map(Tuple tuple) {
    ModelExternal dto = new ModelExternal();

    ModelEntity entity = tuple.get(0, ModelEntity.class);
    dto.setData(entity.getData());
    return dto;
  }

  @Override
  public ModelEntity unmap(ModelExternal dto) {
    ModelEntity entity;
    if (dto.getId() == null) {
      entity = new ModelEntity();
    } else {
      entity = entityManager.find(ModelEntity.class, dto.getId());
      if (entity == null) {
        throw new RuntimeException("Couldn't find entity");
      }
    }
    return translateToEntity(dto, entity);
  }

  private ModelEntity translateToEntity(ModelExternal dto, ModelEntity entity) {
    entity.setData(dto.getData);
    return entity;
  }
}
Enter fullscreen mode Exit fullscreen mode

The external user can still use CRNK default search query for the model but you can control what will be seen. If you have data that you want to hide it can be controlled in the mapper and in the external model you don’t have that data.

Modifying repository data

Above gives an opportunity to hide data from external user which may be present in the database. A question and a problem may be if information is going to be presented to the user which is not present in the database. What if, based on query to the API, we want to change the response back?

For this we can’t connect the two models with JpaModuleConfigurator. Both models need to have their own repositories where the internal facing one is the one connecting to the database. The external facing repository will hold the internal facing one as an attribute to be used in the different calls.

For this example, we are assuming a query has been made to the API with a age and we want to give back the every person and their age difference to the query. The new attribute to our ModelExternal is

private int ageDifference;

In previous article we hold the data in memory. This time we are going to create a repository which is connected to a database where data is persisted. As previous, we state which model the repository is using and what attribute is the identifier.


package com.test.store.repository;

import com.test.store.model.ModelEntity;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.UUID;

@Service
public interface ModelEntityCrudRepository extends CrudRepository<ModelEntity, UUID> {
}
Enter fullscreen mode Exit fullscreen mode

This repository doesn’t need to hold much compare to ResourceRepositoryBase as CrudRepository is the communicator with database and we’re not interested into changing anything here. If you want to make specific calls inside your API to the database you can always create specific queries to the database, for example

@Query("select s from model s where partner_id = :partnerId")  
List<StoreEntity> findByPartnerId(@Param("partnerId") String partnerId);
Enter fullscreen mode Exit fullscreen mode

The external facing repository will be a bit trickier to set up. We need to define it as a CRNK repository but also connect it with our ModelEntity above. First we create an interface extending ResourceRepositoryV2 , for us to use CRNK default queries.


package com.test.store.repository;

import com.test.store.model.store.Store;
import io.crnk.core.repository.ResourceRepositoryV2;
import java.util.UUID;

public interface ModelExternalRepository extends 
                             ResourceRepositoryV2<ModelExternal, UUID> {}
Enter fullscreen mode Exit fullscreen mode

Next comes the main repository.


package com.test.store.repository;

import com.test.store.model.ModelExternal;
import com.test.store.model.ModelEntity;
import com.test.store.translator.ModelTranslator;
import io.crnk.core.boot.CrnkBoot;
import io.crnk.core.exception.BadRequestException;
import io.crnk.core.exception.RepositoryNotFoundException;
import io.crnk.core.queryspec.QuerySpec;
import io.crnk.core.repository.ResourceRepositoryBase;
import io.crnk.jpa.JpaEntityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Component
public class ModelExternalRepositoryImpl extends 
                               ResourceRepositoryBase<ModelExternal, UUID> implements ModelExternalRepository {

  private JpaEntityRepository modelEntityJpaRepository;@Autowired
  ApplicationContext appContext;

  public ModelExternalRepositoryImpl() {
    super(ModelExternal.class);
    modelEntityJpaRepository = null;
  }

  @Override
  public ModelExternal findOne(UUID id, QuerySpec querySpec) {
    if (modelEntityJpaRepository == null) {
      initJpaRepositoryEntity();
    }
    return ModelTranslator.map((ModelEntity)  modelEntityJpaRepository.findOne(id, querySpec));
  }

  @Override
  public ModelExternalResourceList findAll(QuerySpec querySpec) {
    if (modelEntityJpaRepository == null) {
      initJpaRepositoryEntity();
    }
    ResourceList<ModelEntity> modelEntityResourceList = modelEntityJpaRepository.findAll(querySpec);
    ...

modelResourceList.addAll(ModelTranslator.translate(modelEntityResourceList));
    ...

    return modelResourceList;
  }

  /**
   * Initialize the JpaRepository.
  */
  private void initJpaRepositoryEntity() {
    CrnkBoot crnkBoot = appContext.getBean(CrnkBoot.class);
    modelEntityJpaRepository = crnkBoot.getModuleRegistry()
                                           .getRepositories()
                                           .stream()
                                           .filter(repository -> repository instanceof JpaEntityRepository)
                                           .map(repository -> (JpaEntityRepository) repository)
                                           .filter(repository -> repository.getEntityClass().equals(ModelEntity.class)
                                                           && repository.getResourceClass().equals(ModelEntity.class))
                                           .findFirst()
                                           .orElseThrow(() -> new RepositoryNotFoundException("ModelEntity JpaRepository not found"));
    }

  public static class ModelResourceList extends ResourceListBase<ModelExternal, MetaInformation, LinksInformation> {}
}
Enter fullscreen mode Exit fullscreen mode

We will break this up in importance.

One of the more important bit here is initJpaRepositoryEntity. This is because it can’t be controlled when repositories are initialised and to autowired the internal repository could throw error saying it’s not there yet. To circumvent this with a hack the first call to the data will the two repositories. What happens is that it will search for our ModelEntity repository that is connected to the database and load into this repository.

Breaking in steps:

  1. First we search for our CrnkBoot bean which will hold all our repositories.
  2. Load all the modules register with CrnkBoot and get all of the repositories
  3. Start by going through them and look for each repository which is a JpaRepository
  4. For each JpaRepository look for the one that is connected to our ModelEntity class

After all of these steps we have our database repository loaded in our external facing repository.

A external call will hit our external facing repository which is connected to our database repository. It will get the information from it with the query (and if needed you can always modify the query by creating a new and send that into the internal repository). We get some data out of it and translate that into external facing data. With the translation we can add data that we want the external user to see.

Above is just two ways to separate data from external users and internal user. And we still keep the default CRNK behaviour, with filtering and sorting. A last tip with using two repository and sorting is to modify the query to the repository then get more data than you need. Sort this data in your external repository and give only a set of data back.

Add new filtering

In part 1 we talked about filtering and what the standard filtering CRNK will give you. But later you may want to add filtering for the specific data your repository hold.

To add a filter you need to extend filterOperator


package com.test.store.filter;

import io.crnk.core.queryspec.FilterOperator;

public class PersonFilterOperator extends FilterOperator {

  public PersonFilterOperator(String id) {
    super(id);
  }

  /**
   * Handle new filterOperator. Should in default pass if value2 (value given by request) is given.
   * @param value1 value from DB
   * @param value2 value from request
   * @return true if value from request isn't null
  */
  @Override
  public boolean matches(Object value1, Object value2) {
    /*
     This class and this method is only used for new FilterOperators. FilterOperators are used in making a request and using crnk filtering.
     Value 1 is the data from the DB and Value 2 is from the request. FilterOperator.LIKE may first check if value2 exist then later on
     check value1 == value2. As we don't want to check values on that level we are only interested to check if the user provided a value.
     */
    return value2 != null;
  }
}
Enter fullscreen mode Exit fullscreen mode

The filter above doesn’t do anything more than checking the value that is passed with the request holds data. But you can modify method matches to fit what you want your filtering to do. Now just adding this class won’t add the filtering to your application but you need to add it to the Url mapper.


package com.test.store.configuration;

import com.test.store.filter.StoreFilterOperator;
import io.crnk.core.queryspec.FilterOperator;
import io.crnk.core.queryspec.mapper.DefaultQuerySpecUrlMapper;
import org.springframework.stereotype.Component;

@Component
public class PersonQuerySpec extends DefaultQuerySpecUrlMapper {

  public static final FilterOperator WITHIN= new PersonFilterOperator("WITHIN");
  public static final FilterOperator NEAR = new PersonFilterOperator("NEAR");
  public ModelQuerySpec() {
    super.addSupportedOperator(WITHN);
    super.addSupportedOperator(NEAR);
  }
}
Enter fullscreen mode Exit fullscreen mode

We add a new filterOperator to DefaultQuerySpecUrlMapper so when a call comes in with the new filter CRNK knows what to do with it instead. The above example uses the same class as above which doesn’t do anything but a filter call now with

/person?filter[NEAR][age]=25
Enter fullscreen mode Exit fullscreen mode

works. What is important when using your own filterOperator is, as mentioned in Part 1, that filter work both between the request to your API and the query from the repository to the underlying data structure. This mean that if using specific filterOperator you will have to implement a repository class to unpack your querySpec and modify the request.

Summary

Doing more specific things with CRNK can prove to be a problem as this article point out. CRNK give help with connecting two models, external and internal, to help hide data but if data is needed in the response it get trickier. There isn’t, what we have seen, a way to control how the repositories are loaded. This means that to have different repositories talking to each other hacks need to be put in place.

CRNK is relative new for now so hopefully some of these things can be simplified and the community grow around it.

Top comments (1)

Collapse
 
rehabreda0 profile image
rehab-reda0

Thank you : ) how u will make it use the personQuerySpec not the default one should we use something like crnkclient.seturlmapper can u provid a sample of this please