5.6 Service

"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 Structure

Figure 5.14. Service Structure


Service operations map to indiviudal use cases of the domain with the following functional and non-functional characteristics

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:

  • correct wiring of the stated dependencies based on the mechanism provided by the spring⇒IOC

  • a member variable of the given dependency type in the generated service implementation class

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 with support for runtime filter expressions

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.

  1. define some arbitrary ql select statement

    repository CustomerDao for Customer {
       operation Customer[] findAllLikeLastName(lastName):
         from Customer customer where customer.lastName like :lastName
    }

  2. 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);
    

  3. 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.

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.



[5] Evans, 2003, S. 105.