org.structr.cloud.CloudConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.cloud.CloudConnection.java

Source

/**
 * Copyright (C) 2010-2016 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;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.digest.DigestUtils;
import org.structr.cloud.message.DataContainer;
import org.structr.cloud.message.FileNodeChunk;
import org.structr.cloud.message.FileNodeDataContainer;
import org.structr.cloud.message.FileNodeEndChunk;
import org.structr.cloud.message.Message;
import org.structr.cloud.message.NodeDataContainer;
import org.structr.cloud.message.RelationshipDataContainer;
import org.structr.cloud.message.SyncableInfo;
import org.structr.cloud.sync.Ping;
import org.structr.common.AccessMode;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.Services;
import org.structr.core.TransactionSource;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.Principal;
import org.structr.core.entity.SchemaNode;
import org.structr.core.entity.SchemaRelationshipNode;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.RelationshipInterface;
import org.structr.core.graph.Tx;
import org.structr.core.property.PropertyMap;
import org.structr.dynamic.File;
import org.structr.schema.ConfigurationProvider;
import org.structr.web.entity.Folder;
import org.structr.web.entity.User;
import org.structr.web.entity.dom.DOMNode;
import org.structr.web.entity.dom.Page;
import org.structr.web.entity.dom.ShadowDocument;

/**
 *
 *
 */
public class CloudConnection<T> extends Thread implements TransactionSource {

    // the logger
    private static final Logger logger = Logger.getLogger(CloudConnection.class.getName());

    // containers
    private final Map<String, FileNodeDataContainer> fileMap = new LinkedHashMap<>();
    private final Map<String, String> idMap = new LinkedHashMap<>();
    private final Map<String, Object> data = new LinkedHashMap<>();

    // private fields
    private final ConfigurationProvider config = Services.getInstance().getConfigurationProvider();
    private App app = null;
    private CloudListener listener = null;
    private long transmissionAbortTime = 0L;
    private boolean authenticated = false;
    private String errorMessage = null;
    private String remoteAddress = null;
    private int errorCode = 0;
    private String password = null;
    private Cipher encrypter = null;
    private Cipher decrypter = null;
    private Receiver receiver = null;
    private Sender sender = null;
    private Socket socket = null;
    private T payload = null;
    private Tx tx = null;
    private int count = 0;
    private int total = 0;

    public CloudConnection(final SecurityContext securityContext, final Socket socket,
            final CloudListener listener) {

        super("CloudConnection(" + socket.getRemoteSocketAddress() + ")");

        this.app = StructrApp.getInstance(securityContext);
        this.remoteAddress = socket.getInetAddress().getHostAddress();
        this.listener = listener;
        this.socket = socket;

        this.setDaemon(true);

        logger.log(Level.INFO, "New connection from {0}", socket.getRemoteSocketAddress());
    }

    @Override
    public void start() {

        // setup read and write threads for the connection
        if (socket.isConnected() && !socket.isClosed()) {

            try {

                decrypter = Cipher.getInstance(CloudService.STREAM_CIPHER);
                encrypter = Cipher.getInstance(CloudService.STREAM_CIPHER);

                // this key is only used for the first two packets
                // of a transmission, it is replaced by the users
                // password hash afterwards.
                setEncryptionKey("StructrInitialEncryptionKey", 128);

                sender = new Sender(this, new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(
                        new CipherOutputStream(socket.getOutputStream(), encrypter), 32768, true))));
                receiver = new Receiver(this, new DataInputStream(new BufferedInputStream(
                        new GZIPInputStream(new CipherInputStream(socket.getInputStream(), decrypter), 32768))));

                receiver.start();
                sender.start();

                // start actual thread
                super.start();

            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
    }

