/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.camel.component.salesforce.api.dto.composite;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamConverter;
import com.thoughtworks.xstream.annotations.XStreamOmitField;
import org.apache.camel.component.salesforce.api.dto.AbstractDescribedSObjectBase;
import org.apache.camel.component.salesforce.api.dto.AbstractSObjectBase;
import org.apache.camel.component.salesforce.api.dto.RestError;
import org.apache.camel.component.salesforce.api.dto.SObjectDescription;
import org.apache.camel.util.ObjectHelper;
/**
* Represents one node in the SObject tree request. SObject trees ({@link SObjectTree}) are composed from instances of
* {@link SObjectNode}s. Each {@link SObjectNode} contains {@link Attributes}, the SObject ({@link AbstractSObjectBase})
* and any child records linked to it. SObjects at root level are added to {@link SObjectTree} using
* {@link SObjectTree#addObject(AbstractSObjectBase)}, then you can add child records on the {@link SObjectNode}
* returned by using {@link #addChild(AbstractDescribedSObjectBase)},
* {@link #addChildren(AbstractDescribedSObjectBase, AbstractDescribedSObjectBase...)} or
* {@link #addChild(String, AbstractSObjectBase)} and
* {@link #addChildren(String, AbstractSObjectBase, AbstractSObjectBase...)}.
* <p/>
* Upon submission to the Salesforce Composite API the {@link SObjectTree} and the {@link SObjectNode}s in it might
* contain errors that you need to fetch using {@link #getErrors()} method.
*
* @see SObjectTree
* @see RestError
*/
@XStreamAlias("records")
@XStreamConverter(SObjectNodeXStreamConverter.class)
public final class SObjectNode implements Serializable {
private static final String CHILD_PARAM = "child";
private static final String SOBJECT_TYPE_PARAM = "type";
private static final long serialVersionUID = 1L;
@JsonProperty
final Attributes attributes;
@JsonUnwrapped
final AbstractSObjectBase object;
final Map<String, List<SObjectNode>> records = new HashMap<>();
private List<RestError> errors;
@XStreamOmitField
private final ReferenceGenerator referenceGenerator;
SObjectNode(final SObjectTree tree, final AbstractSObjectBase object) {
this(tree.referenceGenerator, typeOf(object), object);
}
private SObjectNode(final ReferenceGenerator referenceGenerator, final String type,
final AbstractSObjectBase object) {
this.referenceGenerator = requireNonNull(referenceGenerator, "ReferenceGenerator cannot be null");
this.object = requireNonNull(object, "Root SObject cannot be null");
attributes = new Attributes(referenceGenerator.nextReferenceFor(object),
requireNonNull(type, "Object type cannot be null"));
}
static String pluralOf(final AbstractDescribedSObjectBase object) {
final SObjectDescription description = object.description();
return description.getLabelPlural();
}
static String typeOf(final AbstractDescribedSObjectBase object) {
final SObjectDescription description = object.description();
return description.getName();
}
static String typeOf(final AbstractSObjectBase object) {
return object.getClass().getSimpleName();
}
/**
* Add a described child with the metadata needed already present within it to the this node.
*
* @param child
* to add
* @return the newly created node, used in builder fashion to add more child objects to it (on the next level)
*/
public SObjectNode addChild(final AbstractDescribedSObjectBase child) {
ObjectHelper.notNull(child, CHILD_PARAM);
return addChild(pluralOf(child), child);
}
/**
* Add a child that does not contain the required metadata to the this node. You need to specify the plural form of
* the child (e.g. `Account` its `Accounts`).
*
* @param labelPlural
* plural form
* @param child
* to add
* @return the newly created node, used in builder fashion to add more child objects to it (on the next level)
*/
public SObjectNode addChild(final String labelPlural, final AbstractSObjectBase child) {
ObjectHelper.notNull(labelPlural, "labelPlural");
ObjectHelper.notNull(child, CHILD_PARAM);
final SObjectNode node = new SObjectNode(referenceGenerator, typeOf(child), child);
return addChild(labelPlural, node);
}
/**
* Add multiple described children with the metadata needed already present within them to the this node..
*
* @param first
* first child to add
* @param others
* any other children to add
*/
public void addChildren(final AbstractDescribedSObjectBase first, final AbstractDescribedSObjectBase... others) {
ObjectHelper.notNull(first, "first");
ObjectHelper.notNull(others, "others");
addChild(pluralOf(first), first);
Arrays.stream(others).forEach(this::addChild);
}
/**
* Add a child that does not contain the required metadata to the this node. You need to specify the plural form of
* the child (e.g. `Account` its `Accounts`).
*
* @param labelPlural
* plural form
* @param first
* first child to add
* @param others
* any other children to add
*/
public void addChildren(final String labelPlural, final AbstractSObjectBase first,
final AbstractSObjectBase... others) {
ObjectHelper.notNull(labelPlural, "labelPlural");
ObjectHelper.notNull(first, "first");
ObjectHelper.notNull(others, "others");
addChild(labelPlural, first);
Arrays.stream(others).forEach(c -> addChild(labelPlural, c));
}
/**
* Returns all children of this node (one level deep).
*
* @return children of this node
*/
@JsonIgnore
public Stream<SObjectNode> getChildNodes() {
return records.values().stream().flatMap(List::stream);
}
/**
* Returns all children of this node (one level deep) of certain type (in plural form).
*
* @param type
* type of child requested in plural form (e.g for `Account` is `Accounts`)
* @return children of this node of specified type
*/
public Stream<SObjectNode> getChildNodesOfType(final String type) {
ObjectHelper.notNull(type, SOBJECT_TYPE_PARAM);
return records.getOrDefault(type, Collections.emptyList()).stream();
}
/**
* Returns child SObjects of this node (one level deep).
*
* @return child SObjects of this node
*/
@JsonIgnore
public Stream<AbstractSObjectBase> getChildren() {
return records.values().stream().flatMap(List::stream).map(SObjectNode::getObject);
}
/**
* Returns child SObjects of this node (one level deep) of certain type (in plural form)
*
* @param type
* type of child requested in plural form (e.g for `Account` is `Accounts`)
* @return child SObjects of this node
*/
public Stream<AbstractSObjectBase> getChildrenOfType(final String type) {
ObjectHelper.notNull(type, SOBJECT_TYPE_PARAM);
return records.getOrDefault(type, Collections.emptyList()).stream().map(SObjectNode::getObject);
}
/**
* Errors reported against this this node received in response to the SObject tree being submitted.
*
* @return errors for this node
*/
@JsonIgnore
public List<RestError> getErrors() {
return Optional.ofNullable(errors).orElse(Collections.emptyList());
}
/**
* SObject at this node.
*
* @return SObject
*/
@JsonIgnore
public AbstractSObjectBase getObject() {
return object;
}
/**
* Are there any errors resulted from the submission on this node?
*
* @return true if there are errors
*/
public boolean hasErrors() {
return errors != null && !errors.isEmpty();
}
/**
* Size of the branch beginning with this node (number of SObjects in it).
*
* @return number of objects within this branch
*/
public int size() {
return 1 + records.values().stream().flatMapToInt(r -> r.stream().mapToInt(SObjectNode::size)).sum();
}
@Override
public String toString() {
return "Node<" + getObjectType() + ">";
}
SObjectNode addChild(final String labelPlural, final SObjectNode node) {
List<SObjectNode> children = records.get(labelPlural);
if (children == null) {
children = new ArrayList<>();
records.put(labelPlural, children);
}
children.add(node);
return node;
}
@JsonAnyGetter
Map<String, Map<String, List<SObjectNode>>> children() {
return records.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> Collections.singletonMap("records", e.getValue())));
}
Attributes getAttributes() {
return attributes;
}
@JsonIgnore
String getObjectType() {
return attributes.type;
}
Stream<Class> objectTypes() {
return Stream.concat(Stream.of((Class) object.getClass()), getChildNodes().flatMap(SObjectNode::objectTypes));
}
void setErrors(final List<RestError> errors) {
this.errors = errors;
}
}