/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.cloud.message; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.graph.Node; import org.structr.api.graph.PropertyContainer; import org.structr.api.graph.Relationship; import org.structr.cloud.CloudConnection; import org.structr.cloud.CloudListener; import org.structr.cloud.sync.Diff; import org.structr.cloud.sync.EndOfSync; import org.structr.cloud.sync.Ping; import org.structr.cloud.sync.ReplicationStatus; import org.structr.cloud.sync.Synchronize; import org.structr.common.error.FrameworkException; import org.structr.core.GraphObject; import org.structr.core.graph.SyncCommand; /** * * */ public abstract class Message<T> { private static final Logger logger = LoggerFactory.getLogger(Message.class.getName()); private static final AtomicLong idGenerator = new AtomicLong(); private static final Map<String, Class> typeMap = new LinkedHashMap<>(); private static final Set<String> ignoredPropertyKeys = new HashSet<>(); static { // initialize type map, this is basically the instruction set of the CloudService typeMap.put(AuthenticationRequest.class.getSimpleName(), AuthenticationRequest.class); typeMap.put(AuthenticationResponse.class.getSimpleName(), AuthenticationResponse.class); typeMap.put(Begin.class.getSimpleName(), Begin.class); typeMap.put(Crypt.class.getSimpleName(), Crypt.class); typeMap.put(Delete.class.getSimpleName(), Delete.class); typeMap.put(Diff.class.getSimpleName(), Diff.class); typeMap.put(End.class.getSimpleName(), End.class); typeMap.put(EndOfSync.class.getSimpleName(), EndOfSync.class); typeMap.put(Error.class.getSimpleName(), Error.class); typeMap.put(FileNodeChunk.class.getSimpleName(), FileNodeChunk.class); typeMap.put(FileNodeDataContainer.class.getSimpleName(), FileNodeDataContainer.class); typeMap.put(FileNodeEndChunk.class.getSimpleName(), FileNodeEndChunk.class); typeMap.put(Finish.class.getSimpleName(), Finish.class); typeMap.put(ListSyncables.class.getSimpleName(), ListSyncables.class); typeMap.put(NodeDataContainer.class.getSimpleName(), NodeDataContainer.class); typeMap.put(Ping.class.getSimpleName(), Ping.class); typeMap.put(PullChunk.class.getSimpleName(), PullChunk.class); typeMap.put(PullFile.class.getSimpleName(), PullFile.class); typeMap.put(PullNode.class.getSimpleName(), PullNode.class); typeMap.put(PullNodeRequestContainer.class.getSimpleName(), PullNodeRequestContainer.class); typeMap.put(PullRelationship.class.getSimpleName(), PullRelationship.class); typeMap.put(RelationshipDataContainer.class.getSimpleName(), RelationshipDataContainer.class); typeMap.put(ReplicationStatus.class.getSimpleName(), ReplicationStatus.class); typeMap.put(Synchronize.class.getSimpleName(), Synchronize.class); // add local property keys that should be ignored ignoredPropertyKeys.add(GraphObject.createdDate.dbName()); ignoredPropertyKeys.add(GraphObject.lastModifiedDate.dbName()); } protected long id = 0; protected int sendCount = 0; public abstract void onRequest(final CloudConnection serverConnection) throws IOException, FrameworkException; public abstract void onResponse(final CloudConnection clientConnection) throws IOException, FrameworkException; public abstract void afterSend(final CloudConnection connection); protected abstract void deserializeFrom(final DataInputStream inputStream) throws IOException; protected abstract void serializeTo(final DataOutputStream outputStream) throws IOException; public Message() { this.id = idGenerator.incrementAndGet(); } public Message(final long id, final int sendCount) { this.id = id; this.sendCount = sendCount; } public long getId() { return id; } @Override public String toString() { return getClass().getSimpleName() + "(" + getId() + ")"; } public void serialize(final DataOutputStream outputStream) throws IOException { // write type final String type = getClass().getSimpleName(); SyncCommand.serialize(outputStream, type); // write ID SyncCommand.serialize(outputStream, id); SyncCommand.serialize(outputStream, ++sendCount); // write attributes serializeTo(outputStream); outputStream.flush(); } public boolean wasSentFromHere() { return sendCount > 1; } // ----- protected methods ----- protected String contentHashCode(final GraphObject graphObject) { if (graphObject != null) { if (graphObject.isNode()) { return contentHashCode(graphObject.getSyncNode().getNode()); } else { return contentHashCode(graphObject.getSyncRelationship().getRelationship()); } } return null; } protected String contentHashCode(final Node node) { return contentHashCode(node, null); } protected String contentHashCode(final Node node, final Set<Long> visitedObjectIDs) { int hashCode = propertyHashCode(node, id, visitedObjectIDs); return Integer.toString(hashCode); } protected String contentHashCode(final Relationship relationship) { return contentHashCode(relationship, null); } protected String contentHashCode(final Relationship relationship, final Set<Long> visitedObjectIDs) { int hashCode = propertyHashCode(relationship, id, visitedObjectIDs); hashCode ^= getNodeIdHashCode(relationship.getStartNode()); hashCode ^= getNodeIdHashCode(relationship.getEndNode()); return Integer.toString(hashCode); } protected void sendKeepalive(final CloudConnection connection) throws IOException, FrameworkException { // send keepalive randomly if (Math.random() < 0.05) { final String message = "Current batch: " + connection.getCount() + ", total: " + connection.getTotal(); // send message to remote connection.send(new Ping(message)); // update progress locally final CloudListener listener = connection.getListener(); if (listener != null) { listener.transmissionProgress(message); } } } // ----- private methods ----- private int propertyHashCode(final PropertyContainer propertyContainer, final long id, final Set<Long> visitedObjectIDs) { // can be null when only single content hashes are to be calculated if (visitedObjectIDs != null) { visitedObjectIDs.add(id); } final Set<String> sortedKeys = new TreeSet<>(); int hashCode = 34262; // fixed initial value // sort keys before accessing for (final String key : propertyContainer.getPropertyKeys()) { // filter unwanted property keys if (!ignoredPropertyKeys.contains(key)) { sortedKeys.add(key); } } for (final String key : sortedKeys) { final Object value = propertyContainer.getProperty(key, null); if (value != null) { hashCode ^= key.hashCode(); hashCode ^= value.hashCode(); } } return hashCode; } private int getNodeIdHashCode(final Node node) { final Object uuid = node.getProperty("id", ""); if (uuid != null && uuid instanceof String) { final String id = (String)uuid; return id.hashCode(); } return -1; } private void setId(final long id) { this.id = id; } private void setSendCount(final int sendCount) { this.sendCount = sendCount; } // ----- public static methods ----- public static Message deserialize(final DataInputStream inputStream) throws IOException { // read type final String type = (String)SyncCommand.deserialize(inputStream); if (type != null) { final Class clazz = typeMap.get(type); if (clazz != null) { try { final Message msg = (Message)clazz.newInstance(); msg.setId((Long)SyncCommand.deserialize(inputStream)); msg.setSendCount((Integer)SyncCommand.deserialize(inputStream)); msg.deserializeFrom(inputStream); return msg; } catch (Throwable t) { logger.warn("", t); } } else { logger.warn("Invalid CloudService message: unknown type {}", type); throw new EOFException("Invalid type, aborting."); } } else { logger.warn("Invalid CloudService message: no type found."); } return null; } }