All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.onegini.sdk.model.config.v2.MergeUtil Maven / Gradle / Ivy

There is a newer version: 5.92.0
Show newest version
/*
 * Copyright 2013-2020 Onegini b.v.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.onegini.sdk.model.config.v2;

import static lombok.AccessLevel.PRIVATE;
import static org.springframework.beans.BeanUtils.getPropertyDescriptor;
import static org.springframework.beans.BeanUtils.getPropertyDescriptors;
import static org.springframework.beans.BeanUtils.isSimpleProperty;

import java.beans.FeatureDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import com.onegini.sdk.exception.MergeException;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@NoArgsConstructor(access = PRIVATE)
@Slf4j
public class MergeUtil {

  /**
   * Merges the non-null values into the mergeDestination object. Supports nested objects
   *
   * @param mergeFrom        - the object whose non-null values will overwrite the calling class.
   * @param mergeDestination - the object where the values will be merged into.
   * @param               - The type of the two classes to merge
   * @return the merged object
   */
  public static  T merge(final T mergeFrom, final T mergeDestination) {
    Assert.notNull(mergeFrom, "mergeFrom must not be null");
    if(mergeDestination == null) {
      return mergeFrom;
    }

    final Class mergeToClass = mergeDestination.getClass();
    final PropertyDescriptor[] mergeDestinationPds = getPropertyDescriptors(mergeToClass);
    //Get a list of getters that are returning null, we don't want to overwrite things to null on the destination object
    final Collection ignoreList = getNullPropertyNames(mergeFrom);
    for (PropertyDescriptor mergeDestinationPd : mergeDestinationPds) {
      final Method mergeDestinationSetter = mergeDestinationPd.getWriteMethod();
      if (mergeDestinationSetter != null && !ignoreList.contains(mergeDestinationPd.getName())) {
        final PropertyDescriptor mergeFromPd = getPropertyDescriptor(mergeToClass, mergeDestinationPd.getName());
        final Method mergeFromGetter = mergeFromPd != null ? mergeFromPd.getReadMethod() : null;
        if (mergeFromGetter != null && ClassUtils.isAssignable(mergeDestinationSetter.getParameterTypes()[0], mergeFromGetter.getReturnType())) {
          mergeFromToDestination(mergeFrom, mergeDestination, mergeFromPd, mergeDestinationPd);
        }
      }
    }
    //For 2 top level maps as they dont have the same kind of property descriptors
    if (mergeFrom instanceof Map) {
      return (T) mergeMap(((Map) mergeFrom), ((Map) mergeDestination));
    }
    return mergeDestination;
  }

  private static  void mergeFromToDestination(final T mergeFrom,
                                                 final T mergeTo,
                                                 final PropertyDescriptor mergeFromPd,
                                                 final PropertyDescriptor mergeDestinationPd) {
    try {
      final Object mergeDestinationValue = mergeDestinationPd.getReadMethod().invoke(mergeTo);
      final Object mergeFromValue = mergeFromPd.getReadMethod().invoke(mergeFrom);
      if (mergeDestinationValue == null) {
        mergeDestinationPd.getWriteMethod().invoke(mergeTo, mergeFromValue);
      } else if (mergeFromValue instanceof Collection) {
        final Collection objCol = mergeCollection((Collection) mergeFromValue, (Collection) mergeDestinationValue);
        mergeDestinationPd.getWriteMethod().invoke(mergeTo, objCol);
      } else if (mergeFromValue instanceof Map) {
        final Map newMap = mergeMap(((Map) mergeFromValue), ((Map) mergeDestinationValue));
        mergeDestinationPd.getWriteMethod().invoke(mergeTo, newMap);
      } else if (isSimpleProperty(mergeFromValue.getClass())) {
        mergeDestinationPd.getWriteMethod().invoke(mergeTo, mergeFromValue);
      } else {
        merge(mergeFromValue, mergeDestinationValue);
      }
    } catch (final Exception ex) {
      throw new MergeException("Could not copy value '" + mergeFromPd.getName() + "' from mergeFrom to mergeDestination", ex);
    }
  }

  /**
   * Merges simple and collections that have {@link ObjectWithId}. Uses recursion for Collections whose object extends ObjectWithId to merge the internals
   * based on the id field.
   *
   * @param mergeFromCollection - updating collection
   * @param mergeToCollection   - existing collection
   * @return the merged collection
   */
  private static Collection mergeCollection(final Collection mergeFromCollection, final Collection mergeToCollection) {
    final Collection newCollection = new HashSet<>();
    final Collection origMergeToCollection = new HashSet<>(mergeToCollection);
    boolean isCollectionWithObjectId = false;
    for (final Object mergeFromSub : mergeFromCollection) {
      if (mergeFromSub instanceof ObjectWithId) {
        isCollectionWithObjectId = true;
        final ObjectWithId mergeFromObjectWithId = (ObjectWithId) mergeFromSub;
        final ObjectWithId match = mergeToCollection
            .stream()
            .filter(ObjectWithId.class::isInstance)
            .map(ObjectWithId.class::cast)
            .filter(o -> o.getId().equals(mergeFromObjectWithId.getId()))
            .findFirst()
            .orElse(null);
        origMergeToCollection.remove(match);
        if (match != null) {
          newCollection.add(merge(mergeFromObjectWithId, match));
        } else {
          newCollection.add(mergeFromObjectWithId);
        }
      } else {
        //full replace of "simple" collection
        return mergeFromCollection;
      }
    }
    //need to add existing objects that were not matched
    if (isCollectionWithObjectId) {
      newCollection.addAll(origMergeToCollection);
    }
    return newCollection;
  }

  private static Map mergeMap(final Map fromMap, final Map destinationMap) {
    final Map newMap = new HashMap<>();
    if(destinationMap != null) {
      newMap.putAll(destinationMap);
    }
    for (Map.Entry entry : fromMap.entrySet()) {
      final Object entryValue = entry.getValue();
      if (isSimpleProperty(entryValue.getClass())) {
        newMap.put(entry.getKey(), entryValue);
      } else {
        final Object mergedValue = merge(entryValue, newMap.get(entry.getKey()));
        newMap.put(entry.getKey(), mergedValue);
      }
    }
    return newMap;
  }

  private static Collection getNullPropertyNames(Object source) {
    final BeanWrapper src = new BeanWrapperImpl(source);
    return Arrays.stream(src.getPropertyDescriptors())
        .map(FeatureDescriptor::getName)
        .filter(name -> src.getPropertyValue(name) == null)
        .collect(Collectors.toSet());
  }
}