Skip to content

Commit a6d7c68

Browse files
committed
Merge remote-tracking branch 'origin/develop' into fb_issue1058
2 parents ec19b7a + 8cb8ba7 commit a6d7c68

31 files changed

Lines changed: 1174 additions & 199 deletions

File tree

announcements/src/org/labkey/announcements/announcementThread.jsp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ if (!announcementModel.getAttachments().isEmpty())
127127
{
128128
ActionURL downloadURL = AnnouncementsController.getDownloadURL(announcementModel, d.getName());
129129
%>
130-
<a href="<%=h(downloadURL)%>"><img alt="" src="<%=getWebappURL(d.getFileIcon())%>">&nbsp;<%=h(d.getName())%></a>&nbsp;<%
130+
<%=d.renderDownloadLink(downloadURL)%>&nbsp;<%
131131
} %>
132132
</div></td>
133133
</tr><%
@@ -210,7 +210,7 @@ if (!announcementModel.getResponses().isEmpty())
210210
{
211211
ActionURL downloadURL = AnnouncementsController.getDownloadURL(r, rd.getName());
212212
%>
213-
<a href="<%=h(downloadURL)%>"><img alt="" src="<%=getWebappURL(rd.getFileIcon())%>">&nbsp;<%=h(rd.getName())%></a>&nbsp;<%
213+
<%=rd.renderDownloadLink(downloadURL)%>&nbsp;<%
214214
}
215215
%>
216216
</div></td>

announcements/src/org/labkey/announcements/announcementWebPartSimple.jsp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ for (AnnouncementModel a : bean.announcementModels)
195195
for (Attachment d : a.getAttachments())
196196
{
197197
ActionURL downloadURL = AnnouncementsController.getDownloadURL(a, d.getName());
198-
%><a href="<%=h(downloadURL)%>"><img src="<%=getWebappURL(d.getFileIcon())%>">&nbsp;<%=h(d.getName())%></a>&nbsp;<%
198+
%><%=d.renderDownloadLink(downloadURL)%>&nbsp;<%
199199
}
200200
%></td></tr><%
201201
}

announcements/src/org/labkey/announcements/announcementWebPartWithExpandos.jsp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ for (AnnouncementModel a : bean.announcementModels)
217217
for (Attachment d : a.getAttachments())
218218
{
219219
ActionURL downloadURL = AnnouncementsController.getDownloadURL(a, d.getName());
220-
%><a href="<%=h(downloadURL)%>"><img src="<%=getWebappURL(d.getFileIcon())%>">&nbsp;<%=h(d.getName())%></a>&nbsp;<%
220+
%><%=d.renderDownloadLink(downloadURL)%>&nbsp;<%
221221
}
222222
%></td></tr><%
223223
}

