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;
}
}