ArtiSynth model components have the ability to save and restore themselves from persistent storage. They do this by implementing the write() and scan() methods of maspack.util.Scannable, and the postscan() method of ModelComponent:
The operation and implementation of these methods will now be described in detail. A summary of the key points is given in Section 3.5.
The write() method writes information about the component to a PrintWriter, using NumberFormat to format floating point numbers where appropriate. The ref argument is used to provide additional context information for generating the output, and is specifically used to generate path names for other components that are referenced by the component being written (Section 3.1.1).
In general, each component writes out its attributes as a list of name/value pairs, each of the form
with the list itself enclosed between square brackets ’[ ]’ which serve as begin and end delimiters. The value associated with each attribute name may itself be a quantity (such as a vector, matrix, or another component) delimited by square brackets. For example, the output for a Particle component may look like this:
The output begins with an openning square bracket, followed by four attribute/value pairs and a closing square bracket. The position attribute is a 3-vector also enclosed between square brackets.
Within the write() method, the above output could be produced with code like this:
Output indentation can be controlled by using an IndentingPrintWriter, and IndentingPrintWriter.addIndentation() will increase (or decrease) output indentation if pw is an instance of IndentingPrintWriter.
In practice, components do not generally need to provide explicit code to write out all their attribute values. In particular, any information that is associated with a Property (see the Property section of the Maspack Reference Manual) can be written out automatically using a code fragment of the form:
In addition, any attribute information contained in a component’s superclass will usually be written by that superclass. The default write() definition for a model component is usually looks something like this:
The write() and dowrite() methods take care of writting the square brackets and setting up the initial indentation. Then a call to writeItems() prints out all necessary property values. The ancestor argument obtained from ref will be discussed in Section 3.1.1.
If all a component’s attribute information is associated with property values, then it is usually not necessary to provide any component-specific code for writing the component: the default implementations of write() and writeItems() will handle it. If a component does have attribute information that is not associated with a property, then it is usually sufficient to handle this by overriding writeItems(). For example, if a component has a non-property “centroid” attribute, it can be written by an override of writeItems() constructed like this:
ArtiSynth components often contain references to other components that are not part of their ancestor hierarchy. For example, a two-point spring will contain references to its two end-point particles, and a FemElement3d will contain references to its nodes. The set of all references refered to by a component is returned by the combination of the component’s getHardReferences() getSoftReferences() methods.
Because they generally reside outside a component’s immediate ancestor hierarchy, information about each reference’s location needs to be explicitly written and scanned as part of writing and scanning a component. The location information is stored using the component’s path with respect to some known ancestor. This ancestor is passed to the component’s write() method through the ref argument, and is cast explicitly to CompositeComponent and passed to writeItems() as the ancestor argument. A component can then use ComponentUtils.getWritePathName() to obtain the path name of each reference with respect to the ancestor, and write this to the output.
As an example, here is a possible implementation of writeItems() for a two-point spring:
This will produce an output like this,
where models/points/n gives the path name of the n-th point with respect to the ancestor. Alternatively, if a component has a variable number of references, they can be written out as a list between square brackets:
The above code will produce output like this:
The ancestor used for reading and writing references will always be a common ancestor of both the references and the refering component. This may sometimes be the root model (i.e., the top of the hierarchy), but more typically it will be the first common ancestor for which hierarchyContainsReferences() returns true (implying that all references are contained within the ancestors’s descendants). This allows paths to be written more compactly. Most Model components presently enforce the hierarchyContainsReferences() condition.
If a component is a CompositeComponent, then it also needs to write out its child components along with its attribute information. This can be done by recursively calling the children’s write() methods.
If the child component configuration is fixed (i.e., the component does not implement MutableCompositeComponent) and the children are created in the composite’s constructor, then attribute names can be used to delimit each child. For example, suppose a component contains two children: a list of particles and a list of springs. This component could then be written using a code construction such as:
If the composite component has been implemented internally using an instance of ComponentListImpl (Section 1.4), then the above can be written using the latter’s writeComponentsByName() method, which writes each child, using its component name as an attribute name:
One the other hand, composite components which are instances of MutableCompositeComponent may not have predetermined component arrangements and so these components cannot be identified by an attribute name. Instead, the components must simply be printed out in sequence. For example, the writeItems() method for a list of particles could be programmed like this:
This would produce output such as:
However, when scanning a MutableCompositeComponent, the scan() method (discussed below) needs to create and scan new child components as it encounters them in the input. It is therefore necessary for scan() to know what class of component to create. Typically, the composite component has a default component type; for example, the default type for ComponentList<Particle> is Particle. However, in some cases the components may be subclasses of the default type, or the default type may be an interface or abstract class and hence not instantiable. In such instances, the output needs to be augmented with class type information, which is placed before the openning ’[’ of the component output. The details of how to do this are beyond the scope of this document. However, if the composite component has been implemented internally using an instance of ComponentListImpl, then one can use the latter’s writeComponents() method to automatically write out all the components, along with the necessary class information:
The result may look something like this:
In this example, the second and fourth particles have class types that differ from the default and so class information for each is prepended to the output.
The scan() method reads the component in from a token stream provided by a ReaderTokenizer. This translates the input into a stream of tokens, including words, numbers, and special token characters (such as ’[’, ’]’, and ’=’), which are then used to parse the input. Authors implementing scanning code should have some familiarity with ReaderTokenizer. A description is beyond the scope of this document but good documentation is available in the ReaderTokenizer class header.
The main code inside the default scan() method for a model component looks roughly like this:
The method looks for and scans the initial ’[’ character (and will throw an IOException if this is not found). It then reads other tokens (using nextToken()) until the terminating ’]’ character is found. After each token is inspected, it is pushed back into the token stream using pushBack() and scanItem() is called to try and read an individual attribute or subcomponent from the input. If scanItem() cannot match the input to any attributes or child components it returns false.
Note: ReaderTokenizer allows one token of look-ahead, so that any read token can be pushed back once. In particular, in the following sequence, t1 and t2 should be the same:t1 = rtok.nextToken(); rtok.pushBack(); t2 = rtok.nextToken();
The default implementation of scanItem() provides code for reading property values and looks something like this:
The method begins by getting the next token and then calling ScanWriteUtils.scanProperty() to see if the token is a word matching the name of one of the component’s properties. If so, then scanProperty() scans and sets the property value and returns true, and scanItem() itself returns true. Otherwise, scanItem() returns false indicating that it was unable to find a match for the input. The tokens argument is used to store information whose processing must be deferred until the post-scan step, as discussed in Section 3.2.1.
Typically, component implementations will not need to override scan() unless the scanning procedure calls for pre- or post-processing, as in:
Note that if a class makes use of the post-scan step (Section 3.2.1), then it may be necessary to do the post-processing in an override of postscan() instead.
Component implementations often will need to override scanItem() to scan additional attribute information. For example:
First, the method gets the next token. Then it checks if its type (ttype) corresponds to a WORD token and if the word’s string value (sval) equals the attribute name attributeXXX. If so, then it scans the ’=’ character following attribute name, scans whatever information is associated with the attribute, and returns true. Otherwise, if no expected attribute name is matched, the current token is pushed back, and the superclass method is called to see if it can match the current input.
Most implementations of ModelComponent provide the convenience method scanAttributeName(rtok, name) which allows the code fragment
to be replaced with
Employing this in a larger example, we have
Here, if any of the attribute names name, position, mass, or dynamic are matched, then the corresponding string, vector, numeric, or boolean attribute values are scanned using scanQuotedString(), the vector’s own scan() method, scanNumber(), or scanBoolean(). Each of these will throw an IOException if the input token sequence does not match what is expected.
When scanning a component that contains references, the path for each reference is used to locate the referenced component within the component hierarchy. However, this poses a problem: because components are created only as the component hierarchy is recursively scanned, it is possible that some references may not yet exist at the time when the component is scanned. For example, if the points referenced by a two-point spring belong to part of the hierarchy further "to the right" of the spring components, then when the spring is scanned the points won’t yet exist and the scanning method will be unable to find them.
The solution to this problem is to employ a two-step scanning process in which the initial scan is followed by a secondary "post-scan" which can be used to resolve references. Each reference path found during the initial scan is saved for later use in the post-scan step, by which time all components are guaranteed to have been created. Reference information, along with any other information needed for the post-scan step, is saved in a queue of ScanTokens supplied to the scan() method through the ref argument. Several different types of ScanTokens allow different types of information to be stored: StringTokens are use to store attribute names and reference paths; ObjectTokens are use to store object pointers; and special marker tokens, ScanToken.BEGIN and ScanToken.END, can be used as delimiters.
At a minimum, scanning each component causes BEGIN and END tokens to be added to the token queue, with additional tokens added in between as necessary. Revisiting the basic scan() method code shown at the top of Section 3.2, we show the additional code that is needed to handle this:
The token queue itself, called tokens, is obtained from the ref argument via an explicit cast. BEGIN and END tokens are added at the beginning and end of the scan. In between, the token queue is passed to scanItem(), which adds addtional tokens when necessary.
Within scanItem(), tokens are added to provide whatever information is needed for the post-scan step. This information is often provided in the form of two or more tokens comprising an attribute name/value pair, so that the post-scan step is not sensitive to input ordering. In this sense, the information stored in the token queue will reflect the same structure as the tokens in the original input.
Consider the first example in 3.1.1 where the reference information for a two-point spring was output as:
To process this inside scanItem(), we check for the attribute names point0 and point1 and if either is found, we store both the attribute name and the reference path in the token queue using StringTokens. For point0, the corresponding code looks like
Most implementations of ModelComponent provide the convenience method scanAndStoreReference (rtok, name, tokens) which allows this to be compressed into
One may also use ScanWriteUtils.scanAndStoreReference() for the same purpose. The scanItem() method for a two-point spring can then be written as:
Alternatively, if we have an attribute followed by a list of references enclosed in square brackets, such as
then we want to store a sequence of tokens consisting of the attribute name, a BEGIN token, the reference paths, and an END token:
That can be done by a code sequence that looks like
and which is available in most ModelComponent implementations via the convenience method scanAndStoreReferences():
One may also use ScanWriteUtils.scanAndStoreReferences() for the same purpose.
In addition to their attributes, composite components need to scan in their child components. This can be done by recursively calling the children’s scan() methods.
If the child component configuration is fixed (i.e., the component does not implement MutableCompositeComponent), and the children are created in the composite’s constructor and written out using attribute names as delimiters, then these attributes names can be used to drive the scanning. The example composite from Section 3.1.2, comprising a list of particles and a list of springs, could be scanned in using code such as:
Here, an ObjectToken() identifying each scanned componet is stored on the token queue for later use in the post-scan step. If the composite component has been implemented internally using an instance of ComponentListImpl (Section 1.4), then the above can be written more succinctly using the latter’s scanAndStoreComponentByName() method:
On the other hand, composite components which are instances of MutableCompositeComponent are written out in sequence, without using attribute names but with possible prefixed information giving information about the component’s class. When scanning in these children, scanItem() must determine the class for the child, create an instance of the child, and then scan it in. The code required for these steps is beyond the scope of this document. However, if the mutable composite has been implemented internally using an instance of ComponentListImpl, then one can use the latter’s methods scanBegin(), scanAndStoreComponent(), and scanEnd() to handle the scanning.
First, scanBegin() is called in an override of scan():
scanAndStoreComponent() can then be called in scanItem() to handle scanning of individual components:
The method checks to see if the input contains child component information, and if so, scans the information, creates an instance of the component if necessary, stores a copy of the component in the token queue for use in post-scanning, and returns true. Note that in this case one should call scanAndStoreComponent() after the super method to avoid confusing attribute names with class information.
scanAndStoreComponent() will not create a new component if a fixed component (Section 1.4) of the appropriate class already exists at the current list position. Instead, the existing component will be scanned “in place”.
Finally, scanEnd() is called in an override of postscan() method (discussed below):
Once the token queue has been built by the scan() methods, it is processed in the post-scan step. This is done by each component using a postscan() method that takes as arguments the token queue and the ancestor with respect to which reference paths should be evaluated. The default postscan() method for most components looks something like this:
postscanBeginToken() gets the next token on the queue, checks that it is a BEGIN token, and throws an exception if this is not the case. Then the method simply calls postScanItem(), which does the actual token handling work, until a terminating END token is found.
As is the case with scan(), subclasses typically do not need to override postscan(). The exception to this is when post-processing is required after the scan process:
However, any component which adds tokens in its scanItem() method will need to process those tokens in an override of postscanItem(). Tokens can be removed from the queue using the queue’s poll() method, and can be examined (without removing them) using the queue’s peek() method. More usefully, the utility class ScanWriteUtils provides a number of methods for token processing, including:
ModelComponentBase also makes convenience wrappers for these directly available within the class. postscanAttributeName() checks if the next token in the queue is a StringToken matching name, and if it is, consumes that token and returns true. postscanReference() checks that the next token is a StringToken containing a path reference, finds the component referenced by that path relative to ancestor, checks that it is an instance of clazz, and returns it. postscanReferences() obtains a set of component references described by a sequence of StringTokens located between BEGIN and END tokens, and returns the referenced components in an array. These methods will throw an IOException if they encounter unexpected tokens or if referenced components cannot be found.
Employing these methods to handle the point0, point1 reference example above, we obtain:
Similarly, the points reference example can be handled as:
Finally, for composite components, it is necessary to call postscan() for each of their children. For composites implemented using ComponentListImpl, this can be done by calling the latter’s postscanComponent() method. For both CompositeComponent and MutableCompositeComponent implementations, the corresponding code looks like this:
postscanComponent() checks to see if the next token is an ObjectToken containing a ModelComponent, and if it is, it removes that token, calls the component’s postscan() method, and returns true.
As described in 3.2, base implementations of scanItem() automatically read in and set property values for the component. However, in a few cases it may be necessary to defer setting the property value until the post-scan step, either because it depends on component references, or requires the component structure to be fully realized. A current example of this is the surfaceRendering property for FemModel.
Deferring the settting of property values until the post-scan step can be done by saving the scanned property values in the token queue, and then actually setting the properties during the post-scan. Two convenience methods, ScanWriteUtils.scanAndStorePropertyValue() and ScanWriteUtils.postscanPropertyValue() allow this to be done fairly easily:
Note that it is often not necessary to provide a component-specific implementation of postscanItem() since the defaullt implementation for most components already contains a call to ScanWriteUtils.postscanPropertyValues().
If a property value depends on references, it is also important to ensure that reference information is written out before the property information, so that in the post-scan step, it will be set before the property values. In such cases, that means that writeItems() should be structured as follows:
The two-step scanning process means that for the top-level invocation of scan, the application needs to create a token queue, call scan() with this queue as the ref argument, and then call postscan():
For convenience, the above code fragment is encapsulated into the method ScanWriteUtils.scanfull().
If we are scanning an entire hierarchy from scratch, then comp will be the root component of the hierarchy and ancestor will equal comp. Otherwise, if we are scanning a new sub-hierarchy, then comp will be the root of the sub-hierarchy and ancestor may be some component higher in the existing hierarchy.
Because Artisynth components are responsible for writing and scanning themselves, there is no mandatory file structure imposed per-se. However, there is a structure that should be adhered to whenever possible. Expressed loosely as a production grammar, with ’*’ expressing repetition of zero or more times, this is:
Here, NAME and WORD are identifiers that consist of alphanumerics, ’$’, or ’_’, and do not begin with a digit. CLASSINFO is the classname for a component, or an alias that can be mapped to a classname using ClassAliases.resolveClass().
Within an ArtiSynth file, ’#’ is a comment character, causing all remaining characters on the line to be discarded.
When tokens are saved for the post-scan step, they should arranged in a structure similiar to that used for the file itself:
Here, BEGIN and END are ScanToken.BEGIN and ScanToken.END, NAME is a StringToken with an attribute name as a value, COMPONENT is a ObjectToken with a reference to the object as a value, and STRING and OBJECT are StringToken and ObjectToken, respectively.
Debugging write and scan methods is generally not too difficult because of the ascii nature of the data files. A good first test is to write components out, read them back in, and then write them out a second time and make sure that the second output equals the first. Scan methods will generally throw IOExceptions when unexpected input is encountered, and these usually provide the offending line number.
Problems that occur in post-scan can be slightly harder to solve because the token queue is not normally written out in any place where it can be inspected. To help with this, once can use ScanWriteUtils.setTokenPrinting() to enable the token queue produced by ScanWriteUtils.scanfull() to be printed to the standard output. It is also possible to print a token queue directly using ScanWriteUtils.printTokens().
The main points concerning component writing and scanning are as follows:
Where possible, the structure described in Section 3.3 should be adhered to.
Scanning is a two-step process, involving a scan step and a post-scan step. This is to accomodate the fact that some aspects of scanning (most importantly the evaluation of references) cannot be done until the entire component hierarchy has been constructed. Information needed for the post-scan step (such as reference path names) should be stored in the token queue passed to scan() and scanItems().
It is usually only necessary for a component implementation to override writeItems(), scanItem(), and postscanItem(). Property values are usually written and scanned automatically by the base implementations of writeItems() and scanItem(). If a component does not contain references or non-property attributes, it may not be necessary for the implementation to override any methods at all.
Composite components need to call write(), scan(), and postscan() for their child components. Composites implemented using ComponentListImpl can do this using methods supplied by that class, such as writeComponents(), scanAndStoreComponent(), and postscanComponent().
A complete scan operation involves creating a token queue and then calling both scan() and postscan() for the top-level component. This can be done using the convenience method ScanWriteUtils.scanfull().
The utility class ScanWriteUtils contains a large number of methods that facilitate writing and scanning.