FQLExpression.java
/*
** Module : FQLExpression.java
** Abstract : FQL expression with substitution parameters.
**
** Copyright (c) 2014-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 SVL 20140210 Created initial version.
** 002 ECF 20150715 Added toFinalExpression variant which optionally emits simple placeholders.
** 003 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 004 AL2 20211101 Changed copy constructor.
** 20221102 Rename class to FQLExpression.
** 005 OM 20231115 Added multiplexed property to mark the object as already containing the multiplex and
** this constraint into outer FQL is not needed and even illegal.
** 006 CA 20240812 Performance improvement - reduce the number of StringBuilder instances.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import com.goldencode.p2j.util.*;
import java.util.*;
/**
* Represents an FQL expression: FQL where clause, FQL statement or FQL snippet. It allows to
* concatenate a string containing substitution parameters and then emit resulting string with
* proper placeholders, e.g. with placeholders for JPA-style positional parameters which have
* throughout numeration. Example of usage:<br><br>
* <code>
* FQLExpression fql = new FQLExpression("where ");<br>
* fql.append("a = ", true);<br>
* fql.append(" and b");<br>
* fql.append(' = ", true);<br>
* fql.toFinalExpression; -> "where a = ?0 and b = ?1"
* </code>
*/
public class FQLExpression
{
/**
* Parts of the FQL expression separated by substitutions.
*/
private List<String> parts = new ArrayList<>();
/**
* <code>true</code> if the FQL expression ends with a substitution.
*/
private boolean endsWithSubst = false;
/**
* String builder to speed up sequential concatenations. Can be <code>null</code> or contain
* the last part for {@link #parts}.
*/
private StringBuilder lastPart = new StringBuilder(64);
/** Flag indicating if {@link #lastPart} is used. */
private boolean hasLastPart = false;
/** By default, FQL expressions, including the empty ones are not (yet) multiplexed. */
private boolean multiplexed = false;
/**
* Create an empty FQL expression.
*/
public FQLExpression()
{
}
/**
* Create an FQL expression starting with the specified sub-expression.
*
* @param starting
* Staring sub-expression.
*/
public FQLExpression(FQLExpression starting)
{
this.parts = new ArrayList<>(starting.parts);
if (starting.hasLastPart)
{
this.lastPart.append(starting.lastPart);
this.hasLastPart = true;
}
this.endsWithSubst = starting.endsWithSubst;
this.multiplexed = starting.multiplexed;
}
/**
* Create an FQL expression starting with the specified sub-expression.
*
* @param starting
* Staring sub-expression.
*/
public FQLExpression(String starting)
{
this(starting, false);
}
/**
* Create an FQL expression starting with the specified sub-expression.
*
* @param starting
* Staring sub-expression.
* @param endsWithSubst
* <code>true</code> if the starting sub-expression ends with a substitution.
*/
public FQLExpression(String starting, boolean endsWithSubst)
{
append(starting, endsWithSubst);
}
/**
* Append the expression with the specified sub-expression.
*
* @param subExpression
* Sub-expression to append.
*
* @return this expression.
*/
public FQLExpression append(String subExpression)
{
return append(subExpression, false);
}
/**
* Append the expression with the specified sub-expression.
*
* @param subExpression
* Sub-expression to append.
* @param endsWithSubst
* <code>true</code> if the appended sub-expression ends with a substitution.
*
* @return this expression.
*/
public FQLExpression append(String subExpression, boolean endsWithSubst)
{
if (this.endsWithSubst || parts.isEmpty())
{
writeLastPart();
parts.add(subExpression);
}
else
{
int lastIndex = parts.size() - 1;
if (!hasLastPart)
{
String part = parts.get(lastIndex);
hasLastPart = true;
lastPart.append(part);
}
lastPart.append(subExpression);
}
this.endsWithSubst = endsWithSubst;
return this;
}
/**
* Append the expression with the specified sub-expression.
*
* @param subExpression
* Sub-expression to append.
*
* @return this expression.
*/
public FQLExpression append(FQLExpression subExpression)
{
if (!subExpression.isEmpty())
{
subExpression.writeLastPart();
int len = subExpression.parts.size();
for (int i = 0; i < len; i++)
{
boolean last = (i == len - 1);
append(subExpression.parts.get(i), !last || subExpression.endsWithSubst);
}
}
return this;
}
/**
* Returns the final form of the FQL expression with placeholders emitted. Placeholders are
* for JPA-style positional parameters, i.e. ?0, ?1 etc.
*
* @return See above.
*/
public String toFinalExpression()
{
return toFinalExpression(true);
}
/**
* Returns the final form of the FQL expression with placeholders emitted. Placeholders are
* for JPA-style positional parameters, i.e. ?0, ?1 etc. if <code>jpsStyle</code> is
* <code>true</code>; otherwise, simple substitution placeholders (i.e., ?) are emitted.
*
* @param jpaStyle
* <code>true</code> to emit JPA-style positional parameter placeholders;
* <code>false</code> to emit simple placeholders.
*
* @return See above.
*/
public String toFinalExpression(boolean jpaStyle)
{
writeLastPart();
int index = 0;
int len = parts.size();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++)
{
sb.append(parts.get(i));
boolean last = (i == len - 1);
if (!last || endsWithSubst)
{
sb.append("?");
if (jpaStyle)
{
sb.append(index++);
}
}
}
return sb.toString();
}
/**
* If this expression is empty.
*
* @return <code>true</code> if this expression is empty.
*/
public boolean isEmpty()
{
return parts.isEmpty();
}
/**
* Trim the expression.
*
* @return this expression.
*/
public FQLExpression trim()
{
if (!isEmpty())
{
writeLastPart();
String first = TextOps.leftTrim(parts.get(0)).toJavaType();
parts.set(0, first);
if (!endsWithSubst)
{
int lastIndex = parts.size() - 1;
String last = TextOps.rightTrim(parts.get(lastIndex)).toJavaType();
parts.set(lastIndex, last);
}
}
return this;
}
/**
* Returns the string representation of this expression.
*
* @return the string representation of this expression.
*/
public String toString()
{
return toFinalExpression();
}
/**
* If the last part of the expression is contained in {@link #lastPart} buffer, flush it to
* {@link #parts}.
*/
private void writeLastPart()
{
if (hasLastPart)
{
parts.set(parts.size() - 1, lastPart.toString());
lastPart.setLength(0);
hasLastPart = false;
}
}
/**
* Test whether the multiplex check was added to this expression.
*
* @return {@code true} if this expression was marked as multiplexed after the condition was added.
*/
public boolean wasMultiplexed()
{
return multiplexed;
}
/**
* Sets the expression's multiplexed property. Normally, it makes sense to call this only with {@code true}
* parameter, after adding the multiplex check condition to expression. The opposite would be useful if the
* expression is cleared, but this feature is not currently available.
*
* @param multiplexed
* The new value of the property.
*/
public void setMultiplexed(boolean multiplexed)
{
this.multiplexed = multiplexed;
}
}