Package org.lilyproject.util.repo

Source Code of org.lilyproject.util.repo.RecordEvent$IndexRecordFilterData

/*
* Copyright 2010 Outerthought bvba
*
* 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.lilyproject.util.repo;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonToken;
import org.codehaus.jackson.util.ByteArrayBuilder;
import org.lilyproject.repository.api.IdGenerator;
import org.lilyproject.repository.api.Record;
import org.lilyproject.repository.api.SchemaId;
import org.lilyproject.util.ObjectUtils;
import org.lilyproject.util.json.JsonFormat;

/**
* Represents the payload of an event about a create-update-delete operation on the repository.
*
* <p>The actual payload is json, this class helps in parsing or constructing that json.
*/
public class RecordEvent {
    private long versionCreated = -1;
    private long versionUpdated = -1;
    private Type type;
    private String tableName;
    private Set<SchemaId> updatedFields;
    private boolean recordTypeChanged = false;
    /** For index-type events: affected vtags */
    private Set<SchemaId> vtagsToIndex;
    private IndexRecordFilterData indexRecordFilterData;
    /** A copy of the attributes supplied via {@link Record#setAttributes(Map)}. */
    private Map<String, String> attributes;

    public enum Type {
        CREATE("repo:record-created"),
        UPDATE("repo:record-updated"),
        DELETE("repo:record-deleted"),
        INDEX("repo:index");

        private String name;

