Skip to content

Commit 8952e8c

Browse files
committed
feat(tables): improve/formalize support for tables being added to more than 1 app.
- Add map of redirects to MetaDataOutput (to redirect a user from links to a table through an app that they don't have access to, instead to an app that they do have access to). - Add concept of app-affinity to QAppMetaData (where a table (etc) is placed in an app, the developer can then also specify the affinity value for that table in that app). - Update QInstance.getTablePath to return the path with the highest affinity value for a table in multiple apps Also, in MetaDataAction: - consolidate calls to PermissionsHelper.getPermissionCheckResult and customizer.allowXyz into new denyObject() method (one, to shorten all call sites, but also, to catch some cases where one was called but not the other)
1 parent 5fcabb7 commit 8952e8c

File tree

7 files changed

+1024
-63
lines changed

7 files changed

+1024
-63
lines changed

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java

Lines changed: 276 additions & 56 deletions
Large diffs are not rendered by default.

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
package com.kingsrook.qqq.backend.core.model.actions.metadata;
2323

2424

25+
import java.util.LinkedHashMap;
2526
import java.util.List;
2627
import java.util.Map;
2728
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
@@ -54,6 +55,7 @@ public class MetaDataOutput extends AbstractActionOutput
5455
private QBrandingMetaData branding;
5556
private Map<String, List<QHelpContent>> helpContents;
5657

58+
private Map<String, String> redirects;
5759

5860

5961
/*******************************************************************************
@@ -273,4 +275,68 @@ public Map<String, List<QHelpContent>> getHelpContents()
273275
{
274276
return helpContents;
275277
}
278+
279+
280+
281+
/*******************************************************************************
282+
* Getter for redirects
283+
* @see #withRedirects(Map)
284+
*******************************************************************************/
285+
public Map<String, String> getRedirects()
286+
{
287+
return (this.redirects);
288+
}
289+
290+
291+
292+
/*******************************************************************************
293+
* Setter for redirects
294+
* @see #withRedirects(Map)
295+
*******************************************************************************/
296+
public void setRedirects(Map<String, String> redirects)
297+
{
298+
this.redirects = redirects;
299+
}
300+
301+
302+
303+
/*******************************************************************************
304+
* Fluent setter for redirects
305+
*
306+
* @param redirects
307+
* Map of string to string, where the key is the URL path to redirect from, and
308+
* the value is the URL path to redirect to.
309+
*
310+
* <p>The core {@link com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction}
311+
* will build some of these redirects automatically, e.g., for the use case of a
312+
* table in multiple apps, but where a user doesn't have permission to all of those apps.</p>
313+
*
314+
* <p>An app may build its own redirects via a
315+
* {@link com.kingsrook.qqq.backend.core.actions.metadata.MetaDataActionCustomizerInterface},
316+
* for whatever custom logic is needed.</p>
317+
*
318+
* @return this
319+
*******************************************************************************/
320+
public MetaDataOutput withRedirects(Map<String, String> redirects)
321+
{
322+
this.redirects = redirects;
323+
return (this);
324+
}
325+
326+
327+
328+
/***************************************************************************
329+
* Fluent setter to add a single redirect.
330+
* @see #withRedirects(Map)
331+
***************************************************************************/
332+
public MetaDataOutput withRedirect(String from, String to)
333+
{
334+
if(this.redirects == null)
335+
{
336+
this.redirects = new LinkedHashMap<>();
337+
}
338+
this.redirects.put(from, to);
339+
return (this);
340+
}
341+
276342
}

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424

2525
import java.util.ArrayList;
2626
import java.util.Collections;
27+
import java.util.Comparator;
2728
import java.util.HashMap;
2829
import java.util.LinkedHashMap;
2930
import java.util.LinkedHashSet;
3031
import java.util.List;
3132
import java.util.Map;
33+
import java.util.Objects;
3234
import java.util.Optional;
3335
import java.util.Set;
3436
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -153,6 +155,8 @@ public class QInstance
153155
private Map<String, String> memoizedTablePaths = new HashMap<>();
154156
private Map<String, String> memoizedProcessPaths = new HashMap<>();
155157

158+
private ListingHash<String, PathWithAffinity> memoizedPathsMap = null;
159+
156160
private JoinGraph joinGraph;
157161

158162