announcements/src/org/labkey/announcements/update.jsp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ if (settings.hasExpires())
140140
<tbody>
141141
<%
142142
int x = -1;
143-
String id;
144143
for (Attachment att : ann.getAttachments())
145144
{
146145
x++;

api/src/org/labkey/api/assay/AbstractAssayProvider.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
import javax.script.ScriptEngine;
125125
import java.io.File;
126126
import java.io.IOException;
127+
import java.io.InputStream;
127128
import java.net.URI;
128129
import java.net.URL;
129130
import java.sql.ResultSet;
@@ -1274,9 +1275,9 @@ public Pair<ValidationException, Pair<String, String>> setValidationAndAnalysisS
12741275
if (!(engine instanceof ExternalScriptEngine && ((ExternalScriptEngine) engine).isBinary(scriptFile)))
12751276
{
12761277
String scriptText;
1277-
try
1278+
try (InputStream is = scriptFile.openInputStream())
12781279
{
1279-
scriptText = IOUtils.toString(scriptFile.openInputStream(), StringUtilsLabKey.DEFAULT_CHARSET);
1280+
scriptText = IOUtils.toString(is, StringUtilsLabKey.DEFAULT_CHARSET);
12801281
}
12811282
catch (IOException e)
12821283
{

api/src/org/labkey/api/attachments/Attachment.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
import org.labkey.api.security.User;
2222
import org.labkey.api.security.UserManager;
2323
import org.labkey.api.services.ServiceRegistry;
24+
import org.labkey.api.util.DOM;
25+
import org.labkey.api.util.HtmlString;
2426
import org.labkey.api.util.MemTracker;
2527
import org.labkey.api.util.MimeMap;
28+
import org.labkey.api.util.PageFlowUtil;
2629
import org.labkey.api.util.Path;
30+
import org.labkey.api.view.ActionURL;
2731
import org.labkey.api.view.ViewServlet;
2832
import org.labkey.api.webdav.WebdavResolver;
2933

@@ -350,4 +354,29 @@ public void setDocumentSize(int documentSize)
350354
{
351355
_documentSize = documentSize;
352356
}
357+
358+
/**
359+
* Returns an HtmlString rendering a download link: an anchor containing a file type icon and the filename.
360+
* The icon is marked aria-hidden since it is decorative; the link text serves as the accessible name.
361+
*/
362+
public HtmlString renderDownloadLink(ActionURL downloadURL)
363+
{
364+
return renderDownloadLink(downloadURL, getName());
365+
}
366+
367+
/**
368+
* Returns an HtmlString rendering a download link: an anchor containing a file type icon and custom link text.
369+
* Use this overload when the visible link label differs from the filename (e.g. "Study Protocol Document").
370+
* The icon is marked aria-hidden since it is decorative; linkText serves as the accessible name.
371+
*/
372+
public HtmlString renderDownloadLink(ActionURL downloadURL, String linkText)
373+
{
374+
return DOM.createHtmlFragment(
375+
DOM.A(DOM.at(DOM.Attribute.href, downloadURL.toString()),
376+
DOM.IMG(DOM.at(DOM.Attribute.alt, "").at(DOM.Attribute.src, PageFlowUtil.staticResourceUrl(getFileIcon()))),
377+
HtmlString.NBSP,
378+
linkText
379+
)
380+
);
381+
}
353382
}

api/src/org/labkey/api/audit/AbstractAuditTypeProvider.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.labkey.api.data.DbSchema;
3030
import org.labkey.api.data.DbSchemaType;
3131
import org.labkey.api.data.DbScope;
32+
import org.labkey.api.data.MultiChoice;
3233
import org.labkey.api.data.MutableColumnInfo;
3334
import org.labkey.api.data.Table;
3435
import org.labkey.api.data.TableInfo;
@@ -377,6 +378,12 @@ else if (value instanceof Date date)
377378
String formatted = DateUtil.toISO(date);
378379
stringMap.put(entry.getKey(), formatted);
379380
}
381+
else if (value instanceof java.sql.Array arr)
382+
{
383+
// GitHub Issue 1073: Updating a List MVTC field shows array in audit for values with quotes
384+
var arrayVal = MultiChoice.Converter.getInstance().convert(MultiChoice.Array.class, arr);
385+
stringMap.put(entry.getKey(), PageFlowUtil.joinValuesToStringForExport(arrayVal));
386+
}
380387
else
381388
stringMap.put(entry.getKey(), value == null ? null : value.toString());
382389
}

