All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.xfa.data.DataWindow Maven / Gradle / Ivy

There is a newer version: 2024.11.18598.20241113T125352Z-241000
Show newest version
package com.adobe.xfa.data;


import com.adobe.xfa.Arg;
import com.adobe.xfa.Document;
import com.adobe.xfa.Element;
import com.adobe.xfa.LogMessage;
import com.adobe.xfa.Node;
import com.adobe.xfa.NodeList;
import com.adobe.xfa.Obj;
import com.adobe.xfa.ScriptTable;
import com.adobe.xfa.STRS;
import com.adobe.xfa.XFA;
import com.adobe.xfa.ut.ExFull;
import com.adobe.xfa.ut.FindBugsSuppress;
import com.adobe.xfa.ut.Peer;
import com.adobe.xfa.ut.ResId;
import com.adobe.xfa.ut.IntegerHolder;

import java.util.ArrayList;
import java.util.List;


/**
 *
 * @exclude from published api -- Mike Tardif, May 2006.
 */
public final class DataWindow extends Obj {

	private static final int DEFAULT_GRANULARITY = 16;
	private String maRecordName;
	private boolean mbDefined;
	private boolean mbLazyLoading;
	private boolean mbUpToDate;
	private DataModel mDataModel;
	private final List mFilters = new ArrayList();
	private int mnAbsRecordIndex;
	private int mnCurrentRecordIndex;
	private int mnDesiredRecordsAfter;
	private int mnDesiredRecordsBefore;
	private int mnRecordLevel;
	private int mnRecordsAfter;
	private int mnRecordsBefore;
	private int mnSpecifiedRecordLevel;
	private final List mRecordIndexes = new ArrayList();
	private final ArrayList mRecords = new ArrayList(DEFAULT_GRANULARITY);
	private Element mStartNode;

	public DataWindow() {
	}

	void addFilter(DataWindowFilter oFilter) {
		mFilters.add(oFilter);
	}

	/**
	 * @exclude from public api.
	 */
	public void addRecordGroup(DataNode dataGroup) {
		if (mRecords.size() == DEFAULT_GRANULARITY)
			mRecords.ensureCapacity(1024);
		for (int i = 0; i < mFilters.size(); i++) {
			DataWindowFilter f = mFilters.get(i);
			if (! f.filterRecord(dataGroup,mnAbsRecordIndex)) {
				mnAbsRecordIndex++;
				return; // stop looping it didn't pass all filters so don't add it
			}
		}
		//
		// Add record.
		//
		mRecords.add(dataGroup);
		mRecordIndexes.add(Integer.valueOf(mnAbsRecordIndex));
		mnAbsRecordIndex++;
	}

	private void adjustRecordLevel(boolean bUseRecordName /* = true */) {
		assert(mnRecordLevel == 0);
		Element oNewRoot = findStartNode(mStartNode, true);
		if (oNewRoot == null)
			return;		// no data

		Element parent = oNewRoot.getXFAParent();
		Document doc = oNewRoot.getOwnerDocument();

		while (parent != doc && parent != null) {
			mnRecordLevel++;
			parent = parent.getXFAParent();
		}
		mnRecordLevel += mnSpecifiedRecordLevel;
		//
		// Vantive 591676: check record name!
		// initialize() call this with bUseRecordName set to false, because
		// it does its own searching for records by name.
		//
		if (bUseRecordName && maRecordName != null) {
			IntegerHolder recordLevel = new IntegerHolder(mnRecordLevel);
			if (findRecordByName(oNewRoot, recordLevel, maRecordName))
				mnRecordLevel = recordLevel.value;
		}
	}


	/**
	 * Return the current record number.
	 *
	 * @return the current record number.
	 */
	public int currentRecordNumber() {
		validate(false);
		return mnCurrentRecordIndex;
	}

