/*
* 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.nifi.controller.repository;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.nifi.controller.repository.claim.ContentClaim;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.flowfile.attributes.CoreAttributes;
/**
* <p>
* A flow file is a logical notion of an item in a flow with its associated attributes and identity which can be used as a reference for its actual content.
* </p>
*
* <b>Immutable - Thread Safe</b>
*
*/
public final class StandardFlowFileRecord implements FlowFile, FlowFileRecord {
private final long id;
private final long entryDate;
private final long lineageStartDate;
private final long lineageStartIndex;
private final long size;
private final long penaltyExpirationMs;
private final Map<String, String> attributes;
private final ContentClaim claim;
private final long claimOffset;
private final long lastQueueDate;
private final long queueDateIndex;
private StandardFlowFileRecord(final Builder builder) {
this.id = builder.bId;
this.attributes = builder.bAttributes == null ? Collections.emptyMap() : builder.bAttributes;
this.entryDate = builder.bEntryDate;
this.lineageStartDate = builder.bLineageStartDate;
this.lineageStartIndex = builder.bLineageStartIndex;
this.penaltyExpirationMs = builder.bPenaltyExpirationMs;
this.size = builder.bSize;
this.claim = builder.bClaim;
this.claimOffset = builder.bClaimOffset;
this.lastQueueDate = builder.bLastQueueDate;
this.queueDateIndex = builder.bQueueDateIndex;
}
@Override
public long getId() {
return id;
}
@Override
public long getEntryDate() {
return entryDate;
}
@Override
public long getLineageStartDate() {
return lineageStartDate;
}
@Override
public Long getLastQueueDate() {
return lastQueueDate;
}
@Override
public boolean isPenalized() {
return penaltyExpirationMs > 0 ? penaltyExpirationMs > System.currentTimeMillis() : false;
}
@Override
public String getAttribute(final String key) {
return attributes.get(key);
}
@Override
public long getSize() {
return size;
}
@Override
public Map<String, String> getAttributes() {
return Collections.unmodifiableMap(this.attributes);
}
@Override
public ContentClaim getContentClaim() {
return this.claim;
}
@Override
public long getContentClaimOffset() {
return this.claimOffset;
}
@Override
public long getLineageStartIndex() {
return lineageStartIndex;
}
@Override
public long getQueueDateIndex() {
return queueDateIndex;
}
/**
* Provides the natural ordering for FlowFile objects which is based on their identifier.
*
* @param other other
* @return standard compare contract
*/
@Override
public int compareTo(final FlowFile other) {
return new CompareToBuilder().append(id, other.getId()).toComparison();
}
@Override
public boolean equals(final Object other) {
if (this == other) {
return true;
}
if (!(other instanceof FlowFile)) {
return false;
}
final FlowFile otherRecord = (FlowFile) other;
return new EqualsBuilder().append(id, otherRecord.getId()).isEquals();
}
@Override
public String toString() {
final ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
builder.append("uuid", getAttribute(CoreAttributes.UUID.key()));
builder.append("claim", claim == null ? "" : claim.toString());
builder.append("offset", claimOffset);
builder.append("name", getAttribute(CoreAttributes.FILENAME.key())).append("size", size);
return builder.toString();
}
@Override
public int hashCode() {
return new HashCodeBuilder(7, 13).append(id).toHashCode();
}
public static final class Builder {
private long bId;
private long bEntryDate = System.currentTimeMillis();
private long bLineageStartDate = bEntryDate;
private long bLineageStartIndex = 0L;
private final Set<String> bLineageIdentifiers = new HashSet<>();
private long bPenaltyExpirationMs = -1L;
private long bSize = 0L;
private ContentClaim bClaim = null;
private long bClaimOffset = 0L;
private long bLastQueueDate = System.currentTimeMillis();
private long bQueueDateIndex = 0L;
private Map<String, String> bAttributes;
private boolean bAttributesCopied = false;
public Builder id(final long id) {
bId = id;
return this;
}
public Builder entryDate(final long epochMs) {
bEntryDate = epochMs;
return this;
}
public Builder lineageStart(final long lineageStartDate, final long lineageStartIndex) {
bLineageStartDate = lineageStartDate;
bLineageStartIndex = lineageStartIndex;
return this;
}
public Builder penaltyExpirationTime(final long epochMilliseconds) {
bPenaltyExpirationMs = epochMilliseconds;
return this;
}
public Builder size(final long bytes) {
if (bytes >= 0) {
bSize = bytes;
}
return this;
}
private Map<String, String> initializeAttributes() {
if (bAttributes == null) {
bAttributes = new HashMap<>();
bAttributesCopied = true;
} else if (!bAttributesCopied) {
bAttributes = new HashMap<>(bAttributes);
bAttributesCopied = true;
}
return bAttributes;
}
public Builder addAttribute(final String key, final String value) {
if (key != null && value != null) {
initializeAttributes().put(FlowFile.KeyValidator.validateKey(key), value);
}
return this;
}
public Builder addAttributes(final Map<String, String> attributes) {
final Map<String, String> initializedAttributes = initializeAttributes();
if (null != attributes) {
for (final String key : attributes.keySet()) {
FlowFile.KeyValidator.validateKey(key);
}
for (final Map.Entry<String, String> entry : attributes.entrySet()) {
final String key = entry.getKey();
final String value = entry.getValue();
if (key != null && value != null) {
initializedAttributes.put(key, value);
}
}
}
return this;
}
public Builder removeAttributes(final String... keys) {
if (keys != null) {
for (final String key : keys) {
if (CoreAttributes.UUID.key().equals(key)) {
continue;
}
initializeAttributes().remove(key);
}
}
return this;
}
public Builder removeAttributes(final Set<String> keys) {
if (keys != null) {
for (final String key : keys) {
if (CoreAttributes.UUID.key().equals(key)) {
continue;
}
initializeAttributes().remove(key);
}
}
return this;
}
public Builder removeAttributes(final Pattern keyPattern) {
if (keyPattern != null) {
final Iterator<String> iterator = initializeAttributes().keySet().iterator();
while (iterator.hasNext()) {
final String key = iterator.next();
if (CoreAttributes.UUID.key().equals(key)) {
continue;
}
if (keyPattern.matcher(key).matches()) {
iterator.remove();
}
}
}
return this;
}
public Builder contentClaim(final ContentClaim claim) {
this.bClaim = claim;
return this;
}
public Builder contentClaimOffset(final long offset) {
this.bClaimOffset = offset;
return this;
}
public Builder lastQueued(final long lastQueueDate, final long queueDateIndex) {
this.bLastQueueDate = lastQueueDate;
this.bQueueDateIndex = queueDateIndex;
return this;
}
public Builder fromFlowFile(final FlowFileRecord specFlowFile) {
if (specFlowFile == null) {
return this;
}
bId = specFlowFile.getId();
bEntryDate = specFlowFile.getEntryDate();
bLineageStartDate = specFlowFile.getLineageStartDate();
bLineageStartIndex = specFlowFile.getLineageStartIndex();
bLineageIdentifiers.clear();
bPenaltyExpirationMs = specFlowFile.getPenaltyExpirationMillis();
bSize = specFlowFile.getSize();
// If this is a StandardFlowFileRecord, access the attributes map directly. Do not use the
// getAttributes() method, because that will wrap the original in an UnmodifiableMap. As a result,
// a Processor that continually calls session.append() for instance will have a FlowFile whose attributes
// Map is wrapped thousands of times until it hits a StackOverflowError. We want the getter to return
// UnmodifiableMap, though, so that Processors cannot directly modify that Map.
bAttributes = specFlowFile instanceof StandardFlowFileRecord ? ((StandardFlowFileRecord) specFlowFile).attributes : specFlowFile.getAttributes();
bAttributesCopied = false;
bClaim = specFlowFile.getContentClaim();
bClaimOffset = specFlowFile.getContentClaimOffset();
bLastQueueDate = specFlowFile.getLastQueueDate();
bQueueDateIndex = specFlowFile.getQueueDateIndex();
return this;
}
public FlowFileRecord build() {
return new StandardFlowFileRecord(this);
}
}
@Override
public long getPenaltyExpirationMillis() {
return penaltyExpirationMs;
}
}