diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionCoordinatorApi.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionCoordinatorApi.java index b9c04ec29ce7c6efb02640a50273d950200a3a41..c5bf432cdee4bdc58ccd88615fafa01a5fb1fca4 100644 --- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionCoordinatorApi.java +++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionCoordinatorApi.java @@ -10,6 +10,8 @@ public interface ITransactionCoordinatorApi extends ITransactionCoordinator, IRp String SERVICE_URL = "/rmi-" + SERVICE_NAME; + String JSON_SERVICE_URL = SERVICE_URL + ".json"; + String APPLICATION_SERVER_PARTICIPANT_ID = "application-server"; String AFS_SERVER_PARTICIPANT_ID = "afs-server"; diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionParticipantApi.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionParticipantApi.java index e3920987b4177727a9d7a412c612ddee2e55e66e..c4a03aaa26ce6ce73e3eb960be0fe57df6d2f1de 100644 --- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionParticipantApi.java +++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/ITransactionParticipantApi.java @@ -10,4 +10,6 @@ public interface ITransactionParticipantApi extends ITransactionParticipant, IRp String SERVICE_URL = "/rmi-" + SERVICE_NAME; + String JSON_SERVICE_URL = SERVICE_URL + ".json"; + } \ No newline at end of file diff --git a/api-openbis-javascript/src/v3/openbis.js b/api-openbis-javascript/src/v3/openbis.js index 35085226c7282a60e67c30f8a389475bc6216c75..737c46e6822bd2d2109232e0eaea17337c70bc17 100644 --- a/api-openbis-javascript/src/v3/openbis.js +++ b/api-openbis-javascript/src/v3/openbis.js @@ -87,6 +87,32 @@ define([ 'jquery', 'util/Json', 'as/dto/datastore/search/DataStoreSearchCriteria return dfd.promise(); }; + this.checkSessionTokenExists = function(){ + if (!this.sessionToken) + { + throw new Error("Session token hasn't been set"); + } + } + + this.checkInteractiveSessionKeyExists = function(){ + if (!this.interactiveSessionKey) + { + throw new Error("Interactive session token hasn't been set"); + } + } + + this.checkTransactionDoesNotExist = function(){ + if (this.transactionId){ + throw new Error("Operation cannot be executed. Expected no active transactions, but found transaction '" + this.transactionId + "'."); + } + } + + this.checkTransactionExists = function(){ + if (!this.transactionId){ + throw new Error("Operation cannot be executed. No active transaction found."); + } + } + this.log = function(msg) { if (console) { console.log(msg); @@ -484,10 +510,48 @@ define([ 'jquery', 'util/Json', 'as/dto/datastore/search/DataStoreSearchCriteria } + // parseUri 1.2.2 (c) Steven Levithan <stevenlevithan.com> MIT License (see http://blog.stevenlevithan.com/archives/parseuri) + + var parseUri = function(str) { + var options = { + strictMode: false, + key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + q: { + name: "queryKey", + parser: /(?:^|&)([^&=]*)=?([^&]*)/g + }, + parser: { + strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ + } + }; + + var o = options, + m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), + uri = {}, + i = 14; + + while (i--) uri[o.key[i]] = m[i] || ""; + + uri[o.q.name] = {}; + uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { + if ($1) uri[o.q.name][$1] = $2; + }); + + return uri; + } + var facade = function(openbisUrl, afsUrl) { + var transactionCoordinatorUrl = "/openbis/openbis/rmi-transaction-coordinator.json"; + if (!openbisUrl) { openbisUrl = "/openbis/openbis/rmi-application-server-v3.json"; + } else { + var openbisUrlParts = parseUri(openbisUrl) + if (openbisUrlParts.protocol && openbisUrlParts.authority) { + transactionCoordinatorUrl = openbisUrlParts.protocol + "://" + openbisUrlParts.authority + "/openbis/openbis/rmi-transaction-coordinator.json" + } } this._private = new __private(); @@ -542,6 +606,60 @@ define([ 'jquery', 'util/Json', 'as/dto/datastore/search/DataStoreSearchCriteria }); } + this.setInteractiveSessionKey = function(interactiveSessionKey) { + this._private.interactiveSessionKey = interactiveSessionKey; + } + + this.beginTransaction = function() { + var thisFacade = this; + + thisFacade._private.checkTransactionDoesNotExist(); + thisFacade._private.checkSessionTokenExists(); + thisFacade._private.checkInteractiveSessionKeyExists(); + + thisFacade._private.transactionId = crypto.randomUUID(); + + return thisFacade._private.ajaxRequest({ + url : transactionCoordinatorUrl, + data : { + "method" : "beginTransaction", + "params" : [ thisFacade._private.transactionId, thisFacade._private.sessionToken, thisFacade._private.interactiveSessionKey ] + } + }); + } + + this.commitTransaction = function(){ + var thisFacade = this; + + thisFacade._private.checkTransactionExists(); + thisFacade._private.checkSessionTokenExists(); + thisFacade._private.checkInteractiveSessionKeyExists(); + + return thisFacade._private.ajaxRequest({ + url : transactionCoordinatorUrl, + data : { + "method" : "commitTransaction", + "params" : [ thisFacade._private.transactionId, thisFacade._private.sessionToken, thisFacade._private.interactiveSessionKey ] + } + }); + } + + this.rollbackTransaction = function(){ + var thisFacade = this; + + thisFacade._private.checkTransactionExists(); + thisFacade._private.checkSessionTokenExists(); + thisFacade._private.checkInteractiveSessionKeyExists(); + + return thisFacade._private.ajaxRequest({ + url : transactionCoordinatorUrl, + data : { + "method" : "rollbackTransaction", + "params" : [ thisFacade._private.transactionId, thisFacade._private.sessionToken, thisFacade._private.interactiveSessionKey ] + } + }); + } + this.getSessionInformation = function() { var thisFacade = this; return thisFacade._private.ajaxRequest({ diff --git a/api-openbis-typescript/source/java/ch/ethz/sis/openbis/generic/typescript/dto/OpenBISJavaScriptFacade.java b/api-openbis-typescript/source/java/ch/ethz/sis/openbis/generic/typescript/dto/OpenBISJavaScriptFacade.java index 62bfcaa899ef2cc16b914efbada6f6b4f1132072..cb1cb28377795d66cf6b5f7992296c72e2738e45 100644 --- a/api-openbis-typescript/source/java/ch/ethz/sis/openbis/generic/typescript/dto/OpenBISJavaScriptFacade.java +++ b/api-openbis-typescript/source/java/ch/ethz/sis/openbis/generic/typescript/dto/OpenBISJavaScriptFacade.java @@ -299,7 +299,26 @@ public class OpenBISJavaScriptFacade implements IApplicationServerApi @TypeScriptMethod @Override public void logout(final String sessionToken) { + } + + @TypeScriptMethod(sessionToken = false) + public void setInteractiveSessionKey(String interactiveSessionKey){ + } + @TypeScriptMethod(sessionToken = false) + public String beginTransaction() + { + return null; + } + + @TypeScriptMethod(sessionToken = false) + public void commitTransaction() + { + } + + @TypeScriptMethod(sessionToken = false) + public void rollbackTransaction() + { } @TypeScriptMethod diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/TransactionCoordinatorJsonServer.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/TransactionCoordinatorJsonServer.java new file mode 100644 index 0000000000000000000000000000000000000000..7bfa11c29ab877df954e1a5e927d95041a48f39b --- /dev/null +++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/TransactionCoordinatorJsonServer.java @@ -0,0 +1,70 @@ +/* + * Copyright ETH 2009 - 2023 Zürich, Scientific IT Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ch.ethz.sis.openbis.generic.server.asapi.v3; + +import java.io.IOException; + +import javax.annotation.Resource; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpMethod; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ch.ethz.sis.openbis.generic.asapi.v3.ITransactionCoordinatorApi; +import ch.ethz.sis.openbis.generic.server.sharedapi.v3.json.ObjectMapperResource; +import ch.systemsx.cisd.openbis.common.api.server.AbstractApiJsonServiceExporter; + +/** + * @author pkupczyk + */ +@Controller +public class TransactionCoordinatorJsonServer extends AbstractApiJsonServiceExporter +{ + @Resource(name = ObjectMapperResource.NAME) + private ObjectMapper objectMapper; + + @Autowired + private ITransactionCoordinatorApi transactionCoordinatorApi; + + @Override + public void afterPropertiesSet() throws Exception + { + setObjectMapper(objectMapper); + establishService(ITransactionCoordinatorApi.class, transactionCoordinatorApi, ITransactionCoordinatorApi.SERVICE_NAME, + ITransactionCoordinatorApi.JSON_SERVICE_URL); + super.afterPropertiesSet(); + } + + @RequestMapping({ ITransactionCoordinatorApi.JSON_SERVICE_URL, "/openbis" + ITransactionCoordinatorApi.JSON_SERVICE_URL }) + @Override + public void handleRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, + IOException + { + if (request.getMethod().equals(HttpMethod.OPTIONS.name())) + { + return; + } + + super.handleRequest(request, response); + } +} diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/TransactionParticipantJsonServer.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/TransactionParticipantJsonServer.java new file mode 100644 index 0000000000000000000000000000000000000000..2f513e774ceac0a2c11920d36649552dd45a89f4 --- /dev/null +++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/TransactionParticipantJsonServer.java @@ -0,0 +1,70 @@ +/* + * Copyright ETH 2009 - 2023 Zürich, Scientific IT Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ch.ethz.sis.openbis.generic.server.asapi.v3; + +import java.io.IOException; + +import javax.annotation.Resource; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpMethod; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ch.ethz.sis.openbis.generic.asapi.v3.ITransactionParticipantApi; +import ch.ethz.sis.openbis.generic.server.sharedapi.v3.json.ObjectMapperResource; +import ch.systemsx.cisd.openbis.common.api.server.AbstractApiJsonServiceExporter; + +/** + * @author pkupczyk + */ +@Controller +public class TransactionParticipantJsonServer extends AbstractApiJsonServiceExporter +{ + @Resource(name = ObjectMapperResource.NAME) + private ObjectMapper objectMapper; + + @Autowired + private ITransactionParticipantApi transactionParticipantApi; + + @Override + public void afterPropertiesSet() throws Exception + { + setObjectMapper(objectMapper); + establishService(ITransactionParticipantApi.class, transactionParticipantApi, ITransactionParticipantApi.SERVICE_NAME, + ITransactionParticipantApi.JSON_SERVICE_URL); + super.afterPropertiesSet(); + } + + @RequestMapping({ ITransactionParticipantApi.JSON_SERVICE_URL, "/openbis" + ITransactionParticipantApi.JSON_SERVICE_URL }) + @Override + public void handleRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, + IOException + { + if (request.getMethod().equals(HttpMethod.OPTIONS.name())) + { + return; + } + + super.handleRequest(request, response); + } +} diff --git a/test-api-openbis-javascript/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/main.js b/test-api-openbis-javascript/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/main.js index d6b0d9cd0adcd5200b62d21b7dff6cbdb9680cde..674b2ae9f450ca9edd0ed49b7d037408c5452dca 100644 --- a/test-api-openbis-javascript/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/main.js +++ b/test-api-openbis-javascript/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/main.js @@ -20,6 +20,7 @@ define([ "test-compiled/test-import-export", "test-compiled/test-typescript", "test-compiled/test-afs", + "test-compiled/test-transactions", ], function () { var testSuites = arguments return async function () { diff --git a/test-api-openbis-javascript/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-transactions.ts b/test-api-openbis-javascript/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-transactions.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ea017f02d84cc41d6f3ae78cb09d5f9600a1e82 --- /dev/null +++ b/test-api-openbis-javascript/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-transactions.ts @@ -0,0 +1,44 @@ +import jquery from "./types/jquery" +import underscore from "./types/underscore" +import common from "./types/common" +import openbis from "./types/openbis.esm" + +exports.default = new Promise((resolve) => { + require(["jquery", "underscore", "openbis", "test/common", "test/dtos"], function ( + $: jquery.JQueryStatic, + _: underscore.UnderscoreStatic, + openbisRequireJS, + common: common.CommonConstructor, + dtos + ) { + var executeModule = function (moduleName: string, facade: openbis.openbis, dtos: openbis.bundle) { + QUnit.module(moduleName) + + QUnit.test("begin() and rollback()", async function (assert) { + try { + var c = new common(assert, dtos) + c.start() + + await c.login(facade) + + facade.setInteractiveSessionKey("test-interactive-session-key") + + await facade.beginTransaction() + + await facade.rollbackTransaction() + c.finish() + } catch (error) { + c.fail(error) + c.finish() + } + }) + } + + resolve(function () { + var afsServerUrl = "http://localhost:8085/data-store-server" + executeModule("Transactions tests (RequireJS)", new openbisRequireJS(null, afsServerUrl), dtos) + executeModule("Transactions tests (module VAR)", new window.openbis.openbis(null, afsServerUrl), window.openbis) + executeModule("Transactions tests (module ESM)", new window.openbisESM.openbis(null, afsServerUrl), window.openbisESM) + }) + }) +}) diff --git a/test-api-openbis-javascript/servers/common/openBIS-server/etc/service.properties b/test-api-openbis-javascript/servers/common/openBIS-server/etc/service.properties index a2dfb9e879f274a9db6f602b68f0861868358c8c..f69d3687a8d03e5d83cbe6d0878d8c9be0927eef 100644 --- a/test-api-openbis-javascript/servers/common/openBIS-server/etc/service.properties +++ b/test-api-openbis-javascript/servers/common/openBIS-server/etc/service.properties @@ -219,4 +219,41 @@ openbisDB2.database-password= authorization-component-factory=active-authorization script-folder=../../../../server-application-server/source jython-version=2.7 -project-samples-enabled=true \ No newline at end of file +project-samples-enabled=true + +# +# Transactions +# + +# Global switch to enable or disabled the transaction functionality. Default: false. +api.v3.transaction.enabled = true + +# A secret known only to the transaction coordinator that proves its identity to the transaction participants. Default: a secure random key gets generated at startup. +api.v3.transaction.coordinator-key = test-transaction-coordinator-key + +# A secret known only to chosen users of the API that proves they are allowed to use transactions. Default: a secure random key gets generated at startup. +api.v3.transaction.interactive-session-key = test-interactive-session-key + +# A maximum number of simultaneous transactions. Default: 10. +# api.v3.transaction.transaction-count-limit = + +# A timeout in seconds for transactions. After such an inactivity period a transaction will be regarded as abandoned and will be automatically rolled back. Default: 3600. +api.v3.transaction.transaction-timeout = 60 + +# An interval in seconds that controls how often a task that finishes failed and abandoned transactions runs. Default: 600. +api.v3.transaction.finish-transactions-interval = 15 + +# A path to a folder where transaction statuses are stored. Default: transaction-logs. +api.v3.transaction.transaction-log-folder-path = targets/transaction-logs + +# An url of the application server that participates in the two phase commit (e.g. https://localhost:8443) +api.v3.transaction.participant.application-server.url = http://localhost:20000 + +# A timeout in seconds for the application server operations. Default: 3600 +# api.v3.transaction.participant.application-server.timeout = + +# An url of the afs server that participates in the two phase commit (e.g. https://localhost:8085/data-store-server) +api.v3.transaction.participant.afs-server.url = http://localhost:8085/data-store-server + +# A timeout in seconds for the afs server operations. Default: 3600 +# api.v3.transaction.participant.afs-server.timeout = diff --git a/test-api-openbis-javascript/servers/common/openBIS-server/targets/.gitignore b/test-api-openbis-javascript/servers/common/openBIS-server/targets/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..215427be6b007ff7eb797e4c9c8736565ab139f5 --- /dev/null +++ b/test-api-openbis-javascript/servers/common/openBIS-server/targets/.gitignore @@ -0,0 +1 @@ +transaction-logs \ No newline at end of file