Working with CDS Data
This section describes how CDS data is represented and used in CAP Java.
Predefined Types
Thepredefined 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.UInt8 | java.lang.Short | |
cds.Int16 | java.lang.Short | |
cds.Int32 | java.lang.Integer | |
cds.Integer | java.lang.Integer | |
cds.Int64 | java.lang.Long | |
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 withsec 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 |
cds.Vector | com.sap.cds.CdsVector | forvector embeddings |
SAP HANA-Specific Data Types
To facilitate using legacy CDS models, the followingSAP 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) inSAP HANA,PostgreSQL, andH2. See sectionDatabase 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 theModel Reflection API, they're represented by the enumCdsBaseType.
Structured Data
In CDS, structured data is used as payload ofInsert,Update, andUpsert statements. Also the query result ofSelect may be structured. CAP Java represents data of entities and structured types asMap<String, Object>
and provides theCdsData
interface as an extension ofMap
with additional convenience methods.
In the following we use this CDS model:
entity Books { keyID : Integer; title : String; author : Association to oneAuthors;}entity Authors { keyID : Integer; name : String; books : Association to manyBooks on books.author = $self;}entity Orders { keyID : Integer; header : Composition of oneOrderHeaders; items : Composition of manyOrderItems;}entity OrderHeaders { keyID : Integer; status : String;}aspect OrderItems { keyID : Integer; book : Association to oneBooks;}
Find this source also incap/samples.
In this model, there is a bidirectional many-to-one association betweenBooks
andAuthors
, which is managed by theBooks.author
association. TheOrders
entity owns the compositionheader
, which relates it to theOrderHeaders
entity, and the compositionitems
, which relates the order to theOrderItems
. The items are modeled using a managed composition of aspects.
TIP
UseManaged Compositions of Aspects to model unidirectional one-to-many compositions.
Relationships to other entities
Relationships to other entities are modeled as associations or compositions. Whileassociations capture relationships between entities,compositions constitute document structures through 'contained-in' relationships.
Entities and Structured Types
Entities and structured types are represented in Java as aMap<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"}
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" }}
Using plain maps:
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);
Using theputPath
method ofCdsData
:
CdsData book= Struct.create(CdsData.class);book.put("ID",97);book.putPath("author.ID",23);book.putPath("author.name","Bram Stoker");
Using the generatedaccessor interfaces:
Authors author= Authors.create();author.setId(23);author.setName("Bram Stoker");Books book= Books.create();book.setId(97);book.setAuthor(author);
Ato-many association is represented by aList<Map<String, Object>>
.
The following example shows JSON data and how it can be constructed in Java:
{ "ID" :23, "name" :"Bram Stoker", "books" : [ {"ID" :97,"title" :"Dracula" }, {"ID" :98,"title" :"Miss Betty" } ]}
// javaMap<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("name","Bram Stoker");author.put("books", Arrays.asList(book1, book2));
CDS Data
In CAP Java data is represented in maps. To simplify data access in custom code, CAP Java additionally provides generatedaccessor interfaces which extendCdsData, enhancing theMap
interface with path access to nested data and build-in serialization to JSON.
TheRow
s of aquery result as well as thegenerated accessor interfaces already extendCdsData
. Using the helper classStruct you can extend anyMap<String, Object>
with the CdsDatainterface
:
Map<String,Object> map= new HashMap<>();CdsData data= Struct.access(map).as(CdsData.class);
Or create an emptyCdsData
map usingStruct.create
:
CdsData data= Struct.create(CdsData.class);
Path Access
Manipulate deeply nested data usingCdsData.putPath
:
data.putPath("author.name","Bram Stoker");
This results in a nested data structure:{ "author" : { "name" : "Bram Stoker" } }
. The path access inputPath
is null-safe, nested maps are created on the fly if required.
Read nested data usingCdsData.getPath
:
String authorName= data.getPath("author.name");
To check if the data contains a value in a nested map with a specific path usecontainsPath
:
boolean b= data.containsPath("author.name");
To do a deep remove useremovePath
:
String authorName= data.removePath("author.name");
Empty nested maps are automatically removed byremovePath
.
TIP
Use path access methods ofCdsData
to conveniently manipulate nested data structures.
Serialization
CDS Data has built-in serialization to JSON, which is helpful for debugging:
CdsData person= Struct.create(CdsData.class);person.put("salutation","Mr.");person.putPath("name.first","Frank");// path accessperson.toJson();// { "salutation" : "Mr.", name : { "first" : "Frank" } }
WARNING
Avoid cyclic relationships between CdsData objects when using toJson.
Vector Embeddingsbeta
In CDSvector embeddings are stored in elements of typecds.Vector
:
entity Books : cuid { title : String(111); description : LargeString; embedding : Vector(1536);// vector space w/ 1536 dimensions}
In CAP Java, vector embeddings are represented by theCdsVector
type, which allows a unified handling of different vector representations such asfloat[]
andString
:
// Vector embedding of text, for example, from SAP GenAI Hub or via LangChain4jfloat[] embedding= embeddingModel.embed(bookDescription).content().vector();CdsVector v1= CdsVector.of(embedding);// float[] formatCdsVector v2= CdsVector.of("[0.42, 0.73, 0.28, ...]");// String format
You can use the functions,CQL.cosineSimilarity
orCQL.l2Distance
(Euclidean distance) in queries to compute the similarity or distance of embeddings in the vector space. To use vector embeddings in functions, wrap them usingCQL.vector
:
CqnVector v= CQL.vector(embedding);Result similarBooks= service.run(Select.from(BOOKS).where(b-> CQL.cosineSimilarity(b.embedding(), v).gt(0.9)));
You can also use parameters for vectors in queries:
var similarity= CQL.cosineSimilarity(CQL.get(Books.EMBEDDING), CQL.param(0).type(VECTOR));CqnSelect query= Select.from(BOOKS) .columns(b-> b.title(), b-> similarity.as("similarity")) .where(b-> b.ID().ne(bookId).and(similarity.gt(0.9))) .orderBy(b-> b.get("similarity").desc());Result similarBooks= db.run(select, CdsVector.of(embedding));
In CDS QL queries, elements of typecds.Vector
are not included in selectall queries. They must be explicitly added to the select list:
CdsVector embedding= service.run(Select.from(BOOKS).byId(101) .columns(b-> b.embedding())).single(Books.class).getEmbedding();
Data in CDS Query Language (CQL)
This section shows examples using structured data inCQL statements.
Deep Inserts through Compositions and Cascading Associations
Deep Inserts create new target entities along compositions and associations thatcascade the insert operation. In this example an order with a header in status 'open' is created via a deep insert along theheader
composition.
OrderHeaders header= OrderHeaders.create();header.setId(11);header.setStatus("open");Orders order= Orders.create();order.setId(1);order.setHeader(header);Insert insert= Insert.into(ORDERS).entry(order);
Setting Managed Associations to Existing Target Entities
If you're using associations that don't cascade the insert and update operations, those associations can only be set to existing target entities. The data is structured in the same way as in deep inserts, but the insert operation isflat, only the target values that are required to set the association are considered, all other target values are ignored:
Authors author= Authors.create();author.setId(100);Books book= Books.create();book.setId(101);book.setAuthor(author);Insert insert= Insert.into(BOOKS).entry(book);
TIP
Set managed associations using theassociation element and avoid using generated foreign key elements.
Inserts through Compositions via Paths
To insert via compositions, use paths ininto
. In the following example we add an order item to the set of items of the order 100:
OrderItems orderItem= OrderItems.create();orderItem.setId(1);orderItem.putPath("book.ID",201);// set association to book 201Insert.into(ORDERS, o-> o.filter(o.Id().eq(100)).items()) .entry(orderItem);
TIP
Access child entities of a composition using a path expression from the parent entity instead of accessing the child entities directly.
Select Managed Associations
To select the mapping elements of a managed association, simply add theassociation to the select list:
CqnSelect select= Select.from(BOOKS).byId(123) .columns(b-> b.author());Row row= persistence.run(select).single();Integer authorId= row.getPath("author.ID");
TIP
Don't select from and rely on compiler generated foreign key elements of managed associations.
Select with Paths in Matching
Paths are also supported inmatching, for example, to select allorders that are in statuscanceled:
Map<String,Object> order= new HashMap<>();order.put("header.status","canceled");CqnSelect select= Select.from("bookshop.Orders").matching(order);Result canceledOrders= persistence.run(select);
Typed Access
Representing data given asMap<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 simplify the handling of data, CAP Java additionally providestyped access to data throughaccessor 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 agenerated accessor interface. If you define an interface yourself, it could look like the following example:
interface Books extends Map<String,Object> { @CdsName("ID")// name of the CDS element IntegergetID(); StringgetTitle(); void setTitle(Stringtitle);}
Struct
At runtime, theStruct.access
method is used to create aproxy that gives typed access to the data through the accessor interface:
import static com.sap.cds.Struct.access;...Books book= access(data).as(Books.class);String title= book.getTitle();// read the value of the element 'title' from the underlying mapbook.setTitle("Miss Betty");// update the element 'title' in the underlying maptitle= data.get("title");// direct access to the underlying maptitle= book.get("title");// hybrid access to the underlying map through the accessor interface
To supporthybrid access, like simultaneous typedand generic access, the accessor interface just needs to extendMap<String, Object>
.
TIP
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 theCDS Maven Plugin. The generated accessor interfaces allow for hybrid access and easy serialization to JSON.
By default, the accessor interfaces provide the setter and getter methods inspired by the JavaBeans specification.
Following example uses accessor interfaces that have been generated with the default (JavaBeans) style:
Authors author= Authors.create(); author.setName("Emily Brontë"); Books book= Books.create(); book.setAuthor(author); book.setTitle("Wuthering Heights");
Alternatively, you can generate accessor interfaces influent style. In this mode, the getter methods are named after the property names. To enable fluent chaining, the setter methods return the accessor interface itself.
Following is an example of the fluent style:
Authors author= Authors.create().name("Emily Brontë"); Books.create().author(author).title("Wuthering Heights");
The generation mode is configured by the property<methodStyle>
of the goalcds:generate
provided by the CDS Maven Plugin. The selected<methodStyle>
affects all entities and event contexts in your services. The default value isBEAN
, which represents JavaBeans-style interfaces.
Once, when starting a project, decide on the style of the interfaces that is best for your team and project. We recommend the default JavaBeans style.
The way the interfaces are generated determines only how data is accessed by custom code. It does not affect how the data is represented in memory and handled by the CAP Java runtime.
Moreover, it doesn't change the way how event contexts and entities, delivered by CAP, look like. Such interfaces from CAP are always modelled in the default JavaBeans style.
Renaming Elements in Java
Element names used in the CDS model might conflict with reservedJava 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 andstatic model interfaces. The element name used as key in the underlying map fordynamic access isn't affected by this annotation.
See the following example:
entity Equity { @cds.java.name : 'clazz' class : String;}
interface Equity { @CdsName("class") StringgetClazz(); @CdsName("class") void setClazz(Stringclazz);}
Renaming Types in Java
For entities and types it is recommended to use@cds.java.this.name
to specify an alternative name for the accessor interfaces andstatic model interfaces. The annotation@cds.java.this.name
- in contrast to@cds.java.name
- is not propagated, along projections, includes or from types to elements.
Unexpected effects of@cds.java.name
on entities and types
The annotation propagation behaviour applied to@cds.java.name
can have unexpected side effects when used to rename entities or types, as it is propagated along projections, includes or from structured types to (flattened) elements. Nevertheless it might be useful in simple 1:1-projection scenarios, where the base entity and the projected entity should be renamed in the same way.
See the following example, renaming an entity:
@cds.java.this.name: 'Book'entity Books { // ...}
@CdsName("Books")public interface Book extends CdsData { // ...}
Here is another example, renaming a type:
@cds.java.this.name: 'MyName'type Name { firstName: String; lastName: String;}entity Person { publicName: Name; secretName: Name;}
@CdsName("Name")public interface MyName extends CdsData { // ...}@CdsName("Person")public interface Person extends CdsData { String PUBLIC_NAME= "publicName"; String SECRET_NAME= "secretName"; MyNamegetPublicName(); void setPublicName(MyNamepublicName); MyNamegetSecretName(); void setSecretName(MyNamesecretName);}
See how the previous example would turn out with@cds.java.name
@cds.java.name: 'MyName'type Name { firstName: String; lastName: String;}entity Person { publicName: Name; secretName: Name;}
@CdsName("Name")public interface MyName extends CdsData { // ...}@CdsName("Person")public interface Person extends CdsData { String MY_NAME= "publicName"; String MY_NAME= "secretName"; MyNamegetMyName(); void setMyName(MyNamemyName); MyNamegetMyName(); void setMyName(MyNamemyName);}
Note, that the propagated annotation@cds.java.name
creates attribute and method conflicts inPerson
.
WARNING
This feature requires version 8.2.0 of theCDS Command Line Interface.
Entity Inheritance in Java
In CDS models it is allowed to extend a definition (for example, of an entity) with one or more namedaspects. 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 theinclude 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. This feature comes with many limitations and does not promise support in all scenarios.
The@cds.java.extends
annotation can contain an array of string values, each of which denote the fully qualified name of a CDS definition (typically an aspect) that is extended. In the following example, the Java accessor interface generated for theAuthorManager
entity shall extend the accessor interface of the aspecttemporal
for which the Java accessor interfacecds.gen.Temporal
is generated.
using {temporal }from '@sap/cds/common';@cds.java.extends: ['temporal']entity AuthorManager : temporal { keyID : Integer; name : String(30);}
The accessor interface generated for theAuthorManager
entity is shown in the following sample:
import cds.gen.Temporal;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) IntegergetId(); @CdsName(ID) void setId(Integerid); StringgetName(); void setName(Stringname); static AuthorManagercreate() { return Struct.create(AuthorManager.class); }}
In CDS, annotations on an entity are propagated to views on that entity. If a view projects 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 theAuthorManager
entity and the inherited annotation overwritten via@cds.java.extends : null
to avoid the accessor interface ofAuthorManagerService
to extend the interface generated fortemporal
.
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 theStruct.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 staticcreate
method that further facilitates the usage:
Book book= Books.create();book.setTitle("Dracula");String title= book.getTitle();// title: "Dracula"
If the entity has a single key, the generated interface has an additional staticcreate
method that has the key as the argument. For example, given that theBook
entity has keyID
of typeString
, you can create the entity and set a key like that:
Book book= Books.create("9780141439846");String id= book.getId();// id: "9780141439846"
For entities that have more than one key, for example, for draft-enabled entities, the additionalcreate
method isn't generated and only the default one is available.
Read-Only Access
Create a typed read-only view usingaccess
. 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 asIterable<Map<String, Object>>
can also bestreamed:
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 theprocessing of query result.
Data Processor
TheCdsDataProcessor
allows to process deeply nested maps of CDS data, by executing a sequence of registered actions (validators,converters, andgenerators).
Using thecreate
method, a new instance of theCdsDataProcessor
can be created:
CdsDataProcessor processor= CdsDataProcessor.create();
Validators,converters, andgenerators can be added using the respectiveadd
method, which takes a filter and an action as arguments and is executed when thefilter
is matching.
processor.addValidator(filter, action);
When calling theprocess
method of theCdsDataProcessor
, the actions are executed sequentially in order of the registration.
List<Map<String,Object>> data;// data to be processedCdsStructuredType rowType;// row type of the dataprocessor.process(data, rowType);
The process method can also be used on CDS.ql results that have a row type:
CqnSelect query;// some queryResult result= service.run(query);processor.process(result);
Element Filters
Filters can be defined as lambda expressions onpath
,element
, andtype
, 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 ofelement
and provides access to the data values of each path segmentelement
is the CDS elementtype
- 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. Newvalidators can be added using theaddValidator
method. The following example adds avalidator that logs a warning if the CDS elementquantity
has a negative value. The warning message contains thepath
to theelement
.
processor.addValidator( (path, element, type)-> element.getName().equals("quantity"),// filter (path, element, value)-> {// validator if ((int) value< 0) { log.warn("Negative quantity: " + path.toRef()); } });
By default, validators are called if the data mapcontains a value for an element. This can be changed via theprocessing mode, which can be set to:
CONTAINS
(default): The validator is called for declared elements for which the data map contains any value, includingnull
.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 containsnull
or no value mapping, usingABSENT
as a placeholder value.DECLARED
: The validator is called for all declared elements, usingABSENT
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. Newconverters can be added using theaddConverter
method. The following example adds aconverter that formats elements with nameprice
.
processor.addConverter( (path, element, type)-> element.getName().equals("price"),// filter (path, element, value)-> formatter.format(value));// converter
To remove a value from the data, returnConverter.REMOVE
. The following example adds aconverter 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. Newgenerators can be added using theaddGenerator
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
Diff Processor
To react on changes in entity data, you need to compare the image of an entity after a certain operation with the image before the operation. To facilitate this task, use theCdsDiffProcessor
, similar to theData Processor. The Diff Processor traverses through two images (entity data maps) and allows to register handlers that react on changed values.
Create an instance of theCdsDiffProcessor
using thecreate()
method:
CdsDiffProcessor diff= CdsDiffProcessor.create();
You can compare the data represented asstructured data, which is a result of the CQN statements or arguments of event handlers. For a comparison with theCdsDiffProcessor
, the data maps that are compared need to adhere to the following requirements:
- The data map must include values for all key elements.
- The names in the data map must match the elements of the entity.
- Associations must be represented asnested structures and associations according to the associations` cardinalities.
Thedelta representation of collections is also supported. Results of the CQN statements fulfill these conditions if the typethat comes with the result is used, not the entity type.
To run the comparison, call theprocess()
method and provide the new and old image of the data as aMap
(or a collection of them) and the type of the compared entity:
List<Map<String,Object>> newImage;List<Map<String,Object>> oldImage;CdsStructuredType type;diff.process(newImage, oldImage, type);
Result newImage= service.run(Select.from(...));Result oldImage= service.run(Select.from(...));diff.process(newImage, oldImage, newImage.rowType());
Comparing draft-enabled entities
If you compare the active image of a draft-enabled entity with the inactive one, make sure that theIsActiveEntity
values are either absent or the same in both images.
In case one of the images is empty, theCdsDiffProcessor
traverses through the existing image treating it as an addition or removal mirroring the logic accordingly.
Changes detected byCdsDiffProcessor
are reported to one or more visitors implementing the interfaceCdsDiffProcessor.DiffVisitor
.
The visitor is added toCdsDiffProcessor
with theadd()
method before starting the processing.
diff.add(new DiffVisitor() { @Override public void changed(PathnewPath, PatholdPath, CdsElementelement, ObjectnewValue, ObjectoldValue) { // changes } @Override public void added(PathnewPath, PatholdPath, CdsElementassociation, Map<String,Object>newValue) { // additions } @Override public void removed(PathnewPath, PatholdPath, CdsElementassociation, Map<String,Object>oldValue) { // removals }});
The visitor can be added together with theelement filter that limits the subset of changes reported to the visitor.
diff.add( new Filter() { @Override public boolean test(Pathpath, CdsElementelement, CdsTypetype) { return true; } }, new DiffVisitor() { ... });
You may add as many visitors as you need by chaining theadd()
calls. Each instance of theCdsDiffProcessor
can have its own set of visitors added to it.
If your visitors need to be stateful, prefer one-time disposable objects for them.CdsDiffProcessor
does not manage their state.
All values are compared using the standard Javaequals()
method, including elements with a structured or arrayed type.
Implementing a DiffVisitor
Additions and removals in the entity image are reported as calls to the methodsadded()
orremoved()
. The called methods always receive the complete added or removed content for the entity or an association.
The methodsadded()
andremoved()
have the following arguments:
newPath
and theoldPath
as instances ofPath
reflecting the new and old image of the entity.association
as an instance ofCdsElement
given that the association is present.- Changed data as a
Map
, as either thenewValue
oroldValue
.
The instances of thePath
represent the placement of the changed item within the whole entity as a prefix to the data that is either added or removed. While these paths always have the same structure,oldPath
andnewPath
can have empty values, which represent the absence of data.
Theassociation
value foradded()
andremoved()
is only provided if data is compared along associations or compositions. Null value represents the complete entity that is added or removed.
Let's break it down with the examples:
Given that we have a collection of books each has a composition of many editions.
When a new book is added to the collection, the method
added()
is called once with thePath
instance with one segment representing a book as thenewPath
,association
will be null and thenewValue
will also be the content of the book.Old image (primary keys are omitted for brevity) of the book collection is:
json[ { "title":"Wuthering Heights", "editions": [] } ]
New image of the book collection is:
json[ { "title":"Wuthering Heights", "editions": [] }, { "title":"Catweazle", "editions": [] } ]
The content of the entity that visitor will observe in the
added()
method asnewValue
:json{ "title":"Catweazle", "editions": [] }
association
is null in this exact case.When new editions are added to two of the books in the collection, one per each book, the method
added()
is called twice with thePath
instance with two segments representing the book and the association to the edition. The association element is the value of the argumentassociation
, the data of the edition is thenewValue
. In this case, each added edition is accompanied by the content of the respective book.Old image of the book collection is:
json[ { "title":"Wuthering Heights", "editions": [] }, { "title":"Catweazle", "editions": [] } ]
New image of the book collection is:
json[ { "title":"Wuthering Heights", "editions": [ { "title":"Wuthering Heights: 100th Anniversary Edition" } ] }, { "title":"Catweazle", "editions": [ { "title":"Catweazle: Director's Cut" } ] } ]
In the first
added()
call, the first added edition will be available and the paths will have the first book as the root.json{ "title":"Wuthering Heights: 100th Anniversary Edition"}
In the second call - the second added edition with the second book as the root of the path.
json{ "title":"Catweazle: Director's Cut"}
Given the previous example, there are two new editions added to one of the books: the
added()
method will be called once per edition added. Path instances with same book (same primary key) tell you which edition belongs to which book.Old image is the same as before, new image of the book collection is:
json[ { "title":"Wuthering Heights", "editions": [ { "title":"Wuthering Heights: 100th Anniversary Edition" } ] }, { "title":"Catweazle", "editions": [ { "title":"Catweazle: Director's Cut" }, { "title":"Catweazle: Complete with Extras" } ] } ]
First
added()
call will observe the new edition of the first book:json{ "title":"Wuthering Heights: 100th Anniversary Edition"}
The following two calls will observe each added edition of the second book:
json{ "title":"Catweazle: Director's Cut"}
json{ "title":"Catweazle: Complete with Extras"}
Methodchanged()
is called for each change in the element values and has the following arguments:
- A pair of
Path
instances (newPath
andoldPath
) reflecting the new and old data of the entity. - The changed element as an instance of
CdsElement
. - The new and old value as
Object
instances.
Paths have the same target, that is, the entity where changed element is. But their values represent the old and new image of the entity as a whole including non-changed elements. You may expect that each change is visited at most once.
Let's break it down with the examples:
Given the collection of books with editions, as before.
[ { "title":"Wuthering Heights", "editions": [ { "title":"Wuthering Heights: 100th Anniversary Edition" } ] }, { "title":"Catweazle", "editions": [ { "title":"Catweazle: Director's Cut" } ] } ]
When book title is changed from one value to the other, the method
changed()
is called once with bothPath
instances representing a book images, elementtitle
is available as an instance ofCdsElement
, the new and old value of the title are available asnewValue
andoldValue
.New image:
json[ { "title":"Wuthering Heights", "editions": [ { "title":"Wuthering Heights: 100th Anniversary Edition" } ] }, { "title":"Catweazle, the series", "editions": [ { "title":"Catweazle: Director's Cut" } ] } ]
The Diff Visitor will observe the
Catweazle, the series
andCatweazle
as the new and the old value.When title of the edition is changed for one of the books, the
changed()
method is called once, the paths include the book and the edition. Element reference and values are set accordingly.New image:
json[ { "title":"Wuthering Heights", "editions": [ { "title":"Wuthering Heights: 100th Anniversary Edition" } ] }, { "title":"Catweazle", "editions": [ { "title":"Catweazle: Unabridged" } ] } ]
Visitor will observe the
Catweazle: Unabridged
andCatweazle: Director's Cut
as the new and the old value.
For changes in the associations, when association data is present in both images, even if key values are different, thechange()
method will always be called for the content of the association traversing it value-by-value. In case data is absent in one of them, theadded()
orremoved()
will be called instead.
Several visitors added to theCdsDiffProcessor
are called one by one, but you should not expect the guaranteed order of the calls for them. Consider them as an independent.
Immutable data
Do not modify the state of the images inside the visitors. Consider the data presented to it immutable.
Filtering for DiffVisitor
Element filters are useful if you want to extract some common condition out of your visitor implementation so that you don't have to branch in all methods of your visitor.
As a general rule, you may assume that element filter is called at least once for each changed value you have in your image and the visitor supplied next to the filter is called for elements where the element filter condition is evaluated totrue
.
In the implementation of the filter you can use the definition of theCdsElement
, its type or aPath
to decide if you want your visitor to be notified about the detected change.
In simple cases, you may use the element and its type to limit the visitor so that it observes only elements having a certain annotation or having a certain common type, for example, only numbers.
If you compare a collection of books to find out of there is a differences in it, but you are only interested in authors, you can write a filter using the entity type that is either the target of some association or the parent of the current element.
diff.add(new Filter() { @Override public boolean test(Pathpath, CdsElementelement, CdsTypetype) { return element.getType().isAssociation() && element.getType().as(CdsAssociationType.class).getTarget().getQualifiedName().equals(Authors_.CDS_NAME) || path.target().type().equals(Authors_.CDS_NAME); }}, ...);
Filters cannot limit the nature of the changes your visitor will observe and are always positive.
Deep Traversal
For documents that have a lot of associations or a compositions and are changed in a deep way you might want to see additions for each level separately.
To enable this, you create an instance ofCdsDiffProcessor
like that:
CdsDiffProcessor diff= CdsDiffProcessor.create().forDeepTraversal();
In this mode, the methodsadded()
andremoved()
are called not only for the root of the added or removed data, but also traverse the added or removed data, entity by entity.
It's useful, when you want to track the additions and removals of certain entities on the leaf levels or as part of visitors tailored for generic use cases.
Media Type Processing
The data formedia type entity properties (annotated with@Core.MediaType
) - as with any other CDS property with primitive type - can be retrieved by their CDS name from theentity data argument. See alsoStructured Data andTyped Access for more details. The Java data type for such byte-based properties isInputStream
, and for character-based properties it isReader
(see alsoPredefined Types).
Processing such elements within a custom event handler requires some care though, as such anInputStream
orReader
isnon-resettable. That means, the data can only be read once. This has some implications you must be aware of, depending on what you want to do.
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 stream.
No Custom Processing
Media Upload
If you just want to pass the uploaded stream to the persistence layer of the CAP architecture to have the data written into the database, you don't have to implement any custom handler. This is the simplest scenario and our defaultOn
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 defaultOn
handler reads from the database and passes the stream 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 stream with custom logic (for example, to parse a stream of CSV data), the best place to do that is in a customOn
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);}
WARNING
After you have fully consumed the stream in your handler logic, passing the sameInputStream
orReader
instance for further consumption would result in no bytes returned, because anon-resettable stream can only be consumed once. In particular, make sure that the defaultOn
handler is not called after your custom processing.
Using a customOn
handler and settingcontext.setResult(books)
prevents the execution of the defaultOn
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 a stream proxy described below.
Pre- or Post-Processing Using a Stream 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 stream is consumed by several parties (for example, the virus scanner and the persistence layer). To achieve this, implement a proxy that wraps the originalInputStream
orReader
instance and executes the processing logic within theread()
methods on the data read directly. Such a proxy can be implemented by extending aFilterInputStream, aProxyInputStream, aFilterReader or aProxyReader.
The following example uses aFilterInputStream:
public class CoverImagePreProcessor extends FilterInputStream {public CoverImagePreProcessor(InputStreamwrapped) {super(wrapped);}@Overridepublic int read()throws IOException {int nextByte= super.read();// ... your custom processing code on nextBytereturn nextByte;}@Overridepublic int read(byte[]bts,int off,int len)throws IOException {int bytesRead= super.read(bts, off, len);// ... your custom processing code on bts arrayreturn bytesRead;}}
This proxy is then used to wrap the originalInputStream
. This works for both upload and download scenarios.
Media Upload
For uploads, you can either use a customBefore
orOn
handler to wrap the proxy implementation around the original stream before passing it to its final destination.
Using a customBefore
handler makes sense if the stream'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 in this example is implemented in theread()
methods of theFilterInputStream
and is only called when the data is streamed, during theOn
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 originalInputStream
is replaced by the proxy implementation in thecoverImage
element of thebook
entity and passed along. Every further code trying to access thecoverImage
element will use the proxy implementation instead.
Using a customOn
handler makes sense if you want to prevent that the defaultOn
handler is executed and to control the final destination for the stream. You then have the option to pass the streamed data on to some other service for persistence:
@On(event = CdsService.EVENT_UPDATE)public ResultprocessCoverImage(CdsUpdateEventContext context, List<Books> books) {books.forEach(book-> {book.setCoverImage(new CoverImagePreProcessor(book.getCoverImage()));});// example for invoking some CQN-based servicereturn service.run(Update.entity(Books_.CDS_NAME).entries(books));}
Media Download
For download scenarios, the stream to wrap is only available inAfter
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 theInputStream
orReader
instance that is passed around. Once fully consumed, it can no longer be read from in remaining event phases.