api/src/org/labkey/api/data/DbScope.java

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import jakarta.servlet.ServletContext;
2020
import jakarta.servlet.ServletException;
2121
import org.apache.commons.collections4.IteratorUtils;
22-
import org.apache.commons.lang3.StringUtils;
22+
import org.apache.commons.lang3.Strings;
2323
import org.apache.logging.log4j.Level;
2424
import org.apache.logging.log4j.Logger;
2525
import org.jetbrains.annotations.NotNull;
@@ -110,7 +110,7 @@
110110
/**
111111
* Class that wraps a data source and is shared amongst that data source's DbSchemas. Allows "nested" transactions,
112112
* implemented via a reference-counting style approach. Each (potentially nested) set of code should call
113-
* {@code ensureTransaction()}. This will either start a new transaction, or join an existing one. Once the outermost
113+
* {@code ensureTransaction()}. This will either start a new transaction or join an existing one. Once the outermost
114114
* caller calls commit(), the WHOLE transaction will be committed at once. The most common usage scenario looks
115115
* something like:
116116
* <pre>{@code
@@ -121,8 +121,27 @@
121121
* transaction.commit();
122122
* }
123123
*
124-
* The DbScope.Transaction class implements AutoCloseable, so it will be cleaned up automatically by JDK 7's try {}
125-
* resource handling.
124+
* }
125+
* </pre>
126+
* The DbScope.Transaction class implements AutoCloseable, so it will be cleaned up automatically via try-with-resources.
127+
* <p>
128+
* All return pathways inside the transaction must commit, or the transaction will be considered abandoned to be rolled
129+
* back. There should be no further database work prior to exiting the try block after the commit. Example:
130+
* <pre>{@code
131+
* DbScope scope = dbSchemaInstance.getScope();
132+
* try (DbScope.Transaction transaction = scope.ensureTransaction())
133+
* {
134+
* // Do some work
135+
* if (condition) {
136+
* transaction.commit();
137+
* return true;
138+
* }
139+
* // Do some other work
140+
* boolean result = determineResult();
141+
* transaction.commit();
142+
* return result;
143+
* }
144+
*
126145
* }
127146
* </pre>
128147
*/
@@ -869,7 +888,7 @@ public Transaction beginTransaction(TransactionKind transactionKind, Lock... loc
869888
int stackDepth;
870889
synchronized (_transaction)
871890
{
872-
List<TransactionImpl> transactions = _transaction.computeIfAbsent(getEffectiveThread(), k -> new ArrayList<>());
891+
List<TransactionImpl> transactions = _transaction.computeIfAbsent(getEffectiveThread(), _ -> new ArrayList<>());
873892
transactions.add(result);
874893
stackDepth = transactions.size();
875894
}
@@ -1093,7 +1112,7 @@ public boolean isTransactionActive()
10931112
synchronized (_transaction)
10941113
{
10951114
List<TransactionImpl> transactions = _transaction.get(thread);
1096-
return transactions == null ? null : transactions.get(transactions.size() - 1);
1115+
return transactions == null ? null : transactions.getLast();
10971116
}
10981117
}
10991118

@@ -1242,7 +1261,7 @@ private ConnectionHolder getConnectionHolder()
12421261
// Synchronize just long enough to get a ConnectionHolder into the map
12431262
synchronized (_threadConnections)
12441263
{
1245-
return _threadConnections.computeIfAbsent(thread, t -> new ConnectionHolder());
1264+
return _threadConnections.computeIfAbsent(thread, _ -> new ConnectionHolder());
12461265
}
12471266
}
12481267

@@ -1301,7 +1320,7 @@ private static void ensureDelegation(Connection conn)
13011320
try
13021321
{
13031322
// Test the method to make sure we can access it
1304-
Connection test = (Connection) methodGetInnermostDelegate.invoke(conn);
1323+
var _ = (Connection) methodGetInnermostDelegate.invoke(conn);
13051324
isDelegating = true;
13061325
return;
13071326
}
@@ -2000,7 +2019,7 @@ private static void createDataBase(SqlDialect dialect, LabKeyDataSource ds) thro
20002019

20012020
LOG.info("Attempting to create database \"{}\"", dbName);
20022021

2003-
String defaultUrl = StringUtils.replace(url, dbName, dialect.getDefaultDatabaseName());
2022+
String defaultUrl = Strings.CS.replace(url, dbName, dialect.getDefaultDatabaseName());
20042023
String createSql = "(undefined)";
20052024

20062025
try (Connection conn = getRawConnection(defaultUrl, ds))
@@ -2415,8 +2434,8 @@ public <T extends Runnable> T add(TransactionImpl transaction, T task)
24152434

