"A SERVICE is an operation offered as an interface that stands alone in the model, without encapsulating state, as ENTITIES and VALUE OBJECTS do. SERVICES are a common pattern in technical frameworks, but they can also apply in the domain layer." [5] [DDD2003]
A Service maps to the same concept in ⇒MDD
and represents the service layer of a domain and are used as the external
entry point to an application's business logic. Services are copposed of
normal (to be manually implemented) operations and so
called 'delegate dataaccess operations' which are
used to operate on DataViews retrieved and converted from
internal entity access methods (i.e. finders and dataaccess operations).
Since the default generator doesn't support the returning or passing of
entity type structures from or to the external service layer it will
replace all entity references with the corresponding default
DataView instead. For example if an operation (i.e. exported
repository or manual operation) has an entity return type or declares an
entity type parameter it will be automatically replaced with the default
DataView in the generated source code.
Service operations map to indiviudal use cases of the domain with the following functional and non-functional characteristics
transaction demarcation based on the respective use-case demands
security (⇒RBACL)
logging, journaling and auditing (maybe compliance with any revision demands)
any means to create/provide necessary context/state (e.g. client language or locale,user,roles) objects for the called operation
validation of incoming state
business logic
An example of a service expressed with the domain model dsl is given in the following listing which defines a service named OrderReportService with two required dependencies and one abstract service operation.
// Represents a service used to generate and mail an aggregated report of customer orders for the given year and month
service OrderReportService uses OrderDao, EmailService {
operation generateAndMailOrderReport(YearMonth yearMonth, String emailAddress)
}The OrderReportService example above contains a few important concepts which also apply to other model elements. First and foremost it gives the service a name: 'OrderReportService'. The name is a required identifier token and primarily used for the following things:
to reference this service by name from within another service which depends on the 'OrderReportService' , like it is done with the 'uses EmailService' in the example above.
the name (in combination with the containing package folder) is used as the default name of the generated java service interface and java implemenation.
it is used as the identifier of the corresponding spring bean configuration generated by default.
The next thing to note is the uses keyword. The
uses keyword defines a dependency to another Service or
DataAccessObject
which are required to implement the behaviour of the user defined
'generateAndMailOrderReport' operation. If you declare a dependency to
another Service or ⇒DAO the default generator
templates provides you with the following:
Services allow to define so called abstract service
operations as a placeholder for custom behaviour which has to
be implemented manually within concrete service class implementations.
Service operations start with the operation or
op keyword followed by a name and and optional list of
input parameters.
service OrderReportService uses OrderDao,CustomerDao,EmailService {
operation generateAndMailOrderReport(YearMonth yearMonth, String emailAddress)
}
service OrderServiceFacade uses CustomerDao {
operation CustomerOrderView findByCustomerOid(String oid)
operation OrderStateView[] findAllOrderStates(String oid)
OrderStateView Order.findByOrderNumber
}
service EmailService {
operation sendEmail(String emailAddress, String subject, String text)
}In addition to operations which must be manually implemented
(s.a.), services also support the notion of fully generated dataacess
operations used to access and modify entities via
dataviews. The definition of such an operation consists of
a (matching) dataview returntype, an repository reference,
an access operation type or reference and an optional
dataview parameter. Dependent on the particular type of
access operation the return type and/or view parameter is required or
optional. (e.g. a read operation must always define a
dataview return type otherwise the model is considered
invalid and will not be processed correctly). The following listing
gives an example of standard crud dataaccess operations to
read and modify Customer entities.
Note: dont go with crud as the default. only specify whats actually required from yout use-cases
service CustomerService {
Customer.create(CustomerView)
CustomerView Customer.read
Customer.update(CustomerView)
Customer.delete
}A shortcut syntax for the generation of all individual dataaccess
operation types (create,update,delete,read) is available
with the crud keyword. If create and
update (or crud) is defined, a saveOrUpdate
functionallity will be generated as well.
service CustomerService {
Customer.crud
}Note: Prior to including
a repository data access operation it has to be declared in the
referenced repository first like in the following
example.entity Customer {
id String oid
version Timestamp ^version
String(25) firstName
String(25) lastName
String(40) emailAddress
Date("Medium") birthDate
}
repository CustomerDao for Customer {
operation Customer findByEmailAddress(emailAddress):
from Customer customer where customer.emailAddress = :emailAddress
}
service CustomerService {
CustomerDao.findByEmailAddress
}Actually this operation does two things: First it delegates to the included finder operation on the customer repository (with the given emailAddress parameter) and afterwards it converts the entity result to the default (implicitly created) customer dataview. It is also possible to override entity return types of included repository operations with an arbitrary dataview as long as the specified dataview includes (maps) attributes of the returned entity as shown in the following example.
entity Customer {
id String oid
version Timestamp ^version
String(25) firstName
String(25) lastName
String(40) emailAddress
Date("Medium") birthDate
}
dataview CustomerName {
Customer.firstName
Customer.lastName
}
repository CustomerDao for Customer {
operation Customer[] findAllLikeLastName(lastName):
from Customer customer where customer.lastName like :lastName
}
service CustomerService {
CustomerName CustomerDao.findAllLikeLastName
}Note: you dont have to repeat
the [] collection brackets in the overriden service
operation return type since its automatically derived from the
delegating dao operation signature.
Service operations, which delegate to ql-based repository
operations, support the specification of some boolean flag
(filter=true ) to enable the generation of an additional
method parameter. This parameter represents an expression
(QueryObject) wich is used to further restrict the existing
ql WHERE clause with the specification of additional
criterias.
Note: this additional filter expression is always additive to the existing where clause and any existing named parameter in the original ql statement has to be provided as well
The main target of this feature is the support of UI's with pageable und filterable table requirements for which the user wants to add additional filter and sort criterias at runtime. The following two steps are required to enable the usage of the above mentioned filter and paging feature from the service layer.
define some arbitrary ql select statement
repository CustomerDao for Customer {
operation Customer[] findAllLikeLastName(lastName):
from Customer customer where customer.lastName like :lastName
}first include the repository operation in some service in
order make it available to clients AND enable the generation of
the additional filter parameter
filter=true
service CustomerService {
CustomerDao.findAllLikeLastName
filter=true // this triggers the generation of an additional filter method parameter
}
// this will generate the following two methods within the CustomerService interface
Collection<CustomerView> findAllLikeLastName(String lastName);
Collection<CustomerView> findAllLikeLastName(String lastName,Expression _filter);
some manual client code using the expression filter to specify additional constraints
and customerim0_.FIRST_NAME=? order by customerim0_.LAST_NAME ascQueryObject queryObject = new QueryObject();
Expression eqFirstName = queryObject.get("firstName").eq(customerEditDto.getFirstName());
Expression lastNameAsc = queryObject.get("lastName").asc();
Collection<CustomerView> collection = customerService.findAllLikeLastName(customerEditDto.getLastName(), queryObject.where(eqFirstName).orderBy(lastNameAsc));
// creates the following pseudo sql (note the additional where predicate and order by clause compared to the original ql statement in 1.)
select ... from CUSTOMER customerim0_
where ( customerim0_.LAST_NAME like ? )
Note: if a filterable service operation (see above) is bound to some presentation model component which supports paging and filtering, e.g. paging table and table customizer, the manual declaration of the filter expression parameter above is not necessary since it will be handled automatically by those components at runtime.
For each service element one interface and one java class is generated.
Note: There is currently no built-in support to customize this behaviour without to change the default provided workflow and templates.
The org.openxma.dsl.platform library supplies the
service layer with it's own set of generic and specialized exceptions -
implemented in the org.openxma.dsl.platform.excpetions
package - which are supposed to be the only exceptions a client of the
service layer should deal with. For resolution of the corresponding
error messages you will find support in the
org.openxma.dsl.platform.i18n package.
The platform library provides the generic
SystemException for any technical problems and the
ApplicationException for custom business exceptions. They
both derive from the basic ErrorCodedException which is
based on the idea of a generic exception class that is used for all
exceptional cases, communicating the error case by the help of an
error code.
In contrast to the XMA core
at.spardat.enterprise.exc.SysException and
at.spardat.enterprise.exc.AppException
(epclient library) the platform exceptions:
use a String error code instead of an
int for better readability and ease of message
resolution
don't transport the final error message but keep the given message parameters
always require a default message which shall be used for exception logging
There is a handful of ApplicationException children
that cover special exceptional cases:
ConcurrentUpdateException for cases of
concurrent updates
NonUniqueException for violations of unique
constraints
BeanValidationException for violations of bean
constraints, container of BeanValidationException's,
which will usually be a list of:
PropertyValidationException for violations of
bean property constraints, derives from
BeanValidationException
ExceptionSupport supports the creation of
validation exceptions from Spring error objects
As of XMA version 4.1 automatic validation of data is done in
the XMA GUI only. The generated validators of the service layer extend
org.openxma.dsl.platform.validation.Validators and
implement checks for the constraint definitions of the entities'
attributes. As a limitation they operate on the generated entity
classes only. Therefor the view objects (data transfer objects of the
service layer) have to be mapped to entities before they can be
validated in the service methods.
Here as an example is the Book entity (Book.dml) of a library administration application:
import at.spardat.xma.types.*
entity Book {
id Long oid
version FipDate fipDate
CategoryEnum category
required=true
title="Category"
Integer(3) bookNumber
required = true
title = "Nr"
String(100) bookTitle
required = true
title = "Title"
String(100) subTitle
title = "Subtitle"
String(4) year
title = "Year"
LanguageEnum language
required = true
title = "Language"
unique categoryAndBookNumber(category, bookNumber)
Author[] authors
Lending[] lendings oppositeof book
}Here you see the generated validator for the Book class, which checks for the 'required' and the length constraints:
import org.springframework.validation.Errors;
import at.spardat.seabooks.book.model.Book;
import org.openxma.dsl.platform.validation.Validators;
import org.springframework.validation.ValidationUtils;
@SuppressWarnings("boxing")
public abstract class BookGenValidator extends Validators {
public boolean isCategoryRequired(Book book) {
return Boolean.TRUE;
}
public boolean isBookNumberRequired(Book book) {
return Boolean.TRUE;
}
public boolean isBookTitleRequired(Book book) {
return Boolean.TRUE;
}
public boolean isLanguageRequired(Book book) {
return Boolean.TRUE;
}
@SuppressWarnings("unchecked")
public boolean supports(Class clazz) {
return Book.class.isAssignableFrom(clazz);
}
public Errors validate(Book book) {
Errors errors = createErrors(book);
validate(book,errors);
return errors;
}
public void validate(Object object, Errors errors) {
Book book = (Book) object;
if (isCategoryRequired(book)) {
ValidationUtils.rejectIfEmpty(errors, "category", "field.required");
}
if (isBookNumberRequired(book)) {
ValidationUtils.rejectIfEmpty(errors, "bookNumber", "field.required");
}
if (isBookTitleRequired(book)) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "bookTitle", "field.required");
}
if (isLanguageRequired(book)) {
ValidationUtils.rejectIfEmpty(errors, "language", "field.required");
}
rejectIfMaxIntegerDigits(errors,"bookNumber",book.getBookNumber(),3);
rejectIfMaxLength(errors,"bookTitle",book.getBookTitle(),100);
rejectIfMaxLength(errors,"subTitle",book.getSubTitle(),100);
rejectIfMaxLength(errors,"year",book.getYear(),4);
}
}The Errors object returned from the validate method is a member of the Spring validation framework.
Here is an example for the use of the validator in the update method of the BookDas service:
@Service("bookDas")
public class BookDasImpl extends BookDasGenImpl {
private BookValidator validator = new BookValidator();
@Transactional
public void update(BookView bookView) {
Assert.notNull(bookView, "parameter 'bookView' must not be null");
validate(bookView);
// ...do the update...
}
protected void validate(BookView bookView) {
// apply validators
Book book = this.mapper.createAndMapOne(bookView, Book.class,"saveBook");
Errors errors = this.validator.validate(book);
if (errors.hasErrors()) {
throw ExceptionSupport.mapToValidationException(errors, book);
}
}
// ...
}The unique constraint shown in the Book entity does neither melt down to any constraint check in the generated code nor is a unique constraint index in the DB schema derived from it. The only artifact you will get from it is a load method in the DAO of the Book entity:
public interface BookDaoGen extends GenericDao<Book,Long>, EntityFactory<Book> {
// ...
Book loadBycategoryAndBookNumber(CategoryEnum category,Integer bookNumber);
}The load method can be used in the Book service validation like this:
@Service("bookDas")
public class BookDasImpl extends BookDasGenImpl {
private BookValidator validator = new BookValidator();
@Transactional
public void update(BookView bookView) {
Assert.notNull(bookView, "parameter 'bookView' must not be null");
validate(bookView);
// ...do the update...
}
protected void checkConstraints(BookView bookView) {
// check unique constraints 'manually'
Book uniqueBook = this.bookDao.loadBycategoryAndBookNumber(bookView.getCategory(), bookView.getBookNumber());
if (uniqueBook != null && ! uniqueBook.getOid().equals(bookView.getOid())) {
throw new NonUniqueException("Book", new String[] {"category", "bookNumber"}, "Book with same category & bookNumber already exists");
}
}
protected void validate(BookView bookView) {
// apply validators
Book book = this.mapper.createAndMapOne(bookView, Book.class,"saveBook");
Errors errors = this.validator.validate(book);
if (errors.hasErrors()) {
throw ExceptionSupport.mapToValidationException(errors, book);
}
checkConstraints(bookView);
}
// ...
}The base exception class ErrorCodedException
implements the interface MessageResolvable which supplies
all information necessary for the resolution of messages. The error
code of the exception is returned as key in order to be used for
searching resource bundles:
package org.openxma.dsl.platform.i18n;
public interface MessageResolvable {
public String getKey();
public Object[] getArguments();
public String getDefaultMessage();
}The org.openxma.dsl.platform.i18n package
furthermore provides the interface MessageProvider that
defines the methods for resolving a bundle key or a
MessageResolvable to the corresponding message. The
SimpleErrorMessageProvider is a base
MessageProvider implementation that allows for the
resolution of error messages from platform exceptions. It can be given
application specific resource bundles, in case an error code (=bundle
key) is not found there the resource bundle
org.openxma.dsl.platform.exceptions.ErrorMessage will be
searched for it as a default. There you will find the English messages
for all the error codes used by the DSL platform exceptions
(validation errors, etc.). The default message, which should always be
available, will be returned in case the error code is not found in any
of the resource bundles.
Here you see how the base openXMA DSL server-side component
org.openxma.dsl.platform.xma.SpringComponentServer
converts SystemException's and
ApplicationException's to openXMA core exceptions while
injecting the error messages by the help of the
SimpleErrorMessageProvider.
public abstract class SpringComponentServer extends DslComponentServer {
protected ApplicationContext applicationContext;
protected MessageProvider messageProvider = new SimpleErrorMessageProvider();
//...
@Override
public Throwable convertToBaseException(Throwable detail) {
if (detail instanceof ApplicationException) {
return new AppException(detail, this.messageProvider.getMessage((ApplicationException) detail,
getSession().getContext().getLocale()));
} else if (detail instanceof SystemException) {
return new SysException(detail, this.messageProvider.getMessage((SystemException) detail,
getSession().getContext().getLocale()));
}
return super.convertToBaseException(detail);
}
//...
}Note:Both exception types could be handled in one place as
ErrorCodedException but we preferred to treat them
seperately. Any uncaught Spring exceptions that are returned from
service methods will be converted to
at.spardat.enterprise.exc.SysException, which is the
default processing of openXMA RPC's.