HHH-13001 - NPE rendering nested criteria expressions
This commit is contained in:
parent
23c6565f72
commit
a2b26b81b3
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package org.hibernate.internal.util.collections;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class SingletonStack<T> implements Stack<T> {
|
||||
private final T instance;
|
||||
|
||||
public SingletonStack(T instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push(T newCurrent) {
|
||||
throw new UnsupportedOperationException( "Cannot push to a singleton Stack" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public T pop() {
|
||||
throw new UnsupportedOperationException( "Cannot pop from a singleton Stack" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getCurrent() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getPrevious() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int depth() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitCurrentFirst(Consumer<T> action) {
|
||||
action.accept( instance );
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> X findCurrentFirst(Function<T, X> action) {
|
||||
return action.apply( instance );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package org.hibernate.internal.util.collections;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Stack implementation exposing useful methods for Hibernate needs.
|
||||
*
|
||||
* @param <T> The type of things stored in the stack
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public interface Stack<T> {
|
||||
/**
|
||||
* Push the new element on the top of the stack
|
||||
*/
|
||||
void push(T newCurrent);
|
||||
|
||||
/**
|
||||
* Pop (remove and return) the current element off the stack
|
||||
*/
|
||||
T pop();
|
||||
|
||||
/**
|
||||
* The element currently at the top of the stack
|
||||
*/
|
||||
T getCurrent();
|
||||
|
||||
/**
|
||||
* The element previously at the top of the stack before the current one
|
||||
*/
|
||||
T getPrevious();
|
||||
|
||||
/**
|
||||
* How many elements are currently on the stack?
|
||||
*/
|
||||
int depth();
|
||||
|
||||
/**
|
||||
* Are there no elements currently in the stack?
|
||||
*/
|
||||
boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Remmove all elements from the stack
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* Visit all elements in the stack, starting with the current and working back
|
||||
*/
|
||||
void visitCurrentFirst(Consumer<T> action);
|
||||
|
||||
/**
|
||||
* Find an element on the stack and return a value. The first non-null element
|
||||
* returned from `action` stops the iteration and is returned from here
|
||||
*/
|
||||
<X> X findCurrentFirst(Function<T, X> action);
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package org.hibernate.internal.util.collections;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A general-purpose stack impl.
|
||||
*
|
||||
* @param <T> The type of things stored in the stack
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class StandardStack<T> implements Stack<T> {
|
||||
private LinkedList<T> internalStack = new LinkedList<>();
|
||||
|
||||
public StandardStack() {
|
||||
}
|
||||
|
||||
public StandardStack(T initial) {
|
||||
internalStack.add( initial );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push(T newCurrent) {
|
||||
internalStack.addFirst( newCurrent );
|
||||
}
|
||||
|
||||
@Override
|
||||
public T pop() {
|
||||
return internalStack.removeFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getCurrent() {
|
||||
return internalStack.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getPrevious() {
|
||||
if ( internalStack.size() < 2 ) {
|
||||
return null;
|
||||
}
|
||||
return internalStack.get( internalStack.size() - 2 );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int depth() {
|
||||
return internalStack.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return internalStack.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
internalStack.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitCurrentFirst(Consumer<T> action) {
|
||||
internalStack.forEach( action );
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> X findCurrentFirst(Function<T, X> function) {
|
||||
for ( T t : internalStack ) {
|
||||
final X result = function.apply( t );
|
||||
if ( result != null ) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import org.hibernate.query.criteria.internal.compile.InterpretedParameterMetadat
|
|||
import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
||||
import org.hibernate.query.criteria.internal.path.RootImpl;
|
||||
import org.hibernate.query.spi.QueryImplementor;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
|
||||
/**
|
||||
* Base class for commonality between {@link javax.persistence.criteria.CriteriaUpdate} and
|
||||
|
@ -155,9 +156,17 @@ public abstract class AbstractManipulationCriteriaQuery<T> implements Compilable
|
|||
}
|
||||
|
||||
protected void renderRestrictions(StringBuilder jpaql, RenderingContext renderingContext) {
|
||||
if ( getRestriction() != null) {
|
||||
if ( getRestriction() == null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderingContext.getClauseStack().push( Clause.WHERE );
|
||||
try {
|
||||
jpaql.append( " where " )
|
||||
.append( ( (Renderable) getRestriction() ).render( renderingContext ) );
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.hibernate.query.criteria.internal.compile.ImplicitParameterBinding;
|
|||
import org.hibernate.query.criteria.internal.compile.InterpretedParameterMetadata;
|
||||
import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
||||
import org.hibernate.query.spi.QueryImplementor;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
import org.hibernate.type.Type;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
@ -291,16 +292,7 @@ public class CriteriaQueryImpl<T> extends AbstractNode implements CriteriaQuery<
|
|||
|
||||
queryStructure.render( jpaqlBuffer, renderingContext );
|
||||
|
||||
if ( ! getOrderList().isEmpty() ) {
|
||||
jpaqlBuffer.append( " order by " );
|
||||
String sep = "";
|
||||
for ( Order orderSpec : getOrderList() ) {
|
||||
jpaqlBuffer.append( sep )
|
||||
.append( ( ( Renderable ) orderSpec.getExpression() ).render( renderingContext ) )
|
||||
.append( orderSpec.isAscending() ? " asc" : " desc" );
|
||||
sep = ", ";
|
||||
}
|
||||
}
|
||||
renderOrderByClause( renderingContext, jpaqlBuffer );
|
||||
|
||||
final String jpaqlString = jpaqlBuffer.toString();
|
||||
|
||||
|
@ -385,4 +377,25 @@ public class CriteriaQueryImpl<T> extends AbstractNode implements CriteriaQuery<
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected void renderOrderByClause(RenderingContext renderingContext, StringBuilder jpaqlBuffer) {
|
||||
if ( getOrderList().isEmpty() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderingContext.getClauseStack().push( Clause.ORDER );
|
||||
try {
|
||||
jpaqlBuffer.append( " order by " );
|
||||
String sep = "";
|
||||
for ( Order orderSpec : getOrderList() ) {
|
||||
jpaqlBuffer.append( sep )
|
||||
.append( ( (Renderable) orderSpec.getExpression() ).render( renderingContext ) )
|
||||
.append( orderSpec.isAscending() ? " asc" : " desc" );
|
||||
sep = ", ";
|
||||
}
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
|||
import org.hibernate.query.criteria.internal.expression.DelegatedExpressionImpl;
|
||||
import org.hibernate.query.criteria.internal.expression.ExpressionImpl;
|
||||
import org.hibernate.query.criteria.internal.path.RootImpl;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
|
||||
/**
|
||||
* The Hibernate implementation of the JPA {@link Subquery} contract. Mostlty a set of delegation to its internal
|
||||
|
@ -257,14 +258,13 @@ public class CriteriaSubqueryImpl<T> extends ExpressionImpl<T> implements Subque
|
|||
|
||||
@Override
|
||||
public String render(RenderingContext renderingContext) {
|
||||
if ( renderingContext.getClauseStack().getCurrent() == Clause.SELECT ) {
|
||||
throw new IllegalStateException( "Subquery cannot occur in select clause" );
|
||||
}
|
||||
|
||||
StringBuilder subqueryBuffer = new StringBuilder( "(" );
|
||||
queryStructure.render( subqueryBuffer, renderingContext );
|
||||
subqueryBuffer.append( ')' );
|
||||
return subqueryBuffer.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renderProjection(RenderingContext renderingContext) {
|
||||
throw new IllegalStateException( "Subquery cannot occur in select clause" );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import javax.persistence.metamodel.SingularAttribute;
|
|||
|
||||
import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
||||
import org.hibernate.query.criteria.internal.path.SingularAttributePath;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
|
||||
/**
|
||||
* Hibernate implementation of the JPA 2.1 {@link CriteriaUpdate} contract.
|
||||
|
@ -122,16 +123,23 @@ public class CriteriaUpdateImpl<T> extends AbstractManipulationCriteriaQuery<T>
|
|||
}
|
||||
|
||||
private void renderAssignments(StringBuilder jpaql, RenderingContext renderingContext) {
|
||||
jpaql.append( " set " );
|
||||
boolean first = true;
|
||||
for ( Assignment assignment : assignments ) {
|
||||
if ( ! first ) {
|
||||
jpaql.append( ", " );
|
||||
renderingContext.getClauseStack().push( Clause.UPDATE );
|
||||
|
||||
try {
|
||||
jpaql.append( " set " );
|
||||
boolean first = true;
|
||||
for ( Assignment assignment : assignments ) {
|
||||
if ( !first ) {
|
||||
jpaql.append( ", " );
|
||||
}
|
||||
jpaql.append( assignment.attributePath.render( renderingContext ) )
|
||||
.append( " = " )
|
||||
.append( assignment.value.render( renderingContext ) );
|
||||
first = false;
|
||||
}
|
||||
jpaql.append( assignment.attributePath.render( renderingContext ) )
|
||||
.append( " = " )
|
||||
.append( assignment.value.render( renderingContext ) );
|
||||
first = false;
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import javax.persistence.metamodel.EntityType;
|
|||
import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
||||
import org.hibernate.query.criteria.internal.path.RootImpl;
|
||||
import org.hibernate.query.criteria.internal.path.RootImpl.TreatedRoot;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
|
||||
/**
|
||||
* Models basic query structure. Used as a delegate in implementing both
|
||||
|
@ -230,37 +231,34 @@ public class QueryStructure<T> implements Serializable {
|
|||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public void render(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
|
||||
jpaqlQuery.append( "select " );
|
||||
if ( isDistinct() ) {
|
||||
jpaqlQuery.append( "distinct " );
|
||||
}
|
||||
if ( getSelection() == null ) {
|
||||
jpaqlQuery.append( locateImplicitSelection().renderProjection( renderingContext ) );
|
||||
}
|
||||
else {
|
||||
jpaqlQuery.append( ( (Renderable) getSelection() ).renderProjection( renderingContext ) );
|
||||
}
|
||||
renderSelectClause( jpaqlQuery, renderingContext );
|
||||
|
||||
renderFromClause( jpaqlQuery, renderingContext );
|
||||
|
||||
if ( getRestriction() != null) {
|
||||
jpaqlQuery.append( " where " )
|
||||
.append( ( (Renderable) getRestriction() ).render( renderingContext ) );
|
||||
renderWhereClause( jpaqlQuery, renderingContext );
|
||||
|
||||
renderGroupByClause( jpaqlQuery, renderingContext );
|
||||
}
|
||||
|
||||
protected void renderSelectClause(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
|
||||
renderingContext.getClauseStack().push( Clause.SELECT );
|
||||
|
||||
try {
|
||||
jpaqlQuery.append( "select " );
|
||||
|
||||
if ( isDistinct() ) {
|
||||
jpaqlQuery.append( "distinct " );
|
||||
}
|
||||
|
||||
if ( getSelection() == null ) {
|
||||
jpaqlQuery.append( locateImplicitSelection().render( renderingContext ) );
|
||||
}
|
||||
else {
|
||||
jpaqlQuery.append( ( (Renderable) getSelection() ).render( renderingContext ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! getGroupings().isEmpty() ) {
|
||||
jpaqlQuery.append( " group by " );
|
||||
String sep = "";
|
||||
for ( Expression grouping : getGroupings() ) {
|
||||
jpaqlQuery.append( sep )
|
||||
.append( ( (Renderable) grouping ).renderGroupBy( renderingContext ) );
|
||||
sep = ", ";
|
||||
}
|
||||
|
||||
if ( getHaving() != null ) {
|
||||
jpaqlQuery.append( " having " )
|
||||
.append( ( (Renderable) getHaving() ).render( renderingContext ) );
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,48 +288,106 @@ public class QueryStructure<T> implements Serializable {
|
|||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
private void renderFromClause(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
|
||||
jpaqlQuery.append( " from " );
|
||||
String sep = "";
|
||||
for ( Root root : getRoots() ) {
|
||||
( (FromImplementor) root ).prepareAlias( renderingContext );
|
||||
jpaqlQuery.append( sep );
|
||||
jpaqlQuery.append( ( (FromImplementor) root ).renderTableExpression( renderingContext ) );
|
||||
sep = ", ";
|
||||
}
|
||||
renderingContext.getClauseStack().push( Clause.FROM );
|
||||
|
||||
for ( Root root : getRoots() ) {
|
||||
renderJoins( jpaqlQuery, renderingContext, root.getJoins() );
|
||||
if (root instanceof RootImpl) {
|
||||
Set<TreatedRoot> treats = ((RootImpl)root).getTreats();
|
||||
for ( TreatedRoot treat : treats ) {
|
||||
renderJoins( jpaqlQuery, renderingContext, treat.getJoins() );
|
||||
}
|
||||
try {
|
||||
jpaqlQuery.append( " from " );
|
||||
String sep = "";
|
||||
for ( Root root : getRoots() ) {
|
||||
( (FromImplementor) root ).prepareAlias( renderingContext );
|
||||
jpaqlQuery.append( sep );
|
||||
sep = ", ";
|
||||
jpaqlQuery.append( ( (FromImplementor) root ).renderTableExpression( renderingContext ) );
|
||||
}
|
||||
renderFetches( jpaqlQuery, renderingContext, root.getFetches() );
|
||||
}
|
||||
|
||||
if ( isSubQuery ) {
|
||||
if ( correlationRoots != null ) {
|
||||
for ( FromImplementor<?,?> correlationRoot : correlationRoots ) {
|
||||
final FromImplementor correlationParent = correlationRoot.getCorrelationParent();
|
||||
correlationParent.prepareAlias( renderingContext );
|
||||
final String correlationRootAlias = correlationParent.getAlias();
|
||||
for ( Join<?,?> correlationJoin : correlationRoot.getJoins() ) {
|
||||
final JoinImplementor correlationJoinImpl = (JoinImplementor) correlationJoin;
|
||||
// IMPL NOTE: reuse the sep from above!
|
||||
jpaqlQuery.append( sep );
|
||||
correlationJoinImpl.prepareAlias( renderingContext );
|
||||
jpaqlQuery.append( correlationRootAlias )
|
||||
.append( '.' )
|
||||
.append( correlationJoinImpl.getAttribute().getName() )
|
||||
.append( " as " )
|
||||
.append( correlationJoinImpl.getAlias() );
|
||||
sep = ", ";
|
||||
renderJoins( jpaqlQuery, renderingContext, correlationJoinImpl.getJoins() );
|
||||
for ( Root root : getRoots() ) {
|
||||
renderJoins( jpaqlQuery, renderingContext, root.getJoins() );
|
||||
if ( root instanceof RootImpl ) {
|
||||
Set<TreatedRoot> treats = ( (RootImpl) root ).getTreats();
|
||||
for ( TreatedRoot treat : treats ) {
|
||||
renderJoins( jpaqlQuery, renderingContext, treat.getJoins() );
|
||||
}
|
||||
}
|
||||
renderFetches( jpaqlQuery, renderingContext, root.getFetches() );
|
||||
}
|
||||
|
||||
if ( isSubQuery ) {
|
||||
if ( correlationRoots != null ) {
|
||||
for ( FromImplementor<?, ?> correlationRoot : correlationRoots ) {
|
||||
final FromImplementor correlationParent = correlationRoot.getCorrelationParent();
|
||||
correlationParent.prepareAlias( renderingContext );
|
||||
final String correlationRootAlias = correlationParent.getAlias();
|
||||
for ( Join<?, ?> correlationJoin : correlationRoot.getJoins() ) {
|
||||
final JoinImplementor correlationJoinImpl = (JoinImplementor) correlationJoin;
|
||||
// IMPL NOTE: reuse the sep from above!
|
||||
jpaqlQuery.append( sep );
|
||||
correlationJoinImpl.prepareAlias( renderingContext );
|
||||
jpaqlQuery.append( correlationRootAlias )
|
||||
.append( '.' )
|
||||
.append( correlationJoinImpl.getAttribute().getName() )
|
||||
.append( " as " )
|
||||
.append( correlationJoinImpl.getAlias() );
|
||||
sep = ", ";
|
||||
renderJoins( jpaqlQuery, renderingContext, correlationJoinImpl.getJoins() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
|
||||
protected void renderWhereClause(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
|
||||
if ( getRestriction() == null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderingContext.getClauseStack().push( Clause.WHERE );
|
||||
try {
|
||||
jpaqlQuery.append( " where " )
|
||||
.append( ( (Renderable) getRestriction() ).render( renderingContext ) );
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
|
||||
protected void renderGroupByClause(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
|
||||
if ( getGroupings().isEmpty() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderingContext.getClauseStack().push( Clause.GROUP );
|
||||
try {
|
||||
jpaqlQuery.append( " group by " );
|
||||
String sep = "";
|
||||
for ( Expression grouping : getGroupings() ) {
|
||||
jpaqlQuery.append( sep )
|
||||
.append( ( (Renderable) grouping ).render( renderingContext ) );
|
||||
sep = ", ";
|
||||
}
|
||||
|
||||
renderHavingClause( jpaqlQuery, renderingContext );
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderHavingClause(StringBuilder jpaqlQuery, RenderingContext renderingContext) {
|
||||
if ( getHaving() == null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderingContext.getClauseStack().push( Clause.HAVING );
|
||||
try {
|
||||
jpaqlQuery.append( " having " ).append( ( (Renderable) getHaving() ).render( renderingContext ) );
|
||||
}
|
||||
finally {
|
||||
renderingContext.getClauseStack().pop();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
|
|
|
@ -10,38 +10,15 @@ package org.hibernate.query.criteria.internal;
|
|||
import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
||||
|
||||
/**
|
||||
* TODO : javadoc
|
||||
* Contract for nodes in the JPA Criteria tree that can be rendered
|
||||
* as part of criteria "compilation"
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public interface Renderable {
|
||||
|
||||
/**
|
||||
* Render clause
|
||||
*
|
||||
* @param renderingContext context
|
||||
* @return rendered expression
|
||||
* Perform the rendering, returning the rendition
|
||||
*/
|
||||
String render(RenderingContext renderingContext);
|
||||
|
||||
/**
|
||||
* Render SELECT clause
|
||||
*
|
||||
* @param renderingContext context
|
||||
* @return rendered expression
|
||||
*/
|
||||
default String renderProjection(RenderingContext renderingContext) {
|
||||
return render( renderingContext );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render GROUP BY clause
|
||||
*
|
||||
* @param renderingContext context
|
||||
*
|
||||
* @return rendered expression
|
||||
*/
|
||||
default String renderGroupBy(RenderingContext renderingContext) {
|
||||
return render( renderingContext );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,12 @@ import org.hibernate.engine.jdbc.spi.JdbcServices;
|
|||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||
import org.hibernate.engine.spi.SessionImplementor;
|
||||
import org.hibernate.internal.util.StringHelper;
|
||||
import org.hibernate.internal.util.collections.Stack;
|
||||
import org.hibernate.internal.util.collections.StandardStack;
|
||||
import org.hibernate.query.criteria.LiteralHandlingMode;
|
||||
import org.hibernate.query.criteria.internal.expression.function.FunctionExpression;
|
||||
import org.hibernate.query.spi.QueryImplementor;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
import org.hibernate.type.Type;
|
||||
|
||||
/**
|
||||
|
@ -63,6 +67,9 @@ public class CriteriaCompiler implements Serializable {
|
|||
private int aliasCount;
|
||||
private int explicitParameterCount;
|
||||
|
||||
private final Stack<Clause> clauseStack = new StandardStack<>();
|
||||
private final Stack<FunctionExpression> functionContextStack = new StandardStack<>();
|
||||
|
||||
public String generateAlias() {
|
||||
return "generatedAlias" + aliasCount++;
|
||||
}
|
||||
|
@ -71,6 +78,16 @@ public class CriteriaCompiler implements Serializable {
|
|||
return "param" + explicitParameterCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stack<Clause> getClauseStack() {
|
||||
return clauseStack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stack<FunctionExpression> getFunctionStack() {
|
||||
return functionContextStack;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public ExplicitParameterInfo registerExplicitParameter(ParameterExpression<?> criteriaQueryParameter) {
|
||||
|
|
|
@ -9,7 +9,10 @@ package org.hibernate.query.criteria.internal.compile;
|
|||
import javax.persistence.criteria.ParameterExpression;
|
||||
|
||||
import org.hibernate.dialect.Dialect;
|
||||
import org.hibernate.internal.util.collections.Stack;
|
||||
import org.hibernate.query.criteria.LiteralHandlingMode;
|
||||
import org.hibernate.query.criteria.internal.expression.function.FunctionExpression;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
|
||||
/**
|
||||
* Used to provide a context and services to the rendering.
|
||||
|
@ -67,4 +70,8 @@ public interface RenderingContext {
|
|||
default LiteralHandlingMode getCriteriaLiteralHandlingMode() {
|
||||
return LiteralHandlingMode.AUTO;
|
||||
}
|
||||
|
||||
Stack<Clause> getClauseStack();
|
||||
|
||||
Stack<FunctionExpression> getFunctionStack();
|
||||
}
|
||||
|
|
|
@ -77,15 +77,17 @@ public class CompoundSelectionImpl<X>
|
|||
if ( isConstructor ) {
|
||||
buff.append( "new " ).append( getJavaType().getName() ).append( '(' );
|
||||
}
|
||||
|
||||
String sep = "";
|
||||
for ( Selection selection : selectionItems ) {
|
||||
buff.append( sep )
|
||||
.append( ( (Renderable) selection ).renderProjection( renderingContext ) );
|
||||
buff.append( sep ).append( ( (Renderable) selection ).render( renderingContext ) );
|
||||
sep = ", ";
|
||||
}
|
||||
|
||||
if ( isConstructor ) {
|
||||
buff.append( ')' );
|
||||
}
|
||||
|
||||
return buff.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,37 +47,54 @@ public class LiteralExpression<T> extends ExpressionImpl<T> implements Serializa
|
|||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public String render(RenderingContext renderingContext) {
|
||||
switch ( renderingContext.getClauseStack().getCurrent() ) {
|
||||
case SELECT: {
|
||||
return renderProjection();
|
||||
}
|
||||
case GROUP: {
|
||||
// technically a literal in the group-by clause
|
||||
// would be a reference to the position of a selection
|
||||
//
|
||||
// but this is what the code used to do...
|
||||
return renderProjection();
|
||||
}
|
||||
default: {
|
||||
return normalRender( renderingContext );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String normalRender(RenderingContext renderingContext) {
|
||||
LiteralHandlingMode literalHandlingMode = renderingContext.getCriteriaLiteralHandlingMode();
|
||||
|
||||
switch ( literalHandlingMode ) {
|
||||
case AUTO:
|
||||
case AUTO: {
|
||||
if ( ValueHandlerFactory.isNumeric( literal ) ) {
|
||||
return ValueHandlerFactory.determineAppropriateHandler( (Class) literal.getClass() ).render( literal );
|
||||
}
|
||||
else {
|
||||
return bindLiteral( renderingContext );
|
||||
}
|
||||
case BIND:
|
||||
}
|
||||
case BIND: {
|
||||
return bindLiteral( renderingContext );
|
||||
case INLINE:
|
||||
}
|
||||
case INLINE: {
|
||||
Object literalValue = literal;
|
||||
if ( String.class.equals( literal.getClass() ) ) {
|
||||
literalValue = renderingContext.getDialect().inlineLiteral( (String) literal );
|
||||
}
|
||||
|
||||
return ValueHandlerFactory.determineAppropriateHandler( (Class) literal.getClass() ).render( literalValue );
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
throw new IllegalArgumentException( "Unexpected LiteralHandlingMode: " + literalHandlingMode );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String bindLiteral(RenderingContext renderingContext) {
|
||||
final String parameterName = renderingContext.registerLiteralParameterBinding( getLiteral(), getJavaType() );
|
||||
return ':' + parameterName;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public String renderProjection(RenderingContext renderingContext) {
|
||||
private String renderProjection() {
|
||||
// some drivers/servers do not like parameters in the select clause
|
||||
final ValueHandlerFactory.ValueHandler handler =
|
||||
ValueHandlerFactory.determineAppropriateHandler( literal.getClass() );
|
||||
|
@ -89,9 +106,9 @@ public class LiteralExpression<T> extends ExpressionImpl<T> implements Serializa
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renderGroupBy(RenderingContext renderingContext) {
|
||||
return renderProjection( renderingContext );
|
||||
private String bindLiteral(RenderingContext renderingContext) {
|
||||
final String parameterName = renderingContext.registerLiteralParameterBinding( getLiteral(), getJavaType() );
|
||||
return ':' + parameterName;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.hibernate.query.criteria.internal.ParameterRegistry;
|
|||
import org.hibernate.query.criteria.internal.PathImplementor;
|
||||
import org.hibernate.query.criteria.internal.Renderable;
|
||||
import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
|
||||
/**
|
||||
* TODO : javadoc
|
||||
|
@ -48,17 +49,17 @@ public class MapEntryExpression<K,V>
|
|||
}
|
||||
|
||||
public String render(RenderingContext renderingContext) {
|
||||
if ( renderingContext.getClauseStack().getCurrent() == Clause.SELECT ) {
|
||||
return "entry(" + path( renderingContext ) + ")";
|
||||
}
|
||||
|
||||
// don't think this is valid outside of select clause...
|
||||
throw new IllegalStateException( "illegal reference to map entry outside of select clause." );
|
||||
}
|
||||
|
||||
public String renderProjection(RenderingContext renderingContext) {
|
||||
return "entry(" + path( renderingContext ) + ")";
|
||||
}
|
||||
|
||||
private String path(RenderingContext renderingContext) {
|
||||
return origin.getPathIdentifier()
|
||||
+ '.'
|
||||
+ ( (Renderable) getAttribute() ).renderProjection( renderingContext );
|
||||
+ ( (Renderable) getAttribute() ).render( renderingContext );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
|
|||
import org.hibernate.query.criteria.internal.ParameterRegistry;
|
||||
import org.hibernate.query.criteria.internal.compile.RenderingContext;
|
||||
import org.hibernate.query.criteria.internal.expression.function.CastFunction;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
|
||||
/**
|
||||
* Represents a <tt>NULL</tt>literal expression.
|
||||
|
@ -28,10 +29,13 @@ public class NullLiteralExpression<T> extends ExpressionImpl<T> implements Seria
|
|||
}
|
||||
|
||||
public String render(RenderingContext renderingContext) {
|
||||
if ( renderingContext.getClauseStack().getCurrent() == Clause.SELECT ) {
|
||||
// in the select clause render the ``null` using a cast so the db analyzer/optimizer
|
||||
// understands the type
|
||||
return CastFunction.CAST_NAME + "( null as " + renderingContext.getCastType( getJavaType() ) + ')';
|
||||
}
|
||||
|
||||
// otherwise, just render `null`
|
||||
return "null";
|
||||
}
|
||||
|
||||
public String renderProjection(RenderingContext renderingContext) {
|
||||
return CastFunction.CAST_NAME + "( null as " + renderingContext.getCastType( getJavaType() ) + ')';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,41 +106,18 @@ public class SearchedCaseExpression<R>
|
|||
}
|
||||
|
||||
public String render(RenderingContext renderingContext) {
|
||||
return render(
|
||||
renderingContext,
|
||||
(Renderable expression, RenderingContext context) -> expression.render( context )
|
||||
);
|
||||
}
|
||||
|
||||
public String renderProjection(RenderingContext renderingContext) {
|
||||
return render(
|
||||
renderingContext,
|
||||
(Renderable expression, RenderingContext context) -> expression.renderProjection( context )
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renderGroupBy(RenderingContext renderingContext) {
|
||||
return render(
|
||||
renderingContext,
|
||||
(Renderable expression, RenderingContext context) -> expression.renderGroupBy( context )
|
||||
);
|
||||
}
|
||||
|
||||
private String render(
|
||||
RenderingContext renderingContext,
|
||||
BiFunction<Renderable, RenderingContext, String> formatter) {
|
||||
StringBuilder caseStatement = new StringBuilder( "case" );
|
||||
for ( WhenClause whenClause : getWhenClauses() ) {
|
||||
caseStatement.append( " when " )
|
||||
.append( formatter.apply( (Renderable) whenClause.getCondition(), renderingContext ) )
|
||||
.append( ( (Renderable) whenClause.getCondition() ).render( renderingContext ) )
|
||||
.append( " then " )
|
||||
.append( formatter.apply( ((Renderable) whenClause.getResult()), renderingContext ) );
|
||||
.append( ( (Renderable) whenClause.getResult() ).render( renderingContext ) );
|
||||
}
|
||||
|
||||
caseStatement.append( " else " )
|
||||
.append( formatter.apply( (Renderable) getOtherwiseResult(), renderingContext ) )
|
||||
.append( ( (Renderable) getOtherwiseResult() ).render( renderingContext ) )
|
||||
.append( " end" );
|
||||
|
||||
return caseStatement.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ package org.hibernate.query.criteria.internal.expression;
|
|||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import javax.persistence.criteria.CriteriaBuilder.SimpleCase;
|
||||
import javax.persistence.criteria.Expression;
|
||||
|
||||
|
@ -118,44 +117,21 @@ public class SimpleCaseExpression<C,R>
|
|||
|
||||
@Override
|
||||
public String render(RenderingContext renderingContext) {
|
||||
return render(
|
||||
renderingContext,
|
||||
(Renderable expression, RenderingContext context) -> expression.render( context )
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renderProjection(RenderingContext renderingContext) {
|
||||
return render(
|
||||
renderingContext,
|
||||
(Renderable expression, RenderingContext context) -> expression.renderProjection( context )
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renderGroupBy(RenderingContext renderingContext) {
|
||||
return render(
|
||||
renderingContext,
|
||||
(Renderable expression, RenderingContext context) -> expression.renderGroupBy( context )
|
||||
);
|
||||
}
|
||||
|
||||
private String render(
|
||||
RenderingContext renderingContext,
|
||||
BiFunction<Renderable, RenderingContext, String> formatter) {
|
||||
StringBuilder caseExpr = new StringBuilder();
|
||||
caseExpr.append( "case " )
|
||||
.append( formatter.apply( (Renderable) getExpression(), renderingContext ) );
|
||||
.append( ( (Renderable) getExpression() ).render( renderingContext ) );
|
||||
|
||||
for ( WhenClause whenClause : getWhenClauses() ) {
|
||||
caseExpr.append( " when " )
|
||||
.append( formatter.apply( whenClause.getCondition(), renderingContext ) )
|
||||
.append( whenClause.getCondition().render( renderingContext ) )
|
||||
.append( " then " )
|
||||
.append( formatter.apply( (Renderable) whenClause.getResult(), renderingContext ) );
|
||||
.append( ( (Renderable) whenClause.getResult() ).render( renderingContext ) );
|
||||
}
|
||||
|
||||
caseExpr.append( " else " )
|
||||
.append( formatter.apply( (Renderable) getOtherwiseResult(), renderingContext ) )
|
||||
.append( ( (Renderable) getOtherwiseResult() ).render( renderingContext ) )
|
||||
.append( " end" );
|
||||
|
||||
return caseExpr.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
package org.hibernate.query.criteria.internal.expression.function;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
|
||||
import org.hibernate.query.criteria.internal.ParameterRegistry;
|
||||
|
@ -47,10 +48,17 @@ public class CastFunction<T,Y>
|
|||
|
||||
@Override
|
||||
public String render(RenderingContext renderingContext) {
|
||||
return CAST_NAME + '(' +
|
||||
castSource.render( renderingContext ) +
|
||||
" as " +
|
||||
renderingContext.getCastType( getJavaType() ) +
|
||||
')';
|
||||
renderingContext.getFunctionStack().push( this );
|
||||
try {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"cast(%s as %s)",
|
||||
castSource.render( renderingContext ),
|
||||
renderingContext.getCastType( getJavaType() )
|
||||
);
|
||||
}
|
||||
finally {
|
||||
renderingContext.getFunctionStack().pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,16 +86,26 @@ public class LocateFunction
|
|||
|
||||
@Override
|
||||
public String render(RenderingContext renderingContext) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
buffer.append( "locate(" )
|
||||
.append( ( (Renderable) getPattern() ).render( renderingContext ) )
|
||||
.append( ',' )
|
||||
.append( ( (Renderable) getString() ).render( renderingContext ) );
|
||||
if ( getStart() != null ) {
|
||||
buffer.append( ',' )
|
||||
.append( ( (Renderable) getStart() ).render( renderingContext ) );
|
||||
renderingContext.getFunctionStack().push( this );
|
||||
|
||||
try {
|
||||
final StringBuilder buffer = new StringBuilder();
|
||||
buffer.append( "locate(" )
|
||||
.append( ( (Renderable) getPattern() ).render( renderingContext ) )
|
||||
.append( ',' )
|
||||
.append( ( (Renderable) getString() ).render( renderingContext ) );
|
||||
|
||||
if ( getStart() != null ) {
|
||||
buffer.append( ',' )
|
||||
.append( ( (Renderable) getStart() ).render( renderingContext ) );
|
||||
}
|
||||
|
||||
buffer.append( ')' );
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
finally {
|
||||
renderingContext.getFunctionStack().pop();
|
||||
}
|
||||
buffer.append( ')' );
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,19 +93,26 @@ public class ParameterizedFunctionExpression<X>
|
|||
|
||||
@Override
|
||||
public String render(RenderingContext renderingContext) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
if ( isStandardJpaFunction() ) {
|
||||
buffer.append( getFunctionName() )
|
||||
.append( "(" );
|
||||
renderingContext.getFunctionStack().push( this );
|
||||
|
||||
try {
|
||||
final StringBuilder buffer = new StringBuilder();
|
||||
if ( isStandardJpaFunction() ) {
|
||||
buffer.append( getFunctionName() ).append( "(" );
|
||||
}
|
||||
else {
|
||||
buffer.append( "function('" )
|
||||
.append( getFunctionName() )
|
||||
.append( "', " );
|
||||
}
|
||||
|
||||
renderArguments( buffer, renderingContext );
|
||||
|
||||
return buffer.append( ')' ).toString();
|
||||
}
|
||||
else {
|
||||
buffer.append( "function('" )
|
||||
.append( getFunctionName() )
|
||||
.append( "', " );
|
||||
finally {
|
||||
renderingContext.getFunctionStack().pop();
|
||||
}
|
||||
renderArguments( buffer, renderingContext );
|
||||
buffer.append( ')' );
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
protected void renderArguments(StringBuilder buffer, RenderingContext renderingContext) {
|
||||
|
|
|
@ -92,16 +92,24 @@ public class SubstringFunction
|
|||
}
|
||||
|
||||
public String render(RenderingContext renderingContext) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
buffer.append( "substring(" )
|
||||
.append( ( (Renderable) getValue() ).render( renderingContext ) )
|
||||
.append( ',' )
|
||||
.append( ( (Renderable) getStart() ).render( renderingContext ) );
|
||||
if ( getLength() != null ) {
|
||||
buffer.append( ',' )
|
||||
.append( ( (Renderable) getLength() ).render( renderingContext ) );
|
||||
renderingContext.getFunctionStack().push( this );
|
||||
|
||||
try {
|
||||
final StringBuilder buffer = new StringBuilder();
|
||||
buffer.append( "substring(" )
|
||||
.append( ( (Renderable) getValue() ).render( renderingContext ) )
|
||||
.append( ',' )
|
||||
.append( ( (Renderable) getStart() ).render( renderingContext ) );
|
||||
|
||||
if ( getLength() != null ) {
|
||||
buffer.append( ',' )
|
||||
.append( ( (Renderable) getLength() ).render( renderingContext ) );
|
||||
}
|
||||
|
||||
return buffer.append( ')' ).toString();
|
||||
}
|
||||
finally {
|
||||
renderingContext.getFunctionStack().pop();
|
||||
}
|
||||
buffer.append( ')' );
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,26 +102,31 @@ public class TrimFunction
|
|||
|
||||
@Override
|
||||
public String render(RenderingContext renderingContext) {
|
||||
String renderedTrimChar;
|
||||
if ( trimCharacter.getClass().isAssignableFrom(
|
||||
LiteralExpression.class ) ) {
|
||||
// If the character is a literal, treat it as one. A few dialects
|
||||
// do not support parameters as trim() arguments.
|
||||
renderedTrimChar = '\'' + ( (LiteralExpression<Character>)
|
||||
trimCharacter ).getLiteral().toString() + '\'';
|
||||
renderingContext.getFunctionStack().push( this );
|
||||
|
||||
try {
|
||||
String renderedTrimChar;
|
||||
if ( trimCharacter.getClass().isAssignableFrom( LiteralExpression.class ) ) {
|
||||
// If the character is a literal, treat it as one. A few dialects
|
||||
// do not support parameters as trim() arguments.
|
||||
renderedTrimChar = '\'' + ( (LiteralExpression<Character>)
|
||||
trimCharacter ).getLiteral().toString() + '\'';
|
||||
}
|
||||
else {
|
||||
renderedTrimChar = ( (Renderable) trimCharacter ).render( renderingContext );
|
||||
}
|
||||
return new StringBuilder()
|
||||
.append( "trim(" )
|
||||
.append( trimspec.name() )
|
||||
.append( ' ' )
|
||||
.append( renderedTrimChar )
|
||||
.append( " from " )
|
||||
.append( ( (Renderable) trimSource ).render( renderingContext ) )
|
||||
.append( ')' )
|
||||
.toString();
|
||||
}
|
||||
else {
|
||||
renderedTrimChar = ( (Renderable) trimCharacter ).render(
|
||||
renderingContext );
|
||||
finally {
|
||||
renderingContext.getFunctionStack().pop();
|
||||
}
|
||||
return new StringBuilder()
|
||||
.append( "trim(" )
|
||||
.append( trimspec.name() )
|
||||
.append( ' ' )
|
||||
.append( renderedTrimChar )
|
||||
.append( " from " )
|
||||
.append( ( (Renderable) trimSource ).render( renderingContext ) )
|
||||
.append( ')' )
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package org.hibernate.sql.ast;
|
||||
|
||||
import org.hibernate.Incubating;
|
||||
|
||||
/**
|
||||
* Used to indicate which query clause we are currently processing
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@Incubating
|
||||
public enum Clause {
|
||||
/**
|
||||
* The insert values clause
|
||||
*/
|
||||
INSERT,
|
||||
|
||||
/**
|
||||
* The update set clause
|
||||
*/
|
||||
UPDATE,
|
||||
|
||||
/**
|
||||
* Not used in 5.x. Intended for use in 6+ as indicator
|
||||
* of processing predicates (where clause) that occur in a
|
||||
* delete
|
||||
*/
|
||||
DELETE,
|
||||
|
||||
SELECT,
|
||||
FROM,
|
||||
WHERE,
|
||||
GROUP,
|
||||
HAVING,
|
||||
ORDER,
|
||||
LIMIT,
|
||||
CALL,
|
||||
|
||||
/**
|
||||
* Again, not used in 5.x. Used in 6+
|
||||
*/
|
||||
IRRELEVANT
|
||||
|
||||
}
|
Loading…
Reference in New Issue