	@FindBugsSuppress(code="ES")
	void dataGroupAddedOrRemoved (DataNode dataGroup, boolean bAdded) {
		// Is this DataWindow initialized yet?
		if (mDataModel == null)
			return;
		
		//
		// The main purpose of this routine is to (potentially) invalidate this data window.
		// If it's already invalid, there's nothing to do.
		//
		if (! mbUpToDate)
			return;
		if (mbLazyLoading)
			return;
		//
		// First check if the name matches our record name criteria.  If it doesn't, we're
		// not interested in this event.
		//
		if (maRecordName != null && maRecordName != dataGroup.getName())
			return;
		//
		// Check up the data group's lineage to see that its parent is $data (the alias node).
		// If not, then it's not a record group, so we're not interested in this event.
		//
		Element oAliasNode = mDataModel.getAliasNode();
		// watson bug 1649097,  if the alias node is removed invalidate the data window.
		if (oAliasNode == dataGroup) {
			invalidate();
			return;
		}
		Element oParent = dataGroup;
		while (oParent != null) {
			if (oParent == oAliasNode)
				break;			// we are inside $data
			oParent = oParent.getXFAParent();
		}
		if (oParent == null)
			return;			// didn't hit the alias node, so the data group is not under $data
		//	
		// Next check the record level to see if it matches.
		//	
		if (mnRecordLevel == 0) {
			adjustRecordLevel(true);
			if (mnRecordLevel == 0)
				return;
		}
		//
		// Ensure the node add/removed isn't at the record depth or above.
		//
		DataNode oDomPeer = dataGroup;
		if (oDomPeer == null)
			return;
		Element parentCheck = oDomPeer.getXFAParent();
		int nLevel = 1;	
		while (parentCheck != null /* && parentCheck.getNodeType() != Node.DOCUMENT_NODE */ ) {
			nLevel++;
			if (nLevel > mnRecordLevel)
				return;
			parentCheck = parentCheck.getXFAParent();
		}
		if (nLevel == mnRecordLevel)
			invalidate();	// mark data window as out-of-date for next time somebody calls us
	}

	/**
	 * Find a record by name
	 * @return the record level if found. null if not found
	 */
	@FindBugsSuppress(code="ES")
	private boolean findRecordByName(Element root, IntegerHolder recordLevel, String aRecordName) {
		String aName = root.getLocalName();
		String aNewName = mDataModel.getMapping(aName);
		if (aNewName != "")
			aName = aNewName;
		if (aRecordName == aName && ! isExcluded(root))
			return recordLevel.value > 0;
		Node node = root.getFirstXMLChild();
		while (node != null) {
			recordLevel.value++;
			if (node instanceof Element && findRecordByName((Element) node, recordLevel, aRecordName))
				return recordLevel.value > 0;
			recordLevel.value--;
			node = node.getNextXMLSibling();
		}
		return false;
	}

	/**
	 *	 Do a depth-first search starting at node, looking for a start node that is not
	 *	 in a compatible namespace and is not in an excluded namespace.
	 */
	private Element findStartNode(Element node, boolean bCheckParent /* = true */) {
		boolean bHasModelParent = false;
		if (bCheckParent) {
			String aNS = node.getNS();
			if (mDataModel.isCompatibleNS(aNS) && node.getLocalName() == STRS.DATASETS)
				bHasModelParent = true;
		}
		if (! bHasModelParent && ! isExcluded(node))
			return node;	// this node is OK to use

		// dig through children
		for (Node childNode = node.getFirstXMLChild(); childNode != null; childNode = childNode.getNextXMLSibling()) {
			
			if (childNode instanceof Element) {
				Element childElement = (Element)childNode;
			
				Element search = null;
	
				// Only get xfa:data Child.
				// Watson 1114483 - legacy XPF data has "xfa:Data" - so need to check both.
				if (bHasModelParent) {
					
					String aNS = childElement.getNS();
					String aLocalName = childElement.getLocalName();
					if (mDataModel.isCompatibleNS(aNS) && 
						(aLocalName == XFA.DATA || aLocalName == STRS.DATA)) {
						
						search = findStartNode(childElement, false);
					}
	    		}
    			else
    				search = findStartNode(childElement, false);
				
				if (search != null)
					return search;
			}
		}
		
		return null;
	}

	public String getClassAtom() {
		return "dataWindow";
	}

	public String getClassName() {
		return "dataWindow";
	}

	public Arg getScriptProperty(String sPropertyName) {
		throw new ExFull(ResId.UNSUPPORTED_OPERATION, "DataWindow#getScriptProperty");
	}

	public ScriptTable getScriptTable() {
		return DataWindowScript.getScriptTable();
	}

	/**
	 * Move the current record to a specific record.
	 *
	 * @param newRecord the absolute record number.
	 */
	public void gotoRecord(int newRecord) {
		validate(false);
		if ((mbDefined) && (newRecord == currentRecordNumber()))
			return;
		if (mbLazyLoading) {
			int newLastRecord;	// last record in our mRecords Array
			newLastRecord = newRecord + mnDesiredRecordsAfter;
			//
			// mnDesiredRecordsAfter might be UINT_MAX -- check to see if we
			// exceeded the maximum value of a int by checking if
			// we ended up with a smaller number (this will only happen
			// when the value wraps around).
			//
			if (newLastRecord < newRecord)
				newLastRecord = Integer.MAX_VALUE;
			if (mnCurrentRecordIndex + mnRecordsAfter < newLastRecord)
				loadToRecord(newLastRecord);
			else if (mRecords.get(newRecord) == null) { // check if it's been unloaded
				mbDefined = false;
				throw new ExFull(ResId.InvalidRecordException);
			}
		}
		if ((newRecord < 0) || (newRecord >= mRecords.size())) {
			mbDefined = false;
			return;
		}
		mnCurrentRecordIndex = newRecord;
		mbDefined = true;
		mnRecordsAfter = mRecords.size() - mnCurrentRecordIndex - 1;
		if (mnRecordsAfter > mnDesiredRecordsAfter)
			mnRecordsAfter = mnDesiredRecordsAfter;
		mnRecordsBefore = mnCurrentRecordIndex;
		if (mnRecordsBefore > mnDesiredRecordsBefore)
			mnRecordsBefore = mnDesiredRecordsBefore;
		//
		// Evaluate script if applicable.
		// Disable this for now because you can't script on the first record:ready 
		// (the script get registered before the event does). 
		// $form:ready can be used for scripting (it occurs after a record ready)
		//
//		mDataModel.getEventManager().eventOccurred("ready", /* $record object */);
		//
		// Notify peers that the record has changed.
		//
		notifyPeers(Peer.UPDATED, XFA.RECORD, null);
	}

	@FindBugsSuppress(code="ES")
	void initialize(DataModel root,
					Element startNode,
					int recordLevel,
					String aRecordName,
					int nDesiredRecordsBefore,
					int nDesiredRecordsAfter) {
		mbUpToDate = false;		// Setting this to false effectively short-circuits updateFromPeer
								// for efficiency.  It will be set to true after file loading is complete,
								// via updateAfterLoad().

		mStartNode = startNode;
		mnSpecifiedRecordLevel = recordLevel;
		maRecordName = aRecordName;

		Document doc = startNode.getOwnerDocument();
		mbLazyLoading = doc.isIncrementalLoad();

		mnDesiredRecordsBefore = nDesiredRecordsBefore;
		mnDesiredRecordsAfter = nDesiredRecordsAfter;

		mDataModel = root;

		if (mnRecordLevel == 0)
			adjustRecordLevel(false);

		if (maRecordName != null) {
			Element startNodeCopy = findStartNode(startNode, true);
			if (startNodeCopy == null) {
				mDataModel.addErrorList(new ExFull(ResId.CantFindRecordException), LogMessage.MSG_WARNING, null);
				return;
			}

			IntegerHolder recordLevelHolder = new IntegerHolder(mnRecordLevel);
			//
			// Update recordLevel & verify that recordName exists.
			//
			if (! findRecordByName(startNodeCopy, recordLevelHolder, maRecordName)) {
				if (! mbLazyLoading) {
					mDataModel.addErrorList(new ExFull(ResId.CantFindRecordException), LogMessage.MSG_WARNING, null);
					return;
				}
				//
				// If lazy loading, it's possible we just haven't loaded enough of the file yet
				// to see the first record -- load elements until we hit the end of file or
				// we see the named record.
				//
				Element e = doc.loadToNextElement(null);
				while (e != null) {
					String aName = e.getLocalName();
					if (aName == maRecordName && ! isExcluded(e)) {
						if (! findRecordByName(startNodeCopy, recordLevelHolder, maRecordName)) {
							mDataModel.addErrorList(new ExFull(ResId.CantFindRecordException), LogMessage.MSG_WARNING, null);
							return;
						}
						break;
					}
					e = doc.loadToNextElement(null);
				}
				if (e == null) {
					mDataModel.addErrorList(new ExFull(ResId.CantFindRecordException), LogMessage.MSG_WARNING, null);
					return;
				}
			}
			mnRecordLevel = recordLevelHolder.value;
		}
	}

	private void invalidate() {
		mbUpToDate = false;
		mRecords.clear();
		mRecordIndexes.clear();
		//
		// TODO !!!!!! Review this.
		// Vantive roach #565412 for the XFA Plugin
		// this will notify the plugin that our window may have
		// changed. Since the plugin is the only code currently
		// using notifyPeer, and all it does is set a flag, this is
		// ok for now. If it was to ask for next record when it got
		// notified, this would not work.
		//
		notifyPeers(Peer.UPDATED, "", null);
	}

	/**
	 * Determine if the data window is currently in a defined (valid) state.  A data window
	 * is in this state if the current record index indicates a valid record.  A data window
	 * is not defined if there are no records, or if the current record index has
	 * been positioned beyond the end of the records.
	 *
	 * @return true if dataWindow is defined, false otherwise.
	 * @exclude from public api.
	 */
	public boolean isDefined() {
		validate(false);
		return mbDefined;
	}

	private boolean isExcluded(Element node) {
		String aNS = node.getNS();
		return mDataModel.isCompatibleNS(aNS) || mDataModel.excludeNS(aNS);
	}

//	boolean isRecordDepth(int depth) {
//		return depth == mnRecordLevel;
//	}

	boolean isRecordDepth(Node node) {
		Element parentCheck = node.getXMLParent();

		int nLevel = 1;		

		Node oDoc = node.getOwnerDocument();
		
		while (parentCheck != null && parentCheck != oDoc)
		{
			nLevel++;
			if (nLevel > mnRecordLevel)
				return false;
			parentCheck = parentCheck.getXMLParent();
		}

		return nLevel == mnRecordLevel;
	}

	/**
	 * Determine if a data group is a record.
	 *
	 * @param dataGroup the data group in question.
	 * @return true if dataGroup is a data group, false otherwise.
	 */
	@FindBugsSuppress(code="ES")
	public boolean isRecordGroup(DataNode dataGroup) {
		//
		// If a name was specified in record criteria, and the group's name doesn't
		// match that name, return false.
		//
		if (maRecordName != null && maRecordName != dataGroup.getName())
			return false;
		//
		// Check the level of the XML DOM peer of the node.  If it's the same as
		// mnRecordLevel, then it's a record.
		//
		if (dataGroup.getXFAParent() == mDataModel)
			return false;
		Node peer = dataGroup.getXmlPeer();
		return isRecordDepth(peer);
	}

	public boolean isUpToDate () {
		return mbUpToDate;
	}

	/*
	 *	 newRecord is 0-based
	 */
	private void loadToRecord(int newRecord) {
		assert(mbLazyLoading);
		if (newRecord < mnCurrentRecordIndex) {	// can't go backward when lazy loading
			mbDefined = false;
			throw new ExFull(ResId.InvalidRecordException);
		}
		while (mDataModel.loadToNextRecord() != null) {
			//
			// Right now the entire record array is stored and never goes away, although most
			// of the entries will be null and hence won't take up much memory.  Since when
			// lazy-loading we only move forward in the file, run backward through the
			// list deleting records and setting the corresponding entry in mRecords to null.
			//
			int nRecordsSize = mRecords.size();
			int nWindowSize = mnDesiredRecordsAfter + mnDesiredRecordsBefore + 1;
			if (nRecordsSize > nWindowSize) {
				//
				// nLast is the last (i.e. greatest) index to be deleted.  This loop normally
				// only executes once.
				//
				int nLast = nRecordsSize - nWindowSize - 1;
				for (int i = nLast; ; i--) {
					DataNode dataGroup = mRecords.get(i);
					if (dataGroup == null)
						break;
					dataGroup.remove();	// delete this record and corresponding XML DOM subtree
					mRecords.set(i, null);
					if (i == 0)
						break;
				}
			}
			//
			// exit early if we have enough records
			//
			if (nRecordsSize >= newRecord)
				return;
		}
	}

	/**
	 * Move the current record by an amount relative to the current record.
	 *
	 * @param recordOffset the number of records from the current record, positive
	 *						 or negative, by which the current record should move.
	 */
	public void moveCurrentRecord(int recordOffset) {
		validate(false);
		gotoRecord(currentRecordNumber() + recordOffset);
	}

	private void populate(DataNode root) {
		NodeList children = root.getNodes();
		int numChildren = children.length();
		for (int i = 0; i < numChildren; i++) {
			DataNode node = (DataNode) children.item(i);
			//
			// Only operate on datagroups -- ignore datavalues (they won't be recognized
			// as a record).
			//
			if (! node.isSameClass(XFA.DATAGROUP))
				continue;
			if (isRecordGroup(node)) {
				addRecordGroup(node);
			}
			else {
				//
				// Recursively descend into the tree looking for elements
				// at the proper level.  The children of records are not
				// searched because a record can't contain another record.
				//
				populate(node);
			}
		}
	}

	void postLoad() {
		if (mbDefined) {
			//
			// Evaluate script if applicable (for record 1).
			// Disable this for now because you can't script on the first record:ready 
			// (the script get registered before the event does). 
			// $form:ready can be used for scripting (it occurs after a record ready)
			//
//			mDataModel.getEventManager().eventOccurred("ready", /* $record object */);
		}
	}

	/**
	 * Return a record relative to the current record.
	 *
	 * @param recordOffset the number of records from the current record, positive
	 *						 or negative (0 returns current record).
	 * @return the requested record.
	 * @exclude from public api.
	 */
	public DataNode record(int recordOffset) {
		validate(true);
		if (recordOffset < 0 && -recordOffset > recordsBefore())
			throw new ExFull(ResId.OutsideDataWindowException);
		if (recordOffset > 0 && recordOffset > recordsAfter())
			throw new ExFull(ResId.OutsideDataWindowException);
		return mRecords.get(recordOffset + mnCurrentRecordIndex);
	}

	/**
	 * @exclude from public api.
	 */
	public int recordAbsIndex(int recordOffset) {
		validate(true);
		if (recordOffset < 0 && -recordOffset > recordsBefore())
			throw new ExFull(ResId.OutsideDataWindowException);
		if (recordOffset > 0 && recordOffset > recordsAfter())
			throw new ExFull(ResId.OutsideDataWindowException);
		return (mRecordIndexes.get(recordOffset + mnCurrentRecordIndex)).intValue();
	}

	/**
	 * Determine the number of records after the current record in this window.
	 *
	 * @return the number of records after the current record.
	 */
	public int recordsAfter() {
		validate(true);
		return mnRecordsAfter;
	}

	/**
	 * Determine the number of records before the current record in this window.
	 *
	 * @return the number of records before the current record.
	 */
	public int recordsBefore() {
		validate(true);
		return mnRecordsBefore;
	}

	/**
	 * @exclude from public api.
	 */
	public boolean removeRecordGroup(DataNode dataGroup) {
		for (int i = 0; i < mRecords.size(); i++) {
			if (mRecords.get(i) == dataGroup) {
				mRecords.remove(i);
				mRecordIndexes.remove(i);
				mnAbsRecordIndex--;
				return true;
			}
		}
		return false;
	}

	private void reset() {
		mnCurrentRecordIndex = 0;
		mnRecordsBefore = 0;
		mnRecordsAfter = 0;
		mbDefined = false;
		if (mRecords.size() > 0) {
			mnRecordsAfter = mRecords.size() - 1;
			if (mnRecordsAfter > mnDesiredRecordsAfter)
				mnRecordsAfter = mnDesiredRecordsAfter;
			mbDefined = true;
		}
	}

	public void resetRecordDepth() {
		//
		// Resets mnRecordLevel according to the absolute
		// depth in the XML DOM	called if envoloping xml 
		// elements are added to the data.
		//
		mnRecordLevel = 0;
		adjustRecordLevel(true);
		mbUpToDate = false;
		validate(false);
	}

	public void setScriptProperty(String sPropertyName, Arg propertyValue) {
		throw new ExFull(ResId.UNSUPPORTED_OPERATION, "DataWindow#setScriptProperty");
	}

//	void setStartNode(DataNode oStartNode) {
//		mStartNode = oStartNode;
//	}

	void uninitialize() {
		mnRecordLevel = 0;
		mDataModel = null;
		mStartNode = null;
	}

	/**
	 * @exclude from public api.
	 */
	public void updateAfterLoad() {
		if (mbLazyLoading) {
			//
			// load enough records to fill the data window
			//
			int nLastRecord;
			if (mnDesiredRecordsAfter == Integer.MAX_VALUE)
				nLastRecord = Integer.MAX_VALUE;
			else
				nLastRecord = 1 + mnDesiredRecordsAfter;
			loadToRecord(nLastRecord);
		}
		mbUpToDate = true;
		reset();
	}

	private void validate(boolean bMustBeDefined) {
		if (! mbUpToDate && mDataModel != null) {
			invalidate();
			//
			// initialize record list
			//
			populate((DataNode) mDataModel.getAliasNode());
			reset();
			mbUpToDate = true;
		}
		if (bMustBeDefined && ! mbDefined)
			throw new ExFull(ResId.UndefinedDataWindowException);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy