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