Skip to content
7 changes: 7 additions & 0 deletions announcements/resources/schemas/comm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -649,4 +649,11 @@
</column>
</columns>
</table>
<table tableName="PageAliases" tableDbType="TABLE">
<columns>
<column columnName="Container"/>
<column columnName="Alias"/>
<column columnName="PageRowId"/>
</columns>
</table>
</tables>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE comm.PageAliases
(
Container ENTITYID NOT NULL,
Alias VARCHAR(255) NOT NULL,
RowId INT NOT NULL,

CONSTRAINT PK_PageAliases PRIMARY KEY (Container, Alias)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE comm.PageAliases RENAME COLUMN RowId TO PageRowId;

-- Aliases should be case-insensitive
ALTER TABLE comm.PageAliases DROP CONSTRAINT PK_PageAliases;
CREATE UNIQUE INDEX UQ_PageAliases ON comm.PageAliases (Container, LOWER(Alias));
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE comm.PageAliases
(
Container ENTITYID NOT NULL,
Alias NVARCHAR(255) NOT NULL,
RowId INT NOT NULL,

CONSTRAINT PK_PageAliases PRIMARY KEY (Container, Alias)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EXEC sp_rename 'comm.PageAliases.RowId', 'PageRowId', 'COLUMN';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Switch from PK to UNIQUE INDEX to match PostgreSQL
ALTER TABLE comm.PageAliases DROP CONSTRAINT PK_PageAliases;
CREATE UNIQUE INDEX UQ_PageAliases ON comm.PageAliases (Container, Alias);
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public String getName()
@Override
public @Nullable Double getSchemaVersion()
{
return 22.000;
return 22.003;
}

@Override
Expand Down
17 changes: 17 additions & 0 deletions api/src/org/labkey/api/collections/LabKeyCollectors.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ public class LabKeyCollectors
);
}

/**
* Returns a {@link Collector} that builds a {@link CaseInsensitiveHashMap}
*/
public static <T, U> Collector<T, ?, Map<String, U>> toCaseInsensitiveMap(
Function<? super T, String> keyMapper,
Function<? super T, ? extends U> valueMapper)
{
return toMap(
keyMapper,
valueMapper,
(u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
},
CaseInsensitiveHashMap::new
);
}

/**
* Returns a {@link Collector} that accumulates elements into a {@link MultiValuedMap} whose keys and values are the
* result of applying the provided mapping functions to the input elements, an approach that mimics {@link Collectors#toMap(Function, Function)}.
Expand Down
18 changes: 11 additions & 7 deletions api/src/org/labkey/api/util/ExceptionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -632,10 +632,10 @@ static ActionURL handleException(@NotNull HttpServletRequest request, @NotNull H
}

// Do redirects before response.reset() otherwise we'll lose cookies (e.g., login page)
if (ex instanceof RedirectException)
if (ex instanceof RedirectException rex)
{
String url = ((RedirectException) ex).getURL();
doErrorRedirect(response, url);
String url = rex.getURL();
doErrorRedirect(response, url, rex.getHttpStatusCode());
return null;
}

Expand Down Expand Up @@ -1000,10 +1000,16 @@ private static void addDependenciesAndRender(int responseStatus, PageConfig page
errorView.getView().render(errorView.getModel(), request, response);
}


// Temporary redirect
public static void doErrorRedirect(HttpServletResponse response, String url)
{
response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
doErrorRedirect(response, url, HttpServletResponse.SC_MOVED_TEMPORARILY);
}

// Pass in HTTP status code to designate temporary vs. permanent redirect
private static void doErrorRedirect(HttpServletResponse response, String url, int httpStatusCode)
{
response.setStatus(httpStatusCode);
response.setDateHeader("Expires", 0);
response.setHeader("Location", url);
response.setContentType("text/html; charset=UTF-8");
Expand All @@ -1025,8 +1031,6 @@ public static void doErrorRedirect(HttpServletResponse response, String url)
}
}



public enum ExceptionInfo
{
ResolveURL, // suggestion for where to fix this e.g. sourceQuery.view
Expand Down
21 changes: 21 additions & 0 deletions api/src/org/labkey/api/view/PermanentRedirectException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.labkey.api.view;

import org.jetbrains.annotations.NotNull;
import org.labkey.api.util.URLHelper;

import javax.servlet.http.HttpServletResponse;

/** Use when we want search engines, browsers, etc to assume that the redirecting URL is defunct and the target URL should be used going forward */
public class PermanentRedirectException extends RedirectException
{
public PermanentRedirectException(@NotNull URLHelper url)
{
super(url);
}

@Override
public int getHttpStatusCode()
{
return HttpServletResponse.SC_MOVED_PERMANENTLY;
}
}
13 changes: 10 additions & 3 deletions api/src/org/labkey/api/view/RedirectException.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
import org.labkey.api.util.SkipMothershipLogging;
import org.labkey.api.util.URLHelper;