        private Type(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

    public RecordEvent() {
    }

    /**
     * Creates a record event from the json data supplied as bytes.
     */
    public RecordEvent(byte[] data, IdGenerator idGenerator) throws IOException {
        // Using streaming JSON parsing for performance. We expect the JSON to be correct, validation
        // is absent/minimal.

        JsonParser jp = JsonFormat.JSON_FACTORY.createJsonParser(data);

        JsonToken current;
        current = jp.nextToken();

        if (current != JsonToken.START_OBJECT) {
            throw new RuntimeException("Not a JSON object.");
        }

        while (jp.nextToken() != JsonToken.END_OBJECT) {
            String fieldName = jp.getCurrentName();
            current = jp.nextToken(); // move from field name to field value
            if (fieldName.equals("type")) {
                String messageType = jp.getText();
                if (messageType.equals(Type.CREATE.getName())) {
                    type = Type.CREATE;
                } else if (messageType.equals(Type.DELETE.getName())) {
                    type = Type.DELETE;
                } else if (messageType.equals(Type.UPDATE.getName())) {
                    type = Type.UPDATE;
                } else if (messageType.equals(Type.INDEX.getName())) {
                    type = Type.INDEX;
                } else {
                    throw new RuntimeException("Unexpected kind of message type: " + messageType);
                }
            } else if (fieldName.equals("tableName")) {
                this.tableName = jp.getText();
            } else if (fieldName.equals("versionCreated")) {
                versionCreated = jp.getLongValue();
            } else if (fieldName.equals("versionUpdated")) {
                versionUpdated = jp.getLongValue();
            } else if (fieldName.equals("recordTypeChanged")) {
                recordTypeChanged = jp.getBooleanValue();
            } else if (fieldName.equals("updatedFields")) {
                if (current != JsonToken.START_ARRAY) {
                    throw new RuntimeException("updatedFields is not a JSON array");
                }
                while (jp.nextToken() != JsonToken.END_ARRAY) {
                    addUpdatedField(idGenerator.getSchemaId(jp.getBinaryValue()));
                }
            } else if (fieldName.equals("vtagsToIndex")) {
                if (current != JsonToken.START_ARRAY) {
                    throw new RuntimeException("vtagsToIndex is not a JSON array");
                }
                while (jp.nextToken() != JsonToken.END_ARRAY) {
                    addVTagToIndex(idGenerator.getSchemaId(jp.getBinaryValue()));
                }
            } else if (fieldName.equals("attributes")) {
                if (current != JsonToken.START_OBJECT) {
                    throw new RuntimeException("Attributes is not a JSON object");
                }
                this.attributes = new HashMap<String, String>();
                while (jp.nextToken() != JsonToken.END_OBJECT) {
                    String key = jp.getCurrentName();
                    String value = jp.getText();
                    attributes.put(key, value);
                }
            } else if (fieldName.equals("indexFilterData")) {
                this.indexRecordFilterData = new IndexRecordFilterData(jp, idGenerator);
            }
        }
    }

    public long getVersionCreated() {
        return versionCreated;
    }

    public void setVersionCreated(long versionCreated) {
        this.versionCreated = versionCreated;
    }

    public long getVersionUpdated() {
        return versionUpdated;
    }

    public void setVersionUpdated(long versionUpdated) {
        this.versionUpdated = versionUpdated;
    }

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    public String getTableName() {
        return tableName;
    }

    /**
     * Indicates if the record type of the non-versioned scope changed as part of this event.
     * Should return false for newly created records.
     */
    public boolean getRecordTypeChanged() {
        return recordTypeChanged;
    }

    public void setRecordTypeChanged(boolean recordTypeChanged) {
        this.recordTypeChanged = recordTypeChanged;
    }

    public Type getType() {
        return type;
    }

    public void setType(Type type) {
        this.type = type;
    }

    /**
     * The fields which were updated (= added, deleted or changed), identified by their FieldType ID.
     *
     * <p>In case of a delete event, this list is empty.
     */
    public Set<SchemaId> getUpdatedFields() {
        return updatedFields != null ? updatedFields : Collections.<SchemaId>emptySet();
    }

    public void addUpdatedField(SchemaId fieldTypeId) {
        if (updatedFields == null) {
            updatedFields = new HashSet<SchemaId>();
        }
        updatedFields.add(fieldTypeId);
    }

    public Set<SchemaId> getVtagsToIndex() {
        return vtagsToIndex;
    }

    public void addVTagToIndex(SchemaId vtag) {
        if (vtagsToIndex == null) {
            vtagsToIndex = new HashSet<SchemaId>();
        }
        vtagsToIndex.add(vtag);
    }

    /**
     * Transient attributes passed on from the Record during create/update operations,
     * see also {@link Record#setAttributes(Map)}.
     *
     * @return A map of Strings containing attributes.
     */
    public Map<String,String> getAttributes() {
        if (this.attributes == null) {
            this.attributes = new HashMap<String,String>();
        }
        return this.attributes;
    }

    public boolean hasAttributes() {
        return attributes != null && attributes.size() > 0;
    }

    /**
     * Transient attributes passed on from the Record during create/update operations,
     * see also {@link Record#setAttributes(Map)}.
     *
     * @param attributes A map of Strings containing attributes.
     */
    public void setAttributes(Map<String,String> attributes) {
        this.attributes = attributes;
    }

    public IndexRecordFilterData getIndexRecordFilterData() {
        return indexRecordFilterData;
    }

    public void setIndexRecordFilterData(IndexRecordFilterData indexRecordFilterData) {
        this.indexRecordFilterData = indexRecordFilterData;
    }

    public void toJson(JsonGenerator gen) throws IOException {

        gen.writeStartObject();

        if (type != null) {
            gen.writeStringField("type", type.getName());
        }

        if (tableName != null) {
            gen.writeStringField("tableName", tableName);
        }

        if (versionUpdated != -1) {
            gen.writeNumberField("versionUpdated", versionUpdated);
        }

        if (versionCreated != -1) {
            gen.writeNumberField("versionCreated", versionCreated);
        }

        if (recordTypeChanged) {
            gen.writeBooleanField("recordTypeChanged", true);
        }

        if (updatedFields != null && updatedFields.size() > 0) {
            gen.writeArrayFieldStart("updatedFields");
            for (SchemaId updatedField : updatedFields) {
                gen.writeBinary(updatedField.getBytes());
            }
            gen.writeEndArray();
        }

        if (vtagsToIndex != null && vtagsToIndex.size() > 0) {
            gen.writeArrayFieldStart("vtagsToIndex");
            for (SchemaId vtag : vtagsToIndex) {
                gen.writeBinary(vtag.getBytes());
            }
            gen.writeEndArray();
        }

        if (attributes != null && attributes.size() > 0) {
            gen.writeObjectFieldStart("attributes");
            for(String key : attributes.keySet()) {
                gen.writeStringField(key, attributes.get(key));
            }
            gen.writeEndObject();
        }

        if (indexRecordFilterData != null) {
            gen.writeFieldName("indexFilterData");
            indexRecordFilterData.toJson(gen);
        }

        gen.writeEndObject();
        gen.flush();
    }

    public String toJson() {
        try {
            StringWriter writer = new StringWriter();
            JsonGenerator gen = JsonFormat.JSON_FACTORY.createJsonGenerator(writer);
            toJson(gen);
            return writer.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] toJsonBytes() {
        try {
            ByteArrayBuilder bb = new ByteArrayBuilder(JsonFormat.JSON_FACTORY._getBufferRecycler());
            JsonGenerator gen = JsonFormat.JSON_FACTORY.createJsonGenerator(bb, JsonEncoding.UTF8);
            toJson(gen);
            byte[] result = bb.toByteArray();
            bb.release();
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        RecordEvent other = (RecordEvent)obj;

        if (other.type != this.type) {
            return false;
        }

        if (other.recordTypeChanged != this.recordTypeChanged) {
            return false;
        }

        if (other.versionCreated != this.versionCreated) {
            return false;
        }

        if (other.versionUpdated != this.versionUpdated) {
            return false;
        }

        if (!ObjectUtils.safeEquals(other.updatedFields, this.updatedFields)) {
            return false;
        }

        if (!ObjectUtils.safeEquals(other.vtagsToIndex, this.vtagsToIndex)) {
            return false;
        }

        if(!ObjectUtils.safeEquals(other.attributes, this.attributes)) {
            return false;
        }

        if (!ObjectUtils.safeEquals(other.tableName, tableName)) {
            return false;
        }

        // TODO implement equals for IndexRecordFilterData

        return true;
    }

    @Override
    public int hashCode() {
        int result = (int) (versionCreated ^ (versionCreated >>> 32));
        result = 31 * result + (int) (versionUpdated ^ (versionUpdated >>> 32));
        result = 31 * result + (type != null ? type.hashCode() : 0);
        result = 31 * result + (updatedFields != null ? updatedFields.hashCode() : 0);
        result = 31 * result + (recordTypeChanged ? 1 : 0);
        result = 31 * result + (vtagsToIndex != null ? vtagsToIndex.hashCode() : 0);
        result = 31 * result + (attributes != null ? attributes.hashCode() : 0);
        return result;
    }

    /**
     * Data needed for IndexRecordFilter evaluation.
     *
     * <p>Information needed to decide if an IndexRecordFilter matches a record. Contains both the
     * necessary information both from the old and new record state, so that we know what matched
     * before and what matches now, which enables important optimisations.</p>
     *
     * <p>For example, this information is used by the IndexerEditFilter to only sent events
     * to sep consumers from relevant indexes, as well as by IndexUpdater to know what
     * index inclusion rule matches before & now.</p>
     *
     * <p>At the time of this writing, the indexerconf only allows selection
     * based on record type, on 1 field, and on information that is part of the record id.
     * The model here is already a bit more flexible (can contain info on multiple fields) to allow for
     * more powerful selections in the future.</p>
     */
    public static class IndexRecordFilterData {

        /**
         *  A subscription id set that represents as "all the index subscriptions".
         */
        public static final Set<String> ALL_INDEX_SUBSCRIPTIONS = ImmutableSet.of("/");

        private boolean newRecordExists;
        private boolean oldRecordExists;
        private SchemaId newRecordType;
        private SchemaId oldRecordType;
        private List<FieldChange> fieldChanges;

        // All index subscriptions to be either included or excluded when indexing (see also includeSubcriptions)
        private Set<String> indexSubscriptionIds;

        // Flag to determine if the indexSubscriptions set is for inclusion (true) or exclusion (false)
        private boolean includeSubscriptions = true;


        public IndexRecordFilterData() {
        }

        public IndexRecordFilterData(JsonParser jp, IdGenerator idGenerator) throws IOException {
            JsonToken current = jp.getCurrentToken();

            if (current != JsonToken.START_OBJECT) {
                throw new RuntimeException("Not a JSON object.");
            }

            while (jp.nextToken() != JsonToken.END_OBJECT) {
                String fieldName = jp.getCurrentName();
                current = jp.nextToken(); // move from field name to field value
                if (fieldName.equals("old")) {
                    oldRecordExists = jp.getBooleanValue();
                } else if (fieldName.equals("new")) {
                    newRecordExists = jp.getBooleanValue();
                } else if (fieldName.equals("newRecordType")) {
                    newRecordType = idGenerator.getSchemaId(jp.getBinaryValue());
                } else if (fieldName.equals("oldRecordType")) {
                    oldRecordType = idGenerator.getSchemaId(jp.getBinaryValue());
                } else if (fieldName.equals("includeSubscriptions")) {
                    includeSubscriptions = jp.getBooleanValue();
                } else if (fieldName.equals("fields")) {
                    if (current != JsonToken.START_ARRAY) {
                        throw new RuntimeException("updatedFields is not a JSON array");
                    }
                    fieldChanges = new ArrayList<FieldChange>();
                    while (jp.nextToken() != JsonToken.END_ARRAY) {
                        fieldChanges.add(new FieldChange(jp, idGenerator));
                    }
                } else if (fieldName.equals("subscriptions")) {
                    if (current != JsonToken.START_ARRAY) {
                        throw new RuntimeException("subscriptions is not a JSON array");
                    }
                    indexSubscriptionIds = Sets.newHashSet();
                    while (jp.nextToken() != JsonToken.END_ARRAY) {
                        indexSubscriptionIds.add(jp.getText());
                    }
                }
            }
        }

        public boolean getNewRecordExists() {
            return newRecordExists;
        }

        public void setNewRecordExists(boolean newRecordExists) {
            this.newRecordExists = newRecordExists;
        }

        public boolean getOldRecordExists() {
            return oldRecordExists;
        }

        public void setOldRecordExists(boolean oldRecordExists) {
            this.oldRecordExists = oldRecordExists;
        }

        public SchemaId getNewRecordType() {
            return newRecordType;
        }

        public void setNewRecordType(SchemaId newRecordType) {
            this.newRecordType = newRecordType;
        }

        public SchemaId getOldRecordType() {
            return oldRecordType;
        }

        public void setOldRecordType(SchemaId oldRecordType) {
            this.oldRecordType = oldRecordType;
        }

        public void addChangedField(SchemaId id, byte[] oldValue, byte[] newValue) {
            if (fieldChanges == null) {
                fieldChanges = new ArrayList<FieldChange>();
            }
            fieldChanges.add(new FieldChange(id, oldValue, newValue));
        }

        public List<FieldChange> getFieldChanges() {
            return fieldChanges;
        }

        public void toJson(JsonGenerator gen) throws IOException {
            gen.writeStartObject();

            gen.writeBooleanField("old", oldRecordExists);
            gen.writeBooleanField("new", newRecordExists);
            gen.writeBooleanField("includeSubscriptions", includeSubscriptions);

            if (newRecordType != null) {
                gen.writeBinaryField("newRecordType", newRecordType.getBytes());
            }

            if (oldRecordType != null) {
                gen.writeBinaryField("oldRecordType", oldRecordType.getBytes());
            }

            if (fieldChanges != null) {
                gen.writeArrayFieldStart("fields");

                for (FieldChange fieldChange : fieldChanges) {
                    fieldChange.toJson(gen);
                }

                gen.writeEndArray();
            }

            if (indexSubscriptionIds != null) {
                gen.writeArrayFieldStart("subscriptions");
                for (String subscriptionId : indexSubscriptionIds) {
                    gen.writeString(subscriptionId);
                }
                gen.writeEndArray();
            }

            gen.writeEndObject();
        }



        /**
         * Set the index subscription ids to be included when distributing the containing record
         * event to indexers. This cannot be combined with exclusions.
         *
         * @param indexSubscriptionIds Ids of the index subscriptions for which the containing
         *        RecordEvent applies
         */
        public void setSubscriptionInclusions(Set<String> indexSubscriptionIds) {
            if (indexSubscriptionIds.equals(ALL_INDEX_SUBSCRIPTIONS)){
                indexSubscriptionIds = null;
            } else {
                this.indexSubscriptionIds = indexSubscriptionIds;
            }
            includeSubscriptions = true;
        }

        /**
         * Set the index subscription ids to be excluded when distributing the containing record
         * event to indexers. This cannot be combined with inclusions.
         *
         * @param indexSubscriptionIds Ids of the index subscriptions for which the containing
         *        RecordEvent does not apply
         */
        public void setSubscriptionExclusions(Set<String> indexSubscriptionIds) {
            if (indexSubscriptionIds.equals(ALL_INDEX_SUBSCRIPTIONS)){
                indexSubscriptionIds = null;
            } else {
                this.indexSubscriptionIds = indexSubscriptionIds;
            }
            includeSubscriptions = false;
        }

        /**
         * Check if the containing RecordEvent is applicable to an index subscription.
         *
         * @param indexSubscriptionId Id of the index subscription to be checked
         * @return true if the RecordEvent is applicable for the index subscription
         */
        public boolean appliesToSubscription(String indexSubscriptionId) {
            if (includeSubscriptions) {
                return indexSubscriptionIds == null
                        || indexSubscriptionIds.contains(indexSubscriptionId);
            } else {
                return indexSubscriptionIds != null
                        && !indexSubscriptionIds.contains(indexSubscriptionId);
            }
        }

        @Override
        public boolean equals(Object obj) {
            return EqualsBuilder.reflectionEquals(this, obj);
        }

        @Override
        public int hashCode() {
            return HashCodeBuilder.reflectionHashCode(this);
        }
    }

    public static class FieldChange {
        private SchemaId id;
        private byte[] oldValue;
        private byte[] newValue;

        public FieldChange(SchemaId id, byte[] oldValue, byte[] newValue) {
            this.id = id;
            this.oldValue = oldValue;
            this.newValue = newValue;
        }

        public FieldChange(JsonParser jp, IdGenerator idGenerator) throws IOException {
            JsonToken current = jp.getCurrentToken();

            if (current != JsonToken.START_OBJECT) {
                throw new RuntimeException("Not a JSON object.");
            }

            while (jp.nextToken() != JsonToken.END_OBJECT) {
                String fieldName = jp.getCurrentName();
                current = jp.nextToken(); // move from field name to field value
                if (fieldName.equals("id")) {
                    this.id = idGenerator.getSchemaId(jp.getBinaryValue());
                } else if (fieldName.equals("old")) {
                    oldValue = jp.getBinaryValue();
                } else if (fieldName.equals("new")) {
                    newValue = jp.getBinaryValue();
                }
            }
        }

        public SchemaId getId() {
            return id;
        }

        public byte[] getOldValue() {
            return oldValue;
        }

        public byte[] getNewValue() {
            return newValue;
        }

        public void toJson(JsonGenerator gen) throws IOException {
            gen.writeStartObject();

            gen.writeBinaryField("id", id.getBytes());

            if (oldValue != null) {
                gen.writeBinaryField("old", oldValue);
            }

            if (newValue != null) {
                gen.writeBinaryField("new", newValue);
            }

            gen.writeEndObject();
        }

        @Override
        public boolean equals(Object obj) {
            return EqualsBuilder.reflectionEquals(this, obj);
        }

        @Override
        public int hashCode() {
            return HashCodeBuilder.reflectionHashCode(this);
        }
    }
}

TOP

Related Classes of org.lilyproject.util.repo.RecordEvent$IndexRecordFilterData

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.