diff --git a/api/src/org/labkey/api/admin/AdminUrls.java b/api/src/org/labkey/api/admin/AdminUrls.java index f48b1998053..47812f10672 100644 --- a/api/src/org/labkey/api/admin/AdminUrls.java +++ b/api/src/org/labkey/api/admin/AdminUrls.java @@ -45,18 +45,21 @@ public interface AdminUrls extends UrlProvider ActionURL getNotificationsURL(Container c); ActionURL getExportFolderURL(Container c); ActionURL getImportFolderURL(Container c); - ActionURL getFolderSettingsURL(Container c); ActionURL getFileRootsURL(Container c); + /** + * Get the appropriate settings page for the passed in container (root, project, or folder) + */ + ActionURL getLookAndFeelSettingsURL(Container c); ActionURL getSiteLookAndFeelSettingsURL(); + ActionURL getProjectSettingsURL(Container c); + ActionURL getProjectSettingsMenuURL(Container c); + ActionURL getProjectSettingsFileURL(Container c); + ActionURL getFolderSettingsURL(Container c); ActionURL getCreateProjectURL(@Nullable ActionURL returnURL); ActionURL getCreateFolderURL(Container c, @Nullable ActionURL returnURL); ActionURL getMemTrackerURL(); - ActionURL getLookAndFeelSettingsURL(); - ActionURL getProjectSettingsURL(Container c); - ActionURL getProjectSettingsMenuURL(Container c); - ActionURL getProjectSettingsFileURL(Container c); ActionURL getCustomizeEmailURL(Container c, Class selectedTemplate, URLHelper returnURL); ActionURL getFilesSiteSettingsURL(boolean upgrade); ActionURL getSessionLoggingURL(); diff --git a/api/src/org/labkey/api/data/PropertyManager.java b/api/src/org/labkey/api/data/PropertyManager.java index 6409b5addc2..b7cc020f8aa 100644 --- a/api/src/org/labkey/api/data/PropertyManager.java +++ b/api/src/org/labkey/api/data/PropertyManager.java @@ -44,12 +44,6 @@ import java.util.Set; import java.util.concurrent.locks.ReentrantLock; - -/** - * User: mbellew - * Date: Apr 26, 2004 - * Time: 9:57:10 AM - */ public class PropertyManager { public static final User SHARED_USER = User.guest; // Shared properties are saved with the guest user @@ -66,19 +60,16 @@ private PropertyManager() { } - public static PropertyStore getNormalStore() { return STORE; } - public static PropertyStore getEncryptedStore() { return ENCRYPTED_STORE; } - /** * For global system properties that are attached to the root container * Returns an empty map if property set hasn't been created @@ -156,10 +147,10 @@ public static void registerEncryptionMigrationHandler() /** * This is designed to coalesce up the container hierarchy, returning the first non-null value - * If a userId is provided, it first traverses containers using that user. If no value is found, - * it then default to all users (ie. User.guest), then retry all containers + * If a userId is provided, it first traverses containers using that user. If no value is found, + * it then defaults to all users (i.e. User.guest), then retry all containers * - * NOTE: this does not test permissions. Callers should ensure the requested user has the appropriate + * NOTE: this does not test permissions. Callers should ensure the requested user has the appropriate * permissions to read these properties */ public static String getCoalescedProperty(User user, Container c, String category, String name, boolean includeNullUser) @@ -233,7 +224,7 @@ public static String getProperty(User user, Container container, String category /** * Get a list of categories optionally filtered by user, container, and category prefix. - * eturns entries from unencrypted store only. + * Returns entries from unencrypted store only. * * @param user User of the property. If null properties for all users (NOT JUST THE NULL USER) will be found. * @param container Container to search for. If null properties of all containers will be found @@ -780,7 +771,6 @@ public Collection getCategories() }); } - private void testPropertyStore(PropertyStore store, PropertyStoreTest test) { TestContext context = TestContext.get(); @@ -811,7 +801,6 @@ private void testPropertyStore(PropertyStore store, PropertyStoreTest test) } } - private void testProperties(PropertyStore store, User user, Container test, String category) { PropertyMap m = store.getWritableProperties(user, test, category, true); diff --git a/api/src/org/labkey/api/exp/OntologyManager.java b/api/src/org/labkey/api/exp/OntologyManager.java index 19f50aaa321..f4d09948df8 100644 --- a/api/src/org/labkey/api/exp/OntologyManager.java +++ b/api/src/org/labkey/api/exp/OntologyManager.java @@ -32,7 +32,6 @@ import org.labkey.api.data.*; import org.labkey.api.data.DbScope.Transaction; import org.labkey.api.data.dialect.SqlDialect; -import org.labkey.api.dataiterator.AbstractMapDataIterator; import org.labkey.api.dataiterator.DataIterator; import org.labkey.api.dataiterator.DataIteratorContext; import org.labkey.api.dataiterator.DataIteratorUtil; diff --git a/api/src/org/labkey/api/exp/PropertyType.java b/api/src/org/labkey/api/exp/PropertyType.java index aede102d3f1..f97debc307e 100644 --- a/api/src/org/labkey/api/exp/PropertyType.java +++ b/api/src/org/labkey/api/exp/PropertyType.java @@ -45,9 +45,6 @@ import java.util.TimeZone; /** - * User: migra - * Date: Oct 25, 2005 - * * TODO: Add more types? Entity, Lsid, User, ... */ public enum PropertyType diff --git a/api/src/org/labkey/api/query/QueryDefinition.java b/api/src/org/labkey/api/query/QueryDefinition.java index a0a26b44ec1..d849a506b7a 100644 --- a/api/src/org/labkey/api/query/QueryDefinition.java +++ b/api/src/org/labkey/api/query/QueryDefinition.java @@ -16,6 +16,7 @@ package org.labkey.api.query; +import jakarta.servlet.http.HttpServletRequest; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.ColumnInfo; @@ -29,8 +30,8 @@ import org.labkey.api.util.StringExpression; import org.labkey.api.view.ActionURL; import org.labkey.data.xml.TableType; +import org.labkey.data.xml.TablesDocument; -import jakarta.servlet.http.HttpServletRequest; import java.sql.SQLException; import java.util.Collection; import java.util.List; @@ -115,6 +116,7 @@ public interface QueryDefinition String getSql(); String getMetadataXml(); + TablesDocument getMetadataTablesDocument(); String getDescription(); String getModuleName(); diff --git a/api/src/org/labkey/api/query/QueryService.java b/api/src/org/labkey/api/query/QueryService.java index 2014ad09d1f..6cd213afdbf 100644 --- a/api/src/org/labkey/api/query/QueryService.java +++ b/api/src/org/labkey/api/query/QueryService.java @@ -104,6 +104,11 @@ static void setInstance(QueryService impl) QueryDefinition getQueryDef(User user, Container container, String schema, String name); + /** + * Returns the parsed metadata for the specified query.QueryDef row. Could be a query or a built-in table override. + */ + @Nullable TableType getQueryDefMetadata(Container container, int rowId); + QueryDefinition createQueryDef(User user, Container container, SchemaKey schema, String name); QueryDefinition createQueryDef(User user, Container container, UserSchema schema, String name); QueryDefinition createQueryDefForTable(UserSchema schema, String tableName); diff --git a/api/src/org/labkey/api/query/QueryUrls.java b/api/src/org/labkey/api/query/QueryUrls.java index 8168f21b818..15b6ec4d4e9 100644 --- a/api/src/org/labkey/api/query/QueryUrls.java +++ b/api/src/org/labkey/api/query/QueryUrls.java @@ -19,11 +19,6 @@ import org.labkey.api.action.UrlProvider; import org.labkey.api.data.Container; import org.labkey.api.view.ActionURL; -/* - * User: Karl Lum - * Date: Jul 15, 2008 - * Time: 2:53:11 PM - */ public interface QueryUrls extends UrlProvider { @@ -42,4 +37,5 @@ public interface QueryUrls extends UrlProvider * @param query Query name */ ActionURL urlExecuteQuery(Container c, String schema, String query); + ActionURL urlMetadataQuery(Container c, String schemaName, String queryName); } diff --git a/api/src/org/labkey/api/reports/report/ModuleReportResource.java b/api/src/org/labkey/api/reports/report/ModuleReportResource.java index f3b9a2dde12..ad0fadd94f4 100644 --- a/api/src/org/labkey/api/reports/report/ModuleReportResource.java +++ b/api/src/org/labkey/api/reports/report/ModuleReportResource.java @@ -15,28 +15,22 @@ */ package org.labkey.api.reports.report; -import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.xmlbeans.XmlException; -import org.labkey.api.data.ContainerManager; import org.labkey.api.resource.Resource; import org.labkey.api.util.DateUtil; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.Path; +import org.labkey.api.util.logging.LogHelper; import org.labkey.query.xml.ReportDescriptorType; import java.io.IOException; import java.io.InputStream; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.Date; -/** - * User: dax - * Date: 3/14/14 - */ public class ModuleReportResource { + private static final Logger LOG = LogHelper.getLogger(ModuleReportResource.class, "Module report loading and parsing problems"); protected final Resource _sourceFile; protected final ReportDescriptor _reportDescriptor; protected final Resource _metaDataFile; @@ -61,7 +55,7 @@ public void loadScript() } catch(IOException e) { - LogManager.getLogger(ModuleReportResource.class).warn("Unable to load report script from source file " + _sourceFile, e); + LOG.warn("Unable to load report script from source file {}", _sourceFile, e); } } @@ -78,25 +72,24 @@ public ReportDescriptorType loadMetaData() if (createdDateStr != null) { - DateFormat format = new SimpleDateFormat(DateUtil.getDateFormatString(ContainerManager.getRoot())); try { - Date createdDate = format.parse(createdDateStr); + Date createdDate = new Date(DateUtil.parseISODateTime(createdDateStr)); _reportDescriptor.setCreated(createdDate); } - catch (ParseException e) + catch (Exception e) { - LogManager.getLogger(ModuleReportResource.class).warn("Unable to parse moduleReportCreatedDate \"" + createdDateStr + "\" from file " + _sourceFile, e); + LOG.warn("Unable to parse moduleReportCreatedDate \"{}\" from file {}", createdDateStr, _sourceFile, e); } } } catch(IOException e) { - LogManager.getLogger(ModuleReportResource.class).warn("Unable to load report metadata from file " + _metaDataFile, e); + LOG.warn("Unable to load report metadata from file {}", _metaDataFile, e); } catch(XmlException e) { - LogManager.getLogger(ModuleReportResource.class).warn("Unable to load query report metadata from file " + _sourceFile, e); + LOG.warn("Unable to load query report metadata from file {}", _sourceFile, e); } } return d; diff --git a/api/src/org/labkey/api/settings/AbstractSettingsGroup.java b/api/src/org/labkey/api/settings/AbstractSettingsGroup.java index c81c6097e7b..7364fdf687e 100644 --- a/api/src/org/labkey/api/settings/AbstractSettingsGroup.java +++ b/api/src/org/labkey/api/settings/AbstractSettingsGroup.java @@ -26,8 +26,6 @@ /** * Base class for configuration properties persisted through the "prop" schema. - * User: adam - * Date: Aug 2, 2008 */ public abstract class AbstractSettingsGroup { diff --git a/api/src/org/labkey/api/settings/FolderSettingsCache.java b/api/src/org/labkey/api/settings/FolderSettingsCache.java index 54c6001b052..03a425aa8aa 100644 --- a/api/src/org/labkey/api/settings/FolderSettingsCache.java +++ b/api/src/org/labkey/api/settings/FolderSettingsCache.java @@ -27,12 +27,6 @@ import java.util.Collection; import java.util.Collections; -/** - * User: adam - * Date: 12/15/13 - * Time: 10:31 AM - */ - // Folder settings inherit all the way up the folder tree. All the property sets involved should be cached, but the walk // up the tree is a potentially expensive operation just to format a date or number. So, we cache the set of resolved // properties on a per-container basis and clear the entire cache on every change of look and feel settings. diff --git a/api/src/org/labkey/api/settings/LookAndFeelFolderProperties.java b/api/src/org/labkey/api/settings/LookAndFeelFolderProperties.java index 03a6fa07cce..853c5312ed9 100644 --- a/api/src/org/labkey/api/settings/LookAndFeelFolderProperties.java +++ b/api/src/org/labkey/api/settings/LookAndFeelFolderProperties.java @@ -19,7 +19,10 @@ import org.labkey.api.data.Container; import org.labkey.api.util.DateUtil; -import static org.labkey.api.settings.LookAndFeelProperties.Properties.*; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.extraDateParsingPattern; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.extraDateTimeParsingPattern; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.extraTimeParsingPattern; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.restrictedColumnsEnabled; /** * Container-specific configuration settings, primarily related to look-and-feel or parsing options @@ -27,7 +30,7 @@ */ public class LookAndFeelFolderProperties extends AbstractWriteableSettingsGroup { - static final String LOOK_AND_FEEL_SET_NAME = "LookAndFeel"; + public static final String LOOK_AND_FEEL_SET_NAME = "LookAndFeel"; // These are the legacy property names for the format patterns protected static final String defaultDateFormatString = "defaultDateFormatString"; diff --git a/api/src/org/labkey/api/settings/WriteableFolderLookAndFeelProperties.java b/api/src/org/labkey/api/settings/WriteableFolderLookAndFeelProperties.java index 6ea7136cda6..9338e2c465a 100644 --- a/api/src/org/labkey/api/settings/WriteableFolderLookAndFeelProperties.java +++ b/api/src/org/labkey/api/settings/WriteableFolderLookAndFeelProperties.java @@ -20,20 +20,15 @@ import java.text.DecimalFormat; -import static org.labkey.api.settings.LookAndFeelFolderProperties.defaultTimeFormatString; -import static org.labkey.api.settings.LookAndFeelProperties.LOOK_AND_FEEL_SET_NAME; - import static org.labkey.api.settings.LookAndFeelFolderProperties.defaultDateFormatString; import static org.labkey.api.settings.LookAndFeelFolderProperties.defaultDateTimeFormatString; import static org.labkey.api.settings.LookAndFeelFolderProperties.defaultNumberFormatString; - -import static org.labkey.api.settings.LookAndFeelProperties.Properties.*; - -/** - * User: adam - * Date: Aug 1, 2008 - * Time: 9:35:40 PM - */ +import static org.labkey.api.settings.LookAndFeelFolderProperties.defaultTimeFormatString; +import static org.labkey.api.settings.LookAndFeelProperties.LOOK_AND_FEEL_SET_NAME; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.extraDateParsingPattern; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.extraDateTimeParsingPattern; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.extraTimeParsingPattern; +import static org.labkey.api.settings.LookAndFeelProperties.Properties.restrictedColumnsEnabled; // Handles only the properties that can be set at the folder level public class WriteableFolderLookAndFeelProperties extends AbstractWriteableSettingsGroup diff --git a/api/src/org/labkey/api/settings/WriteableLookAndFeelProperties.java b/api/src/org/labkey/api/settings/WriteableLookAndFeelProperties.java index fcda487cef7..a387f3ca329 100644 --- a/api/src/org/labkey/api/settings/WriteableLookAndFeelProperties.java +++ b/api/src/org/labkey/api/settings/WriteableLookAndFeelProperties.java @@ -34,12 +34,6 @@ import static org.labkey.api.settings.LookAndFeelProperties.Properties.*; -/** - * User: adam - * Date: Aug 1, 2008 - * Time: 9:35:40 PM - */ - // Handles all the properties that can be set at the project or site level public class WriteableLookAndFeelProperties extends WriteableFolderLookAndFeelProperties { diff --git a/api/src/org/labkey/api/util/DateUtil.java b/api/src/org/labkey/api/util/DateUtil.java index 0b5c64511cb..6c5b4fc5f58 100644 --- a/api/src/org/labkey/api/util/DateUtil.java +++ b/api/src/org/labkey/api/util/DateUtil.java @@ -53,6 +53,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NavigableMap; +import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; @@ -82,6 +83,48 @@ private DateUtil() private static final String[] SIMPLE_TIME_FORMATS_WITH_AMPM = {"hh:mm:ss.SSS a", "hh:mm:ss a", "hh:mm a"}; private static final String[] SIMPLE_TIME_FORMATS_NO_AMPM = {"HH:mm:ss.SSS", "HH:mm:ss", "HH:mm"}; + public static final Set STANDARD_DATE_DISPLAY_FORMATS = PageFlowUtil.set( + "yyyy-MM-dd", + "yyyy-MMM-dd", + "dd-MMM-yyyy", + "dd-MMM-yy", + "ddMMMyyyy", + "ddMMMyy" + ); + + public static final Set STANDARD_TIME_DISPLAY_FORMATS = PageFlowUtil.set( + "HH:mm:ss", + "HH:mm", + "HH:mm:ss.SSS", + "hh:mm a" + ); + + public static boolean isStandardDateDisplayFormat(String dateFormat) + { + return STANDARD_DATE_DISPLAY_FORMATS.contains(dateFormat); + } + + public static boolean isStandardDateTimeDisplayFormat(String dateTimeFormat) + { + // Tolerate any amount of whitespace between the parts + String[] parts = dateTimeFormat.split("\\s+"); + + // If one part, must be standard date format + // If two parts, must be standard date format followed by standard time format + // Otherwise, it's non-standard + return switch (parts.length) + { + case 1 -> isStandardDateDisplayFormat(parts[0]); + case 2 -> isStandardDateDisplayFormat(parts[0]) && isStandardTimeDisplayFormat(parts[1]); + default -> false; + }; + } + + public static boolean isStandardTimeDisplayFormat(String timeFormat) + { + return STANDARD_TIME_DISPLAY_FORMATS.contains(timeFormat); + } + /** * GregorianCalendar is expensive because it calls computeTime() in setTimeInMillis() * (which is called in the constructor) diff --git a/api/src/org/labkey/api/util/PageFlowUtil.java b/api/src/org/labkey/api/util/PageFlowUtil.java index 3114372ba45..16278d420e3 100644 --- a/api/src/org/labkey/api/util/PageFlowUtil.java +++ b/api/src/org/labkey/api/util/PageFlowUtil.java @@ -2175,6 +2175,10 @@ public static JSONObject jsInitObject(ContainerUser context, @Nullable PageConfi String numberFormat = Formats.getNumberFormatString(settingsContainer); if (null != numberFormat) json.put("extDefaultNumberFormat", ExtUtil.toExtNumberFormat(numberFormat)); + json.put("standardDisplayFormats", Map.of( + "dateFormats", DateUtil.STANDARD_DATE_DISPLAY_FORMATS, + "timeFormats", DateUtil.STANDARD_TIME_DISPLAY_FORMATS + )); json.put("useMDYDateParsing", LookAndFeelProperties.getInstance(ContainerManager.getRoot()).getDateParsingMode().getDayMonth() == DateUtil.MonthDayOption.MONTH_DAY); diff --git a/core/api-src/org/labkey/api/admin/TableXmlUtils.java b/core/api-src/org/labkey/api/admin/TableXmlUtils.java index 1974f9f9589..fb8c4fe7b1e 100644 --- a/core/api-src/org/labkey/api/admin/TableXmlUtils.java +++ b/core/api-src/org/labkey/api/admin/TableXmlUtils.java @@ -31,11 +31,6 @@ import java.util.SortedMap; import java.util.TreeMap; -/** - * User: phussey - * Date: Sep 19, 2005 - * Time: 11:09:11 PM - */ public class TableXmlUtils { private static final Logger _log = LogManager.getLogger(TableXmlUtils.class); diff --git a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationProvider.java b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationProvider.java index 1b0a92b4e79..f55f8392f65 100644 --- a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationProvider.java +++ b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationProvider.java @@ -19,26 +19,9 @@ import org.labkey.api.data.Container; import org.labkey.api.security.User; -public interface SiteValidationProvider extends SiteValidatorDescriptor +public interface SiteValidationProvider { - /** - * Return false to indicate the validator shouldn't be run for that container. - * Useful if we know in advance the validator isn't applicable; e.g., the - * validator is module-dependent and that module isn't enabled in this container. - */ - default boolean shouldRun(Container c, User u) - { - return true; - } - - /** - * Return true to indicate this is a site-wide validator. - * False to indicate the validator should only run at container scope - */ - default boolean isSiteScope() - { - return true; - } + SiteValidationProviderFactory getFactory(); @Nullable SiteValidationResultList runValidation(Container c, User u); diff --git a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationProviderFactory.java b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationProviderFactory.java new file mode 100644 index 00000000000..cfdaea4e393 --- /dev/null +++ b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationProviderFactory.java @@ -0,0 +1,20 @@ +package org.labkey.api.admin.sitevalidation; + +/** + * We register a factory that creates a new validation provider on each run. This gives the provider an opportunity + * to initialize state before being called on every container. The provider could, for example, execute a single + * cross-container query instead of one query per container. + */ +public interface SiteValidationProviderFactory extends SiteValidatorDescriptor +{ + /** + * Return true to indicate this is a site-wide validator. + * False to indicate the validator should only run at container scope + */ + default boolean isSiteScope() + { + return false; + } + + SiteValidationProvider getSiteValidationProvider(); +} diff --git a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResult.java b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResult.java index e4f85c93ef9..bcb3dd71b51 100644 --- a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResult.java +++ b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResult.java @@ -17,12 +17,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.labkey.api.util.HtmlString; +import org.labkey.api.util.HtmlStringBuilder; import org.labkey.api.view.ActionURL; -/** - * User: tgaluhn - * Date: 4/8/2015 - */ public class SiteValidationResult { public enum Level @@ -31,19 +29,19 @@ public enum Level WARN, ERROR; - public SiteValidationResult create() { return create("");} - public SiteValidationResult create(String message) { return create(message, null);} - public SiteValidationResult create(String message, ActionURL link) { return new SiteValidationResult(this, message, link);} + public SiteValidationResult create() { return create(HtmlString.EMPTY_STRING);} + public SiteValidationResult create(HtmlString html) { return create(html, null);} + public SiteValidationResult create(HtmlString message, @Nullable ActionURL link) { return new SiteValidationResult(this, message, link);} } private final Level level; - private final StringBuilder sb; + private final HtmlStringBuilder builder; private final ActionURL link; - private SiteValidationResult(@NotNull Level level, @NotNull String message, @Nullable ActionURL link) + private SiteValidationResult(@NotNull Level level, @NotNull HtmlString message, @Nullable ActionURL link) { this.level = level; - this.sb = new StringBuilder(message); + this.builder = HtmlStringBuilder.of(message); this.link = link; } @@ -52,9 +50,9 @@ public Level getLevel() return level; } - public String getMessage() + public HtmlString getMessage() { - return sb.toString(); + return builder.getHtmlString(); } public ActionURL getLink() @@ -62,9 +60,14 @@ public ActionURL getLink() return link; } - public SiteValidationResult append(Object o) + public SiteValidationResult append(String message) { - sb.append(o); + builder.append(message); return this; } + + public SiteValidationResult append(Object o) + { + return append(o.toString()); + } } diff --git a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResultList.java b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResultList.java index 6500e2eae4d..6b02567924f 100644 --- a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResultList.java +++ b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationResultList.java @@ -16,6 +16,8 @@ package org.labkey.api.admin.sitevalidation; import org.jetbrains.annotations.Nullable; +import org.labkey.api.admin.sitevalidation.SiteValidationResult.Level; +import org.labkey.api.util.HtmlString; import org.labkey.api.view.ActionURL; import java.util.ArrayList; @@ -28,12 +30,14 @@ public class SiteValidationResultList { private final List results = new ArrayList<>(); - public SiteValidationResult addResult(SiteValidationResult.Level level, String message) + // TODO: Add more HtmlString taking addResult() methods. + + public SiteValidationResult addResult(Level level, String message, @Nullable ActionURL link) { - return addResult(level, message, null); + return addResult(level, HtmlString.of(message), link); } - public SiteValidationResult addResult(SiteValidationResult.Level level, String message, ActionURL link) + public SiteValidationResult addResult(Level level, HtmlString message, @Nullable ActionURL link) { SiteValidationResult result = level.create(message, link); results.add(result); @@ -45,9 +49,9 @@ public SiteValidationResult addInfo(String message) return addInfo(message, null); } - public SiteValidationResult addInfo(String message, ActionURL link) + public SiteValidationResult addInfo(String message, @Nullable ActionURL link) { - return addResult(SiteValidationResult.Level.INFO, message, link); + return addResult(Level.INFO, message, link); } public SiteValidationResult addWarn(String message) @@ -55,9 +59,14 @@ public SiteValidationResult addWarn(String message) return addWarn(message, null); } - public SiteValidationResult addWarn(String message, ActionURL link) + public SiteValidationResult addWarn(String message, @Nullable ActionURL link) + { + return addWarn(HtmlString.of(message), link); + } + + public SiteValidationResult addWarn(HtmlString message, @Nullable ActionURL link) { - return addResult(SiteValidationResult.Level.WARN, message, link); + return addResult(Level.WARN, message, link); } public SiteValidationResult addError(String message) @@ -65,9 +74,9 @@ public SiteValidationResult addError(String message) return addError(message, null); } - public SiteValidationResult addError(String message, ActionURL link) + public SiteValidationResult addError(String message, @Nullable ActionURL link) { - return addResult(SiteValidationResult.Level.ERROR, message, link); + return addResult(Level.ERROR, message, link); } public List getResults() @@ -75,7 +84,7 @@ public List getResults() return getResults(null); } - public List getResults(@Nullable SiteValidationResult.Level level) + public List getResults(@Nullable Level level) { if (null == level) return results; diff --git a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationService.java b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationService.java index 9dbf49d5850..bb33de895a1 100644 --- a/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationService.java +++ b/core/api-src/org/labkey/api/admin/sitevalidation/SiteValidationService.java @@ -41,10 +41,10 @@ static void setInstance(SiteValidationService impl) ServiceRegistry.get().registerService(SiteValidationService.class, impl); } - void registerProvider(String module, SiteValidationProvider provider); + void registerProviderFactory(String module, SiteValidationProviderFactory factory); - Map> getSiteProviders(); - Map> getContainerProviders(); + Map> getSiteFactories(); + Map> getContainerFactories(); /** * Returns a map of module name -> map of ValidatorDescriptor -> result list for all validators registered by that module @@ -57,14 +57,10 @@ static void setInstance(SiteValidationService impl) /** * Returns a map of module names -> map ValidatorProviders -> map of projects names -> * map of container names within the project -> result list of all validators appropriate for each container - * If run from the site level, maps of project names will be all projects on the site. + * If run from the site level, maps of project names will be root plus all projects on the site. * If run from folder level, there will be one entry in the project name maps, the project for that folder - * * If no validators are appropriate for any container in a project, that project will not be in the project map. * If no validation errors are found for a container, that container's SiteValidationResultList will be empty. - * - * TODO: Not validating root folder, should we? Or would anything for it be covered by site-wide validators? - * */ @NotNull Map>>> runContainerScopeValidators(Container topLevel, boolean includeSubFolders, List providers, User u); diff --git a/core/src/org/labkey/core/CoreModule.java b/core/src/org/labkey/core/CoreModule.java index c929ddfc725..7fd88aba081 100644 --- a/core/src/org/labkey/core/CoreModule.java +++ b/core/src/org/labkey/core/CoreModule.java @@ -210,6 +210,8 @@ import org.labkey.core.admin.AdminController; import org.labkey.core.admin.CopyFileRootPipelineJob; import org.labkey.core.admin.CustomizeMenuForm; +import org.labkey.core.admin.DisplayFormatAnalyzer; +import org.labkey.core.admin.DisplayFormatValidationProviderFactory; import org.labkey.core.admin.FilesSiteSettingsAction; import org.labkey.core.admin.MenuViewFactory; import org.labkey.core.admin.importer.FolderTypeImporterFactory; @@ -275,7 +277,7 @@ import org.labkey.core.security.SecurityApiActions; import org.labkey.core.security.SecurityController; import org.labkey.core.security.SecurityPointcutServiceImpl; -import org.labkey.core.security.validators.PermissionsValidator; +import org.labkey.core.security.validators.PermissionsValidatorFactory; import org.labkey.core.statistics.AnalyticsProviderRegistryImpl; import org.labkey.core.statistics.StatsServiceImpl; import org.labkey.core.statistics.SummaryStatisticRegistryImpl; @@ -304,8 +306,27 @@ import org.quartz.SchedulerException; import org.quartz.impl.StdSchedulerFactory; import org.radeox.test.BaseRenderEngineTest; -import org.radeox.test.macro.list.*; -import org.radeox.test.filter.*; +import org.radeox.test.filter.BasicRegexTest; +import org.radeox.test.filter.BoldFilterTest; +import org.radeox.test.filter.EscapeFilterTest; +import org.radeox.test.filter.FilterPipeTest; +import org.radeox.test.filter.HeadingFilterTest; +import org.radeox.test.filter.HtmlRemoveFilterTest; +import org.radeox.test.filter.ItalicFilterTest; +import org.radeox.test.filter.KeyFilterTest; +import org.radeox.test.filter.LineFilterTest; +import org.radeox.test.filter.LinkTestFilterTest; +import org.radeox.test.filter.ListFilterTest; +import org.radeox.test.filter.NewlineFilterTest; +import org.radeox.test.filter.ParamFilterTest; +import org.radeox.test.filter.SmileyFilterTest; +import org.radeox.test.filter.StrikeThroughFilterTest; +import org.radeox.test.filter.TypographyFilterTest; +import org.radeox.test.filter.UrlFilterTest; +import org.radeox.test.filter.WikiLinkFilterTest; +import org.radeox.test.macro.list.AtoZListFormatterTest; +import org.radeox.test.macro.list.ExampleListFormatterTest; +import org.radeox.test.macro.list.SimpleListTest; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -491,7 +512,8 @@ public QuerySchema createSchema(DefaultSchema schema, Module module) SiteValidationService svc = SiteValidationService.get(); if (null != svc) { - svc.registerProvider("core", new PermissionsValidator()); + svc.registerProviderFactory(getName(), new PermissionsValidatorFactory()); + svc.registerProviderFactory(getName(), new DisplayFormatValidationProviderFactory()); } registerHealthChecks(); @@ -1192,6 +1214,7 @@ public void moduleStartupComplete(ServletContext servletContext) UsageMetricsService.get().registerUsageMetrics(getName(), WebSocketConnectionManager.getInstance()); UsageMetricsService.get().registerUsageMetrics(getName(), DbLoginManager.getMetricsProvider()); UsageMetricsService.get().registerUsageMetrics(getName(), SecurityManager.getMetricsProvider()); + UsageMetricsService.get().registerUsageMetrics(getName(), DisplayFormatAnalyzer.getMetricsProvider()); if (AppProps.getInstance().isDevMode()) AntiVirusProviderRegistry.get().registerAntiVirusProvider(new DummyAntiVirusService.Provider()); @@ -1232,17 +1255,17 @@ public void moduleStartupComplete(ServletContext servletContext) private void checkForMissingDbViews() { ModuleLoader.getInstance().getModules().stream() - .map(FileSqlScriptProvider::new) - .flatMap(p -> p.getSchemas().stream() - .filter(schema-> SchemaUpdateType.Before.getScript(p, schema) != null || SchemaUpdateType.After.getScript(p, schema) != null) - ) - .filter(schema -> TableXmlUtils.compareXmlToMetaData(schema, false, false, true).hasViewProblem()) - .findAny() - .ifPresent(schema -> - { - LOG.warn("At least one database view was not as expected in the {} schema. Attempting to recreate views automatically", schema.getName()); - ModuleLoader.getInstance().recreateViews(); - }); + .map(FileSqlScriptProvider::new) + .flatMap(p -> p.getSchemas().stream() + .filter(schema-> SchemaUpdateType.Before.getScript(p, schema) != null || SchemaUpdateType.After.getScript(p, schema) != null) + ) + .filter(schema -> TableXmlUtils.compareXmlToMetaData(schema, false, false, true).hasViewProblem()) + .findAny() + .ifPresent(schema -> + { + LOG.warn("At least one database view was not as expected in the {} schema. Attempting to recreate views automatically", schema.getName()); + ModuleLoader.getInstance().recreateViews(); + }); } @Override diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 971e3891132..3861ebc9a5a 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -622,12 +622,6 @@ public ActionURL getProjectSettingsURL(Container c) return new ActionURL(ProjectSettingsAction.class, LookAndFeelProperties.getSettingsContainer(c)); } - @Override - public ActionURL getLookAndFeelSettingsURL() - { - return new ActionURL(LookAndFeelSettingsAction.class, ContainerManager.getRoot()); - } - ActionURL getLookAndFeelResourcesURL(Container c) { return c.isRoot() ? new ActionURL(AdminConsoleResourcesAction.class, c) : new ActionURL(ResourcesAction.class, LookAndFeelProperties.getSettingsContainer(c)); @@ -764,6 +758,17 @@ public ActionURL getFileRootsURL(Container c) return new ActionURL(FileRootsAction.class, c); } + @Override + public ActionURL getLookAndFeelSettingsURL(Container c) + { + if (c.isRoot()) + return getSiteLookAndFeelSettingsURL(); + else if (c.isProject()) + return getProjectSettingsURL(c); + else + return getFolderSettingsURL(c); + } + @Override public ActionURL getSiteLookAndFeelSettingsURL() { @@ -1234,16 +1239,11 @@ public boolean handlePost(Object o, BindException errors) throws Exception AdminUrls urls = new AdminUrlsImpl(); + // Folder-level settings are just display formats and measure/dimension flags -- no need to increment L&F revision if (!folder) - { WriteableAppProps.incrementLookAndFeelRevisionAndSave(); - _returnUrl = c.isRoot() ? urls.getLookAndFeelSettingsURL() : urls.getProjectSettingsURL(c); - } - else - { - // Folder-level settings are just display formats and measure/dimension flags -- no need to increment L&F revision - _returnUrl = urls.getFolderSettingsURL(c); - } + + _returnUrl = urls.getLookAndFeelSettingsURL(c); return true; } @@ -4459,7 +4459,7 @@ else if (fixRequested.equalsIgnoreCase("descriptor")) .unsafeAppend(""); for (var r : results) { - HtmlString item = isBlank(r.getMessage()) ? NBSP : HtmlString.of(r.getMessage()); + HtmlString item = r.getMessage().isEmpty() ? NBSP : r.getMessage(); contentBuilder.unsafeAppend("
  • ") .append(item) .unsafeAppend("
  • \n"); @@ -10926,8 +10926,6 @@ public void validateForm(SimpleApiJsonForm form, Errors errors) public Object execute(SimpleApiJsonForm form, BindException errors) { JSONObject json = form.getJsonObject(); - Container c = getContainer(); - boolean saved = saveProjectSettings(json, getUser(), getContainer(), errors); ApiSimpleResponse response = new ApiSimpleResponse(); diff --git a/core/src/org/labkey/core/admin/DateDisplayFormatType.java b/core/src/org/labkey/core/admin/DateDisplayFormatType.java new file mode 100644 index 00000000000..24bc12f3f12 --- /dev/null +++ b/core/src/org/labkey/core/admin/DateDisplayFormatType.java @@ -0,0 +1,108 @@ +package org.labkey.core.admin; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.JdbcType; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.settings.LookAndFeelProperties; +import org.labkey.api.util.DateUtil; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public enum DateDisplayFormatType +{ + Date(JdbcType.DATE, PropertyType.DATE) + { + @Override + boolean isStandardFormat(String formatPattern) + { + return DateUtil.isStandardDateDisplayFormat(formatPattern); + } + + @Override + String getStoredFormat(LookAndFeelProperties laf) + { + return laf.getDefaultDateFormatStored(); + } + }, + DateTime(JdbcType.TIMESTAMP, PropertyType.DATE_TIME) + { + @Override + boolean isStandardFormat(String formatPattern) + { + return DateUtil.isStandardDateTimeDisplayFormat(formatPattern); + } + + @Override + String getStoredFormat(LookAndFeelProperties laf) + { + return laf.getDefaultDateTimeFormatStored(); + } + }, + Time(JdbcType.TIME, PropertyType.TIME) + { + @Override + boolean isStandardFormat(String formatPattern) + { + return DateUtil.isStandardTimeDisplayFormat(formatPattern); + } + + @Override + String getStoredFormat(LookAndFeelProperties laf) + { + return laf.getDefaultTimeFormatStored(); + } + }; + + private static final Map MAP_FOR_TYPE_URI = new HashMap<>(); + private static final Map MAP_FOR_JDBC_TYPE = new HashMap<>(); + + private final JdbcType _jdbcType; + private final String _typeUri; + + DateDisplayFormatType(JdbcType jdbcType, PropertyType propertyType) + { + _jdbcType = jdbcType; + _typeUri = propertyType.getTypeUri(); + } + + static + { + Arrays.stream(values()).forEach(type -> { + MAP_FOR_TYPE_URI.put(type.getTypeUri(), type); + MAP_FOR_JDBC_TYPE.put(type.getJdbcType(), type); + }); + } + + public JdbcType getJdbcType() + { + return _jdbcType; + } + + public String getTypeUri() + { + return _typeUri; + } + + public static List getTypeUris() + { + return Arrays.stream(values()).map(DateDisplayFormatType::getTypeUri).toList(); + } + + public static DateDisplayFormatType getForRangeUri(String rangeUri) + { + return MAP_FOR_TYPE_URI.get(rangeUri); + } + + public static @Nullable DateDisplayFormatType getForJdbcType(JdbcType jdbcType) + { + return MAP_FOR_JDBC_TYPE.get(jdbcType); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + abstract boolean isStandardFormat(String formatPattern); + + abstract String getStoredFormat(LookAndFeelProperties laf); +} diff --git a/core/src/org/labkey/core/admin/DisplayFormatAnalyzer.java b/core/src/org/labkey/core/admin/DisplayFormatAnalyzer.java new file mode 100644 index 00000000000..09ef2ee311c --- /dev/null +++ b/core/src/org/labkey/core/admin/DisplayFormatAnalyzer.java @@ -0,0 +1,196 @@ +package org.labkey.core.admin; + +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.jetbrains.annotations.NotNull; +import org.labkey.api.action.UrlProvider; +import org.labkey.api.admin.AdminUrls; +import org.labkey.api.data.Container; +import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.PropertyManager; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.TableInfo; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.exp.property.DomainUtil; +import org.labkey.api.gwt.client.model.GWTDomain; +import org.labkey.api.query.DefaultSchema; +import org.labkey.api.query.QuerySchema; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUrls; +import org.labkey.api.security.User; +import org.labkey.api.settings.LookAndFeelProperties; +import org.labkey.api.usageMetrics.UsageMetricsProvider; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.data.xml.ColumnType; +import org.labkey.data.xml.TableType; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.labkey.api.settings.AbstractSettingsGroup.SITE_CONFIG_USER; +import static org.labkey.api.settings.LookAndFeelFolderProperties.LOOK_AND_FEEL_SET_NAME; + +public class DisplayFormatAnalyzer +{ + public record DisplayFormatContext(String message, ActionURL url){} + + public interface DisplayFormatHandler + { + void handle(Container c, DateDisplayFormatType type, String format, Supplier contextProvider); + } + + private record NonStandardDefaultFormat(Container container, DateDisplayFormatType type, String format) {} + private record QueryCandidate(Container container, String schemaName, String queryName, int rowId) {} + private record PropertyCandidate(Container container, String tableName, String columnName, String domainUri, String rangeUri, String format) + { + DateDisplayFormatType type() + { + return DateDisplayFormatType.getForRangeUri(rangeUri); + } + } + + private final MultiValuedMap _defaultFormatsMap = new ArrayListValuedHashMap<>(); + private final MultiValuedMap _queryDefXmlCandidatesMap = new ArrayListValuedHashMap<>(); + private final MultiValuedMap _propertyCandidateMap = new ArrayListValuedHashMap<>(); + + private final AdminUrls _adminUrls = urlProvider(AdminUrls.class); + private final QueryUrls _queryUrls = urlProvider(QueryUrls.class); + private final QueryService _queryService = QueryService.get(); + + public DisplayFormatAnalyzer() + { + try (Stream stream = PropertyManager.getNormalStore().streamMatchingContainers(SITE_CONFIG_USER, LOOK_AND_FEEL_SET_NAME)) + { + stream.forEach(c -> { + // Must get the stored values from LookAndFeelProperties since FolderSettingsCache holds inherited values + LookAndFeelProperties laf = LookAndFeelProperties.getInstance(c); + Arrays.stream(DateDisplayFormatType.values()) + .forEach(type -> { + String format = type.getStoredFormat(laf); + if (format != null && !type.isStandardFormat(format)) + _defaultFormatsMap.put(c, new NonStandardDefaultFormat(c, type, format)); + }); + }); + } + + new SqlSelector(CoreSchema.getInstance().getSchema(), + new SQLFragment("SELECT Container, QueryDefId AS RowId, \"schema\" AS SchemaName, Name AS QueryName FROM query.QueryDef WHERE metadata LIKE '%%'") + ) + .stream(QueryCandidate.class) + .forEach(candidate -> _queryDefXmlCandidatesMap.put(candidate.container(), candidate)); + + SQLFragment sql = new SQLFragment( + "SELECT dd.Container, DomainURI, dd.Name AS TableName, pd.Name AS ColumnName, RangeURI, Format FROM " + OntologyManager.getTinfoDomainDescriptor() + " dd\n" + + "\tINNER JOIN " + OntologyManager.getTinfoPropertyDomain() + " pdm ON dd.DomainId = pdm.DomainId\n" + + "\tINNER JOIN " + OntologyManager.getTinfoPropertyDescriptor() + " pd ON pdm.PropertyId = pd.PropertyId\n" + + "\tWHERE Format IS NOT NULL AND RangeURI IN (?, ?, ?)") + .addAll(DateDisplayFormatType.getTypeUris()); + new SqlSelector(CoreSchema.getInstance().getSchema(), sql) + .stream(PropertyCandidate.class) + .filter(candidate -> !candidate.type().isStandardFormat(candidate.format())) + .forEach(candidate -> _propertyCandidateMap.put(candidate.container(), candidate)); + } + + public void handle(Container c, User user, DisplayFormatHandler handler) + { + _defaultFormatsMap.get(c) + .forEach(defaultFormat -> handler.handle(defaultFormat.container(), defaultFormat.type(), defaultFormat.format(), + () -> new DisplayFormatContext( + (c.isRoot() ? "Site" : c.getContainerNoun(true)) + " default display format for " + defaultFormat.type().name() + "s", + _adminUrls.getLookAndFeelSettingsURL(c) + ) + )); + + // First, inspect QueryDef metadata to find columns where XML has explicitly set a display format + // (as opposed to inheriting a display format from another query or table definition). Then inspect + // those columns via the TableInfo to find date-time columns with non-standard formats. + _queryDefXmlCandidatesMap.get(c) + .forEach(candidate -> { + TableType tableType = _queryService.getQueryDefMetadata(c, candidate.rowId()); + + if (tableType != null) + { + ColumnType[] columnTypes = tableType.getColumns().getColumnArray(); + String[] columnsWithDisplayFormats = Arrays.stream(columnTypes) + .filter(ColumnType::isSetFormatString) + .map(ColumnType::getColumnName) + .toArray(String[]::new); + + if (columnsWithDisplayFormats.length > 0) + { + QuerySchema schema = DefaultSchema.get(user, c).getSchema(candidate.schemaName()); + if (schema != null) + { + try + { + TableInfo table = schema.getTable(candidate.queryName()); + if (table != null) + { + table.getColumns(columnsWithDisplayFormats) + .forEach(column -> { + DateDisplayFormatType type = DateDisplayFormatType.getForJdbcType(column.getJdbcType()); + if (type != null && !type.isStandardFormat(column.getFormat())) + handler.handle(c, type, column.getFormat(), + () -> new DisplayFormatContext( + "Metadata for \"" + table.getSchema().getDisplayName() + "." + table.getName() + "." + column.getName() + "\" " + type.name() + " column", + _queryUrls.urlMetadataQuery(c, schema.getName(), table.getName()) + ) + ); + }); + } + } + catch (Exception e) + { + // likely query parsing problem - skip this query/table + } + } + } + } + }); + + _propertyCandidateMap.get(c) + .forEach(candidate -> handler.handle(c, candidate.type(), candidate.format(), + () -> { + GWTDomain domain = DomainUtil.getDomainDescriptor(user, candidate.domainUri(), c); + return new DisplayFormatContext( + candidate.type().name() + " property \"" + domain.getSchemaName() + "." + domain.getQueryName() + "." + candidate.columnName() + "\"", + _queryUrls.urlSchemaBrowser(c, domain.getSchemaName(), domain.getQueryName()) + ); + })); + } + + public void handleAll(User user, DisplayFormatHandler handler) + { + Set containers = new HashSet<>(_defaultFormatsMap.keySet()); + containers.addAll(_queryDefXmlCandidatesMap.keySet()); + containers.addAll(_propertyCandidateMap.keySet()); + + containers.forEach(c -> handle(c, user, handler)); + } + + public static UsageMetricsProvider getMetricsProvider() + { + // Collect the unique set of non-standard date display formats across the entire site + Set badFormats = new HashSet<>(); + return () -> { + DisplayFormatAnalyzer analyzer = new DisplayFormatAnalyzer(); + analyzer.handleAll(User.getAdminServiceUser(), (c, type, format, contextProvider) -> badFormats.add(format)); + return Map.of("nonStandardDateDisplayFormats", badFormats); + }; + } + + private

    @NotNull P urlProvider(Class

    inter) + { + P provider = PageFlowUtil.urlProvider(inter); + if (provider == null) + throw new IllegalStateException("No urlProvider found for " + inter.getName()); + + return provider; + } +} diff --git a/core/src/org/labkey/core/admin/DisplayFormatValidationProviderFactory.java b/core/src/org/labkey/core/admin/DisplayFormatValidationProviderFactory.java new file mode 100644 index 00000000000..0543c401254 --- /dev/null +++ b/core/src/org/labkey/core/admin/DisplayFormatValidationProviderFactory.java @@ -0,0 +1,59 @@ +package org.labkey.core.admin; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.admin.sitevalidation.SiteValidationProvider; +import org.labkey.api.admin.sitevalidation.SiteValidationProviderFactory; +import org.labkey.api.admin.sitevalidation.SiteValidationResultList; +import org.labkey.api.data.Container; +import org.labkey.api.security.User; +import org.labkey.api.util.HtmlString; +import org.labkey.api.util.HtmlStringBuilder; + +public class DisplayFormatValidationProviderFactory implements SiteValidationProviderFactory +{ + @Override + public String getName() + { + return "Display Format Validator"; + } + + @Override + public String getDescription() + { + return "Report non-standard date and time display formats"; + } + + @Override + public SiteValidationProvider getSiteValidationProvider() + { + return new SiteValidationProvider() + { + private final DisplayFormatAnalyzer _analyzer = new DisplayFormatAnalyzer(); + + @Override + public SiteValidationProviderFactory getFactory() + { + return DisplayFormatValidationProviderFactory.this; + } + + @Override + public @Nullable SiteValidationResultList runValidation(Container container, User u) + { + SiteValidationResultList results = new SiteValidationResultList(); + _analyzer.handle(container, u, (c, type, format, contextProvider) -> { + DisplayFormatAnalyzer.DisplayFormatContext context = contextProvider.get(); + results.addWarn( + HtmlStringBuilder.of(context.message() + ": ") + .append(HtmlString.unsafe("")) + .append(format) + .append(HtmlString.unsafe("")) + .getHtmlString(), + context.url() + ); + }); + + return results.nullIfEmpty(); + } + }; + } +} diff --git a/core/src/org/labkey/core/admin/sitevalidation/SiteValidationServiceImpl.java b/core/src/org/labkey/core/admin/sitevalidation/SiteValidationServiceImpl.java index 0d917291de0..50bd6653db1 100644 --- a/core/src/org/labkey/core/admin/sitevalidation/SiteValidationServiceImpl.java +++ b/core/src/org/labkey/core/admin/sitevalidation/SiteValidationServiceImpl.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.labkey.api.admin.sitevalidation.SiteValidationProvider; +import org.labkey.api.admin.sitevalidation.SiteValidationProviderFactory; import org.labkey.api.admin.sitevalidation.SiteValidationResultList; import org.labkey.api.admin.sitevalidation.SiteValidationService; import org.labkey.api.admin.sitevalidation.SiteValidatorDescriptor; @@ -38,42 +39,42 @@ public class SiteValidationServiceImpl implements SiteValidationService { - private final Map> siteValidators = new ConcurrentSkipListMap<>(); - private final Map> containerValidators = new ConcurrentSkipListMap<>(); + private final Map> _siteFactories = new ConcurrentSkipListMap<>(); + private final Map> _containerFactories = new ConcurrentSkipListMap<>(); @Override - public void registerProvider(String module, SiteValidationProvider provider) + public void registerProviderFactory(String module, SiteValidationProviderFactory factory) { - if (provider.isSiteScope()) + if (factory.isSiteScope()) { - Set moduleSet = siteValidators.computeIfAbsent(module, k -> new ConcurrentSkipListSet<>()); - moduleSet.add(provider); + Set moduleSet = _siteFactories.computeIfAbsent(module, k -> new ConcurrentSkipListSet<>()); + moduleSet.add(factory); } else { - Set moduleSet = containerValidators.computeIfAbsent(module, k -> new ConcurrentSkipListSet<>()); - moduleSet.add(provider); + Set moduleSet = _containerFactories.computeIfAbsent(module, k -> new ConcurrentSkipListSet<>()); + moduleSet.add(factory); } } @Override - public Map> getSiteProviders() + public Map> getSiteFactories() { - return getImmutable(siteValidators); + return getImmutable(_siteFactories); } @Override - public Map> getContainerProviders() + public Map> getContainerFactories() { - return getImmutable(containerValidators); + return getImmutable(_containerFactories); } - private Map> getImmutable(Map> map) + private Map> getImmutable(Map> map) { - Map> copy = new TreeMap<>(); + Map> copy = new TreeMap<>(); for (String key : map.keySet()) { - Set set = Collections.unmodifiableSet(new TreeSet<>(map.get(key))); + Set set = Collections.unmodifiableSet(new TreeSet<>(map.get(key))); copy.put(key, set); } return Collections.unmodifiableMap(copy); @@ -86,22 +87,22 @@ public Map> runSi Map> siteResults = new LinkedHashMap<>(); Container root = ContainerManager.getRoot(); - for (Map.Entry> moduleValidators : siteValidators.entrySet()) + for (Map.Entry> moduleFactories : _siteFactories.entrySet()) { - List validatorsToRun = getValidatorsToRun(moduleValidators, providers); + List providersToRun = getValidatorsToRun(moduleFactories, providers); // Skip work if there are no selected validators - if (!validatorsToRun.isEmpty()) + if (!providersToRun.isEmpty()) { Map moduleResults = new TreeMap<>(); - siteResults.put(moduleValidators.getKey(), moduleResults); + siteResults.put(moduleFactories.getKey(), moduleResults); - for (SiteValidationProvider validator : validatorsToRun) + for (SiteValidationProvider provider : providersToRun) { - SiteValidatorDescriptor descriptor = new SiteValidatorDescriptorImpl(validator.getName(), validator.getDescription()); - SiteValidationResultList resultList = validator.runValidation(root, u); - if (null != resultList) - moduleResults.put(descriptor, resultList); + SiteValidationProviderFactory factory = provider.getFactory(); + SiteValidatorDescriptor descriptor = new SiteValidatorDescriptorImpl(factory.getName(), factory.getDescription()); + SiteValidationResultList resultList = provider.runValidation(root, u); + moduleResults.put(descriptor, null != resultList ? resultList : new SiteValidationResultList()); } } } @@ -121,37 +122,41 @@ public Map> moduleValidators : containerValidators.entrySet()) + for (Map.Entry> moduleFactories : _containerFactories.entrySet()) { Map>> moduleResults = new TreeMap<>(); - List validatorsToRun = getValidatorsToRun(moduleValidators, providers); + List validatorsToRun = getValidatorsToRun(moduleFactories, providers); // Skip work if there are no selected validators from this module if (!validatorsToRun.isEmpty()) { // Initialize maps for each provider - for (SiteValidationProvider validator : validatorsToRun) - { - SiteValidatorDescriptor descriptor = new SiteValidatorDescriptorImpl(validator.getName(), validator.getDescription()); - moduleResults.put(descriptor, new TreeMap<>()); - } + validatorsToRun.stream() + .map(SiteValidationProvider::getFactory) + .forEach(factory -> { + SiteValidatorDescriptor descriptor = new SiteValidatorDescriptorImpl(factory.getName(), factory.getDescription()); + moduleResults.put(descriptor, new TreeMap<>()); + }); for (Container parent : parentList) { - List allChildren = includeSubFolders ? ContainerManager.getAllChildren(parent, u) : List.of(parent); + List allChildren = includeSubFolders && !parent.isRoot() ? ContainerManager.getAllChildren(parent, u) : List.of(parent); for (Container c : allChildren) { - validatorsToRun.stream().filter(validator -> validator.shouldRun(c, u)).forEach(validator -> { + validatorsToRun.forEach(validator -> { SiteValidationResultList resultList = validator.runValidation(c, u); if (null != resultList && !resultList.getResults().isEmpty()) { - SiteValidatorDescriptor descriptor = new SiteValidatorDescriptorImpl(validator.getName(), validator.getDescription()); + SiteValidationProviderFactory factory = validator.getFactory(); + SiteValidatorDescriptor descriptor = new SiteValidatorDescriptorImpl(factory.getName(), factory.getDescription()); Map> providerResults = moduleResults.get(descriptor); String projectPath = null == parent.getProject() ? parent.getName() : StringUtils.substringAfter(parent.getProject().getPath(), "/"); Map projectResults = providerResults.computeIfAbsent(projectPath, k -> new TreeMap<>()); @@ -161,7 +166,7 @@ public Map getValidatorsToRun(Map.Entry> moduleValidators, List providers) + private List getValidatorsToRun(Map.Entry> factories, List providers) { Set includedProviderNames = new HashSet<>(); if (providers != null) includedProviderNames.addAll(providers); - return moduleValidators.getValue().stream() + return factories.getValue().stream() .filter(validator -> includedProviderNames.contains(validator.getName())) + .map(SiteValidationProviderFactory::getSiteValidationProvider) .toList(); } } diff --git a/core/src/org/labkey/core/admin/sitevalidation/SiteValidatorDescriptorImpl.java b/core/src/org/labkey/core/admin/sitevalidation/SiteValidatorDescriptorImpl.java index 5f6a6e26903..c2a711f3423 100644 --- a/core/src/org/labkey/core/admin/sitevalidation/SiteValidatorDescriptorImpl.java +++ b/core/src/org/labkey/core/admin/sitevalidation/SiteValidatorDescriptorImpl.java @@ -17,14 +17,10 @@ import org.labkey.api.admin.sitevalidation.SiteValidatorDescriptor; -/** - * User: tgaluhn - * Date: 10/30/2016 - */ public class SiteValidatorDescriptorImpl implements SiteValidatorDescriptor { - final String _name; - final String _description; + private final String _name; + private final String _description; public SiteValidatorDescriptorImpl(String name, String description) { diff --git a/core/src/org/labkey/core/admin/sitevalidation/configureSiteValidation.jsp b/core/src/org/labkey/core/admin/sitevalidation/configureSiteValidation.jsp index 946c69e9037..450e7927733 100644 --- a/core/src/org/labkey/core/admin/sitevalidation/configureSiteValidation.jsp +++ b/core/src/org/labkey/core/admin/sitevalidation/configureSiteValidation.jsp @@ -15,13 +15,15 @@ * limitations under the License. */ %> -<%@ page import="org.labkey.api.admin.sitevalidation.SiteValidationProvider" %> +<%@ page import="jakarta.servlet.jsp.JspWriter" %> +<%@ page import="org.labkey.api.admin.sitevalidation.SiteValidationProviderFactory" %> <%@ page import="org.labkey.api.admin.sitevalidation.SiteValidationService" %> <%@ page import="org.labkey.api.util.DOM" %> <%@ page import="org.labkey.api.util.HtmlString" %> <%@ page import="org.labkey.core.admin.AdminController.SiteValidationAction" %> <%@ page import="org.labkey.core.admin.AdminController.SiteValidationBackgroundAction" %> <%@ page import="java.io.IOException" %> +<%@ page import="java.lang.String" %> <%@ page import="java.util.Collection" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Map" %> @@ -29,17 +31,17 @@ <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%! - private void renderProviderList(String title, Map> providerMap, JspWriter out) throws IOException + private void renderProviderList(String title, Map> factoryMap, JspWriter out) throws IOException { - if (!providerMap.isEmpty()) + if (!factoryMap.isEmpty()) { renderTitle(title, out); - List providers = providerMap.values().stream() + List factories = factoryMap.values().stream() .flatMap(Collection::stream) .toList(); - for (SiteValidationProvider provider : providers) + for (SiteValidationProviderFactory provider : factories) { out.println( DOM.createHtmlFragment( @@ -71,15 +73,15 @@ Clicking the "Validate" button will run the selected validators in the designate <% if (getContainer().isRoot()) - renderProviderList("Site Validation Providers", validationService.getSiteProviders(), out); - renderProviderList("Folder Validation Providers", validationService.getContainerProviders(), out); + renderProviderList("Site Validation Providers", validationService.getSiteFactories(), out); + renderProviderList("Folder Validation Providers", validationService.getContainerFactories(), out); renderTitle("Folder Validation Options", out); if (getContainer().isRoot()) { out.println( DOM.createHtmlFragment( input().name("includeSubfolders").type("radio").value("true").checked(true), - "All projects and folders in this site", + "The root plus all projects and folders in this site", DOM.BR(), input().name("includeSubfolders").type("radio").value("false").checked(false), "Just the projects", diff --git a/core/src/org/labkey/core/admin/sitevalidation/siteValidation.jsp b/core/src/org/labkey/core/admin/sitevalidation/siteValidation.jsp index 28bdf84e5bd..4173053520b 100644 --- a/core/src/org/labkey/core/admin/sitevalidation/siteValidation.jsp +++ b/core/src/org/labkey/core/admin/sitevalidation/siteValidation.jsp @@ -21,6 +21,7 @@ <%@ page import="org.labkey.api.admin.sitevalidation.SiteValidationService" %> <%@ page import="org.labkey.api.admin.sitevalidation.SiteValidatorDescriptor" %> <%@ page import="org.labkey.core.admin.AdminController.SiteValidationForm" %> +<%@ page import="java.lang.String" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Map" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> @@ -50,7 +51,7 @@ %> Site Level Validation Results <% - if (validationService.getSiteProviders().isEmpty()) + if (validationService.getSiteFactories().isEmpty()) { info(form, "No site-wide validators are registered"); %> @@ -60,7 +61,7 @@ else { info(form, "Running selected site-wide validators"); - Map> siteResults = validationService.runSiteScopeValidators(form.getProviders(), getUser()); + Map> siteResults = validationService.runSiteScopeValidators(form.getProviders(), getUser()); if (siteResults.isEmpty()) { info(form, "No site-wide validators were selected"); @@ -105,7 +106,7 @@ { %>

  • - <%=h(result.getMessage())%> + <%=result.getMessage()%> <% if (null != result.getLink()) { @@ -128,7 +129,7 @@ { %>
  • - <%=h(result.getMessage())%> + <%=result.getMessage()%> <% if (null != result.getLink()) { @@ -155,7 +156,7 @@ { %>
  • - <%=h(result.getMessage())%> + <%=result.getMessage()%> <% if (null != result.getLink()) { @@ -228,25 +229,28 @@ { for (Map.Entry> projectResult : validatorResults.getValue().entrySet()) { + String projectKey = projectResult.getKey(); %> -
  • <%=h("Project: " + projectResult.getKey())%> +
  • <%=h(!projectKey.isEmpty() ? "Project: " + projectKey : "Root: /")%>
      <% for (Map.Entry subtreeResult : projectResult.getValue().entrySet()) { + String subtreeKey = subtreeResult.getKey(); + SiteValidationResultList resultList = subtreeResult.getValue(); %> -
    • <%=h("Folder: " + subtreeResult.getKey())%> +
    • <%=h(!projectKey.equals(subtreeKey) ? "Folder: " + subtreeKey : "")%>
        <% - if (subtreeResult.getValue() != null) + if (resultList != null) { - containerInfos = subtreeResult.getValue().getResults(Level.INFO); - containerErrors = subtreeResult.getValue().getResults(Level.ERROR); - containerWarnings = subtreeResult.getValue().getResults(Level.WARN); + containerInfos = resultList.getResults(Level.INFO); + containerErrors = resultList.getResults(Level.ERROR); + containerWarnings = resultList.getResults(Level.WARN); for (SiteValidationResult result : containerInfos) { %> -
      • <%=h(result.getMessage())%> +
      • <%=result.getMessage()%> <% if (null != result.getLink()) { %> <%=link(LINK_HEADING, result.getLink())%> <% } %> @@ -256,7 +260,7 @@
      • Errors:
          <% for (SiteValidationResult result : containerErrors) { %> -
        • <%=h(result.getMessage())%> +
        • <%=result.getMessage()%> <% if (null != result.getLink()) { %> <%=link(LINK_HEADING, result.getLink())%> <% } %> @@ -273,7 +277,7 @@
            <% for (SiteValidationResult result : containerWarnings) { %> -
          • <%=h(result.getMessage())%> +
          • <%=result.getMessage()%> <% if (null != result.getLink()) { %> <%=link(LINK_HEADING, result.getLink())%> <% } %> diff --git a/core/src/org/labkey/core/admin/test/SchemaXMLTestCase.java b/core/src/org/labkey/core/admin/test/SchemaXMLTestCase.java index ddaba701e0c..4daceac861d 100644 --- a/core/src/org/labkey/core/admin/test/SchemaXMLTestCase.java +++ b/core/src/org/labkey/core/admin/test/SchemaXMLTestCase.java @@ -47,7 +47,7 @@ public class SchemaXMLTestCase extends Assert { @Parameterized.Parameters(name = "{1}") - public static Collection schemas() + public static Collection schemas() { List parameters = new ArrayList<>(); @@ -81,9 +81,9 @@ private void testSchemaXml(DbSchema schema) { ActionURL url = new ActionURL(AdminController.GetSchemaXmlDocAction.class, ContainerManager.getRoot()).addParameter("dbSchema", schema.getDisplayName()); fail(DOM.DIV("Errors in schema " + schema.getDisplayName() + ".xml ", - DOM.A(DOM.at(DOM.Attribute.href, url), "Click here for an XML doc with fixes"), - DOM.BR(), - mismatches.getResults().stream().map(r -> DOM.DIV(r.getMessage())) + DOM.A(DOM.at(DOM.Attribute.href, url), "Click here for an XML doc with fixes"), + DOM.BR(), + mismatches.getResults().stream().map(r -> DOM.DIV(r.getMessage())) ).renderToString()); } diff --git a/core/src/org/labkey/core/security/validators/PermissionsValidator.java b/core/src/org/labkey/core/security/validators/PermissionsValidatorFactory.java similarity index 51% rename from core/src/org/labkey/core/security/validators/PermissionsValidator.java rename to core/src/org/labkey/core/security/validators/PermissionsValidatorFactory.java index 45debe09a5d..045e901f053 100644 --- a/core/src/org/labkey/core/security/validators/PermissionsValidator.java +++ b/core/src/org/labkey/core/security/validators/PermissionsValidatorFactory.java @@ -1,71 +1,79 @@ -/* - * Copyright (c) 2015-2016 LabKey Corporation - * - * 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 org.labkey.core.security.validators; - -import org.labkey.api.admin.sitevalidation.SiteValidationProvider; -import org.labkey.api.admin.sitevalidation.SiteValidationResultList; -import org.labkey.api.data.Container; -import org.labkey.api.security.User; -import org.labkey.api.security.permissions.Permission; -import org.labkey.api.security.roles.EditorRole; -import org.labkey.api.security.roles.ReaderRole; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Validates appropriate container level permissions for the guest user. - */ -public class PermissionsValidator implements SiteValidationProvider -{ - @Override - public String getName() - { - return "Permissions Validator"; - } - - @Override - public String getDescription() - { - return "Check that guest user permissions are set appropriately"; - } - - @Override - public boolean isSiteScope() - { - return false; - } - - @Override - public SiteValidationResultList runValidation(Container c, User u) - { - SiteValidationResultList results = new SiteValidationResultList(); - - Set> readerPermissions = new ReaderRole().getPermissions(); - if (c.hasOneOf(User.guest, readerPermissions)) - { - results.addInfo("Guest user has read permission for folder."); - } - Set> editorPermissions = new HashSet<>(new EditorRole().getPermissions()); - editorPermissions.removeAll(readerPermissions); - if (c.hasOneOf(User.guest, Collections.unmodifiableSet(editorPermissions))) - { - results.addWarn("Guest user has edit permission for folder."); - } - return results.nullIfEmpty(); - } -} +/* + * Copyright (c) 2015-2016 LabKey Corporation + * + * 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 org.labkey.core.security.validators; + +import org.labkey.api.admin.sitevalidation.SiteValidationProvider; +import org.labkey.api.admin.sitevalidation.SiteValidationProviderFactory; +import org.labkey.api.admin.sitevalidation.SiteValidationResultList; +import org.labkey.api.data.Container; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.roles.EditorRole; +import org.labkey.api.security.roles.ReaderRole; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Validates appropriate container level permissions for the guest user. + */ +public class PermissionsValidatorFactory implements SiteValidationProviderFactory +{ + @Override + public String getName() + { + return "Permissions Validator"; + } + + @Override + public String getDescription() + { + return "Check that guest user permissions are set appropriately"; + } + + @Override + public SiteValidationProvider getSiteValidationProvider() + { + return new SiteValidationProvider() + { + @Override + public SiteValidationProviderFactory getFactory() + { + return PermissionsValidatorFactory.this; + } + + @Override + public SiteValidationResultList runValidation(Container c, User u) + { + SiteValidationResultList results = new SiteValidationResultList(); + + Set> readerPermissions = new ReaderRole().getPermissions(); + if (c.hasOneOf(User.guest, readerPermissions)) + { + results.addInfo("Guest user has read permission for folder."); + } + Set> editorPermissions = new HashSet<>(new EditorRole().getPermissions()); + editorPermissions.removeAll(readerPermissions); + if (c.hasOneOf(User.guest, Collections.unmodifiableSet(editorPermissions))) + { + results.addWarn("Guest user has edit permission for folder."); + } + return results.nullIfEmpty(); + } + }; + } +} diff --git a/core/src/org/labkey/core/view/template/bootstrap/CoreWarningProvider.java b/core/src/org/labkey/core/view/template/bootstrap/CoreWarningProvider.java index 89375d74a95..dca68a21cef 100644 --- a/core/src/org/labkey/core/view/template/bootstrap/CoreWarningProvider.java +++ b/core/src/org/labkey/core/view/template/bootstrap/CoreWarningProvider.java @@ -191,7 +191,7 @@ private void getDbSchemaWarnings(Warnings warnings, boolean showAllWarnings) schemaProblems.putAll(_dbSchemaWarnings); if (showAllWarnings) { - schemaProblems.put("WorstSchemaEver", List.of(SiteValidationResult.Level.ERROR.create("Your schema is very, very bad"))); + schemaProblems.put("WorstSchemaEver", List.of(SiteValidationResult.Level.ERROR.create(HtmlString.of("Your schema is very, very bad")))); } int count = 0; @@ -205,7 +205,7 @@ private void getDbSchemaWarnings(Warnings warnings, boolean showAllWarnings) } if (++count <= MAX_SCHEMA_PROBLEMS_TO_SHOW) { - warnings.add(HtmlString.of("Problem with '" + entry.getKey() + "' schema. " + result.getMessage())); + warnings.add(HtmlStringBuilder.of("Problem with '" + entry.getKey() + "' schema. ").append(result.getMessage())); } } } diff --git a/pipeline/src/org/labkey/pipeline/PipelineModule.java b/pipeline/src/org/labkey/pipeline/PipelineModule.java index 4b749338382..75337a97356 100644 --- a/pipeline/src/org/labkey/pipeline/PipelineModule.java +++ b/pipeline/src/org/labkey/pipeline/PipelineModule.java @@ -90,7 +90,7 @@ import org.labkey.pipeline.mule.filters.TaskJmsSelectorFilter; import org.labkey.pipeline.status.StatusController; import org.labkey.pipeline.trigger.PipelineTriggerRegistryImpl; -import org.labkey.pipeline.validators.PipelineSetupValidator; +import org.labkey.pipeline.validators.PipelineSetupValidatorFactory; import org.labkey.pipeline.xml.ExecTaskType; import org.labkey.pipeline.xml.ScriptTaskType; @@ -212,7 +212,7 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext) SiteValidationService svc = SiteValidationService.get(); if (null != svc) { - svc.registerProvider(getName(), new PipelineSetupValidator()); + svc.registerProviderFactory(getName(), new PipelineSetupValidatorFactory()); } AuditLogService.get().registerAuditType(new ProtocolManagementAuditProvider()); diff --git a/pipeline/src/org/labkey/pipeline/validators/PipelineSetupValidator.java b/pipeline/src/org/labkey/pipeline/validators/PipelineSetupValidatorFactory.java similarity index 51% rename from pipeline/src/org/labkey/pipeline/validators/PipelineSetupValidator.java rename to pipeline/src/org/labkey/pipeline/validators/PipelineSetupValidatorFactory.java index acdf0a83d10..c7118d5178c 100644 --- a/pipeline/src/org/labkey/pipeline/validators/PipelineSetupValidator.java +++ b/pipeline/src/org/labkey/pipeline/validators/PipelineSetupValidatorFactory.java @@ -1,87 +1,91 @@ -/* - * Copyright (c) 2015-2016 LabKey Corporation - * - * 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 org.labkey.pipeline.validators; - -import org.jetbrains.annotations.Nullable; -import org.labkey.api.admin.sitevalidation.SiteValidationProvider; -import org.labkey.api.admin.sitevalidation.SiteValidationResult; -import org.labkey.api.admin.sitevalidation.SiteValidationResultList; -import org.labkey.api.data.Container; -import org.labkey.api.pipeline.PipeRoot; -import org.labkey.api.pipeline.PipelineService; -import org.labkey.api.pipeline.PipelineUrls; -import org.labkey.api.security.User; -import org.labkey.api.util.PageFlowUtil; - -import java.util.List; - -/** - * User: tgaluhn - * Date: 10/26/2015 - * - * Validates pipeline root for container. - */ -public class PipelineSetupValidator implements SiteValidationProvider -{ - @Override - public String getName() - { - return "Pipeline Validator"; - } - - @Override - public String getDescription() - { - return "Validate pipeline roots"; - } - - @Override - public boolean isSiteScope() - { - return false; - } - - @Nullable - @Override - public SiteValidationResultList runValidation(Container c, User u) - { - PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(c); - if (null != pipeRoot) - { - List errors = pipeRoot.validate(); - if (!errors.isEmpty()) - { - PipelineUrls pipelineUrls = PageFlowUtil.urlProvider(PipelineUrls.class); - SiteValidationResultList results = new SiteValidationResultList(); - for (String error : errors) - { - - if (null != pipelineUrls) - { - SiteValidationResult result = results.addError(error, pipelineUrls.urlSetup(c)); - result.append(" Click link to go to pipeline setup for this folder."); - } - else - { - results.addError(error); - } - } - return results; - } - } - return null; - } -} +/* + * Copyright (c) 2015-2016 LabKey Corporation + * + * 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 org.labkey.pipeline.validators; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.admin.sitevalidation.SiteValidationProvider; +import org.labkey.api.admin.sitevalidation.SiteValidationProviderFactory; +import org.labkey.api.admin.sitevalidation.SiteValidationResult; +import org.labkey.api.admin.sitevalidation.SiteValidationResultList; +import org.labkey.api.data.Container; +import org.labkey.api.pipeline.PipeRoot; +import org.labkey.api.pipeline.PipelineService; +import org.labkey.api.pipeline.PipelineUrls; +import org.labkey.api.security.User; +import org.labkey.api.util.PageFlowUtil; + +import java.util.List; + +/** + * Validates pipeline root for container. + */ +public class PipelineSetupValidatorFactory implements SiteValidationProviderFactory +{ + @Override + public String getName() + { + return "Pipeline Validator"; + } + + @Override + public String getDescription() + { + return "Validate pipeline roots"; + } + + @Override + public SiteValidationProvider getSiteValidationProvider() + { + return new SiteValidationProvider() + { + @Override + public SiteValidationProviderFactory getFactory() + { + return PipelineSetupValidatorFactory.this; + } + + @Nullable + @Override + public SiteValidationResultList runValidation(Container c, User u) + { + PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(c); + if (null != pipeRoot) + { + List errors = pipeRoot.validate(); + if (!errors.isEmpty()) + { + PipelineUrls pipelineUrls = PageFlowUtil.urlProvider(PipelineUrls.class); + SiteValidationResultList results = new SiteValidationResultList(); + for (String error : errors) + { + if (null != pipelineUrls) + { + SiteValidationResult result = results.addError(error, pipelineUrls.urlSetup(c)); + result.append(" Click link to go to pipeline setup for this folder."); + } + else + { + results.addError(error); + } + } + return results; + } + } + return null; + } + }; + } +} diff --git a/query/src/org/labkey/query/LinkedSchemaQueryDefinition.java b/query/src/org/labkey/query/LinkedSchemaQueryDefinition.java index 55769738aff..84ea049ccb5 100644 --- a/query/src/org/labkey/query/LinkedSchemaQueryDefinition.java +++ b/query/src/org/labkey/query/LinkedSchemaQueryDefinition.java @@ -48,12 +48,9 @@ import java.util.Set; /** - * User: kevink - * Date: 12/17/12 - * * Wrapped QueryDefinition used in a LinkedSchema. * The wrapped query has a container of the LinkedSchema instead of the original query's container. - * In addition the query URLs are all null to indicate that they are disabled. + * In addition, the query URLs are all null to indicate that they are disabled. */ public class LinkedSchemaQueryDefinition extends QueryDefinitionImpl { @@ -124,7 +121,7 @@ public Query getQuery(@NotNull QuerySchema schema, List errors, protected TableInfo applyQueryMetadata(UserSchema schema, List errors, Query query, AbstractTableInfo ret) { // First, apply original wrapped query-def's metadata from files (using original schema name and container) - // Second, super.applyQueryMetadata() will also apply orignal wrapped query-def's metadata stored in the database (using original schema name and container) + // Second, super.applyQueryMetadata() will also apply original wrapped query-def's metadata stored in the database (using original schema name and container) LinkedSchema linkedSchema = getSchema(); UserSchema sourceSchema = linkedSchema.getSourceSchema(); super.applyQueryMetadata(sourceSchema, errors, query, ret); diff --git a/query/src/org/labkey/query/QueryDefinitionImpl.java b/query/src/org/labkey/query/QueryDefinitionImpl.java index 408726729b7..23060cb78b2 100644 --- a/query/src/org/labkey/query/QueryDefinitionImpl.java +++ b/query/src/org/labkey/query/QueryDefinitionImpl.java @@ -16,6 +16,7 @@ package org.labkey.query; +import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -72,7 +73,6 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.jdbc.BadSqlGrammarException; -import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -682,6 +682,12 @@ public String getMetadataXml() return _queryDef.getMetaData(); } + @Override + public TablesDocument getMetadataTablesDocument() + { + return _queryDef.getParsedMetadata().getTablesDocument(new ArrayList<>()); + } + @Override public void setDefinitionContainer(Container container) { diff --git a/query/src/org/labkey/query/QueryServiceImpl.java b/query/src/org/labkey/query/QueryServiceImpl.java index 3dc6c5d8291..daeaa0c9820 100644 --- a/query/src/org/labkey/query/QueryServiceImpl.java +++ b/query/src/org/labkey/query/QueryServiceImpl.java @@ -17,6 +17,8 @@ package org.labkey.query; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import org.apache.commons.beanutils.ConversionException; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.SetValuedMap; @@ -40,11 +42,40 @@ import org.labkey.api.cache.CacheManager; import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.api.collections.LabKeyCollectors; -import org.labkey.api.data.*; +import org.labkey.api.data.AuditConfigurable; +import org.labkey.api.data.ColumnHeaderType; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.ColumnRenderPropertiesImpl; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.ConvertHelper; +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbSchemaType; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.Filter; +import org.labkey.api.data.ForeignKey; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.MethodInfo; +import org.labkey.api.data.MutableColumnInfo; +import org.labkey.api.data.Parameter; +import org.labkey.api.data.QueryLogging; +import org.labkey.api.data.Results; +import org.labkey.api.data.ResultsImpl; +import org.labkey.api.data.RuntimeSQLException; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SQLGenerationException; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.Table; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.TableSelector; import org.labkey.api.data.dialect.SqlDialect; import org.labkey.api.exp.property.DomainKind; import org.labkey.api.exp.query.ExpTable; -import org.labkey.api.files.FileContentService; import org.labkey.api.gwt.client.AuditBehaviorType; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; import org.labkey.api.module.Module; @@ -55,8 +86,31 @@ import org.labkey.api.module.ModuleResourceCaches; import org.labkey.api.module.ResourceRootProvider; import org.labkey.api.pipeline.PipelineJob; -import org.labkey.api.query.*; +import org.labkey.api.query.AliasManager; +import org.labkey.api.query.AliasedColumn; +import org.labkey.api.query.CustomView; +import org.labkey.api.query.CustomViewChangeListener; +import org.labkey.api.query.CustomViewInfo; +import org.labkey.api.query.DefaultSchema; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.InvalidNamedSetException; +import org.labkey.api.query.MetadataColumnJSON; +import org.labkey.api.query.MetadataUnavailableException; +import org.labkey.api.query.QueryAction; +import org.labkey.api.query.QueryChangeListener; import org.labkey.api.query.QueryChangeListener.QueryPropertyChange; +import org.labkey.api.query.QueryDefinition; +import org.labkey.api.query.QueryException; +import org.labkey.api.query.QueryIconURLProvider; +import org.labkey.api.query.QueryParam; +import org.labkey.api.query.QueryParseException; +import org.labkey.api.query.QuerySchema; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryView; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.query.SimpleUserSchema; +import org.labkey.api.query.UserSchema; import org.labkey.api.query.column.BuiltInColumnTypes; import org.labkey.api.query.column.ColumnInfoTransformer; import org.labkey.api.query.snapshot.QuerySnapshotDefinition; @@ -100,16 +154,30 @@ import org.labkey.query.persist.ExternalSchemaDef; import org.labkey.query.persist.LinkedSchemaDef; import org.labkey.query.persist.QueryDef; +import org.labkey.query.persist.QueryDefCache; import org.labkey.query.persist.QueryManager; import org.labkey.query.persist.QuerySnapshotDef; -import org.labkey.query.sql.*; +import org.labkey.query.sql.CalculatedExpressionColumn; +import org.labkey.query.sql.Method; +import org.labkey.query.sql.QDot; +import org.labkey.query.sql.QExpr; +import org.labkey.query.sql.QField; +import org.labkey.query.sql.QIfDefined; +import org.labkey.query.sql.QInternalExpr; +import org.labkey.query.sql.QMethodCall; +import org.labkey.query.sql.QNode; +import org.labkey.query.sql.QQuery; +import org.labkey.query.sql.QRowStar; +import org.labkey.query.sql.QUnion; +import org.labkey.query.sql.Query; +import org.labkey.query.sql.QueryRelation; +import org.labkey.query.sql.QuerySelectView; +import org.labkey.query.sql.QueryTableInfo; +import org.labkey.query.sql.SqlBuilder; +import org.labkey.query.sql.SqlParser; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.servlet.mvc.Controller; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; - -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; @@ -948,6 +1016,24 @@ public QueryDefinition getQueryDef(User user, @NotNull Container container, Stri return ret.get(name); } + @Override + public @Nullable TableType getQueryDefMetadata(Container container, int rowId) + { + TableType ret = null; + QueryDef def = QueryDefCache.getQueryDefById(container, rowId); + + if (null != def) + { + TablesDocument tables = def.getParsedMetadata().getTablesDocument(new ArrayList<>()); + if (tables != null) + { + ret = tables.getTables().getTableArray(0); + } + } + + return ret; + } + /** * Get any metadata overrides for built-in tables. */ diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index dc86ed790c4..9bc7e1677a3 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -18,6 +18,10 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import org.antlr.runtime.tree.Tree; import org.apache.commons.beanutils.ConversionException; import org.apache.commons.beanutils.ConvertUtils; @@ -191,10 +195,6 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.ModelAndView; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; @@ -560,12 +560,20 @@ public ActionURL urlExecuteQuery(Container c, String schemaName, String queryNam } @Override - public ActionURL urlCreateExcelTemplate(Container c, String schemaName, String queryName) + public @NotNull ActionURL urlCreateExcelTemplate(Container c, String schemaName, String queryName) { return new ActionURL(ExportExcelTemplateAction.class, c) .addParameter(QueryParam.schemaName, schemaName) .addParameter("query.queryName", queryName); } + + @Override + public ActionURL urlMetadataQuery(Container c, String schemaName, String queryName) + { + return new ActionURL(MetadataQueryAction.class, c) + .addParameter(QueryParam.schemaName, schemaName) + .addParameter(QueryParam.queryName, queryName); + } } @Override diff --git a/query/src/org/labkey/query/persist/QueryDef.java b/query/src/org/labkey/query/persist/QueryDef.java index e9821ffcbb6..29fe795d3f5 100644 --- a/query/src/org/labkey/query/persist/QueryDef.java +++ b/query/src/org/labkey/query/persist/QueryDef.java @@ -221,5 +221,4 @@ public TablesDocument getTablesDocument(Collection errors) return _doc; } } - } diff --git a/wiki/src/org/labkey/wiki/WikiModule.java b/wiki/src/org/labkey/wiki/WikiModule.java index 6fa96e7e263..096fd62a081 100644 --- a/wiki/src/org/labkey/wiki/WikiModule.java +++ b/wiki/src/org/labkey/wiki/WikiModule.java @@ -82,7 +82,7 @@ protected void init() SiteValidationService svc = SiteValidationService.get(); if (null != svc) { - svc.registerProvider(getName(), new WikiValidationProvider()); + svc.registerProviderFactory(getName(), new WikiValidationProviderFactory()); } } diff --git a/wiki/src/org/labkey/wiki/WikiValidationProvider.java b/wiki/src/org/labkey/wiki/WikiValidationProvider.java deleted file mode 100644 index 8c2c74a2c33..00000000000 --- a/wiki/src/org/labkey/wiki/WikiValidationProvider.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.labkey.wiki; - -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.admin.sitevalidation.SiteValidationProvider; -import org.labkey.api.admin.sitevalidation.SiteValidationResultList; -import org.labkey.api.data.Container; -import org.labkey.api.security.User; -import org.labkey.api.util.CspUtils; -import org.labkey.api.util.JSoupUtil; -import org.labkey.api.util.logging.LogHelper; -import org.labkey.api.wiki.FormattedHtml; -import org.labkey.wiki.model.Wiki; -import org.labkey.wiki.model.WikiTree; -import org.w3c.dom.Document; - -import java.util.Collection; -import java.util.LinkedList; -import java.util.Map; -import java.util.Set; - -public class WikiValidationProvider implements SiteValidationProvider -{ - private static final Logger LOG = LogHelper.getLogger(WikiValidationProvider.class, "Wiki rendering exceptions"); - - @Override - public String getName() - { - return "Wiki Validator"; - } - - @Override - public String getDescription() - { - return "Detect wiki rendering and CSP violation issues"; - } - - @Override - public boolean isSiteScope() - { - return false; - } - - @Nullable - @Override - public SiteValidationResultList runValidation(Container c, User u) - { - SiteValidationResultList list = new SiteValidationResultList(); - WikiManager mgr = WikiManager.get(); - Set trees = WikiSelectManager.getWikiTrees(c); - Map nameTitleMap = WikiSelectManager.getNameTitleMap(c); - - for (WikiTree tree : trees) - { - Wiki wiki = WikiSelectManager.getWiki(c, tree.getRowId()); - if (null != wiki) - { - String title = nameTitleMap.get(wiki.getName()); - try - { - FormattedHtml html = mgr.formatWiki(c, wiki, wiki.getLatestVersion()); - Collection errors = new LinkedList<>(); - Document doc = JSoupUtil.convertHtmlToDocument(html.getHtml().toString(), false, errors); - errors.forEach(error -> addResult(list, wiki, title, "error while converting HTML to Document, \"" + error + "\"")); - if (null != doc) - { - CspUtils.enumerateCspViolations(doc, message -> addResult(list, wiki, title, message)); - } - } - catch (Exception e) - { - addResult(list, wiki, title, "exception while rendering, \"" + e.getMessage() + "\""); - LOG.error("Exception while rendering \"" + title + "\" (" + wiki.getName() + ")", e); - } - } - } - - return list; - } - - private void addResult(SiteValidationResultList list, Wiki wiki, String title, String message) - { - list.addWarn(title + " (" + wiki.getName() + "): " + message, wiki.getPageURL()); - } -} \ No newline at end of file diff --git a/wiki/src/org/labkey/wiki/WikiValidationProviderFactory.java b/wiki/src/org/labkey/wiki/WikiValidationProviderFactory.java new file mode 100644 index 00000000000..0f23dffee88 --- /dev/null +++ b/wiki/src/org/labkey/wiki/WikiValidationProviderFactory.java @@ -0,0 +1,91 @@ +package org.labkey.wiki; + +import org.apache.logging.log4j.Logger; +import org.labkey.api.admin.sitevalidation.SiteValidationProvider; +import org.labkey.api.admin.sitevalidation.SiteValidationProviderFactory; +import org.labkey.api.admin.sitevalidation.SiteValidationResultList; +import org.labkey.api.data.Container; +import org.labkey.api.security.User; +import org.labkey.api.util.CspUtils; +import org.labkey.api.util.JSoupUtil; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.wiki.FormattedHtml; +import org.labkey.wiki.model.Wiki; +import org.labkey.wiki.model.WikiTree; +import org.w3c.dom.Document; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +public class WikiValidationProviderFactory implements SiteValidationProviderFactory +{ + private static final Logger LOG = LogHelper.getLogger(WikiValidationProviderFactory.class, "Wiki rendering exceptions"); + + @Override + public String getName() + { + return "Wiki Validator"; + } + + @Override + public String getDescription() + { + return "Detect wiki rendering and CSP violation issues"; + } + + @Override + public SiteValidationProvider getSiteValidationProvider() + { + return new SiteValidationProvider() + { + @Override + public SiteValidationProviderFactory getFactory() + { + return WikiValidationProviderFactory.this; + } + + @Override + public SiteValidationResultList runValidation(Container c, User u) + { + SiteValidationResultList list = new SiteValidationResultList(); + WikiManager mgr = WikiManager.get(); + Set trees = WikiSelectManager.getWikiTrees(c); + Map nameTitleMap = WikiSelectManager.getNameTitleMap(c); + + for (WikiTree tree : trees) + { + Wiki wiki = WikiSelectManager.getWiki(c, tree.getRowId()); + if (null != wiki) + { + String title = nameTitleMap.get(wiki.getName()); + try + { + FormattedHtml html = mgr.formatWiki(c, wiki, wiki.getLatestVersion()); + Collection errors = new LinkedList<>(); + Document doc = JSoupUtil.convertHtmlToDocument(html.getHtml().toString(), false, errors); + errors.forEach(error -> addResult(list, wiki, title, "error while converting HTML to Document, \"" + error + "\"")); + if (null != doc) + { + CspUtils.enumerateCspViolations(doc, message -> addResult(list, wiki, title, message)); + } + } + catch (Exception e) + { + addResult(list, wiki, title, "exception while rendering, \"" + e.getMessage() + "\""); + LOG.error("Exception while rendering \"{}\" ({})", title, wiki.getName(), e); + } + } + } + + return list; + } + + private void addResult(SiteValidationResultList list, Wiki wiki, String title, String message) + { + list.addWarn(title + " (" + wiki.getName() + "): " + message, wiki.getPageURL()); + } + }; + } +} \ No newline at end of file