Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions api/src/org/labkey/api/action/BaseViewAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;

import java.beans.PropertyDescriptor;
Expand All @@ -76,7 +75,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
Expand Down Expand Up @@ -432,13 +430,13 @@ static BindingErrorProcessor getBindingErrorProcessor(final BindingErrorProcesso
return new BindingErrorProcessor()
{
@Override
public void processMissingFieldError(String missingField, BindingResult bindingResult)
public void processMissingFieldError(@NotNull String missingField, @NotNull BindingResult bindingResult)
{
defaultBEP.processMissingFieldError(missingField, bindingResult);
}

@Override
public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult)
public void processPropertyAccessException(@NotNull PropertyAccessException ex, @NotNull BindingResult bindingResult)
{
Object newValue = ex.getPropertyChangeEvent().getNewValue();
if (newValue instanceof String)
Expand Down
24 changes: 20 additions & 4 deletions api/src/org/labkey/api/action/FormViewAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@

package org.labkey.api.action;

import org.jetbrains.annotations.NotNull;
import org.labkey.api.data.ObjectFactory;
import org.labkey.api.miniprofiler.MiniProfiler;
import org.labkey.api.miniprofiler.Timing;
import org.labkey.api.util.ExceptionUtil;
import org.labkey.api.util.URLHelper;
import org.labkey.api.view.HttpView;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Is this better than BaseCommandController? Probably not, but it understands TableViewForm.
Expand Down Expand Up @@ -116,7 +121,7 @@ public ModelAndView handleRequest(FORM form, BindException errors) throws Except
if (errors != null && errors.hasErrors())
{
StringBuilder errorTextBuilder = new StringBuilder();
String newLine = System.getProperty("line.separator");
String newLine = System.lineSeparator();
List<ObjectError> errorsList = errors.getAllErrors();

for (int i = 0; i < errorsList.size(); i++)
Expand All @@ -136,7 +141,6 @@ public ModelAndView handleRequest(FORM form, BindException errors) throws Except
}
}


@Override
protected String getCommandClassMethodName()
{
Expand All @@ -145,11 +149,23 @@ protected String getCommandClassMethodName()

public BindException bindParameters(PropertyValues m) throws Exception
{
return defaultBindParameters(getCommand(), m);
Class<?> commandClass = getCommandClass();
return commandClass.isRecord() ? defaultBindParametersToRecord(commandClass, m) : defaultBindParameters(getCommand(), m);
}

// Very simple binding for Java records: no support for binding errors, arrays, lists, disallowed fields, etc.
private <R> BindException defaultBindParametersToRecord(Class<R> recordClass, PropertyValues m)
{
ObjectFactory<R> factory = ObjectFactory.Registry.getFactory(recordClass);
Map<String, Object> map = m.stream()
.filter(pv -> pv.getValue() != null)
.collect(Collectors.toMap(PropertyValue::getName, PropertyValue::getValue));
R record = factory.fromMap(map);
return new NullSafeBindException(record, "Form");
}

@Override
public void validate(Object target, Errors errors)
public void validate(@NotNull Object target, @NotNull Errors errors)
{
if (target instanceof HasValidator)
{
Expand Down
7 changes: 5 additions & 2 deletions devtools/src/org/labkey/devtools/DevtoolsModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ protected void init()
addController("testsso", TestSsoController.class);
AuthenticationManager.registerProvider(new TestSsoProvider());

OptionalFeatureService.get().addExperimentalFeatureFlag(Domain.EXPERIMENTAL_FUZZ_STORAGE_NAME,
OptionalFeatureService.get().addExperimentalFeatureFlag(
Domain.EXPERIMENTAL_FUZZ_STORAGE_NAME,
"'fuzz' name of database columns used to back domain properties",
"This is dev/test feature and not intended for any production usage.",
false, true);
false,
true
);
}

@Override
Expand Down
75 changes: 49 additions & 26 deletions devtools/src/org/labkey/devtools/ToolsController.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.labkey.api.action.SimpleErrorView;
import org.labkey.api.action.SimpleViewAction;
import org.labkey.api.action.SpringActionController;
import org.labkey.api.cache.Cache;
import org.labkey.api.cache.CacheManager;
import org.labkey.api.collections.ArrayListValuedTreeMap;
import org.labkey.api.collections.LabKeyCollectors;
import org.labkey.api.data.BaseColumnInfo;
Expand All @@ -18,7 +20,9 @@
import org.labkey.api.data.DbSchemaType;
import org.labkey.api.data.DbScope;
import org.labkey.api.data.FileSqlScriptProvider;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.SchemaTableInfo;
import org.labkey.api.data.SqlSelector;
import org.labkey.api.data.TableInfo;
import org.labkey.api.data.TableInfo.IndexDefinition;
import org.labkey.api.data.TableInfo.IndexType;
Expand All @@ -36,6 +40,7 @@
import org.labkey.api.util.Formats;
import org.labkey.api.util.HtmlString;
import org.labkey.api.util.HtmlStringBuilder;
import org.labkey.api.util.LinkBuilder;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.util.StringUtilsLabKey;
import org.labkey.api.util.URLHelper;
Expand Down Expand Up @@ -68,6 +73,7 @@
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -105,7 +111,7 @@ public class BeginAction extends SimpleViewAction<Object>
@Override
public ModelAndView getView(Object o, BindException errors)
{
return new ActionListView(ToolsController.this, actionDescriptor->BeginAction.class != actionDescriptor.getActionClass());
return new ActionListView(ToolsController.this, actionDescriptor -> BeginAction.class != actionDescriptor.getActionClass());
}

@Override
Expand Down Expand Up @@ -163,7 +169,7 @@ public void handle(Path gaPath, Stream<String> stream)
{
out.println("Files listed in " + gaPath + " that don't exist:\n");
List<String> missing = getMissingFiles(gaPath, stream);
missing.forEach(filename->out.println(filter(filename)));
missing.forEach(filename -> out.println(filter(filename)));
if (!missing.isEmpty())
{
out.println();
Expand Down Expand Up @@ -380,14 +386,14 @@ protected void renderInternal(Object model, PrintWriter out)
Set<String> copyOfJspFiles = new HashSet<>(jspFiles);

jspFiles.removeAll(jspReferences);
jspFiles.forEach(path->out.println(filter(path)));
jspFiles.forEach(path -> out.println(filter(path)));

out.println();
out.println("JSP references that couldn't be resolved to JSP files [plus any candidates for resolution]:");
out.println();

jspReferences.removeAll(copyOfJspFiles);
jspReferences.forEach(path-> {
jspReferences.forEach(path -> {
List<String> candidates = jspFiles.stream()
.filter(s -> s.endsWith(path))
.toList();
Expand All @@ -410,7 +416,7 @@ protected void renderInternal(Object model, PrintWriter out)
out.println();
out.println("The following " + (jspFiles.size() == 1 ? "JSP file is a strong candidate" : jspFiles.size() + " JSP files are strong candidates") + " for removal:");
out.println();
jspFiles.forEach(path->out.println(filter(path)));
jspFiles.forEach(path -> out.println(filter(path)));
}

out.println("</pre>");
Expand Down Expand Up @@ -461,7 +467,8 @@ private Collection<String> findJspReferences(Module module, PrintWriter out)
String code = PageFlowUtil.getFileContentsAsString(file.toFile());
JavaScanner scanner = new JavaScanner(code);

scanner.scan(0, new Handler(){
scanner.scan(0, new Handler()
{
@Override
public boolean string(int beginIndex, int endIndex)
{
Expand Down Expand Up @@ -556,7 +563,7 @@ public ModelAndView getView(Object o, BindException errors) throws IOException

List<ControllerActionId> actionIds = new LinkedList<>();

// As of now, Crawler.java and the study tests are the only classes that specify crawler actions
// As of now, these are the only classes that specify crawler actions
for (String path : List.of(
sourcePath + "/../../clientModules/adjudication/test/src/org/labkey/test/tests/adjudication/AdjudicationAbstractBaseTest.java",
sourcePath + "/../../ehrModules/ehr/test/src/org/labkey/test/tests/ehr/ComplianceTrainingTest.java",
Expand Down Expand Up @@ -603,7 +610,7 @@ public ModelAndView getView(Object o, BindException errors) throws IOException
builder
.append("The following " + (missingModuleActions.size() > 1 ? "actions' controllers" : "action's controller") + " could not be resolved to a module running in this deployment:")
.unsafeAppend("<br><br>\n");
missingModuleActions.forEach(id->builder.append(id.toString()).unsafeAppend("<br>\n"));
missingModuleActions.forEach(id -> builder.append(id.toString()).unsafeAppend("<br>\n"));
builder.unsafeAppend("<br>\n");
builder.append("The associated module(s) might not support " + DbScope.getLabKeyScope().getDatabaseProductName() + ".");
builder.unsafeAppend("<br><br>\n");
Expand All @@ -614,7 +621,7 @@ public ModelAndView getView(Object o, BindException errors) throws IOException
builder
.append("The following " + (missingActions.size() > 1 ? "actions were" : "action was") + " not found in the action's controller:")
.unsafeAppend("<br><br>\n");
missingActions.forEach(id->builder.append(id.toString()).unsafeAppend("<br>\n"));
missingActions.forEach(id -> builder.append(id.toString()).unsafeAppend("<br>\n"));
}

return new HtmlView(builder);
Expand Down Expand Up @@ -734,9 +741,9 @@ public void addNavTrail(NavTree root)
public class OverlappingIndicesAction extends AbstractOverlappingIndicesAction
{
@Override
public ModelAndView getView(Object o, boolean reshow, BindException errors)
public ModelAndView getView(OverlappingIndicesForm form, boolean reshow, BindException errors)
{
MultiValuedMap<OverlapType, Overlap> multiMap = getOverlappingIndices();
MultiValuedMap<OverlapType, Overlap> multiMap = getOverlappingIndices(form);

return new VBox(
new HtmlView(DOM.createHtmlFragment(
Expand All @@ -747,7 +754,7 @@ public ModelAndView getView(Object o, boolean reshow, BindException errors)
DOM.TABLE(
multiMap.get(type).stream()
.map(overlap -> DOM.TR(
DOM.TD(at(style, "width:120px;"), overlap.schemaName()),
DOM.TD(at(style, "width:120px;"), LinkBuilder.simpleLink(overlap.schemaName(), new ActionURL(OverlappingIndicesAction.class, getContainer()).addParameter("schemaName", overlap.schemaName()))),
DOM.TD(type.getMessage(overlap)),
"\n"
))
Expand All @@ -757,7 +764,7 @@ public ModelAndView getView(Object o, boolean reshow, BindException errors)
)),
new HtmlView(DOM.createHtmlFragment(
BR(),
new ButtonBuilder("Create SQL Scripts That Drop Overlapping Indices").href(OverlappingIndicesAction.class, getContainer()).usePost())
new ButtonBuilder("Create SQL Scripts That Drop Overlapping Indices").href(getViewContext().getActionURL()).usePost())
)
);
}
Expand All @@ -770,9 +777,9 @@ public void addNavTrail(NavTree root)
}

@Override
public boolean handlePost(Object o, BindException errors)
public boolean handlePost(OverlappingIndicesForm form, BindException errors)
{
MultiValuedMap<OverlapType, Overlap> multiMap = getOverlappingIndices();
MultiValuedMap<OverlapType, Overlap> multiMap = getOverlappingIndices(form);

try
{
Expand Down Expand Up @@ -896,26 +903,29 @@ private void closeAllContexts()
}

@Override
public void validateCommand(Object target, Errors errors)
public void validateCommand(OverlappingIndicesForm form, Errors errors)
{
}

@Override
public URLHelper getSuccessURL(Object o)
public URLHelper getSuccessURL(OverlappingIndicesForm form)
{
return new ActionURL(BeginAction.class, getContainer());
}
}

protected static abstract class AbstractOverlappingIndicesAction extends FormViewAction<Object>
public record OverlappingIndicesForm(String schemaName) {}

protected static abstract class AbstractOverlappingIndicesAction extends FormViewAction<OverlappingIndicesForm>
{
protected MultiValuedMap<OverlapType, Overlap> getOverlappingIndices()
protected MultiValuedMap<OverlapType, Overlap> getOverlappingIndices(OverlappingIndicesForm form)
{
MultiValuedMap<OverlapType, Overlap> multiMap = new ArrayListValuedHashMap<>();
DbScope scope = DbScope.getLabKeyScope();

ModuleLoader.getInstance().getModules().stream()
.flatMap(module -> module.getSchemaNames().stream().filter(name -> !module.getProvisionedSchemaNames().contains(name)))
.filter(schemaName -> form.schemaName() == null || schemaName.equals(form.schemaName()))
.sorted(String.CASE_INSENSITIVE_ORDER)
.map(name -> scope.getSchema(name, DbSchemaType.Module))
.flatMap(schema -> schema.getTableNames().stream().map(schema::getTable))
Expand Down Expand Up @@ -972,13 +982,6 @@ private String getKey(List<ColumnInfo> cols)
.map(col -> col.getName().toLowerCase())
.collect(Collectors.joining(delim)) + delim;
}

private List<String> join(List<ColumnInfo> cols)
{
return cols.stream()
.map(ColumnInfo::getName)
.toList();
}
}

protected record Overlap(String schemaName, String tableName, IndexDefinition indexDef1, IndexDefinition indexDef2) {}
Expand Down Expand Up @@ -1103,6 +1106,26 @@ protected void dropIndex(Writer writer, String schemaName, String tableName, Str
}
}

private record IndexKey(String schemaName, String indexName) {}

// Most, but not all, unique indexes are created by adding a unique constraint; in those cases, we need to drop
// the associated constraint. However, for explicitly created unique indexes, we need to drop the index instead.
// If this is a unique index associated with a constraint, return that constraint name. Otherwise, return null.
private static @Nullable String getConstraintForIndex(String schemaName, String indexName)
{
Cache<String, Map<IndexKey, String>> sharedCache = CacheManager.getSharedCache();
var constraintMap = sharedCache.get("ConstraintForIndexMap", null, (_, _) -> Collections.unmodifiableMap(
new SqlSelector(DbScope.getLabKeyScope(), new SQLFragment("""
SELECT NspName AS SchemaName, RelName AS IndexName, ConName AS ConstraintName FROM pg_index i
INNER JOIN pg_class cl ON cl.oid = i.indexrelid
INNER JOIN pg_namespace schema ON schema.oid = cl.relnamespace
INNER JOIN pg_constraint c ON ConNamespace = schema.oid AND ConIndId = cl.oid AND ConType = 'u'
WHERE IndIsUnique AND NOT NspName IN ('pg_toast', 'pg_catalog')"""
)).mapStream()
.collect(Collectors.toMap(map -> new IndexKey((String)map.get("SchemaName"), (String)map.get("IndexName")), map -> (String)map.get("ConstraintName")))));
return constraintMap.get(new IndexKey(schemaName, indexName));
}

@RequiresPermission(AdminPermission.class)
public class ForeignKeysAction extends SimpleViewAction<Object>
{
Expand Down
Loading