Search

    Working with Data

    This section describes how data is represented and used in the CAP Java SDK.

    Content

    Predefined Types

    The predefined CDS types are mapped to Java types and as follows:

    CDS Type Java Type Remark
    cds.UUID java.lang.String  
    cds.Boolean java.lang.Boolean  
    cds.Integer java.lang.Integer  
    cds.Integer64 java.lang.Long  
    cds.Decimal java.math.BigDecimal  
    cds.DecimalFloat java.math.BigDecimal deprecated
    cds.Double java.lang.Double  
    cds.Date java.time.LocalDate date without a time-zone (year-month-day)
    cds.Time java.time.LocalTime time without a time-zone (hour-minute-second)
    cds.DateTime java.time.Instant instant on the time-line with sec precision
    cds.Timestamp java.time.Instant instant on the time-line with µs precision
    cds.String java.lang.String  
    cds.LargeString java.lang.String java.io.Reader (1) if annotated with @Core.MediaType
    cds.Binary byte[]  
    cds.LargeBinary byte[] java.io.InputStream (1) if annotated with @Core.MediaType

    SAP HANA-Specific Data Types

    To facilitate using legacy CDS models, the following SAP HANA-specific data types are supported:

    CDS Type Java Type Remark
    hana.TINYINT java.lang.Short  
    hana.SMALLINT java.lang.Short  
    hana.SMALLDECIMAL java.math.BigDecimal  
    hana.REAL java.lang.Float  
    hana.CHAR java.lang.String  
    hana.NCHAR java.lang.String  
    hana.VARCHAR java.lang.String  
    hana.CLOB java.lang.String java.io.Reader (1) if annotated with @Core.MediaType
    hana.BINARY byte[]  

    (1) Although the API to handle large objects is the same for every database, the streaming feature, however, is supported (and tested) in SAP HANA, PostgreSQL, and H2. See section Database Support in Java for more details on database support and limitations.

    ❗ Warning
    The framework isn’t responsible for closing the stream when writing to the database. You decide when the stream is to be closed. If you forget to close the stream, the open stream can lead to a memory leak.

    These types are used for the values of CDS elements with primitive type. In the Model Reflection API, they’re represented by the enum CdsBaseType.

    Structured Data

    Structured data is used as input for Insert, Update, and Upsert statements and represents the results of Select statements.

    In the following we use this CDS model:

    entity Books {
        key ID     : Integer;
            title  : String;
            author : Association to one Authors;
    }
    
    entity Authors {
        key ID    : Integer;
            name  : String;
            books : Association to many Books on books.author = $self; 
    }
    

    Find this source also in cap/samples.

    Entities and Structured Types

    Entities or structured types are represented in Java as a Map<String, Object> that maps the element names to the element values.

    The following example shows JSON data and how it can be constructed in Java:

    {
        "ID"    : 97,
        "title" : "Dracula"
    }
    
    //java
    Map<String, Object> book = new HashMap<>();
    book.put("ID", 97);
    book.put("title", "Dracula");
    

    Data of structured types and entities can be sparsely populated.

    Nested Structures and Associations

    Nested structures and single-valued associations, are represented by elements where the value is structured. In Java, the value type for such a representation is a map.

    The following example shows JSON data and how it can be constructed in Java:

    {
        "ID"     : 97,
        "author" : 
            {
                "ID"   : 23,
                "name" : "Bram Stoker"
            }
    }
    
    // java
    Map<String, Object> author = new HashMap<>();
    author.put("ID", 23);
    author.put("name", "Bram Stoker");
    
    Map<String, Object> book = new HashMap<>();
    book.put("ID", 97);
    book.put("author", author);
    

    A to-many association is represented by a List<Map<String, Object>>.

    The following example shows JSON data and how it can be constructed in Java:

    {
        "ID"    : 23,
        "books" : 
            [
                {
                    "ID" : 97,
                    "title" : "Dracula"
                },
                {
                    "ID"   : 98,
                    "name" : "Miss Betty"
                }
            ],
        "name" : "Bram Stoker"
    }
    
    // java
    Map<String, Object> book1 = new HashMap<>();
    book1.put("ID", 97;
    book1.put("title", "Dracula");
    
    Map<String, Object> book2 = new HashMap<>();
    book2.put("ID", 98);
    book2.put("title", "Miss Betty");
    
    Map<String, Object> author = new HashMap<>();
    author.put("ID", 23);
    author.put(books, Arrays.asList(book1, book2));
    author.put("name", "Bram Stoker");
    

    Typed Access

    Representing data given as Map<String, Object> is flexible and interoperable with other frameworks. But it also has some disadvantages:

    • Names of elements are checked only at runtime
    • No code completion in the IDE
    • No type safety

    To ease the handling of data, the CAP Java SDK additionally provides typed access to data through accessor interfaces:

    Let’s assume following data for a book:

    Map<String, Object> book = new HashMap<>();
    book.put("ID", 97);
    book.put("title", "Dracula");
    

    You can now either define an accessor interface or use a generated accessor interface. The accessor interface then looks like in the following example:

    interface Book extends Map<String, Object> {
      @CdsName("ID")   // name of the CDS element
      Integer getID();
    
      String getTitle();
      void setTitle(String title);
    }
    

    At runtime, the Struct.access method is used to create a proxy that gives typed access to the data through the accessor interface:

    import static com.sap.cds.Struct.access;
    ...
    
    Book book = access(data).as(Book.class);
    
    String title = book.getTitle();   // read the value of the element 'title' from the underlying map
    book.setTitle("Miss Betty");      // update the element 'title' in the underlying map
    
    title = data.get("title");        // direct access to the underlying map
    
    title = book.get("title");        // hybrid access to the underlying map through the accessor interface
    

    To support hybrid access, like simultaneous typed and generic access, the accessor interface just needs to extend Map<String, Object>.

    The name of the CDS element referred to by a getter or setter, is defined through @CdsName annotation. If the annotation is missing, it’s determined by removing the get/set from the method name and lowercasing the first character.

    Generated Accessor Interfaces

    For all structured types of the CDS model, accessor interfaces can be generated using the CDS Maven Plugin. The generated accessor interfaces allow for hybrid access and easy serialization to JSON.

    Renaming Elements in Java

    Element names used in the CDS model might conflict with reserved Java keywords (class, private, transient, etc.). In this case, the @cds.java.name annotation must be used to specify an alternative property name that will be used for the generation of accessor interfaces and static model interfaces. The element name used as key in the underlying map for dynamic access isn’t affected by this annotation.

    See the following example:

    entity Equity {
      @cds.java.name : 'clazz'
      class : String;
      ...
    }
    
    interface Equity {
      String getClazz();
    
      void setClazz(String clazz);
    	...
    }
    

    Entity Inheritance in Java

    In CDS models it is allowed to extend a definition (for example, of an entity) with one or more named aspects. The aspect allows to define elements or annotations that are common to all extending definitions in one place.

    This concept is similar to a template or include mechanism as the extending definitions can redefine the included elements, for example, to change their types or annotations. Therefore, Java inheritance cannot be used in all cases to mimic the include mechanism. Instead, to establish Java inheritance between the interfaces generated for an aspect and the interfaces generated for an extending definition, the @cds.java.extends annotation must be used. The @cds.java.extends annotation can contain an array of string values, each of which denoting the fully qualified name of a CDS definition (typically an aspect) that is extended. In the following example, the Java accessor interface generated for the AuthorManager entity shall extend the accessor interface of the aspect temporal for which the Java accessor interface my.model.Temporal is generated.

    using { temporal } from '@sap/cds/common';
    
    @cds.java.extends: ['temporal']
    entity AuthorManager : temporal {
    	key Id : Integer;
    	name 	  : String(30);
    }
    

    The accessor interface generated for the AuthorManager entity is as shown in the following sample:

    import com.sap.cds.CdsData;
    import com.sap.cds.Struct;
    import com.sap.cds.ql.CdsName;
    import java.lang.Integer;
    import java.lang.String;
    
    @CdsName("AuthorManager")
    public interface AuthorManager extends CdsData, Temporal {
      String ID = "Id";
    
      String NAME = "name";
      
      @CdsName(ID)
      Integer getId();
    
      @CdsName(ID)
      void setId(Integer id);
    
      String getName();
    
      void setName(String name);
    
      static AuthorManager create() {
        return Struct.create(AuthorManager.class);
      }
    }
    

    In CDS, annotations on an entity are propagated to views on that entity. If a view does a projection exposing different elements, the inheritance relationship defined on the underlying entity via @cds.java.extends does not hold for the view. Therefore, the @cds.java.extends annotation needs to be overwritten in the view definition. In the following example, a view with projection is defined on the AuthorManager entity and the inherited annotation overwritten via @cds.java.extends : null to avoid the accessor interface of AuthorManagerService to extend the interface generated for temporal.

    service Catalogue {
    	@cds.java.extends : null
    	entity AuthorManagerService as projection on AuthorManager { Id, name, validFrom };
    }
    

    ❗ Warning
    The @cds.java.extends annotation does not support extending another entity.

    Creating a Data Container for an Interface

    To create an empty data container for an interface, use the Struct.create method:

    import static com.sap.cds.Struct.create;
    ...
    
    Book book = create(Book.class);
    
    book.setTitle("Dracula");
    String title = book.getTitle();   // title: "Dracula"
    

    Generated accessor interfaces contain a static create method that further facilitates the usage:

    Book book = Books.create();
    
    book.setTitle("Dracula");
    String title = book.getTitle();   // title: "Dracula"
    

    Read-Only Access

    Create a typed read-only view using access. Calling a setter on the view throws an exception.

    import static com.sap.cds.Struct.access;
    ...
    
    Book book = access(data).asReadOnly(Book.class);
    
    String title = book.getTitle();
    book.setTitle("CDS4j");           // throws Exception
    

    Typed Streaming of Data

    Data given as Iterable<Map<String, Object>> can also be streamed:

    import static com.sap.cds.Struct.stream;
    ...
    
    Stream<Book> books = stream(data).as(Book.class);
    
    List<Book> bookList = books.collect(Collectors.toList());
    

    Typed Access to Query Results

    Typed access through custom or generated accessor interfaces eases the processing of query result.

    CDS Data Processor

    The CdsDataProcessor allows to process deeply nested maps of CDS data, by executing a sequence of registered actions (validators, converters, and generators).

    Using the create method, a new instance of the CdsDataProcessor can be created:

    CdsDataProcessor processor = CdsDataProcessor.create();
    

    Validators, converters, and generators can be added using the respective add method, which takes a filter and an action as arguments and is executed when the filter is matching.

    processor.addValidator(filter, action);
    

    When calling the process method of the CdsDataProcessor, the actions are executed sequentially in order of the registration.

    List<Map<String, Object>> data;  // data to be processed
    CdsStructuredType rowType;       // row type of the data
    
    processor.process(data, rowType);
    

    The process method can also be used on CDS.ql results that have a row type:

    CqnSelect query; // some query
    Result result = service.run(query);
    
    processor.process(result);
    

    Element Filters

    Filters can be defined as lambda expressions on path, element, and type, for instance:

    (path, element, type) -> element.isKey() 
       && type.isSimpleType(CdsBaseType.STRING)
    

    which matches key elements of type String.

    • path describes the path from the structured root type of the data to the parent type of element and provides access to the data values of each path segment
    • element is the CDS element
    • type
      • for primitive elements the element’s CDS type
      • for associations the association’s target type
      • for arrayed elements the array’s item type

    Data Validators

    Validators validate the values of CDS elements matching the filter. New validators can be added using the addValidator method. The following example adds a validator that logs a warning if the CDS element amount has a negative value. The warning message contains the path to the element.

    processor.addValidator(
       (path, element, type) -> element.getName().equals("amount"), // filter
       (path, element, value) -> {                               // validator
          if ((int) value < 0) {
             log.warn("Negative amount: " + path.toRef());
          }
       });
    

    By default, validators are called if the data map contains a value for an element. This can be changed via the processing mode, which can be set to:

    • CONTAINS (default): The validator is called for declared elements for which the data map contains any value, including null.
    • NOT_NULL: The validator is called for declared elements for which the data map contains a non-null value.
    • NULL: The validator is called for declared elements for which the data map contains null or no value mapping, using ABSENT as a placeholder value.
    • DECLARED: The validator is called for all declared elements, using ABSENT as a placeholder value for elements with no value mapping.
    processor.addValidator(
       (p, e, t) -> e.isNotNull(), // filter
       (p, e, v) -> { // validator
          throw new RuntimeException(e.getName() + " must not be null or absent");
       }, Mode.NULL);
    

    Data Converters

    Converters convert or remove values of CDS elements matching the filter and are only called if the data map contains a value for the element matching the filter. New converters can be added using the addConverter method. The following example adds a converter that formats elements with name price.

    processor.addConverter(
       (path, element, type)  -> element.getName().equals("price"), // filter
       (path, element, value) -> formatter.format(value));       // converter
    

    To remove a value from the data, return Converter.REMOVE. The following example adds a converter that removes values of associations and compositions.

    processor.addConverter(
       (path, element, type)  -> element.getType().isAssociation(), // filter
       (path, element, value) -> Converter.REMOVE);                // remover
    

    Data Generators

    Generators generate the values for CDS elements matching the filter and are missing in the data or mapped to null. New generators can be added using the addGenerator method. The following example adds a UUID generator for elements of type UUID that are missing in the data.

    processor.addGenerator(
       (path, element, type)   -> type.isSimpleType(UUID),       // filter
       (path, element, isNull) -> isNull ? null : randomUUID()); // generator
    

    Media Type Processing

    The data for media type entity properties (annotated with @Core.MediaType) - as with any other CDS property with primitive type - can be retrieved by their CDS name from the entity data argument. See also Structured Data and Typed Access for more details. The Java data type for such properties is InputStream.

    Processing such elements within a custom event handler requires some care though, as such an InputStream is non-resettable. That means, the InputStreams can only be read once. This has some implications you must be aware of, depending on what you want to do with the InputStream.

    Let’s assume we have the following CDS model:

    entity Books : cuid, managed {
      title         : String(111);
      descr         : String(1111);
      coverImage    : LargeBinary @Core.MediaType: 'image/png';
    }
    

    When working with media types, we can differentiate upload and download scenarios. Both have their own specifics on how we can deal with the InputStream.

    No Custom Processing

    Media Upload

    If you just want to pass the uploaded InputStream to the persistence layer of the CAP architecture to have the bytes written into the database, you don’t have to implement any custom handler. This is the simplest scenario and our default On handler already takes care of that for you.

    Media Download

    For the download scenario, as well, you don’t need to implement any custom handler logic. The default On handler reads from the database and passes the InputStream to the client that requested the media type element.

    Custom Processing

    Media Upload

    If you want to override the default logic to process the uploaded InputStream with custom logic (for example, to parse a stream of CSV data), the best place to do that is in a custom On handler, as the following examples shows:

    @On(event = CdsService.EVENT_UPDATE)
    public void processCoverImage(CdsUpdateEventContext context, List<Books> books) {
    	books.forEach(book -> {
    		InputStream is = book.getCoverImage();
    		// ... your custom code fully consuming the input stream
    	});
    	context.setResult(books);
    }
    

    After you have fully consumed the InputStream in your handler logic, passing the same InputStream instance for further consumption would result in no bytes returned, because a non-resettable InputStream can only be consumed once. In particular, make sure that the default On handler is not called after your custom processing.

    Using a custom On handler and setting context.setResult(books) prevents the execution of the default On handler.

    Media Download

    The previous described approach is only useful when uploading data. If you need custom processing for media downloads, have a look at the approach using an InputStream proxy described below.

    Pre- or Post-Processing Using an InputStream Proxy

    The following sections describe how to pre-process an uploaded stream of data before it gets persisted or how to post-process a downloaded stream of data before it’s handed over to the client. For example, this is useful if you want to send uploaded data to a virus scanner, before persisting it on the database.

    This requires that the InputStream is consumed by several parties (for example, the virus scanner and the persistence layer). To achieve this, implement an InputStream proxy that wraps the original InputStream and executes the processing logic within the read() methods on the bytes read directly. Such a proxy can be implemented by extending a FilterInputStream or a ProxyInputStream.

    The following example uses a FilterInputStream but you can do the same with a ProxyInputStream:

    public class CoverImagePreProcessor extends FilterInputStream {
    
    	public CoverImagePreProcessor(InputStream wrapped) {
    		super(wrapped);
    	}
    
    	@Override
    	public int read() throws IOException {
    		int nextByte = super.read();
    
    		// ... your custom processing code on nextByte
    
    		return nextByte
    	}
    
    	@Override
    	public int read(byte[] bts, int off, int len) throws IOException {
    		int bytesRead = super.read(bts, off, len);
    		
    		// ... your custom processing code on bts array
    		
    		return bytesRead;
    	}
    }
    

    This proxy is then used to wrap the original InputStream. This works for both upload and download scenarios.

    Media Upload

    For uploads, you can either use a custom Before or On handler to wrap the proxy implementation around the original InputStream before passing it to its final destination.

    Using a custom Before handler makes sense if the InputStream’s final destination is the persistence layer of the CAP Java SDK, which writes the content to the database. Note, that the pre-processing logic that is implemented in the read() methods of the FilterInputStream is only called when the data is streamed, during the On phase of the request:

    @Before(event = CdsService.EVENT_UPDATE)
    public void preProcessCoverImage(CdsUpdateEventContext context, List<Books> books) {
    	books.forEach(book -> {
    		book.setCoverImage(new CoverImagePreProcessor(book.getCoverImage()));
    	});
    }
    

    The original InputStream is replaced by the proxy implementation in the coverImage element of the book entity and passed along. Every further code trying to access the coverImage element will use the proxy implementation instead.

    Using a custom On handler makes sense if you want to prevent that the default On handler is executed and to control the final destination for the InputStream. You then have the option to pass the streamed data on to some other service for persistence:

    @On(event = CdsService.EVENT_UPDATE)
    public Result processCoverImage(CdsUpdateEventContext context, List<Books> books) {
    	books.forEach(book -> {
    		book.setCoverImage(new CoverImagePreProcessor(book.getCoverImage()));
    	});
    
    	// example for invoking some CQN-based service
    	return service.run(Update.entity(Books_.CDS_NAME).entries(books));
    }
    

    Media Download

    For download scenarios, the InputStream to wrap is only available in After handlers as shown in this example:

    @After(event = CdsService.EVENT_READ)
    public void preProcessCoverImage(CdsReadEventContext context, List<Books> books) {
    	books.forEach(book -> {
    		book.setCoverImage(new CoverImagePreProcessor(book.getCoverImage()));
    	});
    }
    

    Reminder

    Be aware in which event phase you do the actual consumption of the InputStream instance that is passed around. Once fully consumed, it can no longer be read from in remaining event phases.

    Show/Hide Beta Features