Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transforming Classes with Java 8

I've been exploring transforming an object from one type of class to another with java 8. What I have are a bunch of xjc generated jaxb classes. The classes don't have a sufficiently friendly structure because they map the xml more structure rather than the business object structure. I'm unwilling to edit the generated classes because I like to regenerate them whenever the schema changes without having to worry about retaining customizations.

I've got a schema something like:

<xs:element name="farm">
    <xs:sequence>
        <xs:element ref="animal" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
</xs:element>

<xs:element name="animal">
    <xs:complexType>
        <xs:sequence>
            <xs:element ref="goat"/>
            <xs:element ref="sheep"/>
        </xs:sequence>
     <xs:complexType>
</xs:element>

<xs:element name="goat">
    <xs:complexType>
        <xs:sequence>
            goat fields
        </xs:sequence>
     <xs:complexType>
</xs:element>

<xs:element name="sheep">
    <xs:complexType>
        <xs:sequence>
            sheep fields
        </xs:sequence>
     <xs:complexType>
</xs:element>

This generates Java something like:

class Farm
    public List<Animal> getAnimals()

class Animal

    public Goat getGoat()
        public String getGoatField()

    public Sheep getSheep()
        public String getSheepField()

getGoat and getSheep can return null, but they can't both be null. Likewise, at least one of them must be null. This is enforced via business rules and database constraints, but not in the xml (although if anyone has a suggestion to structure the xml more like the desired VOs I'm all ears)

I'd like to transform this class into

class FarmWrapper
    public ArrayList<AnimalVO> getAnimals()
    //optional features tbd
    //possibly public ArrayList<GoatVO> getGoats()
    //possibly public ArrayList<SheepVO> getSheep()

class GoatVO extends AnimalVO

class SheepVO extends AnimalVO

My idea was to do something like this:

herd.stream()
.filter(Objects::nonNull)
.map(a -> {
    Optional<AnimalVO> goatVO = Optional.ofNullable(a.getGoat())
            .map(g ->  new GoatVO(g.getGoatField()));
    Optional<AnimalVO> sheepVO = Optional.ofNullable(a.getSheep())
            .map(s ->  new SheepVO(s.getSheepField()));
    return goatVO.orElse(sheepVO.get());
})
.collect(Collectors.toList());

Right now I've been feeding it a list, and as soon as it encounters a null sheep, it throws a NoSuchElementException.

I suppose I a few questions:

  1. Is this an approach worth taking to split my list into classes that use inheritance?
  2. What's the best way to use Optional to protect against incoming potentially null values, when you can't change the classes passing you nulls
  3. What am I missing with goatVO.orElse(sheepVO.get()) working as long as goatVO contains null and then throwing NoSuchElementException when sheepVO contains null

What I'm really doing is working with generated jaxb code and trying to take the generated classes and make the friendlier to work with. Traditionally the project has used a wrapper class that transforms the generated classes to VOs via a substantial amount of null checking and int to BigInteger sort of manipulations.

Editing the generated classes (Goat, Sheep, Animal) is a non starter because I would like to retain the ability to regenerate without worrying

like image 460
monknomo Avatar asked Feb 20 '16 01:02

monknomo


Video Answer


2 Answers

I think you can make your code work with a couple of adjustments:

List<AnimalVO> list = herd.stream()
    .filter(Objects::nonNull)
    .map(a -> Optional.ofNullable(a.getGoat())
        .map(Goat::getGoatField)
        .<AnimalVO>map(GoatVO::new)
        .orElseGet(() -> new SheepVO(a.getSheep().getSheepField())))
    .collect(Collectors.toList());

Note: I prefer method references over lambdas, so I've switched to using them.

Note the hint to the compiler in <AnimalVO>map(GoatVO::new). This is necessary to let the compiler know that the type you are mapping to is always an AnimalVO, otherwise it infers that the first Optional returns a GoatVO and gives a compilation error in the second Optional, which returns a SheepVO (and SheepVO is not a descendant of GoatVO).

Also note that I'm using orElseGet() method instead of orElse(). orElseGet() receives a Supplier of a default value, instead of the default value itself. This means that the default value is picked lazily, only when the value of the first Optional is not present.


EDIT: If you had more animals in your farm, i.e. apart from Goat and Sheep, now you have a Cow, this is how you could do it:

List<AnimalVO> list = herd.stream()
    .filter(Objects::nonNull)
    .map(a -> Optional.ofNullable(a.getGoat())
        .map(Goat::getGoatField)
        .<AnimalVO>map(GoatVO::new)
        .orElseGet(() -> Optional.ofNullable(a.getSheep())
            .map(Sheep::getSheepField)
            .<AnimalVO>map(SheepVO::new)
            .orElseGet(() -> new CowVO(a.getCow().getCowField()))))
    .collect(Collectors.toList());
like image 162
fps Avatar answered Nov 06 '22 15:11

fps


  1. Basically you can do it like that, although inheritance model would be better (Sheep extends Animal, Goat extends Animal)
  2. If you want to protect against potential nulls, you can do something = optionalValue.orElse(defaultValue) which will return value contained by Optional if the value is not null, or defaultValue if the value in Optional is null
  3. Line goatVO.orElse(sheepVO.get()) throws NoSuchElementException because sheepVO contains null. Please note that sheepVO.get() evaluates regardless the value in goatVO.
like image 21
stjepano Avatar answered Nov 06 '22 16:11

stjepano