Skip to content

How to use Merge

Mikle edited this page Jun 20, 2017 · 2 revisions

Available since: WebLaF v1.2.9 release
Required Java version: Java 6 update 30 or any later
Module: core


What is it for?

Merge is a configurable utility allowing you to merge object settings. It was created to replace MergeUtils with a more flexible and configurable option.

It is quite a niche utility and might not be required at all for a wide variety of projects, but due to specifics of WebLaF styling system and some other projects I am working on I decided to make a proper implementation of it.

It is quite flexible since each Merge instance can be configured to behave differently, according to the provided settings.


What should I know about it?

WebLaF core module offers two basic intefaces - Mergeable and MergeBehavior - which are heavily used by Merge feature. Mergeable is a simple indicator interface, just like widely known Cloneable interface. Thought unlike Object.clone() there is no default method for customizing merge of two objects for Mergeable interface, that is why MergeBehavior interface exists and offers merge(...) method, but let me get to the details about it slightly later.

Merge itself is a final class that could be configured through providing different merge behavior settings upon its construction:

  • MergeNullResolver - Defines how exactly null objects should be handled by default. Some GlobalMergeBehavior implementations might provide a different way to handle them. You can provide only one MergeNullResolver implementation.

  • MergePolicy - Defines whether merge operation can be performed on the two specified objects or not. For example ExactTypeMergePolicy require objects to have the same class type. You can provide only one MergePolicy implementation.

  • GlobalMergeBehavior - Defines how exactly two objects will be merged. For example ListMergeBehavior implements merge algorithm of two List objects. You can provide multiple GlobalMergeBehavior implementations at once.

Custom implementations can be created of each of those interfaces to further customize Merge behavior, but there are multiple implementations already available within the library.

For MergeNullResolver:

  • OverwritingNullResolver - Always overwrites base object
  • SkippingNullResolver - Overwrites base object only if merged object is not null

For MergePolicy:

  • AnyTypeMergePolicy - Allows merge of any objects
  • RelatedTypeMergePolicy - Restricts merge to objects with related class types only
  • ExactTypeMergePolicy - Restricts merge to objects with the same class type only
  • SpecificTypesMergePolicy - Restricts merge to objects of the specified types only
  • CompoundMergePolicy - Restricts merge depending on specified policies

For GlobalMergeBehavior:

  • BasicMergeBehavior - Merge behavior for primitives, enumerations and some basic types
  • MergeableMergeBehavior - Merge behavior for proper MergeBehavior implementations usage
  • IndexArrayMergeBehavior - Array merge behavior by their element indices
  • MapMergeBehavior - Map merge behavior
  • ListMergeBehavior - Smart List merge behavior that tracks Identifiable elements
  • ReflectionMergeBehavior - Smart merge behavior for any types of Object with related class types

All of these implementations are quite simple but are enough to support various complex merge algorithms. I will be extending list of available behaviors and settings in the future to provide even more ways to customize Merge.


How to use it?

Main Merge method you will be using is:

public <T> T merge ( final Object object, final Object merged )

Call to this method asks Merge to do its best effort to merge merged object settings on top of object settings, so in most cases merged object will be returned as the result, but with modified settings. When it is not possible to merge two objects - merged will simply replace object, which means merged will be returned as the result instead.

Side note: With the future addition of Clone there will be some extra settings coming to Merge as well which will allow merged and object to be cloned before performing any merge operations. This might be quite important when you want to keep source objects intact.

So, first of all you need to instantiate Merge with the settings you need:

public class MergeExample
{
    public static void main ( final String[] args )
    {
        final Merge merge = new Merge (
                new SkippingNullResolver (),
                new RelatedTypeMergePolicy (),
                new BasicMergeBehavior (),
                new MergeableMergeBehavior (),
                new IndexArrayMergeBehavior (),
                new ReflectionMergeBehavior ()
        );
    }
}