import javax.servlet.http.HttpServletResponse;

/**
* When thrown in the context of an HTTP request, sends the client a redirect in the HTTP response. Not treated
* as a loggable error.
* When thrown in the context of an HTTP request, sends the client a *temporary* redirect in the HTTP response. Not
* treated as a loggable error. See {@link PermanentRedirectException} if a permanent redirect is desired.
*/
public class RedirectException extends RuntimeException implements SkipMothershipLogging
{
String _url;
private final String _url;

public RedirectException(@NotNull URLHelper url)
{
Expand All @@ -41,4 +43,9 @@ public String getURL()
{
return _url;
}

public int getHttpStatusCode()
{
return HttpServletResponse.SC_MOVED_TEMPORARILY;
}
}
5 changes: 5 additions & 0 deletions internal/src/org/labkey/api/announcements/CommSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,10 @@ public TableInfo getTableInfoTours()
{
return getSchema().getTable("Tours");
}

public TableInfo getTableInfoPageAliases()
{
return getSchema().getTable("PageAliases");
}
}

2 changes: 1 addition & 1 deletion search/src/org/labkey/search/SearchController.java
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ public boolean handlePost(AdminForm form, BindException errors)
SearchService ss = SearchService.get();
if (null == ss)
{
errors.reject("Indexing service is not running");
errors.reject(ERROR_MSG, "Indexing service is not running");
return false;
}

