/** * 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.websocket; import com.google.gson.Gson; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jetty.util.ConcurrentHashSet; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.graph.RelationshipType; import org.structr.common.SecurityContext; import org.structr.common.error.FrameworkException; import org.structr.core.GraphObject; import org.structr.core.StructrTransactionListener; import org.structr.core.TransactionSource; import org.structr.core.entity.AbstractNode; import org.structr.core.graph.ModificationEvent; import org.structr.core.graph.NodeInterface; import org.structr.core.graph.RelationshipInterface; import org.structr.core.property.PropertyMap; import org.structr.web.entity.AbstractFile; import org.structr.web.entity.User; import org.structr.web.entity.dom.DOMNode; import org.structr.websocket.message.MessageBuilder; import org.structr.websocket.message.WebSocketMessage; /** * * */ public class WebsocketController implements StructrTransactionListener { private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class.getName()); private static final Set<String> BroadcastCommands = new HashSet<>(Arrays.asList(new String[] { "UPDATE", "ADD", "CREATE" } )); private final Set<StructrWebSocket> clients = new ConcurrentHashSet<>(); private Gson gson = null; public WebsocketController(final Gson gson) { this.gson = gson; } public void registerClient(final StructrWebSocket client) { clients.add(client); } public void unregisterClient(final StructrWebSocket client) { clients.remove(client); } // ----- private methods ----- private void broadcast(final WebSocketMessage webSocketData) { //logger.debug("Broadcasting message to {} clients..", clients.size()); // session must be valid to be received by the client webSocketData.setSessionValid(true); final String pagePath = (String) webSocketData.getNodeData().get("pagePath"); final String encodedPath = URIUtil.encodePath(pagePath); final List<StructrWebSocket> clientsToRemove = new LinkedList<>(); final List<? extends GraphObject> result = webSocketData.getResult(); final String command = webSocketData.getCommand(); final GraphObject obj = webSocketData.getGraphObject(); String message; // create message for (StructrWebSocket socket : clients) { String clientPagePath = socket.getPagePath(); if (clientPagePath != null && !clientPagePath.equals(encodedPath)) { continue; } Session session = socket.getSession(); if (session != null && socket.isAuthenticated()) { final SecurityContext securityContext = socket.getSecurityContext(); // if the object IS NOT of type AbstractNode AND the client is NOT priviledged OR // if the object IS of type AbstractNode AND the client has no access to the node // THEN skip sending a message if (obj instanceof AbstractNode) { final AbstractNode node = (AbstractNode)obj; if (node.isHidden() || !securityContext.isVisible(node)) { continue; } } else { if (!socket.isPrivilegedUser(socket.getCurrentUser())) { continue; } } if (result != null && !result.isEmpty() && BroadcastCommands.contains(command)) { final WebSocketMessage clientData = webSocketData.copy(); clientData.setResult(filter(securityContext, result)); message = gson.toJson(clientData, WebSocketMessage.class); } else { message = gson.toJson(webSocketData, WebSocketMessage.class); } try { session.getRemote().sendString(message); } catch (Throwable t) { if (t instanceof WebSocketException) { WebSocketException wse = (WebSocketException) t; if ("RemoteEndpoint unavailable, current state [CLOSED], expecting [OPEN or CONNECTED]".equals(wse.getMessage())) { clientsToRemove.add(socket); } } logger.debug("Error sending message to client.", t); } } } for (StructrWebSocket s : clientsToRemove) { unregisterClient(s); logger.warn("Client removed from broadcast list: {}", s); } } private <T extends GraphObject> List<T> filter(final SecurityContext securityContext, final List<T> all) { List<T> filteredResult = new LinkedList<>(); for (T obj : all) { if (securityContext.isVisible((AbstractNode) obj)) { filteredResult.add(obj); } } return filteredResult; } // ----- interface StructrTransactionListener ----- @Override public void beforeCommit(final SecurityContext securityContext, final Collection<ModificationEvent> modificationEvents, final TransactionSource source) { } @Override public void afterCommit(final SecurityContext securityContext, final Collection<ModificationEvent> modificationEvents, final TransactionSource source) { for (final ModificationEvent event : modificationEvents) { try { final WebSocketMessage message = getMessageForEvent(securityContext, event); if (message != null) { broadcast(message); } } catch (FrameworkException ignore) { } } } @Override public void simpleBroadcast(final String commandName, final Map<String, Object> data) { broadcast(MessageBuilder.forName(commandName).data(data).build()); } // ----- private methods ----- private WebSocketMessage getMessageForEvent(final SecurityContext securityContext, final ModificationEvent modificationEvent) throws FrameworkException { final String callbackId = modificationEvent.getCallbackId(); if (modificationEvent.isNode()) { final NodeInterface node = (NodeInterface) modificationEvent.getGraphObject(); if (modificationEvent.isDeleted()) { final WebSocketMessage message = createMessage("DELETE", callbackId); message.setId(modificationEvent.getRemovedProperties().get(GraphObject.id)); message.setCode(200); return message; } if (modificationEvent.isCreated()) { final WebSocketMessage message = createMessage("CREATE", callbackId); message.setGraphObject(node); message.setResult(Arrays.asList(new GraphObject[]{node})); message.setCode(201); return message; } if (modificationEvent.isModified()) { final WebSocketMessage message = createMessage("UPDATE", callbackId); message.setGraphObject(node); message.setResult(Arrays.asList(new GraphObject[]{node})); message.setId(node.getUuid()); message.getModifiedProperties().addAll(modificationEvent.getModifiedProperties().keySet()); message.getRemovedProperties().addAll(modificationEvent.getRemovedProperties().keySet()); message.setNodeData(modificationEvent.getData(securityContext)); message.setCode(200); return message; } } else { // handle relationship final RelationshipInterface relationship = (RelationshipInterface) modificationEvent.getGraphObject(); final RelationshipType relType = modificationEvent.getRelationshipType(); // special treatment of CONTAINS relationships if ("CONTAINS".equals(relType.name())) { if (modificationEvent.isDeleted()) { final WebSocketMessage message = createMessage("REMOVE_CHILD", callbackId); message.setNodeData("parentId", relationship.getSourceNodeId()); message.setId(relationship.getTargetNodeId()); message.setCode(200); return message; } if (modificationEvent.isCreated()) { final WebSocketMessage message = new WebSocketMessage(); final NodeInterface startNode = relationship.getSourceNode(); final NodeInterface endNode = relationship.getTargetNode(); // If either start or end node are not visible for the user to be notified, // don't send a notification if (startNode == null || endNode == null) { return null; } message.setResult(Arrays.asList(new GraphObject[]{endNode})); message.setId(endNode.getUuid()); message.setNodeData("parentId", startNode.getUuid()); message.setCode(200); message.setCommand("APPEND_CHILD"); if (endNode instanceof DOMNode) { org.w3c.dom.Node refNode = ((DOMNode) endNode).getNextSibling(); if (refNode != null) { message.setCommand("INSERT_BEFORE"); message.setNodeData("refId", ((AbstractNode) refNode).getUuid()); } } else if (endNode instanceof User) { message.setCommand("APPEND_USER"); message.setNodeData("refId", startNode.getUuid()); } else if (endNode instanceof AbstractFile) { message.setCommand("APPEND_FILE"); message.setNodeData("refId", startNode.getUuid()); } return message; } } if (modificationEvent.isDeleted()) { final WebSocketMessage message = createMessage("DELETE", callbackId); message.setId(modificationEvent.getRemovedProperties().get(GraphObject.id)); message.setCode(200); return message; } if (modificationEvent.isModified()) { final WebSocketMessage message = createMessage("UPDATE", callbackId); message.getModifiedProperties().addAll(modificationEvent.getModifiedProperties().keySet()); message.getRemovedProperties().addAll(modificationEvent.getRemovedProperties().keySet()); message.setNodeData(modificationEvent.getData(securityContext)); message.setGraphObject(relationship); message.setId(relationship.getUuid()); message.setCode(200); final PropertyMap relProperties = relationship.getProperties(); //final NodeInterface startNode = relationship.getSourceNode(); //final NodeInterface endNode = relationship.getTargetNode(); //relProperties.put(new StringProperty("startNodeId"), startNode.getUuid()); //relProperties.put(new StringProperty("endNodeId"), endNode.getUuid()); final Map<String, Object> properties = PropertyMap.javaTypeToInputType(securityContext, relationship.getClass(), relProperties); message.setRelData(properties); return message; } } return null; } private WebSocketMessage createMessage(final String command, final String callbackId) { final WebSocketMessage newMessage = new WebSocketMessage(); newMessage.setCommand(command); if (callbackId != null) { newMessage.setCallback(callbackId); } return newMessage; } }