Class JpqlAccessSupport

java.lang.Object
io.jmix.aitools.dataload.execution.JpqlAccessSupport

@Component("aitls_JpqlAccessSupport") public class JpqlAccessSupport extends Object
Checks a validated data-load query against the current user's permissions for every entity it reads, and returns the text to execute. Kept apart from execution so that any way of executing a generated query can apply the same checks first.

An entity counts as read when the query declares it (root, join, subquery) or when a path passes through it (o.customer.name reads the customer). The platform checks entity READ and applies row-level conditions for the root entity only.

Every entity the query reads is also narrowed to the records the AI may see: records of a @ExcludeFromAi subclass are records of its base entity too, and a query over the base would otherwise return them.

A left join is narrowed in its own on clause, so that a hidden record is joined as an absent one.

  • Field Details

  • Constructor Details

    • JpqlAccessSupport

      public JpqlAccessSupport()
  • Method Details

    • applyAccessConstraints

      public String applyAccessConstraints(String jpql, Collection<String> parameterNames)
      Checks the query and its parameters and returns the text to execute, with the row-level conditions of every non-root entity and the exclusion of @ExcludeFromAi subclasses woven in: into the on clause of a left join, into the where otherwise. A text the JPQL parser cannot read is returned unchanged, since executing it fails on the same parser.
      Parameters:
      jpql - validated query text
      parameterNames - names of the parameters passed with the query
      Returns:
      the text to execute
      Throws:
      AccessDeniedException - if the current user may not read an entity the query reads
      JpqlAccessConstraintException - if the query cannot be narrowed as the current user's permissions require
    • checkParameterNames

      protected void checkParameterNames(Collection<String> parameterNames)
      Refuses query parameters whose values the application supplies itself (current_user_*, session_*, current_locale). Row-level conditions refer to them, and a value passed with the query would take the place of the application's.
      Parameters:
      parameterNames - names of the parameters passed with the query
      Throws:
      JpqlAccessConstraintException - if one of them is supplied by the application
    • readGraph

      protected JpqlAccessSupport.QueryGraph readGraph(String jpql, QueryParser parser)
      Collects every entity the query reads, with the expressions that stand for it in the outer query and whether it also occurs where a condition added to the outer query cannot reach it.
      Parameters:
      jpql - query text
      parser - parser of the query
      Returns:
      the query graph
    • addPathReachedEntities

      protected void addPathReachedEntities(QueryParser.QueryPath path, boolean outer, Map<String,JpqlAccessSupport.EntityOccurrence> entities, Map<String,Integer> pathsPerPath, Map<String,Integer> joinsPerPath)
    • isIdOfReference

      protected boolean isIdOfReference(MetaProperty[] properties)
    • isReadReference

      protected boolean isReadReference(MetaProperty[] properties, QueryParser.QueryPath path, Map<String,Integer> pathsPerPath, Map<String,Integer> joinsPerPath)
      Tells whether a path is a to-one reference of a variable read by itself (`o.customer is null`, `o.customer = c`), not only as the path a join declares.
      Parameters:
      properties - the path's properties
      path - the path
      pathsPerPath - number of times each path occurs in the query, keyed in lower case
      joinsPerPath - number of joins declared over each path, keyed in lower case
      Returns:
      whether the path reads the reference
    • lowerCase

      protected String lowerCase(String name)
    • checkEntityReadPermitted

      protected void checkEntityReadPermitted(JpqlAccessSupport.QueryGraph graph)
    • checkSelectsValues

      protected void checkSelectsValues(QueryParser parser)
    • applyLeftJoinConditions

      protected String applyLeftJoinConditions(String jpql, JpqlAccessSupport.QueryGraph graph)
      Narrows the entities joined by a left join in the join's own on clause, so that a hidden record is joined as an absent one and the row stays. Covers row-level conditions and excluded subclasses alike.
      Parameters:
      jpql - query text
      graph - graph of the query
      Returns:
      the text with the conditions in the on clauses, unchanged when there is nothing to apply
      Throws:
      JpqlAccessConstraintException - if a row-level condition to apply carries a join; if a navigating condition has to be wrapped into a subquery and the entity has no primary key; or if a reference read through a <ref> or <ref>.<id> path occurs in the on clause of the left join declaring its variable
      IllegalStateException - if the query text cannot be rewritten as its graph requires
    • referenceExistsCondition

      protected String referenceExistsCondition(String referencePath, MetaClass owner, JpqlLeftJoinSupport.Rewrite rewrite)
      Returns a condition keeping only the rows whose owner refers to a record through the reference, as the inner join of a selected <ref>.<id> path does. It reads the owner through a subquery: a condition on <owner>.<ref> itself would be merged with the left join added for the path and drop the rows whose record is hidden.
      Parameters:
      referencePath - path from the owner variable to the reference, such as o.customer
      owner - entity the owner variable ranges over
      rewrite - rewrite of the query, used for a fresh variable name
      Returns:
      the condition
      Throws:
      IllegalStateException - if the owner entity has no primary key
    • classifyConditions

      protected JpqlAccessSupport.LeftJoinConditions classifyConditions(MetaClass entity, List<JpqlAccessSupport.RowLevelCondition> rowLevelConditions, @Nullable String primaryKey)
      Sorts the entity's row-level conditions by how they go into a join's on clause: as they are, or wrapped into a subquery on the entity because they navigate a reference of it, which an on clause cannot do. Decided once per condition, not per variable it is applied to.
      Parameters:
      entity - entity the conditions belong to
      rowLevelConditions - the entity's row-level conditions
      primaryKey - name of the entity's primary key, or null if it has none
      Returns:
      the conditions, with the {E} placeholder still in place
      Throws:
      JpqlAccessConstraintException - if a condition navigates and the entity has no primary key to wrap it into a subquery by
    • onConditions

      protected List<String> onConditions(MetaClass entity, JpqlAccessSupport.LeftJoinConditions conditions, Set<String> excludedSubtypes, String variable, JpqlLeftJoinSupport.Rewrite rewrite)
      Returns the conditions narrowing one variable of the entity in its join's on clause.
      Parameters:
      entity - entity the variable ranges over
      conditions - the entity's row-level conditions, sorted by classifyConditions(io.jmix.core.metamodel.model.MetaClass, java.util.List<io.jmix.aitools.dataload.execution.JpqlAccessSupport.RowLevelCondition>, java.lang.String)
      excludedSubtypes - names of the entity's @ExcludeFromAi descendants
      variable - variable to narrow
      rewrite - rewrite of the query, used for fresh variable names
      Returns:
      the conditions, with the variable in place of {E}
    • applyRowLevelConditions

      protected String applyRowLevelConditions(String jpql, JpqlAccessSupport.QueryGraph graph)
      Returns the text to execute with the row-level conditions of every non-root entity applied. These are the conditions the platform would apply to a query rooted at that entity.
      Parameters:
      jpql - validated query text
      graph - graph of the query
      Returns:
      the text to execute, unchanged when there is nothing to apply
      Throws:
      JpqlAccessConstraintException - if an entity with conditions occurs where they cannot be applied, or a condition of a non-root entity carries a join
    • applyExcludedSubtypeConditions

      protected String applyExcludedSubtypeConditions(String jpql, JpqlAccessSupport.QueryGraph graph)
      Keeps the records of @ExcludeFromAi subclasses out of the result: such a subclass is closed to the AI, yet its records are records of every visible ancestor. Unlike row-level conditions, this applies to the root alias too, since the platform knows nothing of the annotation.
      Parameters:
      jpql - query text
      graph - graph of the query
      Returns:
      the text to execute, unchanged when no entity the query reads has an excluded subclass
      Throws:
      JpqlAccessConstraintException - if an entity with an excluded subclass occurs where a condition added to the outer query cannot reach it
    • excludedSubtypeCondition

      protected String excludedSubtypeCondition(String expression, Set<String> excludedSubtypes)
    • excludedSubtypeNames

      protected Set<String> excludedSubtypeNames(MetaClass entity)
    • nullSafe

      protected String nullSafe(@Nullable String nullCheck, String condition)
      Lets through a row that refers to no record, since an absent record carries no data. Absence is checked on the owner's foreign key only, because a check on the joined variable would let an on clause probe a hidden record.
      Parameters:
      nullCheck - path whose is null tells that no record is referred to, or null when absence cannot be told that way (a collection, the inverse side, an entity join)
      condition - condition with the target in place of {E}
      Returns:
      the condition to add to the where
    • targetsOf

      protected Map<String,@Nullable String> targetsOf(MetaClass entity, JpqlAccessSupport.EntityOccurrence occurrence, List<JpqlAccessSupport.RowLevelCondition> conditions, JpqlAccessSupport.QueryGraph graph)
      Returns the expressions to apply the entity's row-level conditions to: every expression standing for it in the outer query except the root alias, which the platform narrows itself.
      Parameters:
      entity - entity the conditions belong to
      occurrence - where the entity occurs in the query
      conditions - the entity's row-level conditions, not empty
      graph - graph of the query
      Returns:
      target expressions, each with its null check or null; empty when the platform applies them all
      Throws:
      JpqlAccessConstraintException - if the entity occurs where the conditions cannot be applied, or a condition to apply carries a join
    • checkNoJoinClause

      protected void checkNoJoinClause(MetaClass entity, List<JpqlAccessSupport.RowLevelCondition> conditions)
    • collectRowLevelConditions

      protected List<JpqlAccessSupport.RowLevelCondition> collectRowLevelConditions(MetaClass entity)
      Returns the row-level conditions that the registered constraints add for the entity, as they would for a query rooted at it.
      Parameters:
      entity - entity to collect the conditions of
      Returns:
      the conditions with the {E} placeholder still in place; empty if none
    • isBlank

      protected boolean isBlank(@Nullable String clause)
    • occurrenceOf