24162435
public interface Transaction extends AutoCloseable
24172436
{
2418-
/*
2419-
* @return the task that was inserted or the existing object (equal to the runnable passed in) that will be run instead
2437+
/**
2438+
* @return the task that was inserted or the existing object (equal to the runnable passed in) that will be run instead
24202439
*/
24212440
@NotNull
24222441
<T extends Runnable> T addCommitTask(@NotNull T runnable, @NotNull CommitTaskOption firstOption, CommitTaskOption... additionalOptions);
@@ -2796,7 +2815,7 @@ private boolean isOutermostTransaction()
27962815
/** Remove current transaction nesting and unlock any locks */
27972816
private void decrement()
27982817
{
2799-
List<Lock> locks = _locks.remove(_locks.size() - 1);
2818+
List<Lock> locks = _locks.removeLast();
28002819
for (Lock lock : locks)
28012820
{
28022821
// Release all the locks
@@ -2853,7 +2872,7 @@ public void increment(boolean releaseOnFinalCommit, List<Lock> extraLocks)
28532872
if (!_locks.isEmpty() && releaseOnFinalCommit)
28542873
{
28552874
// Add the new locks to the outermost set of locks
2856-
_locks.get(0).addAll(extraLocks);
2875+
_locks.getFirst().addAll(extraLocks);
28572876
// Add an empty list to this layer of the transaction
28582877
_locks.add(new ArrayList<>());
28592878
}
@@ -3328,7 +3347,7 @@ public void testNestedMissingCommit()
33283347
//noinspection EmptyTryBlock
33293348
try (Transaction ignored = getLabKeyScope().ensureTransaction())
33303349
{
3331-
// Intentionally don't call t2.commit();
3350+
// Intentionally, don't call t2.commit();
33323351
}
33333352
try
33343353
{
@@ -3359,14 +3378,8 @@ public void testMultipleTransactionKinds()
33593378
try (Transaction t2 = getLabKeyScope().ensureTransaction())
33603379
{
33613380
assertSame(connection, t2.getConnection());
3362-
try (Transaction t3 = getLabKeyScope().ensureTransaction(new TransactionKind()
3363-
{
3364-
@NotNull
3365-
@Override
3366-
public String getKind()
3367-
{
3368-
return "PIPELINESTATUS"; // We can't really see PipelineStatus here, but just need something non-normal to test
3369-
}
3381+
try (Transaction t3 = getLabKeyScope().ensureTransaction(() -> {
3382+
return "PIPELINESTATUS"; // We can't really see PipelineStatus here, but just need something non-normal to test
33703383
}))
33713384
{
33723385
assertTrue(getLabKeyScope().isTransactionActive());
@@ -3392,7 +3405,7 @@ public void testServerRowLock()
33923405
Lock lockUser = new ServerPrimaryKeyLock(true, CoreSchema.getInstance().getTableInfoUsersData(), user.getUserId());
33933406
Lock lockHome = new ServerPrimaryKeyLock(true, CoreSchema.getInstance().getTableInfoContainers(), ContainerManager.getHomeContainer().getId());
33943407

3395-
attemptToDeadlock(lockUser, lockHome, (x) -> {}, PessimisticLockingFailureException.class);
3408+
attemptToDeadlock(lockUser, lockHome, (_) -> {}, PessimisticLockingFailureException.class);
33963409
}
33973410

33983411
private void attemptToDeadlock(Lock lock1, Lock lock2, @NotNull Consumer<Transaction> transactionModifier, Class<? extends Throwable> expectedException)

api/src/org/labkey/api/mcp/McpService.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@
2929
/// ### MCP Development Guide
3030
/// `McpService` lets you expose functionality over the MCP protocol (only simple http for now). This allows external
3131
/// chat sessions to pull information from LabKey Server. Exposed functionality is also made available to chat sessions
32-
/// hosted by LabKey (see `AbstractAgentAction``).
32+
/// hosted by LabKey (see `AbstractAgentAction`).
3333
///
3434
/// ### Adding a new MCP class
3535
/// 1. Create a new class that implements `McpImpl` (see below) in the appropriate module
36-
/// 2. Register that class in your module `init()` method: `McpService.get().register(new MyMcp())`
36+
/// 2. Register that class in your module's `startup()` method: `McpService.get().register(new MyMcp())`
3737
/// 3. Add tools and resources
3838
///
3939
/// ### Adding a new MCP tool
@@ -44,13 +44,13 @@
4444
/// permission annotation is required, otherwise your tool will not be registered.**
4545
/// 4. Add `ToolContext` as the first parameter to the method
4646
/// 5. Add additional required or optional parameters to the method signature, as needed. Note that "required" is the
47-
/// default. Again here, the parameter descriptions are very important. Provide examples.
47+
/// default. Again here, the parameter descriptions are very important. Provide examples of parameter values.
4848
/// 6. Use the helper method `getContext(ToolContext)` to retrieve the current `Container` and `User`
4949
/// 7. Use the helper method `getUser(ToolContext)` in the rare cases where you need just a `User`
5050
/// 8. Perform additional permissions checking (beyond what the annotations offer), where appropriate
5151
/// 9. Filter all results to the current container, of course
5252
/// 10. For any error conditions, throw exceptions with detailed information. These will get translated into appropriate
53-
/// failure responses and the LLM client will attempt to correct the problem.
53+
/// failure responses and the LLM client will attempt to correct any problems (hopefully).
5454
/// 11. For success cases, return a String with a message or JSON content, for example, `JSONObject.toString()`. Spring
5555
/// has some limited ability to convert other objects into JSON strings, but we haven't experimented with that. See
5656
/// `DefaultToolCallResultConverter` and the ability to provide a custom result converter via the `@Tool` annotation.
@@ -126,6 +126,7 @@ static void setInstance(McpService service)
126126

127127
boolean isReady();
128128

129+
// Register MCPs in Module.startup()
129130
default void register(McpImpl mcp)
130131
{
131132
try

api/src/org/labkey/api/module/ModuleLoader.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -580,19 +580,6 @@ private void doInit(Execution execution) throws ServletException
580580
throw new IllegalStateException("Core module was not first or could not find the Core module. Ensure that Tomcat user can create directories under the <LABKEY_HOME>/modules directory.");
581581
setProjectRoot(coreModule);
582582

583-
for (Module module : modules)
584-
{
585-
module.registerFilters(_servletContext);
586-
}
587-
for (Module module : modules)
588-
{
589-
module.registerServlets(_servletContext);
590-
}
591-
for (Module module : modules)
592-
{
593-
module.registerFinalServlets(_servletContext);
594-
}
595-
596583
// Do this after we've checked to see if we can find the core module. See issue 22797.
597584
verifyProductionModeMatchesBuild();
598585

@@ -790,12 +777,34 @@ public void addStaticWarnings(@NotNull Warnings warnings, boolean showAllWarning
790777
if (!modulesRequiringUpgrade.isEmpty() || !additionalSchemasRequiringUpgrade.isEmpty())
791778
setUpgradeState(UpgradeState.UpgradeRequired);
792779

793-
// Don't accept any requests if we're bootstrapping empty schemas or migrating from SQL Server
780+
// Don't accept any requests if we're bootstrapping empty schemas or doing a database migration
794781
if (!shouldInsertData())
795782
execution = Execution.Synchronous;
796783

797784
startNonCoreUpgradeAndStartup(execution, lockFile);
798785

786+
// Register filters and servlets at the last minute, just before Tomcat starts. At this point, the list of
787+
// modules is final. We've had one case where the CSP filter was getting initialized before the core module
788+
// was initialized, GitHub Issue 1008. We have no idea how that happened, but registering late won't hurt.
789+
790+
_log.info("Registering filters");
791+
792+
for (Module module : _modules)
793+
{
794+
module.registerFilters(_servletContext);
795+
}
796+
797+
_log.info("Registering servlets");
798+
799+
for (Module module : _modules)
800+
{
801+
module.registerServlets(_servletContext);
802+
}
803+
for (Module module : _modules)
804+
{
805+
module.registerFinalServlets(_servletContext);
806+
}
807+
799808
_log.info("LabKey Server startup is complete; {}", execution.getLogMessage());
800809
}
801810

0 commit comments

Comments
 (0)