    @Override
    public void run() {

        while (isConnected()) {

            try {

                final Message request = receiver.receive();
                if (request != null) {

                    logDebug("RECEIVED ", request);

                    // refresh transmission timeout
                    refreshTransmissionTimeout();

                    if (request.wasSentFromHere()) {

                        request.onResponse(this);

                    } else {

                        request.onRequest(this);
                    }
                }

                if (count >= 100) {

                    final String message = "Committing batch..";

                    sender.send(new Ping(message));

                    if (listener != null) {
                        listener.transmissionProgress(message);
                    }

                    // intermediate commit
                    this.commitTransaction();
                    this.endTransaction();
                    this.beginTransaction();

                    count = 0;

                }

            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

        shutdown();

        logger.log(Level.INFO, "Transmission finished");

    }

    public void send(final Message message) throws IOException, FrameworkException {

        logDebug("SEND", message);
        sender.send(message);
    }

    /**
     * This method is private to prevent calling it from a different thread.
     */
    private void shutdown() {

        close();
        endTransaction();
    }

    public void close() {

        try {

            socket.close();

        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    public void waitForAuthentication() throws FrameworkException {

        final long abortTime = System.currentTimeMillis() + CloudService.AUTH_TIMEOUT;

        while (!authenticated) {

            if (errorMessage != null) {
                throw new FrameworkException(errorCode, errorMessage);
            }

            if (System.currentTimeMillis() > abortTime) {

                throw new FrameworkException(401, "Authentication failed.");
            }

            try {

                Thread.sleep(10);

            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
    }

    public void refreshTransmissionTimeout() {
        transmissionAbortTime = System.currentTimeMillis() + CloudService.DEFAULT_TIMEOUT;
    }

    public void waitForTransmission() throws FrameworkException {

        transmissionAbortTime = System.currentTimeMillis() + CloudService.DEFAULT_TIMEOUT;

        while (isConnected()) {

            if (errorMessage != null) {
                throw new FrameworkException(errorCode, errorMessage);
            }

            if (System.currentTimeMillis() > transmissionAbortTime) {

                throw new FrameworkException(504, "Timeout while waiting for response.");
            }

            try {

                Thread.sleep(10);

            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
    }

    public void waitForClose(int timeout) throws FrameworkException {

        final long abortTime = System.currentTimeMillis() + CloudService.DEFAULT_TIMEOUT;

        while (isConnected() && System.currentTimeMillis() < abortTime) {

            try {

                Thread.sleep(10);

            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

    }

    public void setEncryptionKey(final String key, final int keyLength) throws InvalidKeyException {

        try {

            SecretKeySpec skeySpec = new SecretKeySpec(CloudService.trimToSize(DigestUtils.sha256(key), keyLength),
                    CloudService.STREAM_CIPHER);

            decrypter.init(Cipher.DECRYPT_MODE, skeySpec);
            encrypter.init(Cipher.ENCRYPT_MODE, skeySpec);

        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    public boolean isConnected() {
        return socket.isConnected() && !socket.isClosed();
    }

    public void setAuthenticated() {
        authenticated = true;
    }

    public void setPassword(final String password) {
        this.password = password;
    }

    public String getPassword() {
        return password;
    }

    public App getApplicationContext() {
        return app;
    }

    public NodeInterface storeNode(final DataContainer receivedData) throws FrameworkException {

        final SecurityContext securityContext = SecurityContext.getSuperUserInstance();
        final NodeDataContainer receivedNodeData = (NodeDataContainer) receivedData;
        final String typeName = receivedNodeData.getType();
        final Class nodeType = config.getNodeEntityClass(typeName);

        if (nodeType == null) {

            logger.log(Level.SEVERE, "Unknown entity type {0}", typeName);
            return null;
        }

        // skip builtin schema node types
        if (Boolean.TRUE.equals(receivedNodeData.getProperties().get(SchemaNode.isBuiltinType.dbName()))) {
            return null;
        }

        final String uuid = receivedNodeData.getSourceNodeId();
        GraphObject newOrExistingNode = app.get(nodeType, uuid);

        if (newOrExistingNode != null) {

            // merge properties
            newOrExistingNode.updateFromPropertyMap(receivedNodeData.getProperties());

        } else {

            final PropertyMap properties = PropertyMap.databaseTypeToJavaType(securityContext, nodeType,
                    receivedNodeData.getProperties());
            final List<DOMNode> existingChildren = new LinkedList<>();

            // special handling for ShadowDocument (all others must be deleted)
            if (ShadowDocument.class.getSimpleName().equals(typeName)) {

                // delete shadow document
                for (ShadowDocument existingDoc : app.nodeQuery(ShadowDocument.class).includeDeletedAndHidden()
                        .getAsList()) {

                    existingChildren.addAll(existingDoc.getProperty(Page.elements));
                    app.delete(existingDoc);
                }

                // add existing children to new shadow document
                properties.put(Page.elements, existingChildren);
            }

            // create node
            newOrExistingNode = app.create(nodeType, properties);
        }

        idMap.put(receivedNodeData.getSourceNodeId(), newOrExistingNode.getUuid());

        count++;
        total++;

        return (NodeInterface) newOrExistingNode;
    }

    public RelationshipInterface storeRelationship(final DataContainer receivedData) throws FrameworkException {

        final RelationshipDataContainer receivedRelationshipData = (RelationshipDataContainer) receivedData;
        final String sourceStartNodeId = receivedRelationshipData.getSourceStartNodeId();
        final String sourceEndNodeId = receivedRelationshipData.getSourceEndNodeId();
        final String uuid = receivedRelationshipData.getRelationshipId();

        // if end node ID was not found in the ID map,
        // assume it already exists in the database
        // (i.e. it was created earlier)
        String targetStartNodeId = idMap.get(sourceStartNodeId);
        if (targetStartNodeId == null) {
            targetStartNodeId = sourceStartNodeId;
        }

        // if end node ID was not found in the ID map,
        // assume it already exists in the database
        // (i.e. it was created earlier)
        String targetEndNodeId = idMap.get(sourceEndNodeId);
        if (targetEndNodeId == null) {
            targetEndNodeId = sourceEndNodeId;
        }

        if (targetStartNodeId != null && targetEndNodeId != null) {

            // Get new start and end node
            final SecurityContext securityContext = SecurityContext.getSuperUserInstance();
            final NodeInterface targetStartNode = app.getNodeById(targetStartNodeId);
            final NodeInterface targetEndNode = app.getNodeById(targetEndNodeId);
            final String typeName = receivedRelationshipData.getType();
            final Class relType = config.getRelationshipEntityClass(typeName);

            if (targetStartNode != null && targetEndNode != null) {

                final RelationshipInterface existingCandidate = app.relationshipQuery().and(GraphObject.id, uuid)
                        .includeDeletedAndHidden().getFirst();

                count++;
                total++;

                if (existingCandidate != null) {

                    // merge properties?
                    existingCandidate.updateFromPropertyMap(receivedRelationshipData.getProperties());

                    return existingCandidate;

                } else {

                    final PropertyMap properties = PropertyMap.databaseTypeToJavaType(securityContext, relType,
                            receivedRelationshipData.getProperties());
                    return app.create(targetStartNode, targetEndNode, relType, properties);
                }

            } else {

                logger.log(Level.WARNING, "Could not store relationship {0} -> {1}",
                        new Object[] { targetStartNode, targetEndNode });

            }

        }

        logger.log(Level.WARNING, "Could not store relationship {0} -> {1}",
                new Object[] { sourceStartNodeId, sourceEndNodeId });

        return null;
    }

    public void delete(final String uuid) throws FrameworkException {

        final GraphObject obj = app.get(uuid);
        if (obj != null) {

            if (obj instanceof NodeInterface) {

                app.delete((NodeInterface) obj);

            } else {

                app.delete((RelationshipInterface) obj);
            }

            count++;
            total++;
        }
    }

    public void deleteRelationship(final String uuid) throws FrameworkException {
        app.delete((RelationshipInterface) app.getRelationshipById(uuid));
    }

    public void beginTransaction() {

        tx = app.tx();
        tx.setSource(this);

        logDebug("######################## OPENING TRANSACTION " + tx + " in thread " + Thread.currentThread(),
                null);
    }

    public void commitTransaction() {

        if (tx != null) {

            try {

                logDebug("######################## COMMITING TRANSACTION " + tx + " in thread "
                        + Thread.currentThread(), null);

                tx.success();

            } catch (Throwable t) {

                // do not catch specific exception only, we need to be able to shut
                // down the connection gracefully, so we must make sure not to be
                // interrupted here
                t.printStackTrace();
            }
        } else {

            System.out.println("NO TRANSACTION!");
        }
    }

    public void endTransaction() {

        if (tx != null) {

            logDebug("######################## CLOSING TRANSACTION " + tx + " in thread " + Thread.currentThread(),
                    null);

            try {

                tx.close();

            } catch (Throwable t) {

                // do not catch specific exception only, we need to be able to shut
                // down the connection gracefully, so we must make sure not to be
                // interrupted here
                t.printStackTrace();
            }

            tx = null;
        }

        data.clear();
    }

    public Principal getUser(String userName) {

        try {

            return app.nodeQuery(User.class).andName(userName).getFirst();

        } catch (Throwable t) {
            t.printStackTrace();
        }

        return null;
    }

    public void impersonateUser(final Principal principal) throws FrameworkException {
        app = StructrApp.getInstance(SecurityContext.getInstance(principal, AccessMode.Backend));
    }

    public void beginFile(final FileNodeDataContainer container) {

        fileMap.put(container.getSourceNodeId(), container);

        count++;
        total++;
    }

    public void finishFile(final FileNodeEndChunk endChunk) throws FrameworkException {

        final FileNodeDataContainer container = fileMap.get(endChunk.getContainerId());
        if (container == null) {

            logger.log(Level.WARNING, "Received file end chunk for ID {0} without file, this should not happen!",
                    endChunk.getContainerId());

        } else {

            container.flushAndCloseTemporaryFile();

            final NodeInterface newNode = storeNode(container);
            final String filesPath = StructrApp.getConfigurationValue(Services.FILES_PATH);
            final String relativePath = newNode.getProperty(File.relativeFilePath);
            String newPath = null;

            if (filesPath.endsWith("/")) {

                newPath = filesPath + relativePath;

            } else {

                newPath = filesPath + "/" + relativePath;
            }

            try {
                container.persistTemporaryFile(newPath);

            } catch (Throwable t) {

                // do not catch specific exception only, we need to be able to shut
                // down the connection gracefully, so we must make sure not to be
                // interrupted here
                t.printStackTrace();
            }

            count++;
            total++;
        }
    }

    public void fileChunk(final FileNodeChunk chunk) {

        final FileNodeDataContainer container = fileMap.get(chunk.getContainerId());

        if (container == null) {

            logger.log(Level.WARNING, "Received file chunk for ID {0} without file, this should not happen!",
                    chunk.getContainerId());

        } else {

            container.addChunk(chunk);

            count++;
            total++;
        }
    }

    public List<SyncableInfo> listSyncables(final Set<Class<? extends GraphObject>> types)
            throws FrameworkException {

        final List<SyncableInfo> syncables = new LinkedList<>();

        if (types == null || types.isEmpty()) {

            for (final Page page : app.nodeQuery(Page.class).includeDeletedAndHidden().getAsList()) {
                syncables.add(new SyncableInfo(page));
            }

            for (final File file : app.nodeQuery(File.class).getAsList()) {
                syncables.add(new SyncableInfo(file));
            }

            for (final Folder folder : app.nodeQuery(Folder.class).getAsList()) {
                syncables.add(new SyncableInfo(folder));
            }

            for (final SchemaNode schemaNode : app.nodeQuery(SchemaNode.class).getAsList()) {
                syncables.add(new SyncableInfo(schemaNode));
            }

            for (final SchemaRelationshipNode schemaRelationship : app.nodeQuery(SchemaRelationshipNode.class)
                    .getAsList()) {
                syncables.add(new SyncableInfo(schemaRelationship));
            }
        }

        for (final Class type : types) {

            if (NodeInterface.class.isAssignableFrom(type)) {

                for (final NodeInterface syncable : (Iterable<NodeInterface>) app.nodeQuery(type)
                        .includeDeletedAndHidden().getAsList()) {
                    syncables.add(new SyncableInfo(syncable));
                }

            } else if (RelationshipInterface.class.isAssignableFrom(type)) {

                for (final RelationshipInterface syncable : (Iterable<RelationshipInterface>) app
                        .relationshipQuery(type).getAsList()) {
                    syncables.add(new SyncableInfo(syncable));
                }
            }
        }

        return syncables;
    }

    public void storeValue(final String key, final Object value) {
        data.put(key, value);
    }

    public Object getValue(final String key) {
        return data.get(key);
    }

    public void removeValue(final String key) {
        data.remove(key);
    }

    public void setPayload(final T payload) {
        this.payload = payload;
    }

    public T getPayload() {
        return payload;
    }

    public void setError(final int errorCode, final String errorMessage) {

        this.errorMessage = errorMessage;
        this.errorCode = errorCode;

        close();
    }

    public void logDebug(final String prefix, final Message request) {

        if (CloudService.DEBUG) {
            System.out.println(Thread.currentThread().getId() + ": " + System.currentTimeMillis() + "        "
                    + prefix + " " + (request != null ? request : "") + ", count: " + count);
        }
    }

    @Override
    public boolean isLocal() {
        return false;
    }

    @Override
    public boolean isRemote() {
        return true;
    }

    @Override
    public String getOriginAddress() {
        return remoteAddress;
    }

    public CloudListener getListener() {
        return listener;
    }

    public int getCount() {
        return count;
    }

    public int getTotal() {
        return total;
    }
}