I picked the most basic set of behaviors - one for primitives and basic types, one for MergeBehavior implementations support, one for arrays merging support and the reflection one for all other cases - I will talk about it slightly later.

Now you can already perform merge operations on any objects.

The most simple case is merging primitives:

final Integer mergedInteger = merge.merge ( 1, 20 );
final Boolean mergedBoolean = merge.merge ( true, false );

Or merging some simple immutable objects like String or Date:

final String mergedText = merge.merge ( "Text 1", "Text 2" );
final Date mergedDate = merge.merge ( new Date (), new Date ( System.currentTimeMillis () - 604800000 ) );

The outcome for those merge operations will be:

20
false

and:

Text 2
Thu Mar 09 13:58:24 MSK 2017

At least today - second date is a week before current date.

All those merge operations in this example are handled by BasicMergeBehavior which simply returns merged value as the result for primitives and immutable types. You can of course change the way Merge behaves by providing different GlobalMergeBehavior implementations, even for primitive types, into Merge constructor.

Example above is not really interesting though, so let's try Merge on something more complex. For that we will create a few custom classes containing some random data:

public class SampleObject
{
    private final String title;
    private final int value;
    private final SampleInnerObject inner;
    private final String[] states;

    public SampleObject ( final String title, final int value, final String inner, final String... states )
    {
        this.title = title;
        this.value = value;
        this.inner = new SampleInnerObject ( inner );
        this.states = states;
    }

    @Override
    public String toString ()
    {
        return title + " - " + value + " - " + inner + " - " + Arrays.toString ( states );
    }
}

And SampleInnerObject class:

public class SampleInnerObject implements MergeBehavior<SampleInnerObject>
{
    private String title;

    public SampleInnerObject ( final String title )
    {
        this.title = title;
    }

    @Override
    public SampleInnerObject merge ( final SampleInnerObject object )
    {
        title = title + " & " + object.title;
        return this;
    }

    @Override
    public String toString ()
    {
        return title;
    }
}

I should straight up mention a few important things here you might miss:

  • All fields within those two classes are private
  • In SampleObject all fields are final
  • There are not getters or settings for any fields
  • Unlike SampleInnerObject class SampleObject doesn't implement MergeBehavior
  • Method toString() is overridden for output purposes only

So let's try merging two instances of SampleObject now:

final SampleObject object1 = new SampleObject ( "Object1", 1, "Inner1", "One", "Two", "Three" );
final SampleObject object2 = new SampleObject ( null, 2, "Inner2", "Three", "Four" );
final SampleObject mergedObject = merge.merge ( object1, object2 );

And the result is (defined by SampleObject.toString() method):

Object1 - 2 - Inner1 & Inner2 - [Three, Four, Three]

And result referenced by mergedObject variable is the same as object1 since all settings got merged into it.

So this is what exactly happened here:

  • All SampleObject fields - even final ones - got their values replaced or updated within object1 instance
  • Field title value was preserved from object1 because it is null in object2 and we are using SkippingNullResolver in Merge
  • Array states received a new value - a combination of values from object2 and object1 according to IndexArrayMergeBehavior merge algorithm
  • Instance of SampleInnerObject referenced by inner field got merged according to its MergeBehavior method implementation

All these actions went according to settings we provided into Merge earlier. We could easily see that if some of the GlobalMergeBehavior implementations would be excluded - we will get different result.

Let's try excluding IndexArrayMergeBehavior and check the result again:

Object1 - 2 - Inner1 & Inner2 - [Three, Four]

Everything is the same except the array - it simply got replaced by object2 value because Merge was not instructed about how it should process arrays.

Now let's exclude MergeableMergeBehavior:

Object1 - 2 - Inner2 - [Three, Four]

Instance of SampleInnerObject from object1 was preserved, but its title field value got replaced by the value from SampleInnerObject in object2.

As you can see - every setting we provided can change the outcome of the merge operation and it is very important to understand how exactly each of the implementations work to make sure you get your objects merged properly.