Expand Down
29 changes: 1 addition & 28 deletions wiki/resources/web/wiki/internal/wikiEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function tinyMceHandleEvent(evt) {
var bindControls = function(props) {
// form controls
var setDirty = function(){LABKEY.setDirty(true)};
$(_idSel + 'name').keypress(setDirty).change(onChangeName);
$(_idSel + 'name').keypress(setDirty).change(setDirty);
$(_idSel + 'title').keypress(setDirty).change(setDirty);
$(_idSel + 'parent').keypress(setDirty).change(setDirty);
$(_idSel + 'body').keypress(setDirty).change(setDirty);
Expand Down Expand Up @@ -363,33 +363,6 @@ function tinyMceHandleEvent(evt) {
window.location = _cancelUrl ? _cancelUrl : getRedirUrl();
};

var onChangeName = function() {
//if this is an existing page, warn the user about changing the name
if (_wikiProps.entityId) {
getExt4(function() {
Ext4.Msg.show({
title: 'Warning',
msg: "Changing the name of this page will break any links to this page embedded in other pages. Are you sure you want to change the name?",
buttons: Ext4.MessageBox.YESNO,
icon: Ext4.MessageBox.WARNING,
fn: function(btnId) {
if (btnId == "yes") {
LABKEY.setDirty(true);
_redirUrl = ''; // clear the redir URL since it will be referring to the old name
onSave();
}
else {
updateControl("name", _wikiProps.name);
}
}
});
});
}
else {
LABKEY.setDirty(true);
}
};

var onConvertSuccess = function(response) {
var respJson = LABKEY.Utils.decode(response.responseText);

Expand Down
55 changes: 29 additions & 26 deletions wiki/src/org/labkey/wiki/WikiCollections.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.announcements.CommSchema;
import org.labkey.api.collections.LabKeyCollectors;
import org.labkey.api.data.Container;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.SimpleFilter;
import org.labkey.api.data.SqlSelector;
import org.labkey.api.data.TableSelector;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.view.NavTree;
import org.labkey.wiki.model.WikiTree;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
Expand All @@ -51,6 +56,8 @@ public class WikiCollections
private final Map<String, WikiTree> _treesByName;
private final Map<String, String> _nameTitleMap;
private final List<String> _names;
private final Map<String, String> _namesByAlias;
private final MultiValuedMap<Integer, String> _aliasesByRowsId;

private final List<NavTree> _adminNavTree;
private final List<NavTree> _nonAdminNavTree;
Expand Down Expand Up @@ -115,8 +122,18 @@ public WikiCollections(Container c)

_adminNavTree = createNavTree(c, true);
_nonAdminNavTree = createNavTree(c, false);

_aliasesByRowsId = new TableSelector(CommSchema.getInstance().getTableInfoPageAliases(), PageFlowUtil.set("Alias", "PageRowId"), SimpleFilter.createContainerFilter(c), null)
.mapStream()
.map(map->new Alias((Integer)map.get("PageRowId"), (String)map.get("Alias")))
.sorted(Comparator.comparing(Alias::alias, String.CASE_INSENSITIVE_ORDER))
.collect(LabKeyCollectors.toMultiValuedMap(record->record.pageRowId, record->record.alias));
_namesByAlias = _aliasesByRowsId.entries().stream()
.filter(e->_treesByRowId.get(e.getKey()) != null) // Just in case - ignore orphaned aliases
.collect(LabKeyCollectors.toCaseInsensitiveMap(Map.Entry::getValue, e->_treesByRowId.get(e.getKey()).getName()));
}

public record Alias(int pageRowId, String alias) {}

private void populateWikiTree(WikiTree parent, MultiValuedMap<Integer, Integer> childMap, Map<Integer, WikiTree> treesByRowId)
{
Expand All @@ -135,7 +152,6 @@ private void populateWikiTree(WikiTree parent, MultiValuedMap<Integer, Integer>
}
}


// Create name list in depth-first order
private void populateNames(WikiTree root, List<String> names)
{
Expand Down Expand Up @@ -177,19 +193,11 @@ private List<NavTree> createNavTree(Container c, String rootId, WikiTree tree, b
return elements;
}


int getPageCount()
{
return getNames().size();
}


WikiTree getWikiTree()
{
return _root;
}


@NotNull List<String> getNames()
{
return _names;
Expand Down Expand Up @@ -218,23 +226,6 @@ List<NavTree> getNonAdminNavTree()
return _treesByRowId.get(rowId);
}

// Returns null for non-existent wiki, empty collection for existing but no children
@Nullable Collection<WikiTree> getChildren(@Nullable String parentName)
{
WikiTree parent = getWikiTree(parentName);

if (null == parent)
return null;

return parent.getChildren();
}

// Returns null for non-existent wiki, empty collection for existing but no children
@Nullable Collection<WikiTree> getChildren(int rowId)
{
return _treesByRowId.get(rowId).getChildren();
}

// TODO: Change to return the root WikiTree?
Map<String, String> getNameTitleMap()
{
Expand Down Expand Up @@ -275,4 +266,16 @@ private Set<WikiTree> populateWikiTrees(WikiTree root, Set<WikiTree> trees)

return trees;
}

// Ordered by alias (case-insensitive)
Collection<String> getAliases(int rowId)
{
return _aliasesByRowsId.get(rowId);
}

// Returns null for no match
@Nullable String getNameForAlias(@Nullable String alias)
{
return _namesByAlias.get(alias);
}
}
Loading