@@ -250,9 +254,30 @@ public String getTablePath(String tableName) throws QException
250254
{
251255
QContext.withTemporaryContext(new CapturedContext(QContext.getQInstance(), new QSystemUserSession()), () ->
252256
{
253-
MetaDataInput input = new MetaDataInput();
254-
MetaDataOutput output = new MetaDataAction().execute(input);
255-
memoizedTablePaths.put(tableName, searchAppTree(output.getAppTree(), tableName, AppTreeNodeType.TABLE, ""));
257+
////////////////////////////////////////////////////////
258+
// get the (memoized) map of name to paths+affinities //
259+
////////////////////////////////////////////////////////
260+
ListingHash<String, PathWithAffinity> memoizedPathsMap = getMemoizedPathsMap();
261+
262+
//////////////////////////////////////////
263+
// get the list of paths for this table //
264+
//////////////////////////////////////////
265+
List<PathWithAffinity> pathWithAffinityList = memoizedPathsMap.get(tableName);
266+
if(CollectionUtils.nullSafeHasContents(pathWithAffinityList))
267+
{
268+
///////////////////////////////////////////////////////////////////////////
269+
// sort the list of paths by affinity (desc), and return the first entry //
270+
///////////////////////////////////////////////////////////////////////////
271+
pathWithAffinityList.sort(Comparator.comparing(PathWithAffinity::affinity).reversed());
272+
memoizedTablePaths.put(tableName, pathWithAffinityList.getFirst().path);
273+
}
274+
else
275+
{
276+
//////////////////////////////////////////////
277+
// else, if there are no paths, return null //
278+
//////////////////////////////////////////////
279+
memoizedTablePaths.put(tableName, null);
280+
}
256281
});
257282
}
258283

@@ -261,6 +286,81 @@ public String getTablePath(String tableName) throws QException
261286

262287

263288

