MultiOp.java

/*
 * Copyright 2021 Global Crop Diversity Trust
 *
 * 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 org.gringlobal.api.v1;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

import org.genesys.blocks.model.EmptyModel;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonIncludeProperties;
import com.fasterxml.jackson.annotation.JsonUnwrapped;

/**
 * The API response for CRUD operations on multiple objects.
 *
 * @param <T> the generic type
 */
public class MultiOp<T> {

	/** List of successful inserts/updates/deletes. */
	@JsonInclude(content = Include.NON_EMPTY)
	public List<T> success;

	/** List of errors. */
	@JsonInclude(content = Include.NON_EMPTY)
	public List<MultiOpError> errors;

	public MultiOp() {
	}

	public MultiOp(List<T> success) {
		this.success = success;
	}

	public MultiOp(List<T> success, List<MultiOpError> errors) {
		this.success = success;
		this.errors = errors;
	}

	/**
	 * The Class MultiOpError.
	 */
	public static class MultiOpError {

		/** The index of the source list with CRUD operation error. */
		public int index;
		
		/** The CRUD operation error. */
		@JsonUnwrapped
		@JsonIncludeProperties({ "message", "localizedMessage" })
		public Throwable error;

		public MultiOpError(int index, Throwable error) {
			this.index = index;
			this.error = error;
			// error should not be recursive :-)
			while (this.error.getCause() != null && !this.error.equals(this.error.getCause())) {
				this.error = this.error.getCause();
			}
		}
	}

	/**
	 * Apply bulk operation to the list or use the one-by-one approach when bulk throws an exception.
	 * 
	 * @param <T>
	 * @param list
	 * @param bulkOp
	 * @param oneOp
	 * @return {@link MultiOp} results of all operations
	 */
	public static <T> MultiOp<T> multiOp(List<T> list, Function<List<T>, MultiOp<T>> bulkOp, Function<T, T> oneOp) {
		try {
			return bulkOp.apply(list);

		} catch (Throwable e) {
			var result = new MultiOp<T>();
			result.success = new ArrayList<T>(list.size());
			result.errors = new ArrayList<>(); // there are errors!

			// Execute oneOp record-by-record
			for (var index = 0; index < list.size(); index++) {
				try {
					T one = list.get(index);
					result.success.add(oneOp.apply(one));
				} catch (Throwable e2) {
					result.errors.add(new MultiOpError(index, e2));
				}
			}
			return result;
		}
	}

	/**
	 * Apply create or update operation to the list of items to insert or update.
	 * 
	 * @param <T>
	 * @param list items to insert or update
	 * @param createOp Create operation
	 * @param updateOp Update operation
	 * @return {@link MultiOp} results of all operations
	 */
	public static <T extends EmptyModel> MultiOp<T> upsert(List<T> list, Function<T, T> createOp, Function<T, T> updateOp) {
		var result = new MultiOp<T>();
		result.success = new ArrayList<T>(list.size());
		result.errors = new ArrayList<>();

		// One-by-one insert or update
		for (var index = 0; index < list.size(); index++) {
			try {
				T one = list.get(index);
				if (one.isNew()) {
					result.success.add(createOp.apply(one));
				} else {
					result.success.add(updateOp.apply(one));
				}
			} catch (Throwable e2) {
				result.errors.add(new MultiOpError(index, e2));
			}
		}
		return result;
	}


	/**
	 * Apply create or update operation to the list of items to insert or update.
	 * 
	 * @param <T>
	 * @param list items to insert or update
	 * @param createOp Create operation
	 * @param updateOp Update operation
	 * @return {@link MultiOp} results of all operations
	 */
	public static <T extends EmptyModel> MultiOp<T> upsert(List<T> list, Function<T, T> createOp, Function<T, T> getOp, BiFunction<T, T, T> updateOp) {
		var result = new MultiOp<T>();
		result.success = new ArrayList<T>(list.size());
		result.errors = new ArrayList<>();

		// One-by-one insert or update
		for (var index = 0; index < list.size(); index++) {
			try {
				T one = list.get(index);
				if (one.isNew()) {
					result.success.add(createOp.apply(one));
				} else {
					result.success.add(updateOp.apply(one, getOp.apply(one)));
				}
			} catch (Throwable e2) {
				result.errors.add(new MultiOpError(index, e2));
			}
		}
		return result;
	}

	/**
	 * Return a new MultiOp with {@code successes} mapped to another type.
	 * 
	 * @param <B> Target type
	 * @param mapper mapping function
	 * @return
	 */
	public <B> MultiOp<B> map(Function<List<T>, List<B>> mapper) {
		var mapped = new MultiOp<B>();
		mapped.errors = this.errors;
		if (this.success != null) {
			mapped.success = mapper.apply(this.success);
		}
		return mapped;
	}
}