package fi.otavanopisto.muikku.plugins.material.coops; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import javax.enterprise.context.Dependent; import javax.enterprise.event.Event; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.codehaus.jackson.map.ObjectMapper; import fi.foyt.coops.CoOpsConflictException; import fi.foyt.coops.CoOpsForbiddenException; import fi.foyt.coops.CoOpsInternalErrorException; import fi.foyt.coops.CoOpsNotFoundException; import fi.foyt.coops.CoOpsNotImplementedException; import fi.foyt.coops.CoOpsUsageException; import fi.foyt.coops.model.File; import fi.foyt.coops.model.Join; import fi.foyt.coops.model.Patch; import fi.otavanopisto.muikku.environment.HttpPort; import fi.otavanopisto.muikku.environment.HttpsPort; import fi.otavanopisto.muikku.plugins.material.HtmlMaterialController; 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.HtmlMaterialRevision; import fi.otavanopisto.muikku.plugins.material.coops.model.HtmlMaterialRevisionExtensionProperty; import fi.otavanopisto.muikku.plugins.material.coops.model.HtmlMaterialRevisionProperty; import fi.otavanopisto.muikku.plugins.material.model.HtmlMaterial; import fi.otavanopisto.muikku.session.SessionController; @Dependent public class CoOpsApiImpl implements fi.foyt.coops.CoOpsApi { private final static String COOPS_PROTOCOL_VERSION = "1.0.0"; @Inject private HtmlMaterialController htmlMaterialController; @Inject private CoOpsSessionController coOpsSessionController; @Inject private CoOpsSessionEventsController coOpsSessionEventsController; @Inject private Event<CoOpsPatchEvent> patchEvent; @Inject private HttpServletRequest httpRequest; @Inject private SessionController sessionController; @Inject @HttpPort private Integer httpPort; @Inject @HttpsPort private Integer httpsPort; public File fileGet(String fileId, Long revisionNumber) throws CoOpsNotImplementedException, CoOpsNotFoundException, CoOpsUsageException, CoOpsInternalErrorException, CoOpsForbiddenException { HtmlMaterial htmlMaterial = findFile(fileId); if (htmlMaterial == null) { throw new CoOpsNotFoundException(); } if (revisionNumber != null) { String data = htmlMaterialController.getRevisionHtml(htmlMaterial, revisionNumber); Map<String, String> properties = htmlMaterialController.getRevisionProperties(htmlMaterial, revisionNumber); return new File(revisionNumber, data, htmlMaterial.getContentType(), properties); } else { Long maxRevisionNumber = htmlMaterialController.lastHtmlMaterialRevision(htmlMaterial); String data = htmlMaterialController.getRevisionHtml(htmlMaterial, maxRevisionNumber); Map<String, String> properties = htmlMaterialController.getRevisionProperties(htmlMaterial, maxRevisionNumber); return new File(maxRevisionNumber, data, htmlMaterial.getContentType(), properties); } } public List<Patch> fileUpdate(String fileId, String sessionId, Long revisionNumber) throws CoOpsNotFoundException, CoOpsInternalErrorException, CoOpsUsageException, CoOpsForbiddenException { CoOpsSession session = coOpsSessionController.findSessionBySessionId(sessionId); if (session == null) { throw new CoOpsUsageException("Invalid session id"); } if (revisionNumber == null) { throw new CoOpsUsageException("revisionNumber parameter is missing"); } HtmlMaterial htmlMaterial = findFile(fileId); if (htmlMaterial == null) { throw new CoOpsNotFoundException(); } List<Patch> updateResults = new ArrayList<>(); List<HtmlMaterialRevision> htmlMaterialRevisions = htmlMaterialController.listRevisionsAfter(htmlMaterial, revisionNumber); if (!htmlMaterialRevisions.isEmpty()) { for (HtmlMaterialRevision htmlMaterialRevision : htmlMaterialRevisions) { String patch = htmlMaterialRevision.getData(); Map<String, String> properties = null; Map<String, Object> extensions = null; List<HtmlMaterialRevisionProperty> revisionProperties = htmlMaterialController.listRevisionProperties(htmlMaterialRevision); if (revisionProperties.size() > 0) { properties = new HashMap<>(); for (HtmlMaterialRevisionProperty revisionProperty : revisionProperties) { properties.put(revisionProperty.getKey(), revisionProperty.getValue()); } } List<HtmlMaterialRevisionExtensionProperty> revisionExtensionProperties = htmlMaterialController.listRevisionExtensionProperties(htmlMaterialRevision); if (revisionExtensionProperties.size() > 0) { extensions = new HashMap<>(); for (HtmlMaterialRevisionExtensionProperty revisionExtensionProperty : revisionExtensionProperties) { extensions.put(revisionExtensionProperty.getKey(), revisionExtensionProperty.getValue()); } } if (patch != null) { updateResults.add(new Patch(htmlMaterialRevision.getSessionId(), htmlMaterialRevision.getRevision(), htmlMaterialRevision.getChecksum(), patch, properties, extensions)); } else { updateResults.add(new Patch(htmlMaterialRevision.getSessionId(), htmlMaterialRevision.getRevision(), null, null, properties, extensions)); } } } return updateResults; } public void filePatch(String fileId, String sessionId, Long revisionNumber, String patch, Map<String, String> properties, Map<String, Object> extensions) throws CoOpsInternalErrorException, CoOpsUsageException, CoOpsNotFoundException, CoOpsConflictException, CoOpsForbiddenException { CoOpsSession session = coOpsSessionController.findSessionBySessionId(sessionId); if (session == null) { throw new CoOpsUsageException("Invalid session id"); } CoOpsDiffAlgorithm algorithm = htmlMaterialController.findAlgorithm(session.getAlgorithm()); if (algorithm == null) { throw new CoOpsUsageException("Algorithm is not supported by this server"); } HtmlMaterial htmlMaterial = findFile(fileId); Long maxRevision = htmlMaterialController.lastHtmlMaterialRevision(htmlMaterial); if (!maxRevision.equals(revisionNumber)) { throw new CoOpsConflictException(); } ObjectMapper objectMapper = new ObjectMapper(); String checksum = null; if (StringUtils.isNotBlank(patch)) { String data = htmlMaterialController.getRevisionHtml(htmlMaterial, maxRevision); if (data == null) { data = ""; } String patched = algorithm.patch(data, patch); checksum = DigestUtils.md5Hex(patched); } Long patchRevisionNumber = maxRevision + 1; HtmlMaterialRevision htmlMaterialRevision = htmlMaterialController.createRevision(htmlMaterial, sessionId, patchRevisionNumber, new Date(), patch, checksum); if (properties != null) { for (String key : properties.keySet()) { String value = properties.get(key); htmlMaterialController.createRevisionProperty(htmlMaterialRevision, key, value); } } if (extensions != null) { for (String key : extensions.keySet()) { String value; try { value = objectMapper.writeValueAsString(extensions.get(key)); } catch (IOException e) { throw new CoOpsInternalErrorException(e); } htmlMaterialController.createRevisionExtensionProperty(htmlMaterialRevision, key, value); } } patchEvent.fire(new CoOpsPatchEvent(fileId, new Patch(sessionId, patchRevisionNumber, checksum, patch, properties, extensions))); } public Join fileJoin(String fileId, List<String> algorithms, String protocolVersion) throws CoOpsNotFoundException, CoOpsNotImplementedException, CoOpsInternalErrorException, CoOpsForbiddenException, CoOpsUsageException { HtmlMaterial htmlMaterial = findFile(fileId); if (!COOPS_PROTOCOL_VERSION.equals(protocolVersion)) { throw new CoOpsNotImplementedException("Protocol version mismatch. Client is using " + protocolVersion + " and server " + COOPS_PROTOCOL_VERSION); } if (algorithms == null || algorithms.isEmpty()) { throw new CoOpsInternalErrorException("Invalid request"); } CoOpsDiffAlgorithm algorithm = htmlMaterialController.findAlgorithm(algorithms); if (algorithm == null) { throw new CoOpsNotImplementedException("Server and client do not have a commonly supported algorithm."); } Long currentRevision = htmlMaterialController.lastHtmlMaterialRevision(htmlMaterial); String data = htmlMaterialController.getRevisionHtml(htmlMaterial, currentRevision); if (data == null) { data = ""; } Map<String, String> properties = htmlMaterialController.getRevisionProperties(htmlMaterial, currentRevision); // TODO: Extension properties... Map<String, Object> extensions = new HashMap<>(); String sessionId = UUID.randomUUID().toString(); CoOpsSession coOpsSession = coOpsSessionController.createSession(htmlMaterial, sessionController.getLoggedUserEntity(), sessionId, currentRevision, algorithm.getName()); addSessionEventsExtension(htmlMaterial, extensions); addWebSocketExtension(htmlMaterial, extensions, coOpsSession); return new Join(coOpsSession.getSessionId(), coOpsSession.getAlgorithm(), coOpsSession.getJoinRevision(), data, htmlMaterial.getContentType(), properties, extensions); } private void addWebSocketExtension(HtmlMaterial htmlMaterial, Map<String, Object> extensions, CoOpsSession coOpsSession) { String wsUrl = String.format("ws://%s:%s%s/ws/coops/%d/%s", httpRequest.getServerName(), httpPort, httpRequest.getContextPath(), htmlMaterial.getId(), coOpsSession.getSessionId()); String wssUrl = String.format("wss://%s:%s%s/ws/coops/%d/%s", httpRequest.getServerName(), httpsPort, httpRequest.getContextPath(), htmlMaterial.getId(), coOpsSession.getSessionId()); Map<String, Object> webSocketExtension = new HashMap<>(); webSocketExtension.put("ws", wsUrl); webSocketExtension.put("wss", wssUrl); extensions.put("webSocket", webSocketExtension); } private void addSessionEventsExtension(HtmlMaterial htmlMaterial, Map<String, Object> extensions) { List<CoOpsSession> openSessions = coOpsSessionController.listSessionsByHtmlMaterialAndClosed(htmlMaterial, Boolean.FALSE); extensions.put("sessionEvents", coOpsSessionEventsController.createSessionEvents(openSessions, "OPEN")); } private HtmlMaterial findFile(String fileId) throws CoOpsUsageException, CoOpsNotFoundException { if (!StringUtils.isNumeric(fileId)) { throw new CoOpsUsageException("fileId must be a number"); } Long id = NumberUtils.createLong(fileId); if (id == null) { throw new CoOpsUsageException("fileId must be a number"); } HtmlMaterial file = htmlMaterialController.findHtmlMaterialById(id); if (file == null) { throw new CoOpsNotFoundException(); } return file; } }