/**
* Copyright (C) 2010-2014 Morgner UG (haftungsbeschränkt)
*
* 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 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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.common.AccessMode;
import org.structr.common.SecurityContext;
import org.structr.common.Syncable;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.Services;
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.relationship.SchemaRelationship;
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.Page;
/**
*
* @author Christian Morgner
*/
public class CloudConnection<T> extends Thread {
// 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 final Set<String> localMessageIds = new LinkedHashSet<>();
private App app = StructrApp.getInstance();
private long transmissionAbortTime = 0L;
private ExportContext context = null;
private boolean authenticated = false;
private String errorMessage = 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;
public CloudConnection(final Socket socket, final ExportContext context) {
super("CloudConnection(" + socket.getRemoteSocketAddress() + ")");
this.socket = socket;
this.context = context;
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 ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(new BufferedOutputStream(socket.getOutputStream()), encrypter), true)));
receiver = new Receiver(this, new ObjectInputStream(new GZIPInputStream(new CipherInputStream(new BufferedInputStream(socket.getInputStream()), decrypter))));
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) {
// inform sender that a message has arrived
sender.messageReceived();
// refresh transmission timeout
refreshTransmissionTimeout();
if (wasSentFromHere(request)) {
request.onResponse(this, context);
} else {
request.onRequest(this, context);
}
if (CloudService.DEBUG) {
System.out.println(" => " + request);
}
}
Thread.yield();
} catch (Throwable t) {
t.printStackTrace();
}
}
shutdown();
logger.log(Level.INFO, "Transmission finished");
}
private boolean wasSentFromHere(final Message message) {
return localMessageIds.contains(message.getId());
}
public void send(final Message message) throws IOException, FrameworkException {
sender.send(message);
if (CloudService.DEBUG) {
System.out.println(message);
}
localMessageIds.add(message.getId());
}
/**
* 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.DEFAULT_TIMEOUT;
while (!authenticated) {
if (errorMessage != null) {
throw new FrameworkException(errorCode, errorMessage);
}
if (System.currentTimeMillis() > abortTime) {
throw new FrameworkException(504, "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 (context.getCurrentProgress() < context.getTotalSize()) {
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;
}
final PropertyMap properties = PropertyMap.databaseTypeToJavaType(securityContext, nodeType, receivedNodeData.getProperties());
final String uuid = receivedNodeData.getSourceNodeId();
NodeInterface newOrExistingNode = null;
final NodeInterface existingCandidate = app.nodeQuery().and(GraphObject.id, uuid).includeDeletedAndHidden().getFirst();
if (existingCandidate != null && existingCandidate instanceof NodeInterface) {
newOrExistingNode = (NodeInterface) existingCandidate;
// merge properties
((Syncable) newOrExistingNode).updateFromPropertyMap(properties);
} else {
// create
newOrExistingNode = app.create(nodeType, properties);
}
idMap.put(receivedNodeData.getSourceNodeId(), newOrExistingNode.getUuid());
return 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 = (NodeInterface) app.get(targetStartNodeId);
final NodeInterface targetEndNode = (NodeInterface) app.get(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();
final PropertyMap properties = PropertyMap.databaseTypeToJavaType(securityContext, relType, receivedRelationshipData.getProperties());
if (existingCandidate != null) {
// merge properties?
((Syncable) existingCandidate).updateFromPropertyMap(properties);
return existingCandidate;
} else {
return app.create(targetStartNode, targetEndNode, relType, properties);
}
}
}
logger.log(Level.WARNING, "Could not store relationship {0} -> {1}", new Object[]{sourceStartNodeId, sourceEndNodeId});
return null;
}
public void beginTransaction() {
tx = app.tx();
if (CloudService.DEBUG) {
System.out.println("############################### OPENING TRANSACTION " + tx + " in Thread" + Thread.currentThread());
}
}
public void commitTransaction() {
if (tx != null) {
try {
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) {
if (CloudService.DEBUG) {
System.out.println("############################### CLOSING TRANSACTION " + tx + " in Thread" + Thread.currentThread());
}
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);
}
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();
}
}
}
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);
}
}
public List<SyncableInfo> listSyncables(final Set<Class<Syncable>> types) throws FrameworkException {
final List<SyncableInfo> syncables = new LinkedList<>();
if (types == null || types.isEmpty()) {
for (final Page page : app.nodeQuery(Page.class).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 SchemaRelationship schemaRelationship : app.relationshipQuery(SchemaRelationship.class).getAsList()) {
syncables.add(new SyncableInfo(schemaRelationship));
}
}
for (final Class<Syncable> type : types) {
Class cls;
try {
cls = Class.forName(type.getName());
} catch (ClassNotFoundException ex) {
continue;
}
if (NodeInterface.class.isAssignableFrom(type)) {
for (final NodeInterface syncable : (Iterable<NodeInterface>) app.nodeQuery(cls).getAsList()) {
syncables.add(new SyncableInfo((Syncable) syncable));
}
} else if (RelationshipInterface.class.isAssignableFrom(type)) {
for (final RelationshipInterface syncable : (Iterable<RelationshipInterface>) app.relationshipQuery(cls).getAsList()) {
syncables.add(new SyncableInfo((Syncable) 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 increaseTotal(final int total) {
context.increaseTotal(total);
}
public void setError(final int errorCode, final String errorMessage) {
this.errorMessage = errorMessage;
this.errorCode = errorCode;
close();
}
}