Skip to content

Commit 8b52019

Browse files
committed
GH-5565 allow users to select transaction isolation level when uploading data in the workbench
1 parent 735d719 commit 8b52019

File tree

4 files changed

+452
-13
lines changed

4 files changed

+452
-13
lines changed

tools/workbench/src/main/java/org/eclipse/rdf4j/workbench/commands/AddServlet.java

Lines changed: 168 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,26 @@
88
*
99
* SPDX-License-Identifier: BSD-3-Clause
1010
*******************************************************************************/
11+
// Some portions generated by Codex
1112
package org.eclipse.rdf4j.workbench.commands;
1213

1314
import java.io.IOException;
1415
import java.io.InputStream;
1516
import java.net.MalformedURLException;
1617
import java.net.URL;
18+
import java.util.ArrayList;
19+
import java.util.LinkedHashSet;
1720
import java.util.List;
21+
import java.util.Locale;
22+
import java.util.Set;
1823

1924
import javax.servlet.http.HttpServletResponse;
2025

26+
import org.eclipse.rdf4j.common.transaction.IsolationLevel;
27+
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
28+
import org.eclipse.rdf4j.common.transaction.TransactionSetting;
29+
import org.eclipse.rdf4j.common.transaction.TransactionSettingRegistry;
30+
import org.eclipse.rdf4j.http.protocol.Protocol;
2131
import org.eclipse.rdf4j.model.Resource;
2232
import org.eclipse.rdf4j.query.QueryResultHandlerException;
2333
import org.eclipse.rdf4j.repository.RepositoryConnection;
@@ -35,6 +45,9 @@
3545
public class AddServlet extends TransformationServlet {
3646

3747
private static final String URL = "url";
48+
private static final String ISOLATION_LEVEL_OPTION = "isolation-level-option";
49+
private static final String ISOLATION_LEVEL_OPTION_LABEL = "isolation-level-option-label";
50+
private static final String ISOLATION_LEVEL_PARAM = Protocol.TRANSACTION_SETTINGS_PREFIX + IsolationLevel.NAME;
3851

3952
private final Logger logger = LoggerFactory.getLogger(AddServlet.class);
4053

@@ -44,37 +57,41 @@ protected void doPost(WorkbenchRequest req, HttpServletResponse resp, String xsl
4457
try {
4558
String baseURI = req.getParameter("baseURI");
4659
String contentType = req.getParameter("Content-Type");
60+
TransactionSetting isolationLevel = parseIsolationLevel(req);
4761
if (req.isParameterPresent(CONTEXT)) {
4862
Resource context = req.getResource(CONTEXT);
4963
if (req.isParameterPresent(URL)) {
50-
add(req.getUrl(URL), baseURI, contentType, context);
64+
add(req.getUrl(URL), baseURI, contentType, isolationLevel, context);
5165
} else {
52-
add(req.getContentParameter(), baseURI, contentType, req.getContentFileName(), context);
66+
add(req.getContentParameter(), baseURI, contentType, req.getContentFileName(), isolationLevel,
67+
context);
5368
}
5469
} else {
5570
if (req.isParameterPresent(URL)) {
56-
add(req.getUrl(URL), baseURI, contentType);
71+
add(req.getUrl(URL), baseURI, contentType, isolationLevel);
5772
} else {
58-
add(req.getContentParameter(), baseURI, contentType, req.getContentFileName());
73+
add(req.getContentParameter(), baseURI, contentType, req.getContentFileName(), isolationLevel);
5974
}
6075
}
6176
resp.sendRedirect("summary");
6277
} catch (BadRequestException exc) {
6378
logger.warn(exc.toString(), exc);
6479
TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
6580
builder.transform(xslPath, "add.xsl");
66-
builder.start("error-message", "baseURI", CONTEXT, "Content-Type");
81+
builder.start("error-message", "baseURI", CONTEXT, "Content-Type", ISOLATION_LEVEL_PARAM);
6782
builder.link(List.of(INFO));
6883
String baseURI = req.getParameter("baseURI");
6984
String context = req.getParameter(CONTEXT);
7085
String contentType = req.getParameter("Content-Type");
71-
builder.result(exc.getMessage(), baseURI, context, contentType);
86+
String isolationLevel = req.getParameter(ISOLATION_LEVEL_PARAM);
87+
builder.result(exc.getMessage(), baseURI, context, contentType, isolationLevel);
7288
builder.end();
7389
}
7490
}
7591

7692
private void add(InputStream stream, String baseURI, String contentType, String contentFileName,
77-
Resource... context) throws BadRequestException, RepositoryException, IOException {
93+
TransactionSetting isolationLevel, Resource... context)
94+
throws BadRequestException, RepositoryException, IOException {
7895
if (contentType == null) {
7996
throw new BadRequestException("No Content-Type provided");
8097
}
@@ -90,13 +107,19 @@ private void add(InputStream stream, String baseURI, String contentType, String
90107
}
91108

92109
try (RepositoryConnection con = repository.getConnection()) {
93-
con.add(stream, baseURI, format, context);
94-
} catch (RDFParseException | IllegalArgumentException exc) {
95-
throw new BadRequestException(exc.getMessage(), exc);
110+
boolean transactionStarted = beginIfRequested(con, isolationLevel);
111+
try {
112+
con.add(stream, baseURI, format, context);
113+
commitIfNeeded(con, transactionStarted);
114+
} catch (RDFParseException | IllegalArgumentException exc) {
115+
rollbackIfNeeded(con, transactionStarted);
116+
throw new BadRequestException(exc.getMessage(), exc);
117+
}
96118
}
97119
}
98120

99-
private void add(URL url, String baseURI, String contentType, Resource... context)
121+
private void add(URL url, String baseURI, String contentType, TransactionSetting isolationLevel,
122+
Resource... context)
100123
throws BadRequestException, RepositoryException, IOException {
101124
if (contentType == null) {
102125
throw new BadRequestException("No Content-Type provided");
@@ -114,7 +137,14 @@ private void add(URL url, String baseURI, String contentType, Resource... contex
114137

115138
try {
116139
try (RepositoryConnection con = repository.getConnection()) {
117-
con.add(url, baseURI, format, context);
140+
boolean transactionStarted = beginIfRequested(con, isolationLevel);
141+
try {
142+
con.add(url, baseURI, format, context);
143+
commitIfNeeded(con, transactionStarted);
144+
} catch (RDFParseException | MalformedURLException | IllegalArgumentException exc) {
145+
rollbackIfNeeded(con, transactionStarted);
146+
throw exc;
147+
}
118148
}
119149
} catch (RDFParseException | MalformedURLException | IllegalArgumentException exc) {
120150
throw new BadRequestException(exc.getMessage(), exc);
@@ -124,11 +154,136 @@ private void add(URL url, String baseURI, String contentType, Resource... contex
124154
@Override
125155
public void service(TupleResultBuilder builder, String xslPath)
126156
throws RepositoryException, QueryResultHandlerException {
127-
// TupleResultBuilder builder = getTupleResultBuilder(req, resp);
128157
builder.transform(xslPath, "add.xsl");
129158
builder.start();
130159
builder.link(List.of(INFO));
131160
builder.end();
132161
}
133162

163+
@Override
164+
protected void service(WorkbenchRequest req, HttpServletResponse resp, String xslPath) throws Exception {
165+
TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
166+
builder.transform(xslPath, "add.xsl");
167+
builder.start(ISOLATION_LEVEL_OPTION, ISOLATION_LEVEL_OPTION_LABEL, ISOLATION_LEVEL_PARAM);
168+
builder.link(List.of(INFO));
169+
String selected = req.getParameter(ISOLATION_LEVEL_PARAM);
170+
if (selected != null && !selected.isBlank()) {
171+
builder.result(selected, isolationLevelLabel(selected), selected);
172+
}
173+
for (String option : determineIsolationLevels()) {
174+
if (!option.equals(selected)) {
175+
builder.result(option, isolationLevelLabel(option), null);
176+
}
177+
}
178+
builder.end();
179+
}
180+
181+
private TransactionSetting parseIsolationLevel(WorkbenchRequest req) throws BadRequestException {
182+
String requested = req.getParameter(ISOLATION_LEVEL_PARAM);
183+
if (requested != null && !requested.isBlank()) {
184+
return TransactionSettingRegistry.getInstance()
185+
.get(IsolationLevel.NAME)
186+
.flatMap(factory -> factory.getTransactionSetting(requested))
187+
.orElseThrow(() -> new BadRequestException("Unknown isolation level: " + requested));
188+
}
189+
return null;
190+
}
191+
192+
private boolean beginIfRequested(RepositoryConnection connection, TransactionSetting isolationLevel)
193+
throws RepositoryException {
194+
if (isolationLevel != null) {
195+
connection.begin(isolationLevel);
196+
return true;
197+
}
198+
return false;
199+
}
200+
201+
private void commitIfNeeded(RepositoryConnection connection, boolean transactionStarted)
202+
throws RepositoryException {
203+
if (transactionStarted && connection.isActive()) {
204+
connection.commit();
205+
}
206+
}
207+
208+
private void rollbackIfNeeded(RepositoryConnection connection, boolean transactionStarted) {
209+
if (transactionStarted) {
210+
try {
211+
if (connection.isActive()) {
212+
connection.rollback();
213+
}
214+
} catch (RepositoryException e) {
215+
logger.warn("Failed to roll back add transaction", e);
216+
}
217+
}
218+
}
219+
220+
List<String> determineIsolationLevels() {
221+
if (repository == null) {
222+
return List.of();
223+
}
224+
Set<String> supported = new LinkedHashSet<>();
225+
try (RepositoryConnection connection = repository.getConnection()) {
226+
IsolationLevel original = connection.getIsolationLevel();
227+
for (IsolationLevels level : IsolationLevels.values()) {
228+
if (supportsIsolationLevel(connection, level)) {
229+
supported.add(isolationLevelName(level));
230+
}
231+
}
232+
if (original != null) {
233+
String originalName = isolationLevelName(original);
234+
if (!supported.contains(originalName)) {
235+
supported.add(originalName);
236+
}
237+
}
238+
} catch (RepositoryException e) {
239+
logger.warn("Unable to determine supported isolation levels", e);
240+
}
241+
return new ArrayList<>(supported);
242+
}
243+
244+
private boolean supportsIsolationLevel(RepositoryConnection connection, IsolationLevel level) {
245+
try {
246+
connection.begin(level);
247+
connection.rollback();
248+
return true;
249+
} catch (RepositoryException e) {
250+
try {
251+
if (connection.isActive()) {
252+
connection.rollback();
253+
}
254+
} catch (RepositoryException ex) {
255+
logger.debug("Unable to rollback after failed isolation test", ex);
256+
}
257+
logger.debug("Isolation level {} is not supported by {}", level, repository.getClass().getSimpleName(), e);
258+
return false;
259+
}
260+
}
261+
262+
private String isolationLevelName(IsolationLevel level) {
263+
String value = level.getValue();
264+
if (value != null && !value.isBlank()) {
265+
return value;
266+
}
267+
return (level instanceof Enum<?>) ? ((Enum<?>) level).name() : level.toString();
268+
}
269+
270+
private String isolationLevelLabel(String value) {
271+
String normalized = value.replace('.', '_');
272+
String[] parts = normalized.toLowerCase(Locale.ROOT).split("_");
273+
StringBuilder label = new StringBuilder();
274+
for (String part : parts) {
275+
if (part.isEmpty()) {
276+
continue;
277+
}
278+
if (label.length() > 0) {
279+
label.append(' ');
280+
}
281+
label.append(Character.toUpperCase(part.charAt(0)));
282+
if (part.length() > 1) {
283+
label.append(part.substring(1));
284+
}
285+
}
286+
return label.length() == 0 ? value : label.toString();
287+
}
288+
134289
}

tools/workbench/src/main/webapp/locale/messages.xsl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@
120120
<variable name="clear-context.label">Clear Context(s)</variable>
121121
<variable name="context.label">Context</variable>
122122
<variable name="data-format.label">Data format</variable>
123+
<variable name="isolation-level.label">Isolation level</variable>
124+
<variable name="isolation-level.default">Repository default</variable>
125+
<variable name="isolation-level.desc">
126+
Choose the transaction isolation level used when uploading data. Leave this at the default to let the repository decide.
127+
</variable>
123128
<variable name="include-inferred.label">
124129
Include inferred statements
125130
</variable>

tools/workbench/src/main/webapp/transformations/add.xsl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
</xsl:variable>
1111

1212
<xsl:include href="template.xsl" />
13+
<xsl:variable name="selectedIsolation"
14+
select="//sparql:binding[@name='transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel']/sparql:literal" />
1315

1416
<xsl:template match="sparql:sparql">
1517
<xsl:if
@@ -72,6 +74,44 @@
7274
</td>
7375
<td></td>
7476
</tr>
77+
<tr>
78+
<th>
79+
<xsl:value-of select="$isolation-level.label" />
80+
</th>
81+
<td>
82+
<select id="transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel" name="transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel">
83+
<option value="">
84+
<xsl:if test="not($selectedIsolation)">
85+
<xsl:attribute name="selected">selected</xsl:attribute>
86+
</xsl:if>
87+
<xsl:value-of select="$isolation-level.default" />
88+
</option>
89+
<xsl:for-each select="//sparql:result[sparql:binding[@name='isolation-level-option']]">
90+
<xsl:variable name="optionValue"
91+
select="sparql:binding[@name='isolation-level-option']/sparql:literal/text()" />
92+
<xsl:variable name="optionLabel"
93+
select="sparql:binding[@name='isolation-level-option-label']/sparql:literal/text()" />
94+
<option value="{$optionValue}">
95+
<xsl:if test="$selectedIsolation=$optionValue">
96+
<xsl:attribute name="selected">selected</xsl:attribute>
97+
</xsl:if>
98+
<xsl:choose>
99+
<xsl:when test="string-length(normalize-space($optionLabel)) &gt; 0">
100+
<xsl:value-of select="$optionLabel" />
101+
</xsl:when>
102+
<xsl:otherwise>
103+
<xsl:value-of select="$optionValue" />
104+
</xsl:otherwise>
105+
</xsl:choose>
106+
</option>
107+
</xsl:for-each>
108+
</select>
109+
<div class="hint">
110+
<xsl:value-of select="$isolation-level.desc" />
111+
</div>
112+
</td>
113+
<td></td>
114+
</tr>
75115

76116
<tr>
77117
<td></td>

0 commit comments

Comments
 (0)