289+
/***************************************************************************
290+
* Get the memoizedPathsMap, building it if necessary.
291+
* @see #buildPathsMap()
292+
***************************************************************************/
293+
private ListingHash<String, PathWithAffinity> getMemoizedPathsMap() throws QException
294+
{
295+
if(memoizedPathsMap == null)
296+
{
297+
memoizedPathsMap = buildPathsMap();
298+
}
299+
300+
return memoizedPathsMap;
301+
}
302+
303+
304+
305+
/***************************************************************************
306+
* build a map (ListingHash) keyed by object (e.g., table, process) names,
307+
* with values being lists (typically singletons, but for tables in multiple
308+
* apps, multi-valued) of records, containing the path to the table within
309+
* an app, and an appAffinity value - indicating which app is preferred to
310+
* be used for the table (e.g., in global contexts).
311+
*
312+
***************************************************************************/
313+
private ListingHash<String, PathWithAffinity> buildPathsMap() throws QException
314+
{
315+
MetaDataInput input = new MetaDataInput();
316+
MetaDataOutput output = new MetaDataAction().execute(input);
317+
ListingHash<String, PathWithAffinity> pathsMap = new ListingHash<>();
318+
populatePathsMap(pathsMap, output.getAppTree(), "");
319+
return (pathsMap);
320+
}
321+
322+
323+
324+
/***************************************************************************
325+
* recursive implementation used by buildPathsMap() to traverse the appTree
326+
* and populate the pathsMap.
327+
*
328+
* @param pathsMap the map to populate (output parameter)
329+
* @param appTreeNodes the nodes to process (for top-level call should be
330+
* the app tree root nodes (from MetaDataAction).
331+
* @param path the current path being built (e.g., accumulates entries with
332+
* recursive calls)
333+
* @see #buildPathsMap()
334+
***************************************************************************/
335+
private void populatePathsMap(ListingHash<String, PathWithAffinity> pathsMap, List<AppTreeNode> appTreeNodes, String path)
336+
{
337+
for(AppTreeNode appTreeNode : appTreeNodes)
338+
{
339+
if(appTreeNode.getType().equals(AppTreeNodeType.APP) && CollectionUtils.nullSafeHasContents(appTreeNode.getChildren()))
340+
{
341+
populatePathsMap(pathsMap, appTreeNode.getChildren(), path + "/" + appTreeNode.getName());
342+
}
343+
else
344+
{
345+
Integer affinity = Objects.requireNonNullElse(appTreeNode.getAppAffinity(), Integer.MIN_VALUE);
346+
pathsMap.add(appTreeNode.getName(), new PathWithAffinity(path + "/" + appTreeNode.getName(), affinity));
347+
}
348+
}
349+
}
350+
351+
352+
353+
/***************************************************************************
354+
* private record to associate a path (through apps to a table, process, etc)
355+
* with an affinity value indicating which app is preferred to be used for
356+
* the table (e.g., in global contexts).
357+
***************************************************************************/
358+
private record PathWithAffinity(String path, Integer affinity)
359+
{
360+
}
361+
362+
363+
264364
/*******************************************************************************
265365
* Get the full path to a process - note - this will be regardless of whether or
266366
* not the active session/user has access to the process. You may want to also call

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
** These objects are organized into a tree - where each Node can have 0 or more
4040
** other Nodes as children.
4141
*******************************************************************************/
42-
public class AppTreeNode
42+
public class AppTreeNode implements Cloneable
4343
{
4444
private AppTreeNodeType type;
4545
private String name;
4646
private String label;
4747
private List<AppTreeNode> children;
4848

49+
private Integer appAffinity;
50+
4951
private QIcon icon;
5052

5153

@@ -166,4 +168,67 @@ public void addChild(AppTreeNode childTreeNode)
166168
}
167169
children.add(childTreeNode);
168170
}
171+
172+
173+
174+
/*******************************************************************************
175+
* Getter for appAffinity
176+
* @see #withAppAffinity(Integer)
177+
*******************************************************************************/
178+
public Integer getAppAffinity()
179+
{
180+
return (this.appAffinity);
181+
}
182+
183+
184+
185+
/*******************************************************************************
186+
* Setter for appAffinity
187+
* @see #withAppAffinity(Integer)
188+
*******************************************************************************/
189+
public void setAppAffinity(Integer appAffinity)
190+
{
191+
this.appAffinity = appAffinity;
192+
}
193+
194+
195+
196+
/*******************************************************************************
197+
* Fluent setter for appAffinity
198+
*
199+
* @param appAffinity
200+
* appAffinity level for this child node, as it is related to its parent app.
201+
* See {@link QAppMetaData#setChildAppAffinity(String, Integer)}.
202+
* @return this
203+
*******************************************************************************/
204+
public AppTreeNode withAppAffinity(Integer appAffinity)
205+
{
206+
this.appAffinity = appAffinity;
207+
return (this);
208+
}
209+
210+
211+
212+
/***************************************************************************
213+
*
214+
***************************************************************************/
215+
@Override
216+
public AppTreeNode clone()
217+
{
218+
try
219+
{
220+
AppTreeNode clone = (AppTreeNode) super.clone();
221+
if(children != null)
222+
{
223+
clone.children = new ArrayList<>();
224+
children.forEach(child -> clone.children.add(child.clone()));
225+
}
226+
return clone;
227+
}
228+
catch(CloneNotSupportedException e)
229+
{
230+
throw new AssertionError();
231+
}
232+
}
233+
169234
}

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@
4444
*******************************************************************************/
4545
public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface
4646
{
47+
public static final Integer DEFAULT_SORT_ORDER = 500;
48+
4749
private String name;
4850
private String label;
4951

50-
private Integer sortOrder = 500;
52+
private Integer sortOrder = DEFAULT_SORT_ORDER;
5153

5254
private QPermissionRules permissionRules;
5355

@@ -59,6 +61,8 @@ public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRu
5961
private List<String> widgets;
6062
private List<QAppSection> sections;
6163

64+
private Map<String, Integer> childAppAffinities;
65+
6266
private Map<String, QSupplementalAppMetaData> supplementalMetaData;
6367

6468

@@ -521,4 +525,44 @@ public QAppMetaData withSupplementalMetaData(Map<String, QSupplementalAppMetaDat
521525
return (this);
522526
}
523527

528+
529+
530+
/***************************************************************************
531+
* set an appAffinity for a child. Higher values are higher affinity, with
532+
* null (the default) considered the lowest. Tiebreaking behavior is not
533+
* specified and may differ by use-case (e.g., by front-end), though app
534+
* sort-order is recommended.
535+
*
536+
* <p>The purpose of this appAffinity is not about ordering the children of this
537+
* app - but rather - for cases where the same object (e.g. a table) is
538+
* referenced by multiple apps, and we want to control which app should be
539+
* displayed first when the object is referenced.</p>
540+
*
541+
* @param childName name of child (e.g., a table name) to set appAffinity for
542+
* @param appAffinity appAffinity level, or null to clear any existing appAffinity.
543+
* Higher values are higher affinity. null is lowest.
544+
***************************************************************************/
545+
public void setChildAppAffinity(String childName, Integer appAffinity)
546+
{
547+
if(this.childAppAffinities == null)
548+
{
549+
this.childAppAffinities = new HashMap<>();
550+
}
551+
552+
this.childAppAffinities.put(childName, appAffinity);
553+
}
554+
555+
556+
557+
/***************************************************************************
558+
* get the appAffinity assigned to this child (by name) for this app - null if not set.
559+
*
560+
* @param childName name of child (e.g., a table name) to get appAffinity for
561+
* @return appAffinity level, or null if not set.
562+
***************************************************************************/
563+
public Integer getChildAppAffinity(String childName)
564+
{
565+
return (this.childAppAffinities == null ? null : this.childAppAffinities.get(childName));
566+
}
567+
524568
}

0 commit comments

Comments
 (0)