/* XBN Java: Generically useful, non-GUI Java code. http://sourceforge.net/projects/xbnjava Copyright (C) 1997-2003, Jeff Epstein All rights reserved. Modifications: No Redistribution in binary form, with or without modifications, are permitted provided that the following conditions are met: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * If modifications are made to source code then this license should indicate that fact in the "Modifications" section above. * Neither the author, nor the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [NOTE: This license contains NO advertising clause.] */ package xbn.template; import xbn.XBNObject; import xbn.placeholder.s_s; import xbn.string.escape.UnescapeStringException; import xbn.string.UtilString; /**
For every line found in a template, this class retrieves surrounding text and gaps.
Source code: TemplateLineAnalyzer.java
Configuration for this class is held in a TParseConfig.
Note that, for speed and efficiency, gaps are validated on the fly, one at a time, as they are retrieved. You will successfully retrieve the first three gaps contained in the line, even when the fourth one is illegally formatted.
@version 0.9b @author Jeff Epstein, http://sourceforge.net/projects/xbnjava. **/ public class TemplateLineAnalyzer extends XBNObject { //ENTIRE strings used four or more times. private String sTemplateName = null; private int iLineNumber = -1; private int iCharsDeleted = 0; private TLAObjects tlao = null; private s_s ssNextSurrTxtAndGap = null; private String sFinalSurrTxt = null; /**Create an TemplateLineAnalyzer.
Equal to TParseConfig(null, -1, sb_line, new TParseConfig())
This constructor is intended for testing purposes only. When using this class for real, it is recommended that you use the TLAObjects version of this class' constructors.
**/ public TemplateLineAnalyzer(StringBuffer sb_line) throws TemplateFormatException { this(null, -1, sb_line); } /**Create an TemplateLineAnalyzer.
Equal to TParseConfig(s_templateName, i_lineNumber, sb_line, (new TParseConfig()))
Create an TemplateLineAnalyzer.
Equal to TemplateLineAnalyzer(s_templateName, i_lineNumber, sb_line, (new TLAObjects(tp_config))
This constructor is intended for testing purposes only. When using this class for real, it is recommended that you use the TLAObjects version of this class' constructors.
**/ public TemplateLineAnalyzer(String s_templateName, int i_lineNumber, StringBuffer sb_line, TParseConfig tp_config) throws TemplateFormatException { this(s_templateName, i_lineNumber, sb_line, (new TLAObjects(tp_config))); } /**Create an TemplateLineAnalyzer.
@param s_templateName The name of the template being analyzed, for debugging purposes only. @param i_lineNumber The current line number, for debugging purposes only. Should be greater than zero, but not enforced. @param sb_line A single line of text that may or may not contain a gap. May not be null. @param tla_objects The TLAObjects, containing some of the internal objects needed by this class, as well as the TParseConfig, which defines what a gap is. @exception AssertException If sb_line contains a line separator. **/ public TemplateLineAnalyzer(String s_templateName, int i_lineNumber, StringBuffer sb_line, TLAObjects tla_objects) throws TemplateFormatException { //Must set tlao before calling initialize. tlao = tla_objects; initialize(); try { if(sb_line.length() < 1) { //There are no characters in this line. This is a very specific //scenario that we can manually handle. This if block *could* be //commented out, although it would be not-as-efficiently handled //by getNextSTG() (not to mention the lines between here and the //call to getNextSTG() would be executed as well). // //However, it would be a good sanity check to comment out this if //block to make sure it works the other way. sFinalSurrTxt = sb_line.toString(); return; } } catch(NullPointerException npx) { throwAX("constructor: sb_line is null."); } //SEEMS THE LINE MUST HAVE THE LINE_SEP, IN ORDER TO PRESERVE //THE INTEGRITY OF THE ORIGINAL TEXT // if(sb_line.indexOf(sLINE_SEP) != -1) { // throwAX("constructor: sb_line contains a line separator (System.getProperty('line.separator')) at array index " + sb_line.indexOf(sLINE_SEP) + "."); // } try { tlao.sbLine = sb_line; } catch(NullPointerException npx) { throwAX("constructor: tla_objects is null."); } sTemplateName = s_templateName; iLineNumber = i_lineNumber; getNextSTG(); } /**Get the TParseConfig for direct manipulation.
@return The TParseConfig as provided to the TLAObjects constructor. **/ public final TParseConfig getTParseConfig() { return tlao.tpc; } /**Get the GapConfig for direct manipulation.
@returngetTParseConfig().getGapConfig()
**/
public final GapConfig getGapConfig() {
return getTParseConfig().getGapConfig();
}
/**
Get the current line number.
@return -1 if the sb_line, as passed into the constructor, is null, or the line was not null, but analysis has been completed.Get the name of the template being analyzed.
@return null if sb_line, as passed into the constructor, is null, or the line was not null, but analysis has been completed.Get the current character array index.
@return The array index of the character immediately following the surrounding-text-and-gap about to be retrieved, or -1 if isDoneAnalyzing equals true. **/ public final int getCharArrIdx() { return getCharArrIdx(0); } /**Has this line been completely analyzed?
@return true If all surrounding-texts-and-gaps and the final surrounding text have been retrieved.Sanity check: When this returns true, hasAnotherGap equals false, getTemplateName equals null and getLineNumber and getCharArrIdx equal -1.
false If there is at least one more surrounding-text-and-gap, or the final surrounding text has not yet been retrieved.
Sanity chock: When this returns false, hasAnotherGap may equal either true or false, getTemplateName and getLineNumber equal [whatever was provided to the constructor] and and getCharArrIdx equals [something greater than -1].
**/ public final boolean isDoneAnalyzing() { return (tlao.sbLine.length() < 1 && ssNextSurrTxtAndGap == null && sFinalSurrTxt == null); } /**Is there another gap in the line, yet to be retrieved?
@return true If there is another surrounding-text-and-gap to be retrieved. isDoneAnalyzing will equal false in this case.Get the next surrounding text and gap.
@return a s_s, where the sOne element is the surrounding text, and sTwo is the gap name. Neither will ever be null, and the gap name will never be empty string. @exception AssertException If hasAnotherGap equals false. **/ public s_s getNextSurrTxtAndGap() throws TemplateFormatException { if(!hasAnotherGap()) { throwAX("getNextSurrTxtAndGap: hasAnotherGap() equals false."); } s_s ts = ssNextSurrTxtAndGap; ssNextSurrTxtAndGap = null; getNextSTG(); return ts; } /**Get the final surrounding text, after all surrounding-text-and-gaps have been retrieved.
@exception AssertException If either hasAnotherGap or isDoneAnalyzing equal true. **/ public String getFinalSurrTxt() { if(hasAnotherGap() || isDoneAnalyzing()) { throwAX("getFinalSurrText: hasAnotherGap() (" + hasAnotherGap() + ") and isDoneAnalyzing() (" + isDoneAnalyzing() + ") must both equal false when calling this function."); } String s = sFinalSurrTxt; initialize(); return s; } private final void initialize() { tlao.initialize(); iCharsDeleted = 0; sTemplateName = null; iLineNumber = -1; sFinalSurrTxt = null; ssNextSurrTxtAndGap = null; } private void throwTFX(String s_errorMsg) throws TemplateFormatException { throwTFX(-1, s_errorMsg); } private void throwTFX(int i_charArrIdx, String s_errorMsg) throws TemplateFormatException { String sCAI = sES; if(i_charArrIdx > -1) { sCAI = ":" + getCharArrIdx(i_charArrIdx); } String sError = "[" + (new UtilString()).getConditional(sES, getTemplateName(), ":") + getLineNumber() + sCAI + "] " + s_errorMsg + "\nEXTRA INFORMATION:\nGap start tag: '" + tlao.sTagStart + "'\nGap end tag: '" + tlao.sTagEnd + "'\nRemaining text on the line (the first character in these remaining contents is array index " + getCharArrIdx() + " of the overall/original line of text):\n--------------\n'" + tlao.sbLine.toString() + "'\n--------------"; initialize(); throw new TemplateFormatException(sError); } private int getCharArrIdx(int i_dx) { return (i_dx + iCharsDeleted); } /* private void deleteFirstChar() { tlao.sbLine.deleteCharAt(0); incDelCharCount(); } */ private void deleteChars(int i_charsToDelete) { tlao.sbLine.delete(0, i_charsToDelete); incDelCharCount(i_charsToDelete); } /* private void incDelCharCount() { iCharsDeleted++; } */ private void incDelCharCount(int i_charsDeleted) { iCharsDeleted += i_charsDeleted; } private void getNextSTG() throws TemplateFormatException { char cTD = getGapConfig().getTagDelimiter(); String sTagStart = tlao.sTagStart; String sTagEnd = tlao.sTagEnd; int iLenTagStart = sTagStart.length(); int iLenTagEnd = sTagEnd.length(); while(tlao.sbLine.length() > 0) { //IS THERE A MORE EFFICIENT WAY TO SEARCH THE CONTENTS OF A //STRINGBUFFER WITHOUT HAVING TO FIRST TRANSLATE IT TO A //STRING? String sLine = tlao.sbLine.toString(); int iIdxStart = sLine.indexOf(sTagStart); int iIdxEnd = sLine.indexOf(sTagEnd); boolean bFoundTagStart = (iIdxStart != -1); boolean bFoundTagEnd = (iIdxEnd != -1); char cEsc = tlao.us.getESConfig().getEscapeChar(); while(iIdxStart > 0 && tlao.sbLine.charAt(iIdxStart - 1) == cEsc) { //The first tag delimiter for this start tag is actually //escaped. Look for the next start tag starting at the //final tag delimiter. The last tag delimiter may be the //first in the next start tag (hence, the minus one). iIdxStart = sLine.indexOf(sTagStart, (iIdxStart + iLenTagStart - 1)); } while(iIdxEnd > 0 && tlao.sbLine.charAt(iIdxEnd - 1) == cEsc) { //Same thing with the end tag. iIdxEnd = sLine.indexOf(sTagEnd, (iIdxEnd + iLenTagEnd - 1)); } //We now have the first start/end tag in the line whose first //tag delimiter is not escaped. if(bFoundTagStart && bFoundTagEnd) { //Both a start and end tag were found. GOOD. if(iIdxStart > iIdxEnd) { throwTFX(0, "Start tag ('" + sTagStart + "') found at index " + getCharArrIdx(iIdxStart) + ", but the end tag was found before it, at index " + getCharArrIdx(iIdxEnd) + "."); } //The start tag is before the end tag. Make sure they don't //share a tag delimiter. if(iIdxStart == (iIdxEnd - iLenTagStart + 1)) { //The last tag delimiter in the start tag is also the //first tag delimiter in the end tag. throwTFX(0, "Start tag ('" + sTagStart + "') found at index " + getCharArrIdx(iIdxStart) + ", and end tag found at index " + getCharArrIdx(iIdxEnd) + ". However, the final tag delimiter ('" + cTD + "') in the start tag is also the first tag delimiter in the end tag."); } //They don't share a delimiter. The start tag is "enough" //before the end one. There may not be any space for a name //(the name wil be analyzed below), but the tags are far //enough apart. //This tag is legal! } else if(bFoundTagStart) { //A start tag was found but an end tag was not. BAD. throwTFX(0, "Start tag ('" + sTagStart + "') found at index " + getCharArrIdx(iIdxStart) + ", but no corresponding end tag ('" + sTagEnd + "') was found after it."); } else if(bFoundTagEnd) { //A start tag was found but an end tag was not. BAD. throwTFX(0, "End tag ('" + sTagEnd + "') found at index " + getCharArrIdx(iIdxEnd) + ", but no corresponding start tag ('" + sTagStart + "') was found before it."); } else { //No gap tags exist on (the remainder of) this line. //The rest of the text on this line is the final //surrounding text. int iCharsDeleted = -1; try { iCharsDeleted = tlao.us.unescape(tlao.sbLine, getCharArrIdx(0)); } catch(UnescapeStringException eusux) { throwTFX("ERROR while attempting to unescape the final surrounding text: " + eusux.getMessage()); } incDelCharCount(iCharsDeleted); sFinalSurrTxt = tlao.sbLine.toString(); return; } //This tag is (still) legal! //Get the surrounding text before the next found start tag, //delete it from the current string, escape it, and add it //to the surrounding text array. tlao.sbSurrounding.append(tlao.sbLine.substring(0, iIdxStart)); int iCharsDeleted = -1; try { iCharsDeleted = tlao.us.unescape(tlao.sbSurrounding, getCharArrIdx(0)); } catch(UnescapeStringException eusux) { throwTFX("ERROR while attempting to unescape the surrounding text before the next gap: " + eusux.getMessage()); } incDelCharCount(iCharsDeleted); //Get the name and validate it...START int iName = (iIdxStart + sTagStart.length()); String sName = tlao.sbLine.substring(iName, iIdxEnd); tlao.sbName.append(sName); //Now that we have the name, delete the surrounding text. //The position of the name of the gap (and the gap start //and end tags) were found based upon the surrounding //text existing. Now we have the name, so we don't need the surrounding text anymore. deleteChars(tlao.sbSurrounding.length() + iCharsDeleted); try { //Delete all escape slashes from the name. iCharsDeleted = tlao.us.unescape(tlao.sbName, getCharArrIdx(iName + 1)); incDelCharCount(iCharsDeleted); } catch(UnescapeStringException eusux) { int iStartInName = sName.indexOf(sTagStart); boolean bUnescStartTag = false; if(iStartInName == -1) { bUnescStartTag = true; } else if(iStartInName == 0 || sName.charAt(iStartInName - 1) != cEsc) { //This gap start tag is definitely unescaped. The first //tag delimiter character is either the first in the line, //or the tag immediately previous to the tag delimiter is //not an escape slash. bUnescStartTag = true; } if(!bUnescStartTag) { //The start tag is escaped. throwTFX(0, "Gap start tag ('" + sTagStart + "') found, followed by a gap end tag ('" + sTagEnd + "') at array index " + getCharArrIdx(iIdxEnd) + ". This appeared to be a legal gap, however, when unescaping the name itself (originally '" + sName + "'), this exception was thrown: " + eusux.getMessage() + "."); } //The start tag NOT escaped. A different error is more //appropriate. throwTFX(0, "Gap start tag ('" + sTagStart + "') found, followed by another gap start tag at array index " + getCharArrIdx(iStartInName) + ". A start tag must be followed by an end tag ('" + sTagEnd + "') before the next start tag."); } //Validate the length of the name. This length check must occur //*after* all escape slashes are deleted. if(tlao.sbName.length() < 1 || tlao.sbName.length() > TConfigDefaults.getMaxGapNameLength()) { throwTFX("Gap start tag ('" + sTagStart + "') and end tag ('" + sTagEnd + "') found. However, the name ('" + tlao.sbName.toString() + "') must be between one and TConfigDefaults.getMaxGapNameLength() (" + TConfigDefaults.getMaxGapNameLength() + ") characters, inclusive. Currently " + tlao.sbName.length() + "."); } //The length of the name is legal. //Everything looks good. sName = tlao.sbName.toString(); //Must delete the gap from sbLine before setting the length //of sbName back to zero. iCharsDeleted + deleteChars(sTagStart.length() + tlao.sbName.length() + iCharsDeleted + sTagEnd.length()); tlao.sbName.setLength(0); //Get the name and validate it...END if(tlao.tpc.getOptrDebug().isOn()) { tlao.tpc.getOptrDebug().write("\t[" + getLineNumber() + ":" + getCharArrIdx() + "] GAP: '" + sName + "'"); } ssNextSurrTxtAndGap = new s_s(tlao.sbSurrounding.toString(), sName); tlao.sbSurrounding.setLength(0); //The next surrounding text and gap name are now in the //s_s, which is retrieved via getNextSurrTxtAndGap() return; } if(sFinalSurrTxt == null) { int iCharsDeleted = -1; try { iCharsDeleted = tlao.us.unescape(tlao.sbLine, getCharArrIdx(0)); } catch(UnescapeStringException eusux) { throwTFX("ERROR while attempting to unescape the final surrounding text: " + eusux.getMessage()); } incDelCharCount(iCharsDeleted); sFinalSurrTxt = tlao.sbLine.toString(); } } }