Java tutorial
/* * Copyright (C) 2015 * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.cleverbus.api.entity; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.cleverbus.api.common.HumanReadable; import org.cleverbus.api.exception.ErrorExtEnum; import org.hibernate.annotations.*; import org.hibernate.annotations.CascadeType; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.util.Assert; import javax.annotation.Nullable; import javax.persistence.*; import javax.persistence.AccessType; import javax.persistence.Entity; import javax.persistence.Table; import java.text.Collator; import java.util.*; /** * Input asynchronous message. * * @author <a href="mailto:petr.juza@cleverlance.com">Petr Juza</a> */ @Entity @Table(name = "message", uniqueConstraints = @UniqueConstraint(name = "uq_correlation_system", columnNames = { "correlation_id", "source_system" })) public class Message implements HumanReadable { /** * Separator that separates business error descriptions. */ public static final String ERR_DESC_SEPARATOR = "||"; @Id @Column(name = "msg_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long msgId; @Column(name = "msg_timestamp", nullable = false) private Date msgTimestamp; @Column(name = "receive_timestamp", nullable = false) private Date receiveTimestamp; @Column(name = "service", length = 30, nullable = false) @Access(AccessType.PROPERTY) private String serviceInternal; @Transient private ServiceExtEnum service; @Column(name = "operation_name", length = 100, nullable = false) private String operationName; @Column(name = "object_id", length = 50, nullable = true) private String objectId; @Column(name = "entity_type", length = 30, nullable = true) @Access(AccessType.PROPERTY) private String entityTypeInternal; @Transient private EntityTypeExtEnum entityType; @Column(name = "correlation_id", length = 100, nullable = false) private String correlationId; @Column(name = "process_id", length = 100, nullable = true) private String processId; // in PostgreSQL it's defined as TEXT @Column(name = "payload", length = Integer.MAX_VALUE, nullable = false) private String payload; // in PostgreSQL it's defined as TEXT @Column(name = "envelope", length = Integer.MAX_VALUE, nullable = true) private String envelope; @Column(name = "source_system", length = 15, nullable = false) @Access(AccessType.PROPERTY) private String sourceSystemInternal; @Transient private ExternalSystemExtEnum sourceSystem; @Enumerated(EnumType.STRING) @Column(name = "state", length = 25, nullable = false) private MsgStateEnum state; @Column(name = "start_process_timestamp", nullable = true) private Date startProcessTimestamp; @Column(name = "failed_count", nullable = false) private int failedCount; @Column(name = "failed_error_code", length = 5, nullable = true) @Access(AccessType.PROPERTY) private String failedErrorCodeInternal; @Transient private ErrorExtEnum failedErrorCode; // in PostgreSQL it's defined as TEXT @Column(name = "failed_desc", length = Integer.MAX_VALUE, nullable = true) private String failedDesc; @Column(name = "last_update_timestamp", nullable = true) private Date lastUpdateTimestamp; @Column(name = "custom_data", length = 20000, nullable = true) private String customData; @Column(name = "business_error", length = 20000, nullable = true) private String businessError; @Column(name = "parent_msg_id", nullable = true) private Long parentMsgId; @Enumerated(EnumType.STRING) @Column(name = "parent_binding_type", length = 25, nullable = true) private BindingTypeEnum parentBindingType; @Column(name = "funnel_component_id", length = 50, nullable = true) private String funnelComponentId; @Column(name = "guaranteed_order", nullable = false) private boolean guaranteedOrder; @Column(name = "exclude_failed_state", nullable = false) private boolean excludeFailedState; @OneToMany(fetch = FetchType.LAZY, mappedBy = "message") private Set<ExternalCall> externalCalls = new TreeSet<ExternalCall>(); @OneToMany(fetch = FetchType.LAZY, mappedBy = "message") private Set<Request> requests = new TreeSet<Request>(); @OneToMany(fetch = FetchType.EAGER, mappedBy = "message") @Cascade(CascadeType.ALL) private Set<Funnel> funnels = new HashSet<Funnel>(); @Transient private boolean parentMessage; @Transient private int processingPriority; /** * Empty (default) constructor. */ public Message() { } /** * Creates message with specified source system and correlation ID. * * @param sourceSystem the source system * @param correlationId the correlation ID */ public Message(ExternalSystemExtEnum sourceSystem, String correlationId) { Assert.notNull(sourceSystem, "the sourceSystem must not be null"); Assert.hasText(correlationId, "the correlationId must not be empty"); setSourceSystem(sourceSystem); this.correlationId = correlationId; } /** * Gets unique message ID. * * @return message ID */ public Long getMsgId() { return msgId; } public void setMsgId(Long msgId) { this.msgId = msgId; } /** * Gets timestamp from source system. * * @return timestamp */ public Date getMsgTimestamp() { return msgTimestamp != null ? new Date(msgTimestamp.getTime()) : null; } public void setMsgTimestamp(Date msgTimestamp) { Assert.notNull(msgTimestamp, "the msgTimestamp must not be null"); this.msgTimestamp = msgTimestamp != null ? new Date(msgTimestamp.getTime()) : null; } /** * Gets timestamp when message was received. * This timestamp determines the order of next processing. * * @return timestamp */ public Date getReceiveTimestamp() { return receiveTimestamp != null ? new Date(receiveTimestamp.getTime()) : null; } public void setReceiveTimestamp(Date receiveTimestamp) { Assert.notNull(receiveTimestamp, "the receiveTimestamp must not be null"); this.receiveTimestamp = receiveTimestamp != null ? new Date(receiveTimestamp.getTime()) : null; } /** * Gets service name, e.g. customer. * * @return service name */ public ServiceExtEnum getService() { return service; } public void setService(ServiceExtEnum service) { this.service = service; this.serviceInternal = service.getServiceName(); } private String getServiceInternal() { return serviceInternal; } private void setServiceInternal(final String service) { this.serviceInternal = service; this.service = new ServiceExtEnum() { @Override public final String getServiceName() { return service; } }; } /** * Gets operation name, e.g. createCustomer. * * @return op. name */ public String getOperationName() { return operationName; } public void setOperationName(String operationName) { this.operationName = operationName; } /** * Gets object ID that will be changed during message processing. * This attribute serves for finding messages in the queue which deals with identical object. * * @return object ID * @see #getEntityTypeInternal() */ @Nullable public String getObjectId() { return objectId; } public void setObjectId(@Nullable String objectId) { this.objectId = objectId; } /** * Gets type of the entity that is being changed. * <p/> * In general it's enough to detect identical changed data by objectId and operation name but there are * few different operations which can change the same data (e.g. setCustomer, setCustomerExt). * <p/> * If defined then it will be used for "obsolete operation call" detection instead of operation name. * * @return entity type * @see #getObjectId() */ @Nullable public EntityTypeExtEnum getEntityType() { return entityType; } public void setEntityType(@Nullable EntityTypeExtEnum entityType) { this.entityType = entityType; this.entityTypeInternal = entityType != null ? entityType.getEntityType() : null; } @Nullable private String getEntityTypeInternal() { return entityTypeInternal; } private void setEntityTypeInternal(@Nullable final String entityTypeInternal) { this.entityTypeInternal = entityTypeInternal; if (entityTypeInternal != null) { this.entityType = new EntityTypeExtEnum() { @Override public String getEntityType() { return entityTypeInternal; } }; } else { this.entityType = null; } } /** * Gets correlation ID that serves for pairing asynchronous request and response. * * @return correlation ID */ public String getCorrelationId() { return correlationId; } public void setCorrelationId(String correlationId) { this.correlationId = correlationId; } /** * Gets process ID that serves for pairing more requests with one process. * * @return process ID */ @Nullable public String getProcessId() { return processId; } public void setProcessId(@Nullable String processId) { this.processId = processId; } /** * Gets body content (XML by default). * * @return body content */ public String getPayload() { return payload; } public void setPayload(String payload) { this.payload = payload; } /** * Gets the whole SOAP envelope (= the full original request: SOAP headers, SOAP body). * * @return the envelope; can be {@code null} because it's possible to turn off saving the envelope * with regards to performance */ @Nullable public String getEnvelope() { return envelope; } public void setEnvelope(@Nullable String envelope) { this.envelope = envelope; } /** * Gets source system. * * @return source system */ public ExternalSystemExtEnum getSourceSystem() { return sourceSystem; } public void setSourceSystem(ExternalSystemExtEnum sourceSystem) { Assert.notNull(sourceSystem, "the sourceSystem must not be null"); this.sourceSystem = sourceSystem; this.sourceSystemInternal = sourceSystem.getSystemName(); } private String getSourceSystemInternal() { return sourceSystemInternal; } private void setSourceSystemInternal(final String sourceSystem) { Assert.notNull(sourceSystem, "the sourceSystem must not be null"); this.sourceSystemInternal = sourceSystem; this.sourceSystem = new ExternalSystemExtEnum() { @Override public String getSystemName() { return sourceSystem; } }; } /** * Gets message state. * * @return msg state */ public MsgStateEnum getState() { return state; } public void setState(MsgStateEnum state) { this.state = state; } /** * Gets timestamp when the message started processing. * When message starts repeatably then this time is valid for last processing. * * @return start date of processing */ public Date getStartProcessTimestamp() { return startProcessTimestamp != null ? new Date(startProcessTimestamp.getTime()) : null; } public void setStartProcessTimestamp(Date startProcessTimestamp) { Assert.notNull(startProcessTimestamp, "the processTimestamp must not be null"); this.startProcessTimestamp = startProcessTimestamp != null ? new Date(startProcessTimestamp.getTime()) : null; } /** * Gets number of failed processing. * * @return number of failed processing */ public int getFailedCount() { return failedCount; } public void setFailedCount(int failedCount) { this.failedCount = failedCount; } /** * Gets error code if last try was unsuccessful with error. * <p/> * Note: there can be only one error during message processing because next processing is stopped * when an error occurred. * * @return error code */ @Nullable public ErrorExtEnum getFailedErrorCode() { return failedErrorCode; } public void setFailedErrorCode(@Nullable ErrorExtEnum failedErrorCode) { this.failedErrorCode = failedErrorCode; this.failedErrorCodeInternal = failedErrorCode != null ? failedErrorCode.getErrorCode() : null; } @Nullable private String getFailedErrorCodeInternal() { return failedErrorCodeInternal; } private void setFailedErrorCodeInternal(@Nullable final String failedErrorCodeInternal) { this.failedErrorCodeInternal = failedErrorCodeInternal; if (failedErrorCodeInternal != null) { this.failedErrorCode = new ErrorExtEnum() { @Override public String getErrorCode() { return failedErrorCodeInternal; } @Override public String getErrDesc() { // no description available return failedErrorCodeInternal; } }; } else { this.failedErrorCode = null; } } /** * Gets error description if last try was unsuccessful with error. * * @return error description */ @Nullable public String getFailedDesc() { return failedDesc; } public void setFailedDesc(@Nullable String failedDesc) { this.failedDesc = failedDesc; } /** * Gets timestamp when the entity was changed last time. * * @return timestamp */ @Nullable public Date getLastUpdateTimestamp() { return lastUpdateTimestamp != null ? new Date(lastUpdateTimestamp.getTime()) : null; } public void setLastUpdateTimestamp(@Nullable Date lastUpdateTimestamp) { this.lastUpdateTimestamp = lastUpdateTimestamp != null ? new Date(lastUpdateTimestamp.getTime()) : null; } /** * Gets custom data. * <p/> * Custom data can be used for saving arbitrary data for transferring state between more processing calls * of the asynchronous message. * * @return the custom data */ @Nullable public String getCustomData() { return customData; } public void setCustomData(@Nullable String customData) { this.customData = customData; } /** * Gets business error descriptions. * <p/> * During processing of asynchronous message can be collected lot of business errors which is suitable * to present to source (callee) system (for example when we want to create new customer and this customer * doesn't have valid customer number). * <p/> * Each error description is separated by {@value #ERR_DESC_SEPARATOR}. * * @return business error descriptions */ @Nullable public String getBusinessError() { return businessError; } /** * Gets list of business error descriptions. * * @return list of descriptions */ public List<String> getBusinessErrorList() { if (getBusinessError() == null) { return Collections.emptyList(); } else { String[] errs = StringUtils.split(getBusinessError(), ERR_DESC_SEPARATOR); return Arrays.asList(errs); } } public void setBusinessError(@Nullable String businessError) { this.businessError = businessError; } /** * Gets ID of the parent message (if any). * * @return parent message ID * @see #getParentBindingType() */ @Nullable public Long getParentMsgId() { return parentMsgId; } public void setParentMsgId(@Nullable Long parentMsgId) { this.parentMsgId = parentMsgId; // set default value if not set if (getParentBindingType() == null) { setParentBindingType(BindingTypeEnum.SOFT); } } /** * Gets type of binding between parent and child message. * * @return binding type * @see #getParentMsgId() */ @Nullable public BindingTypeEnum getParentBindingType() { return parentBindingType; } public void setParentBindingType(@Nullable BindingTypeEnum parentBindingType) { this.parentBindingType = parentBindingType; } /** * Is there message that has parent message with {@link BindingTypeEnum#HARD hard} binding? * * @return {@code true} for hard-binding parent message otherwise {@code false} */ public boolean existHardParent() { return getParentMsgId() != null && getParentBindingType() == BindingTypeEnum.HARD; } /** * Is parent message that has child messages? * <p/> * Note: binding between child and parent message must be {@link BindingTypeEnum#HARD hard}. * * @return {@code true} when this message is parent, otherwise {@code false} */ public boolean isParentMessage() { return parentMessage; } public void setParentMessage(boolean parentMessage) { this.parentMessage = parentMessage; } /** * Gets funnel component identifier. * Each funnel has unique identifier that says where is route currently being processed. * * @return funnel component identifier * @see Funnel */ public String getFunnelComponentId() { return funnelComponentId; } public void setFunnelComponentId(String funnelComponentId) { this.funnelComponentId = funnelComponentId; } /** * Gets flag (true/false) if route should be processed in guaranteed order or not. * * @return {@code true} for guaranteed order otherwise {@code false} * @see Funnel#getFunnelValue() * @see #isExcludeFailedState() */ public boolean isGuaranteedOrder() { return guaranteedOrder; } public void setGuaranteedOrder(boolean guaranteedOrder) { this.guaranteedOrder = guaranteedOrder; } /** * Returns {@code true} if FAILED state should be excluded from guaranteed order. * {@link MsgStateEnum#FAILED FAILED} state is used for guaranteed order by default; * <p/> * This option has influence only if {@link #isGuaranteedOrder() guaranteed processing order} is enabled. * * @return {@code true} if FAILED state should be excluded * @see #isGuaranteedOrder() */ public boolean isExcludeFailedState() { return excludeFailedState; } public void setExcludeFailedState(boolean excludeFailedState) { this.excludeFailedState = excludeFailedState; } /** * Gets the set of referenced external calls. * * @return the set of referenced external calls */ public List<ExternalCall> getExternalCalls() { final List<ExternalCall> result = new ArrayList<ExternalCall>(externalCalls); Collections.sort(result, new Comparator<ExternalCall>() { @Override public int compare(ExternalCall o1, ExternalCall o2) { return o1.getId().compareTo(o2.getId()); } }); return result; } /** * Gets the set of referenced logged requests. * * @return the set of referenced logged requests */ public List<Request> getRequests() { final List<Request> result = new ArrayList<Request>(requests); Collections.sort(result, new Comparator<Request>() { @Override public int compare(Request o1, Request o2) { return o1.getId().compareTo(o2.getId()); } }); return result; } /** * Gets funnel values. * Funnel value gets from {@link Funnel#getFunnelValue()}. * * @return funnel values * @see #getFunnels() * @see Funnel */ public List<String> getFunnelValues() { List<String> result = new ArrayList<String>(funnels.size()); for (Funnel funnel : funnels) { result.add(funnel.getFunnelValue()); } return result; } /** * Sets funnel values. * Funnel value set to {@link Funnel#setFunnelValue(String)}. * * @param funnelValues funnel values * @see #setFunnels(Collection) * @see Funnel */ public void setFunnelValues(Collection<String> funnelValues) { Assert.notNull(funnelValues); //set for better searching funnel values Set<String> funnelValuesInSet = new HashSet<String>(funnelValues); List<Funnel> funnels = new ArrayList<Funnel>(); for (Funnel funnel : this.funnels) { //if funnel is in list from parameter, then we add in if (funnelValuesInSet.contains(funnel.getFunnelValue())) { funnels.add(funnel); funnelValuesInSet.remove(funnel.getFunnelValue()); } } //add new funnel values for (String funnelValue : funnelValuesInSet) { funnels.add(new Funnel(this, funnelValue)); } //set new funnels setFunnels(funnels); } /** * Gets the set of referenced funnel values. * * @return the set of referenced funnel values */ protected List<Funnel> getFunnels() { List<Funnel> result = new ArrayList<Funnel>(funnels); Collections.sort(result, new Comparator<Funnel>() { @Override public int compare(Funnel o1, Funnel o2) { return Collator.getInstance(LocaleContextHolder.getLocale()).compare(o1.getFunnelValue(), o2.getFunnelValue()); } }); return result; } /** * Sets referenced funnel values. * * @param funnels set funnel values */ protected void setFunnels(Collection<Funnel> funnels) { Assert.notNull(funnels, "funnels must not be null"); this.funnels.clear(); this.funnels.addAll(funnels); } public int getProcessingPriority() { return processingPriority; } /** * Sets priority of processing this message. * The higher number the higher priority. * * @param processingPriority the priority number */ public void setProcessingPriority(int processingPriority) { this.processingPriority = processingPriority; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (obj instanceof Message) { Message en = (Message) obj; return new EqualsBuilder().append(msgId, en.msgId).isEquals(); } else { return false; } } @Override public int hashCode() { return new HashCodeBuilder(17, 37).append(msgId).toHashCode(); } @Override public String toString() { return new ToStringBuilder(this).append("msgId", msgId).append("state", state) .append("correlationId", correlationId).append("processId", processId) .append("msgTimestamp", msgTimestamp).append("receiveTimestamp", receiveTimestamp) .append("service", service == null ? null : service.getServiceName()) .append("operationName", operationName).append("objectId", objectId) .append("entityType", entityType != null ? entityType.getEntityType() : null) // .append("payload", StringUtils.substring(payload, 0, 500)) .append("sourceSystem", sourceSystem != null ? sourceSystem.getSystemName() : null) .append("startProcessTimestamp", startProcessTimestamp).append("failedCount", failedCount) .append("failedErrorCode", failedErrorCode) // .append("failedDesc", StringUtils.substring(payload, 0, 200)) .append("lastUpdateTimestamp", lastUpdateTimestamp) .append("customData", StringUtils.substring(customData, 0, 200)) .append("businessError", StringUtils.substring(businessError, 0, 200)) .append("parentMsgId", parentMsgId).append("parentBindingType", parentBindingType) .append("funnelComponentId", funnelComponentId).append("guaranteedOrder", guaranteedOrder) .append("excludeFailedState", excludeFailedState).append("processingPriority", processingPriority) .toString(); } @Override public String toHumanString() { return "(msg_id = " + msgId + ", correlationId = " + correlationId + ")"; } }