Changeset 7895


Ignore:
Timestamp:
Jan 18, 2021, 11:46:25 AM (2 years ago)
Author:
Nicklas Nordborg
Message:

References #2237: Implement extension mechanism for query filtering

Started to work on this. A new action interface QueryFilterAction should be implemented by extensions.

This will probably take effect now for all list pages and item list filters, etc. However, To be really useful for list pages, an extension probably want to pass information also to a related ListColumnAction. For example, the filter implementation will find a list of item ID:s that are used in the query and the column implementation will use cached information to display matched values in the correspoding column.

For this to work, the list pages need to call the new getConfiguredQuery() that also takes a ClientContext parameter. We can probably use the JspContext that is already created in all list pages if we re-order some things in the code.

Location:
trunk/src
Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/clients/web/net/sf/basedb/clients/web/Base.java

    r7890 r7895  
    7474import net.sf.basedb.util.ColorGenerator;
    7575import net.sf.basedb.util.Values;
     76import net.sf.basedb.util.extensions.ClientContext;
    7677import net.sf.basedb.util.filter.Filter;
    7778import net.sf.basedb.util.filter.StaticFilter;
     
    10141015 
    10151016  /**
     1017    @deprecated In 3.18, use {@link #getConfiguredQuery(DbControl, ItemContext, ClientContext, boolean, ItemQuery, ModeInfo)} instead
     1018  */
     1019  @Deprecated
     1020  public static <T extends BasicItem> ItemQuery<T> getConfiguredQuery(DbControl dc, ItemContext cc, boolean autoLeftJoin, ItemQuery<T> query, ModeInfo mode)
     1021  {
     1022    return getConfiguredQuery(dc, cc, null, autoLeftJoin, query, mode);
     1023  }
     1024 
     1025  /**
    10161026    Configures a query. This method calls {@link ItemContext#configureQuery(DbControl, EntityQuery, boolean)}
    10171027    and then adds the following filter:
     
    10291039    @param mode Info about the mode the page has.
    10301040    @return The same query, configured according to information in the current context
    1031     @since 2.8
    1032   */
    1033   public static <T extends BasicItem> ItemQuery<T> getConfiguredQuery(DbControl dc, ItemContext cc, boolean autoLeftJoin, ItemQuery<T> query, ModeInfo mode)
    1034   {
    1035     cc.configureQuery(dc, query, autoLeftJoin);
     1041    @since 3.18
     1042  */
     1043  public static <T extends BasicItem> ItemQuery<T> getConfiguredQuery(DbControl dc, ItemContext cc, ClientContext context, boolean autoLeftJoin, ItemQuery<T> query, ModeInfo mode)
     1044  {
     1045    cc.configureQuery(dc, query, context, autoLeftJoin);
    10361046    final String exclude = (String)cc.getObject("exclude");
    10371047    if (exclude != null)
  • trunk/src/core/core-extensions.xml

    r7535 r7895  
    167167  </extension-point>
    168168 
     169  <extension-point
     170    id="net.sf.basedb.query.entity-query-filter">
     171    <action-class>net.sf.basedb.core.query.QueryFilterAction</action-class>
     172    <name>Query filters</name>
     173    <description>
     174      Extension point for implementing extra filter functionality for
     175      queries generated by the ItemContext.configureQuery() method.
     176      If the call is initiated from the web client the client context is
     177      a JspContext, otherwise it is a ClientContext instance. In both cases
     178      the current item is the ItemContext instance. The ItemContext.getQuery()
     179      method returns the query that was passed to the configureQuery() method.
     180      Factory implementations are recommended to check ItemContext.hasExtensionFilter()
     181      before returning actions, and QueryFilterAction implementations should check
     182      PropertyFilter.isExtensionFilter().
     183    </description>
     184  </extension-point>
     185 
    169186  <extension
    170187    id="net.sf.basedb.core.uri.http-connection-manager"
  • trunk/src/core/net/sf/basedb/core/ItemContext.java

    r7813 r7895  
    2424
    2525import net.sf.basedb.core.query.Annotations;
     26import net.sf.basedb.core.query.QueryFilterAction;
    2627import net.sf.basedb.core.query.EntityQuery;
    2728import net.sf.basedb.core.query.Expressions;
     
    4748import net.sf.basedb.util.encode.EncodeUtil;
    4849import net.sf.basedb.util.encode.TabCrLfEncoderDecoder;
     50import net.sf.basedb.util.extensions.ClientContext;
     51import net.sf.basedb.util.extensions.ExtensionsInvoker;
     52import net.sf.basedb.util.extensions.manager.ExtensionsManager;
    4953import net.sf.basedb.util.filter.Filter;
    5054import net.sf.basedb.util.jep.Jep;
     
    814818 
    815819  /**
     820    Checks if there is an extension filter with a property
     821    starting with !x.prefix.
     822    @param prefix The extension prefix to check, or null to check any extension
     823    @since 3.18
     824  */
     825  public boolean hasExtensionFilter(String prefix)
     826  {
     827    if (propertyFilters == null) return false;
     828    prefix = "!x." + (prefix == null ? "" : prefix);
     829    for (PropertyFilterPair filterPair : propertyFilters.values())
     830    {
     831      if (filterPair.getEffectiveFilter().getProperty().startsWith(prefix)) return true;
     832    }
     833    return false;
     834   
     835  }
     836 
     837  /**
    816838    Get the value of a property filter on row 0.
    817839    @see #getPropertyValue(String, int)
     
    15191541    Arrays.asList("£", "&", "!", "¤", "|", "/"));
    15201542 
     1543  public void configureQuery(DbControl dc, EntityQuery query, boolean autoLeftJoin)
     1544    throws BaseException
     1545  {
     1546    configureQuery(dc, query, null, autoLeftJoin);
     1547  }
     1548
    15211549  /**
    15221550    Use the settings in this context to configure a query.
     
    15431571    @param dc The DbControl that will be used to execute the query
    15441572    @param query The query to configure
     1573    @param context Optional ClientContext for filter extensions (if null, a context is automatically created if needed)
    15451574    @param autoLeftJoin If true, and the sort property and filters are checked
    15461575      for associations to other items, which are automatically left joined to
     
    15481577      is experimental)
    15491578    @throws BaseException If configuring the query fails.
    1550     @since 2.8
    1551   */
    1552   public void configureQuery(DbControl dc, EntityQuery query, boolean autoLeftJoin)
     1579    @since 3.18
     1580  */
     1581  public void configureQuery(DbControl dc, EntityQuery query, ClientContext context, boolean autoLeftJoin)
    15531582    throws BaseException
    15541583  {
     
    16791708    if (permission != null) query.setItemPermission(permission);
    16801709   
     1710    // Initialise query filter extensions
     1711    if (context == null) context = new ClientContext(dc);
     1712    context.setCurrentItem(this);
     1713    setQuery(query);
     1714    ExtensionsManager manager = Application.getExtensionsManager();
     1715    List<QueryFilterAction> xtFilter = new ArrayList<>();
     1716    ExtensionsInvoker<QueryFilterAction> invoker = manager.getRegistry().useExtensions(context,
     1717      manager.getSettings(), "net.sf.basedb.query.entity-query-filter");
     1718    invoker.forEach(xtFilter::add);
     1719   
    16811720    // Add property filters
     1721    QueryRestrictions restrictions = new QueryRestrictions();   
    16821722    if (propertyFilters != null)
    16831723    {
    1684       Map<Integer, List<Restriction>> allRestrictions = new HashMap<Integer, List<Restriction>>();
    1685       Map<Integer, IdListRestriction> rowIdListRestriction = new HashMap<Integer, IdListRestriction>();
     1724      // 1. Filters per column/row
    16861725      for (PropertyFilterPair filterPair : propertyFilters.values())
    16871726      {
     
    16891728        if (propertyInspector == null || propertyInspector.evaluate(filter.getProperty()))
    16901729        {
     1730          int rowIndex = filter.getRowIndex();
     1731          RowRestrictions rowRestrictions = restrictions.getRow(rowIndex);
     1732          Restriction r = null;
    16911733          try
    16921734          {
    1693             Restriction r = filter.getRestriction(dc, query);
    1694             int rowIndex = filter.getRowIndex();
    1695             if (r instanceof IdListRestriction)
     1735            // Get restriction from extension
     1736            for (QueryFilterAction cfa : xtFilter)
    16961737            {
    1697               // Filters that result in a "IdListRestriction" are merged to a single id-list
    1698               // since that typically will result in a shorter list in the final SQL
    1699               IdListRestriction rowIdR = rowIdListRestriction.get(rowIndex);
    1700               if (rowIdR == null)
    1701               {
    1702                 rowIdListRestriction.put(rowIndex, (IdListRestriction)r);
    1703               }
    1704               else
    1705               {
    1706                 rowIdR.intersect((IdListRestriction)r);
    1707                 r = null;
    1708               }
     1738              r = cfa.getColumnRestriction(filter);
     1739              if (r != null) break;
    17091740            }
    1710             if (r != null)
    1711             {
    1712               List<Restriction> rowRestrictions = allRestrictions.get(rowIndex);
    1713               if (rowRestrictions == null)
    1714               {
    1715                 rowRestrictions = new ArrayList<Restriction>();
    1716                 allRestrictions.put(rowIndex, rowRestrictions);
    1717               }
    1718               rowRestrictions.add(r);
    1719               // left join if filter property is association
    1720               if (autoLeftJoin)
    1721               {
    1722                 String filterProperty = filter.getProperty();
    1723                 if (filterProperty != null && !NO_AUTO_JOIN_PREFIXES.contains(filterProperty.substring(0,  1)))
    1724                 {
    1725                   if (filterProperty.startsWith("#") && filterProperty.contains("("))
    1726                   {
    1727                     int joinStart = filterProperty.indexOf('(');
    1728                     int joinEnd = filterProperty.indexOf(')');
    1729                     filterProperty = filterProperty.substring(joinStart+1, joinEnd)+".foobar"; // 'foobar' is removed in the actual join
    1730                   }
    1731                  
    1732                   int lastDotIndex = filterProperty.lastIndexOf('.');
    1733                   int firstDotIndex = filterProperty.indexOf('.');
    1734                   if (lastDotIndex >= 0)
    1735                   {
    1736                     filterProperty = filterProperty.substring(0, lastDotIndex);
    1737                     if (!filterProperty.startsWith("$") || lastDotIndex != firstDotIndex)
    1738                     {
    1739                       leftJoins.add(filterProperty);
    1740                     }
    1741                   }
    1742                 }
    1743               }
    1744             }
     1741           
     1742            // Get default restriction
     1743            if (r == null) r = filter.getRestriction(dc, query);
    17451744          }
    17461745          catch (Throwable ex)
     
    17501749            setMessage(msg + ": " + ex.getMessage());
    17511750          }
     1751         
     1752          if (r != null) rowRestrictions.addRestriction(r);
     1753           
     1754          // left join if filter property is association
     1755          if (autoLeftJoin && r != null)
     1756          {
     1757            String filterProperty = filter.getProperty();
     1758            if (filterProperty != null && !NO_AUTO_JOIN_PREFIXES.contains(filterProperty.substring(0,  1)))
     1759            {
     1760              if (filterProperty.startsWith("#") && filterProperty.contains("("))
     1761              {
     1762                int joinStart = filterProperty.indexOf('(');
     1763                int joinEnd = filterProperty.indexOf(')');
     1764                filterProperty = filterProperty.substring(joinStart+1, joinEnd)+".foobar"; // 'foobar' is removed in the actual join
     1765              }
     1766             
     1767              int lastDotIndex = filterProperty.lastIndexOf('.');
     1768              int firstDotIndex = filterProperty.indexOf('.');
     1769              if (lastDotIndex >= 0)
     1770              {
     1771                filterProperty = filterProperty.substring(0, lastDotIndex);
     1772                if (!filterProperty.startsWith("$") || lastDotIndex != firstDotIndex)
     1773                {
     1774                  leftJoins.add(filterProperty);
     1775                }
     1776              }
     1777            }
     1778          }
    17521779        }
    17531780      }
    1754       if (allRestrictions.size() > 0)
    1755       {
    1756         Restriction[] rows = new Restriction[allRestrictions.size()];
    1757         int rowNo = 0;
    1758         for (List<Restriction> rowRestrictions : allRestrictions.values())
     1781     
     1782      // 2. Let extensions create extra restrictions per row if they want to
     1783      for (RowRestrictions row : restrictions.rowRestrictions.values())
     1784      {
     1785        for (QueryFilterAction act : xtFilter)
    17591786        {
    1760           rows[rowNo] = Restrictions.nullSafeAnd(rowRestrictions);
    1761           rowNo++;
     1787          Restriction r = act.getRowRestriction(row.rowIndex);
     1788          if (r != null) row.addRestriction(r);
    17621789        }
    1763         query.restrict(Restrictions.or(rows));
    1764       }
     1790      }
     1791     
     1792      // 3. The final restriction from filters is added to the query
     1793      Restriction finalRestriction = restrictions.getRestriction();
     1794      if (finalRestriction != null) query.restrict(finalRestriction);
     1795    }
     1796
     1797    // 4. Let extensions create extra restrictions if they want to
     1798    for (QueryFilterAction act : xtFilter)
     1799    {
     1800      Restriction r = act.getQueryRestriction();
     1801      if (r != null) query.restrict(r);
    17651802    }
    17661803   
     
    22882325  }
    22892326 
     2327 
     2328  static class QueryRestrictions
     2329  {
     2330    Map<Integer, RowRestrictions> rowRestrictions = new HashMap<>();
     2331   
     2332    Restriction getRestriction()
     2333    {
     2334      List<Restriction> rows = new ArrayList<Restriction>(rowRestrictions.size());
     2335      for (RowRestrictions row : rowRestrictions.values())
     2336      {
     2337        rows.add(row.getRowRestriction());
     2338      }
     2339      return Restrictions.nullSafeOr(rows);
     2340    }
     2341   
     2342    RowRestrictions getRow(int rowIndex)
     2343    {
     2344      RowRestrictions row = rowRestrictions.get(rowIndex);
     2345      if (row == null)
     2346      {
     2347        row = new RowRestrictions(rowIndex);
     2348        rowRestrictions.put(rowIndex, row);
     2349      }
     2350      return row;
     2351    }
     2352  }
     2353 
     2354 
     2355  static class RowRestrictions
     2356  {
     2357    final int rowIndex;
     2358   
     2359    List<Restriction> restrictions = new ArrayList<>();
     2360    IdListRestriction rowIdList = null;
     2361   
     2362    public RowRestrictions(int rowIndex)
     2363    {
     2364      this.rowIndex = rowIndex;
     2365    }
     2366   
     2367    Restriction getRowRestriction()
     2368    {
     2369      return Restrictions.nullSafeAnd(restrictions);
     2370    }
     2371   
     2372    void addRestriction(Restriction r)
     2373    {
     2374      if (r instanceof IdListRestriction)
     2375      {
     2376        if (rowIdList != null)
     2377        {
     2378          rowIdList.intersect((IdListRestriction)r);
     2379          r = null;
     2380        }
     2381        else
     2382        {
     2383          rowIdList = (IdListRestriction)r;
     2384        }
     2385      }
     2386      if (r != null) restrictions.add(r);
     2387    }
     2388  }
     2389 
    22902390}
  • trunk/src/core/net/sf/basedb/core/PropertyFilter.java

    r7843 r7895  
    240240  {
    241241    return property;
     242  }
     243 
     244  /**
     245    Checks if this filter is an extension filter. An extension
     246    filter has a {@link #getProperty()} that starts with
     247    "!x.prefix".
     248    @param prefix The extension prefix to check, or null to check any extension
     249    @since 3.18
     250  */
     251  public boolean isExtensionFilter(String prefix)
     252  {
     253    return prefix == null ? property.startsWith("!x.") : property.startsWith("!x."+prefix);
    242254  }
    243255 
     
    532544    Eg. &amp;experiments(name). The filter is applied as a subquery.
    533545    <p>
    534     If the property starts with ! it is another special filter. There is currently
    535     one such filter (since BASE 2.15): !sharedTo.name, which is used to filter
    536     shareable items by the name of users/groups/projects it has been shared to.
    537     The 'name' part can be replaced with any other property that is common for
    538     users, groups and projects (eg. description or id).
     546    If the property starts with ! it is another special filter:
     547    <ul>
     548    <li>Since BASE 2.15: !sharedTo.name, which is used to filter
     549      shareable items by the name of users/groups/projects it has been shared to.
     550      The 'name' part can be replaced with any other property that is common for
     551      users, groups and projects (eg. description or id).
     552    <li>Since BASE 3.18: !x., which is intended to be handled by extensions. It
     553      is ignored by the BASE core code. The rest of the property should be a unique
     554      identifier for the extension that is handling the filter.
    539555    <p>
    540556    The property may also start with $@.
     
    549565    String alias = null;
    550566    Expression indexExpression = null;
     567   
     568    // Ignore -- expected to be handled by extensions
     569    if (property != null && property.startsWith("!x.")) return null;
    551570   
    552571    if (property != null && property.startsWith("$"))
  • trunk/src/core/net/sf/basedb/util/extensions/ClientContext.java

    r5614 r7895  
    160160    @return The current item, or null
    161161  */
    162   public Object getCurrentItem()
    163   {
    164     return item;
     162  @SuppressWarnings("unchecked")
     163  public <T> T getCurrentItem()
     164  {
     165    return (T)item;
    165166  }
    166167 
     
    179180    @return The value, or null
    180181  */
    181   public Object getAttribute(String name)
    182   {
    183     return attributes == null ? null : attributes.get(name);
     182  @SuppressWarnings("unchecked")
     183  public <T> T getAttribute(String name)
     184  {
     185    return attributes == null ? null : (T)attributes.get(name);
    184186  }
    185187 
Note: See TracChangeset for help on using the changeset viewer.