org.jclarion.clarion.view.ClarionView Maven / Gradle / Ivy
/**
* Copyright 2010, by Andrew Barnham
*
* The contents of this file are subject to
* GNU Lesser General Public License (LGPL), v.3
* http://www.gnu.org/licenses/lgpl.txt
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied.
*/
package org.jclarion.clarion.view;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jclarion.clarion.BindProcedure;
import org.jclarion.clarion.ClarionFile;
import org.jclarion.clarion.ClarionKey;
import org.jclarion.clarion.ClarionMemoryChangeListener;
import org.jclarion.clarion.ClarionNumber;
import org.jclarion.clarion.ClarionObject;
import org.jclarion.clarion.ClarionSQLFile;
import org.jclarion.clarion.ClarionString;
import org.jclarion.clarion.PropertyObject;
import org.jclarion.clarion.memory.CMem;
import org.jclarion.clarion.runtime.CError;
import org.jclarion.clarion.runtime.CErrorImpl;
import org.jclarion.clarion.runtime.CExprImpl;
import org.jclarion.clarion.runtime.ObjectBindProcedure;
import org.jclarion.clarion.runtime.expr.CExpr;
import org.jclarion.clarion.runtime.expr.CExprError;
import org.jclarion.clarion.runtime.expr.LabelExpr;
import org.jclarion.clarion.util.FileState;
import org.jclarion.clarion.util.FileState.Mode;
import org.jclarion.clarion.constants.*;
/**
* Model a clarion view
*
* Some observations from experiments with views in clarion 5.5 and
* resulting SQL sent to postgres:
*
* When no order is indicated - select is not sorted
* Use of positioning etc has not effect reset will reset from top
*
* Ordering is critical for positioning etc. Clarion assumes that
* order defines a unique position. We will too
*
* changes to prop:order take effect on next set()
*
* Calling reset(view,file) will trigger reset based on position
* inferred in file buffers taking changes to prop:order etc into
* consideration. Clarion also forces a read on the table(s) in question
* ( their primary keys?)
*
* Calling reset(view,int) does *not* do what clarion documentation implies
* and what I originally coded this object to do. Calling reset is very similar
* to calling reset(view,file) except trailing fields in key are zeroed out
*
* calling getposition(), change prop:order call reset(view,string)
* works same as above. Clarion must be encoding more than just ordering
* data. From help files - clarion says it encodes data relating to
* primary key fields. So position for views encodes PK data only.
* Presumably - this is all view needs - pk data. Everything else can
* be derived from there.
*
* All fields in a table that are keyed are implicitly included as
* part of the project list
*
* reget must call get on every underlying file
*
*
*
* Implementation of this is to just create a dynamic ClarionSQLFile
* object with an overloaded open() method where we synthesize the
* FileState object to represent a more sophisticated table structure
* and then methods like next() etc here become simple wrappers for the
* underlying dynamically create file
*
* Items:
*
* Limitiations:
* * Will not handle having tables appear more than once in structre
* Clarion system did not handle this well either - i.e. no way
* to differentiate tables thus selected.
*
* * no intention of implementing mutators like put() etc just yet.
* to do this: 1) save changed params. Call reget() on primary file
* Restroe changed params. put()
*
* * outer joins and positions will not work due to absence of facility
* to record position of nullable parameters in ClarionSQLFile right now.
* Is doable - but tricky because of fact that comparators in ANSI SQL
* do not play nice with nullable columns
*
* * will only work for all File types are SQL
*
* * will only work if all file types share same source
*
* TODO - implement view solution that can cross non SQL implementations
* and different sources.
*
* @author barney
*
*/
public class ClarionView extends PropertyObject implements ViewField
{
private static Logger log = Logger.getLogger(ClarionView.class.getName());
List orderedFields=new ArrayList();
private List joins=new ArrayList();
private List fields=new ArrayList();
private ClarionFile table;
private List allFiles;
private Map allFields;
private StringBuilder tablename;
private ClarionSQLFile file;
private ClarionKey order;
private String filter;
private CExpr clientFilter;
private boolean useClientFilter=false;
private boolean changed=true;
private int limit;
private ClarionObject[] primaryKeyPosition;
private boolean open;
private static class FieldEntry
{
public ClarionObject field;
public int type;
public boolean nonull;
//public int filePosition;
public int viewPosition;
public String viewName;
//public String fileName;
}
public ClarionView()
{
setProperty(Prop.KEYS,1);
}
public void add(ViewJoin aTable)
{
joins.add(aTable);
orderedFields.add(aTable);
}
public void add(ViewProject aProject)
{
fields.add(aProject);
orderedFields.add(aProject);
}
public void setTable(ClarionFile aFile)
{
this.table=aFile;
}
public void setOrder(String order)
{
setProperty(Prop.ORDER,order);
}
public void setFilter(String filter)
{
setProperty(Prop.FILTER,filter);
}
private void figureFiles(ViewJoin vj)
{
if (vj.table!=null) {
allFiles.add(vj.table);
} else if (vj.joinKey!=null) {
allFiles.add(vj.joinKey.getFile());
}
for ( ViewJoin sub_vj : vj.joins ) {
figureFiles(sub_vj);
}
}
private void figureFiles()
{
allFiles=new ArrayList();
allFiles.add(table);
for ( ViewJoin vj : joins ) {
figureFiles(vj);
}
}
public void open()
{
CErrorImpl.getInstance().clearError();
if (open==true) {
CErrorImpl.getInstance().setError(52,"View already open");
return;
}
// first step of opening a view is to coalesce all tables
// and fields involved
allFiles=new ArrayList();
allFields=new IdentityHashMap();
if (!include(table,fields)) return;
tablename = new StringBuilder();
tablename.append(table.getFileState().global.name);
tablename.append(" A");
for (ViewJoin vj : joins ) {
if (!include(1,vj)) return;
}
// build local clarion table
file=new ClarionSQLFile() {
@Override
public int where(Object o)
{
ClarionObject co =(ClarionObject)o;
FieldEntry fe = allFields.get(co);
if (fe==null) return 0;
return fe.viewPosition+1;
}
public void open()
{
FileState fs = getFileState();
fs.global.openCount++;
fs.changed=new boolean[allFields.size()];
fs.isnull=new boolean[allFields.size()];
fs.global.source=table.getFileState().global.source;
fs.global.name=tablename.toString();
fs.changed=new boolean[allFields.size()];
fs.listeners=new ClarionMemoryChangeListener[allFields.size()];
fs.fields=new ClarionObject[allFields.size()];
fs.global.types=new int[allFields.size()];
fs.global.nonull=new boolean[allFields.size()];
fs.global.autoincrementing=new boolean[allFields.size()];
fs.global.fieldNames=new String[allFields.size()];
for ( final FieldEntry e : allFields.values() ) {
fs.fields[e.viewPosition]=e.field;
fs.global.types[e.viewPosition]=e.type;
fs.global.nonull[e.viewPosition]=e.nonull;
fs.global.fieldNames[e.viewPosition]=e.viewName;
}
}
@Override
protected boolean initWhere(StringBuilder sb) {
if (filter==null) return false;
sb.append(filter);
return true;
}
};
file.open();
open=true;
}
public void close()
{
if (!open) return;
file.close();
open=false;
}
public void delete()
{
table.setPrimaryKeyPosition(primaryKeyPosition);
table.delete();
}
public void put()
{
CErrorImpl.getInstance().clearError();
if (!file.testWatch()) return;
table.setPrimaryKeyPosition(primaryKeyPosition);
table.put();
}
public void set()
{
//log.entering("CV","set");
set(0);
}
public void flush()
{
}
/**
* Not really clear when is a good time to call records
*
* Should we regenerate filters as part of record get
* or assume this has already happened?
*
* Will err on side of no regeneration.
*
* Another problem - what about client side filters?
* They will not be taken into consideration. Will consider
* this one to be ok - if anyone wants an accurate count of
* actual records - they need to iterate over them
*
* @return
*/
public int records()
{
//log.entering("CV","record");
if (!testOpen());
return file.records();
}
public void set(int sortOffset)
{
//log.entering("CV","set",sortOffset);
if (!testOpen()) return;
if (!regenOrderAndFilter()) return;
if (sortOffset==0) {
file.set(order);
} else {
CMem sos = CMem.create();
sos.writeByte(1); // inclusive
sos.writeByte(sortOffset); // # of fields
ClarionKey.Order[] bits = order.getOrder();
for (int scan = 0; scan < sortOffset; scan++) {
bits[scan].object.serialize(sos);
}
ClarionString cs = new ClarionString(sos.readString());
file.reset(order, cs);
}
}
public void next()
{
//log.entering("CV","next");
iterate(1);
}
protected void iterate(int dir)
{
if (!testOpen()) return;
if (file.getFileState().mode==Mode.Reset && changed) {
regenOrderAndFilter();
}
while (true ) {
this.table.getFileState().ignoreChange=true;
try {
if (dir>0) {
file.next();
} else {
file.previous();
}
} finally {
this.table.getFileState().ignoreChange=false;
}
if (CError.errorCode()!=0) break;
if (useClientFilter && clientFilter!=null) {
try {
ClarionObject o = clientFilter.eval();
if (o!=null && o.boolValue()) {
primaryKeyPosition=this.table.getPrimaryKeyPosition();
break;
}
} catch (CExprError ex) {
CErrorImpl.getInstance().setError(801,"Client Filter failed. Probably missing binding");
return;
}
} else {
primaryKeyPosition=this.table.getPrimaryKeyPosition();
break;
}
}
}
public void watch()
{
file.watch();
}
public ClarionString getPosition()
{
//log.entering("CV","getPosition");
CMem sos = CMem.create();
for (ClarionFile file : allFiles) {
ClarionKey ck = file.getPrimaryKey();
if (ck==null) continue;
for (ClarionKey.Order o : ck.getFields()) {
o.object.serialize(sos);
}
}
// also write additional order field data onto object
// so that we can reliably check for changes in quick scan mode.
// quick scan mode option only
if (order!=null) {
ClarionKey.Order o[] = order.getOrder();
for (int scan=0;scan0) {
//quick scan optimisation
ClarionString cs = getPosition();
String cs_string=cs.toString();
String position_string = position.toString();
if (!position_string.startsWith(cs_string)) {
if (log.isLoggable(Level.FINE)) {
log.fine("Calling Reget: ["+ClarionString.rtrim(position_string)+"] != ["+cs_string+"]");
}
reget(position);
} else {
if (log.isLoggable(Level.FINE)) {
log.fine("Skipping Reget:"+ClarionString.rtrim(position_string));
}
}
} else {
reget(position);
}
reset(table);
}
public void reget(ClarionString position)
{
//log.entering("CV","reget",position);
CMem sos = CMem.create();
position.serialize(sos);
for (ClarionFile file : allFiles) {
ClarionKey ck = file.getPrimaryKey();
if (ck==null) {
log.warning("File lacks a primary key:"+file);
continue;
}
for (ClarionKey.Order o : ck.getFields()) {
o.object.deserialize(sos);
}
file.get(ck);
}
}
/**
* Reset based on current order parameter's directly from
* ClarionFile buffers
*
* @param position
*/
public void reset(ClarionFile position)
{
//log.entering("CV","reset ClarionFile",position);
if (!testOpen()) return;
if (!regenOrderAndFilter()) return;
if (order!=null) {
order.set();
} else {
file.set();
}
}
public void previous()
{
//log.entering("CV","previous");
iterate(-1);
}
public void buffer(Integer pagesize,Integer behind,Integer ahead,Integer timeout) {
if (pagesize!=null) {
limit=pagesize.intValue()*5;
} else {
limit=0;
}
changed=true;
}
public ClarionFile getFile(int indx)
{
if (allFiles==null) figureFiles();
if (indx<1) return null;
if (indx>allFiles.size()) return null;
return allFiles.get(indx-1);
}
@Override
public PropertyObject getParentPropertyObject() {
return null;
}
@Override
public ClarionObject getLocalProperty(int index) {
if (index==Prop.FILES) {
if (allFiles==null) figureFiles();
return new ClarionNumber(this.allFiles.size());
}
if (index==Prop.FIELDS) {
return new ClarionNumber(getOrderedColumns().size());
}
if (index==Prop.SQL) return file.getProperty(Prop.SQL);
return super.getLocalProperty(index);
}
@Override
protected void notifyLocalChange(int indx, ClarionObject value) {
switch(indx) {
case Prop.SQL:
changed=false;
file.setProperty(Prop.SQL,value);
break;
case Prop.SQLFILTER:
case Prop.FILTER:
case Prop.ORDER:
changed=true;
CErrorImpl.getInstance().clearError();
break;
}
super.notifyLocalChange(indx, value);
}
private boolean regenOrderAndFilter() {
if (!changed) return true;
//log.entering("CV","regenOrderAndFilter");
StringBuilder newFilter=new StringBuilder();
String os = getProperty(Prop.ORDER).toString();
ClarionKey old_order=null;
old_order=order;
order=null;
file.setLimit(limit);
if (limit>0) file.setQuickScan();
os.replaceAll(" ",""); // get rid of whitespace
StringTokenizer tok=new StringTokenizer(os,",");
while (tok.hasMoreTokens()) {
String s = tok.nextToken();
boolean ascend=true;
if (s.startsWith("+")) {
s=s.substring(1);
} else if (s.startsWith("-")) {
s=s.substring(1);
ascend=false;
}
String function=null;
if (s.endsWith(")")) {
int start = s.indexOf('(');
if (start>0) {
function=s.substring(0,start).toLowerCase();
s=s.substring(start+1,s.length()-1);
}
}
// fast option - check bindings
ClarionObject obj = null;
FieldEntry fe = null;
try {
BindProcedure bp = CExprImpl.getInstance().resolveBind(s,false);
if (bp instanceof ObjectBindProcedure) {
obj = bp.execute(null);
fe=allFields.get(obj);
}
} catch (CExprError ex) {
// do nothing - silently fall through to plan B
}
if (obj==null || fe==null) {
CErrorImpl.getInstance().setError(801,"Could not resolve field in order:"+s);
return false;
}
if (order==null) order=new ClarionKey("viewkey");
order.addField(obj,!ascend,function);
}
if (order!=null) {
file.addKey(order);
}
if (newFilter.length()>0) {
newFilter.append(")");
}
String f=getProperty(Prop.FILTER).toString();
String sqlf=getProperty(Prop.SQLFILTER).toString();
f=f.trim();
sqlf=sqlf.trim();
if (sqlf.length()>0) {
if (sqlf.startsWith("+")) {
sqlf=sqlf.substring(1);
} else {
f="";
}
}
clientFilter=null;
useClientFilter=false;
if (f.length()>0) {
StringBuilder sf=new StringBuilder();
clientFilter = CExprImpl.getInstance().compile(f);
if (clientFilter==null) return false;
substituteBindings(clientFilter);
clientFilter.generateString(sf,true);
StringBuilder lf = new StringBuilder();
clientFilter.generateString(lf,false);
if (!lf.toString().equals(sf.toString())) {
useClientFilter=true;
}
if (sf.length()>0) {
if (newFilter.length()>0) {
newFilter.append(" AND (");
} else {
newFilter.append(" WHERE (");
}
newFilter.append(sf);
newFilter.append(')');
}
}
if (sqlf.length()>0) {
if (newFilter.length()>0) {
newFilter.append(" AND (");
} else {
newFilter.append(" WHERE (");
}
newFilter.append(sqlf);
newFilter.append(')');
}
String oldFilter=filter;
if (newFilter.length()>0) {
filter=newFilter.toString();
} else {
filter=null;
}
changed=false;
FileState fs = file.getFileState();
if (fs.scanKey!=null) {
if (fs.scanKey.equals(order)) {
boolean changedFilter=false;
if (oldFilter==null ^ filter==null) {
changedFilter=true;
} else {
if (oldFilter!=null && !oldFilter.equals(newFilter)) {
changedFilter=true;
}
}
if (!changedFilter) fs.scanKey=order;
}
}
if (old_order!=null) {
file.removeKey(old_order);
}
return true;
}
/**
private boolean regenFilter() {
return true;
}
*/
private void substituteBindings(CExpr expr)
{
Iterator scan = expr.iterator();
while (scan.hasNext()) {
CExpr e = scan.next();
if (e instanceof LabelExpr) {
String name = ((LabelExpr)e).getName();
ClarionObject o = e.eval();
FieldEntry fe = allFields.get(o);
if (fe!=null) {
CExprImpl.getInstance().bind(name,o,fe.viewName,fe.type);
} else {
if (o.getOwner() instanceof ClarionFile) {
if (allFiles.contains(o.getOwner())) {
ClarionFile file = (ClarionFile)o.getOwner();
int indx = allFiles.indexOf(file);
char c = (char)('A'+indx);
CExprImpl.getInstance().bind(name,o,c+"."+o.getName(),file.getSQLType(o));
}
}
}
}
}
}
private boolean include(ClarionFile table,FileState fs,ClarionObject obj)
{
if (allFields.containsKey(obj)) return true;
int pos = table.flatWhere(obj)-1;
if (pos==-1) {
CErrorImpl.getInstance().setError(36,"Field not located");
return false;
}
FieldEntry fe = new FieldEntry();
fe.field=obj;
fe.type=fs.global.types[pos];
fe.nonull=fs.global.nonull[pos];
//fe.filePosition=pos;
fe.viewPosition=allFields.size();
//fe.fileName=fs.global.fieldNames[pos];
fe.viewName=(char)('A'-1+allFiles.size())+"."+fs.global.fieldNames[pos];
allFields.put(obj,fe);
return true;
}
private boolean include(int pindx,ViewJoin join)
{
if (join.table==null && join.joinKey!=null) {
join.table=join.joinKey.getFile();
}
if (!include(join.table,join.fields)) return false;
int pos = allFiles.size();
StringBuilder joinClause=new StringBuilder();
// work out join
if (join.joinKey!=null) {
Iterator cscan = join.joinKey.getFields().iterator();
int pscan=0;
// joining from key to parameters - as long as there are params
while (pscan0) joinClause.append(" AND ");
joinClause.append(child.viewName);
joinClause.append('=');
joinClause.append(parent.viewName);
pscan++;
}
} else {
try {
CExpr ex = CExprImpl.getInstance().compile(join.joinExpression);
if (ex==null) return false;
substituteBindings(ex);
StringBuilder loose=new StringBuilder();
ex.generateString(joinClause,true);
ex.generateString(loose,true);
if (!joinClause.toString().equals(loose.toString())) {
CErrorImpl.getInstance().setError(801,"Join Clause appears to contain expression elements which cannot be converted into SQL");
log.warning("Strict and Loose differ");
log.warning("Strict:"+joinClause.toString());
log.warning("Loose:"+loose.toString());
return false;
}
} catch (CExprError ex) {
CErrorImpl.getInstance().setError(801,"Join Clause did not evaluate - missing binds");
return false;
}
}
if ( join.inner ) {
tablename.append(" INNER JOIN ");
} else {
tablename.append(" LEFT OUTER JOIN ");
}
tablename.append(join.table.getFileState().global.name);
tablename.append(' ');
tablename.append((char)('A'-1+allFiles.size()));
tablename.append(" ON (");
tablename.append(joinClause);
tablename.append(')');
for (ViewJoin child : join.joins ) {
if (!include(pos,child)) return false;
}
return true;
}
private boolean include(ClarionFile table,List fields)
{
if (!(table instanceof ClarionSQLFile)) {
CErrorImpl.getInstance().setError(36,"Only SQL files allowed in a view");
return false;
}
FileState fs = table.getFileState();
if (fs.global.openCount==0) {
CErrorImpl.getInstance().setError(36,"Supporting tables must already be open");
return false;
}
allFiles.add(table);
if (fields.size()==0) {
// add all
for (int scan=1;scan<=table.getVariableCount();scan++) {
if (!include(table,fs,table.what(scan))) return false;
}
} else {
for ( ViewProject vp : fields ) {
for ( ClarionObject co : vp.fields ) {
if (!include(table,fs,co)) return false;
}
}
}
if (isProperty(Prop.KEYS)) {
for (ClarionKey key : table.getKeys()) {
for ( ClarionKey.Order o : key.getFields()) {
if (!include(table,fs,o.object)) return false;
}
}
}
return true;
}
private boolean testOpen()
{
CErrorImpl.getInstance().clearError();
if (!open) {
CErrorImpl.getInstance().setError(37,"View not open");
return false;
}
return true;
}
private List orderedColumns;
private List getOrderedColumns()
{
if (orderedColumns==null) {
orderedColumns=new ArrayList();
addOrderedColumns(orderedColumns,this);
}
return orderedColumns;
}
private void addOrderedColumns(List order,ViewField field)
{
ClarionObject o = field.getField();
if (o!=null) order.add(o);
Iterable subs = field.getSubFields();
if (subs!=null) {
for ( ViewField sf : subs ) {
addOrderedColumns(order,sf);
}
}
}
public ClarionFile getFileForField(int indx)
{
return (ClarionFile)getOrderedColumns().get(indx-1).getOwner();
}
public int getFileIndexForField(int indx)
{
ClarionObject co = getOrderedColumns().get(indx-1);
return co.getOwner().where(co);
}
@Override
protected void debugMetaData(StringBuilder sb) {
// TODO Auto-generated method stub
}
@Override
public ClarionObject getField() {
return null;
}
@Override
public Iterable getSubFields() {
return orderedFields;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy