package fi.otavanopisto.muikku.plugins.material.coops.websocket; import java.io.IOException; import java.io.Reader; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.enterprise.event.Observes; import javax.inject.Inject; import javax.transaction.Transactional; import javax.websocket.CloseReason; import javax.websocket.EndpointConfig; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import org.apache.commons.lang3.math.NumberUtils; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import fi.foyt.coops.CoOpsApi; import fi.foyt.coops.CoOpsConflictException; import fi.foyt.coops.CoOpsForbiddenException; import fi.foyt.coops.CoOpsInternalErrorException; import fi.foyt.coops.CoOpsNotFoundException; import fi.foyt.coops.CoOpsUsageException; import fi.foyt.coops.extensions.websocket.ErrorMessage; import fi.foyt.coops.extensions.websocket.PatchMessage; import fi.foyt.coops.extensions.websocket.UpdateMessage; import fi.foyt.coops.model.Patch; import fi.otavanopisto.muikku.plugins.material.coops.CoOpsSessionController; import fi.otavanopisto.muikku.plugins.material.coops.event.CoOpsPatchEvent; import fi.otavanopisto.muikku.plugins.material.coops.model.CoOpsSession; import fi.otavanopisto.muikku.plugins.material.coops.model.CoOpsSessionType; import fi.otavanopisto.muikku.plugins.material.model.HtmlMaterial; @ServerEndpoint("/ws/coops/{HTMLMATERIALID}/{SESSIONID}") @Transactional public class CoOpsDocumentWebSocket { private static final Map<String, Map<String, Session>> fileClients = new HashMap<String, Map<String, Session>>(); @Inject private CoOpsSessionController coOpsSessionController; @Inject private CoOpsApi coOpsApi; @OnOpen public void onOpen(final Session client, EndpointConfig endpointConfig, @PathParam("HTMLMATERIALID") String htmlMaterialId, @PathParam("SESSIONID") String sessionId) throws IOException { synchronized (this) { // // TODO: RequestScope is not available on the websockets, switch to ticket system // // if (!sessionController.isLoggedIn()) { // client.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "Permission denied")); // } // // UserEntity userEntity = sessionController.getLoggedUserEntity(); // // EnvironmentUser environmentUser = environmentUserController.findEnvironmentUserByUserEntity(userEntity); // // if (environmentUser.getRole() == null || environmentUser.getRole().getArchetype() == EnvironmentRoleArchetype.STUDENT) { // client.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "Permission denied")); // } CoOpsSession session = coOpsSessionController.findSessionBySessionId(sessionId); if (session == null) { client.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "Not Found")); return; } if (!session.getHtmlMaterial().getId().equals(NumberUtils.createLong(htmlMaterialId))) { client.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "Session is associated with another fileId")); return; } Map<String, Session> sessions = fileClients.get(htmlMaterialId); if (sessions == null) { fileClients.put(htmlMaterialId, new HashMap<String, Session>()); } fileClients.get(htmlMaterialId).put(client.getId(), client); coOpsSessionController.updateSessionType(session, CoOpsSessionType.WS); HtmlMaterial htmlMaterial = session.getHtmlMaterial(); Long currentRevisionNumber = htmlMaterial.getRevisionNumber(); if (session.getJoinRevision() < currentRevisionNumber) { ObjectMapper objectMapper = new ObjectMapper(); List<Patch> patches; try { patches = coOpsApi.fileUpdate(session.getHtmlMaterial().getId().toString(), session.getSessionId(), session.getJoinRevision()); for (Patch patch : patches) { sendPatch(client, patch); } } catch (CoOpsInternalErrorException e) { client.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "Internal Error")); } catch (CoOpsUsageException e) { client.getAsyncRemote().sendText( objectMapper.writeValueAsString(new ErrorMessage("patchError", 400, e.getMessage()))); } catch (CoOpsNotFoundException e) { client.getAsyncRemote().sendText( objectMapper.writeValueAsString(new ErrorMessage("patchError", 404, e.getMessage()))); } catch (CoOpsForbiddenException e) { client.getAsyncRemote().sendText( objectMapper.writeValueAsString(new ErrorMessage("patchError", 500, e.getMessage()))); } } } } @OnClose public void onClose(final Session session, CloseReason closeReason, @PathParam("HTMLMATERIALID") String fileId, @PathParam("SESSIONID") String sessionId) { synchronized (this) { fileClients.get(fileId).remove(session.getId()); CoOpsSession coOpsSession = coOpsSessionController.findSessionBySessionId(sessionId); if (coOpsSession != null) { closeSession(coOpsSession); } } } @OnMessage public void onMessage(Reader messageReader, Session client, @PathParam("HTMLMATERIALID") String fileId, @PathParam("SESSIONID") String sessionId) throws IOException { CoOpsSession session = coOpsSessionController.findSessionBySessionId(sessionId); if (session == null) { client.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "Not Found")); return; } if (!session.getHtmlMaterial().getId().equals(NumberUtils.createLong(fileId))) { client .close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "Session is associated with another fileId")); return; } ObjectMapper objectMapper = new ObjectMapper(); try { PatchMessage patchMessage; try { patchMessage = objectMapper.readValue(messageReader, PatchMessage.class); } catch (IOException e) { throw new CoOpsInternalErrorException(e); } if (patchMessage == null) { throw new CoOpsInternalErrorException("Could not parse message"); } if (!patchMessage.getType().equals("patch")) { throw new CoOpsInternalErrorException("Unknown message type: " + patchMessage.getType()); } Patch patch = patchMessage.getData(); coOpsApi.filePatch(fileId, patch.getSessionId(), patch.getRevisionNumber(), patch.getPatch(), patch.getProperties(), patch.getExtensions()); } catch (CoOpsInternalErrorException e) { client.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "Internal Error")); } catch (CoOpsUsageException e) { client.getAsyncRemote().sendText( objectMapper.writeValueAsString(new ErrorMessage("patchError", 400, e.getMessage()))); } catch (CoOpsNotFoundException e) { client.getAsyncRemote().sendText( objectMapper.writeValueAsString(new ErrorMessage("patchError", 404, e.getMessage()))); } catch (CoOpsConflictException e) { client.getAsyncRemote().sendText( objectMapper.writeValueAsString(new ErrorMessage("patchRejected", 409, "Conflict"))); } catch (CoOpsForbiddenException e) { client.getAsyncRemote().sendText( objectMapper.writeValueAsString(new ErrorMessage("patchError", 500, e.getMessage()))); } } public void onCoOpsPatch(@Observes CoOpsPatchEvent event) throws JsonGenerationException, JsonMappingException, IOException { synchronized (this) { Map<String, Session> clients = fileClients.get(event.getHtmlMaterialId()); if (clients != null) { for (Session client : clients.values()) { sendPatch(client, event.getPatch()); } } } } private void sendPatch(Session client, Patch patch) throws JsonGenerationException, JsonMappingException, IOException { UpdateMessage updateMessage = new UpdateMessage(patch); String message = (new ObjectMapper()).writeValueAsString(updateMessage); client.getAsyncRemote().sendText(message); } private void closeSession(CoOpsSession session) { coOpsSessionController.closeSession(session); } }