Customizing

To customize Merge behavior you can create new implementations for MergeNullResolver, MergePolicy and GlobalMergeBehavior interfaces.

I have already explained what each of those interfaces exist for, so I will go straight to examples here.

MergeNullResolver

Implementations of MergeNullResolver are quite simple, but they do matter.

Let's make our own one:

public class PreserveNullResolver implements MergeNullResolver
{
    @Override
    public Object resolve ( final Merge merge, final Object object, final Object merged )
    {
        return object;
    }
}

We only get into resolve ( ... ) method call when either object or merged is null, so we basically ask Merge to keep base object in such cases, even if it is the one being null.

This might actually be handy if you have deeper meaning to null values in your objects, for example if you don't want anything to be able to overwrite them.

MergePolicy

Implementations of MergePolicy are basically global filters for merged objects, so when creating one you should consider that it will be applied to all kinds of objects when merging operation is performed.

Let's create one that will cover a wide variety of objects, but will still have a quite specific meaning behind it:

public final class SerializableMergePolicy implements MergePolicy
{
    @Override
    public boolean accept ( final Merge merge, final Object object, final Object merged )
    {
        return object instanceof Serializable && merged instanceof Serializable;
    }
}

This MergePolicy will only allow Merge to merge classes which implement Serializable interface.

It might be useful if you only want to merge objects which are Serializable and nothing else. All non-Serializable base objects will simply be overwritten by merged object.

GlobalMergeBehavior

For this example I'll use sample classes and objects from above and show a custom GlobalMergeBehavior implementation to handle some of the data.

Here is a custom GlobalMergeBehavior implementation for SampleInnerObject:

public class SampleInnerObjectMergeBehavior implements GlobalMergeBehavior<SampleInnerObject, SampleInnerObject, SampleInnerObject>
{
    @Override
    public boolean supports ( final Merge merge, final Object object, final Object merged )
    {
        return object instanceof SampleInnerObject && merged instanceof SampleInnerObject;
    }

    @Override
    public SampleInnerObject merge ( final Merge merge, final SampleInnerObject object, final SampleInnerObject merged )
    {
        return new SampleInnerObject ( "Merge of: " + object.toString () + " & " + merged.toString () );
    }
}

Now we need to modify Merge construction to make use of it:

public class MergeExample
{
    public static void main ( final String[] args )
    {
        final Merge merge = new Merge (
                new SkippingNullResolver (),
                new RelatedTypeMergePolicy (),
                new BasicMergeBehavior (),
                new SampleInnerObjectMergeBehavior (),
                new IndexArrayMergeBehavior (),
                new ReflectionMergeBehavior ()
        );
    }
}

I replaced MergeableMergeBehavior with SampleInnerObjectMergeBehavior as it will do merging for all objects of SampleInnerObject type from now on.

Now we can run merge operation:

final SampleObject object1 = new SampleObject ( "Object1", 1, "Inner1", "One", "Two", "Three" );
final SampleObject object2 = new SampleObject ( null, 2, "Inner2", "Three", "Four" );
final SampleObject mergedObject = merge.merge ( object1, object2 );

And see the result:

Object1 - 2 - Merge of: Inner1 & Inner2 - [Three, Four, Three]

The only thing that changed is the resulting SampleInnerObject value - in this case it was handled by SampleInnerObjectMergeBehavior.

One more thing you might notice if you run a debug - this time SampleInnerObject in the resulting object was a completely new instance unlike before. That is because we are constructing a new object within SampleInnerObjectMergeBehavior upon merge.

This is important thing to mentione because normally when objects are merged all settings are applied to the base object instead of creating a new one, but that behavior might be customized - just like we did right now.

Summary

Merge is a quite simple and yet powerful tool that can help you with merging your objects together.

That can be extremely helpful in some cases, for example you are using object instances to keep settings your application settings and you want to combine them at some point.

Though you should always understand how exactly that merge operation will behave to make sure things would work as intended.