/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (FormatStep.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.format.expert;
import net.time4j.engine.AttributeQuery;
import net.time4j.engine.ChronoCondition;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.ChronoElement;
import net.time4j.format.Attributes;
import net.time4j.format.Leniency;
import net.time4j.history.internal.HistorizedElement;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* <p>Formatierschritt als Delegationsobjekt zum Parsen und Formatieren. </p>
*
* @author Meno Hochschild
* @since 3.15/4.12
*/
final class FormatStep {
//~ Instanzvariablen --------------------------------------------------
private final FormatProcessor<?> processor;
private final int level;
private final int section;
private final AttributeSet sectionalAttrs;
private final AttributeQuery fullAttrs;
private final int reserved;
private final int padLeft;
private final int padRight;
private final boolean orMarker;
private final int lastOrBlockIndex;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Konstruktor mit Delegationsobjekt und Attributen. </p>
*
* @param processor processor which will process all formatting work
* @param level level of optional processing
* @param section identifies the optional attribute section
* @param sectionalAttrs sectional control attributes (optional)
* @throws IllegalArgumentException in case of any inconsistencies
*/
FormatStep(
FormatProcessor<?> processor,
int level,
int section,
AttributeSet sectionalAttrs
) {
this(processor, level, section, sectionalAttrs, null, 0, 0, 0, false, -1);
}
private FormatStep(
FormatProcessor<?> processor,
int level,
int section,
AttributeSet sectionalAttrs,
AttributeQuery fullAttrs,
int reserved,
int padLeft,
int padRight,
boolean orMarker,
int lastOrBlockIndex
) {
super();
if (processor == null) {
throw new NullPointerException("Missing format processor.");
} else if (level < 0) {
throw new IllegalArgumentException("Invalid level: " + level);
} else if (section < 0) {
throw new IllegalArgumentException("Invalid section: " + section);
} else if (reserved < 0) {
throw new IllegalArgumentException("Reserved chars must not be negative: " + reserved);
} else if (padLeft < 0) {
throw new IllegalArgumentException("Invalid pad-width: " + padLeft);
} else if (padRight < 0) {
throw new IllegalArgumentException("Invalid pad-width: " + padRight);
}
this.processor = processor;
this.level = level;
this.section = section;
this.sectionalAttrs = sectionalAttrs;
this.fullAttrs = fullAttrs;
this.reserved = reserved;
this.padLeft = padLeft;
this.padRight = padRight;
this.orMarker = orMarker;
this.lastOrBlockIndex = lastOrBlockIndex;
}
//~ Methoden ----------------------------------------------------------
/**
* <p>Erzeugt eine Textausgabe und speichert sie im angegebenen Puffer. </p>
*
* @param formattable object to be formatted
* @param buffer format buffer any text output will be sent to
* @param attributes non-sectional control attributes
* @param positions positions of elements in text (optional)
* @param quickPath hint for using quick path
* @throws IllegalArgumentException if the object is not formattable
* @throws IOException if writing into buffer fails
* @since 3.15/4.12
*/
void print(
ChronoDisplay formattable,
Appendable buffer,
AttributeQuery attributes,
Set<ElementPosition> positions,
boolean quickPath
) throws IOException {
if (!this.isPrinting(formattable)) {
return;
}
AttributeQuery aq = (quickPath ? this.fullAttrs : this.getQuery(attributes));
if (
(this.padLeft == 0)
&& (this.padRight == 0)
) {
this.processor.print(
formattable,
buffer,
aq,
positions,
quickPath
);
return;
}
StringBuilder collector = new StringBuilder();
int offset = -1;
Set<ElementPosition> posBuf = null;
if (
(buffer instanceof CharSequence)
&& (positions != null)
) {
offset = ((CharSequence) buffer).length();
posBuf = new LinkedHashSet<>();
}
boolean strict = this.isStrict(aq);
char padChar = this.getPadChar(aq);
this.processor.print(
formattable,
collector,
aq,
posBuf,
quickPath
);
int len = collector.length();
int printed = len;
if (this.padLeft > 0) {
if (strict && (len > this.padLeft)) {
throw new IllegalArgumentException(this.padExceeded());
}
while (printed < this.padLeft) {
buffer.append(padChar);
printed++;
}
buffer.append(collector);
if (offset != -1) {
for (ElementPosition ep : posBuf) {
positions.add(
new ElementPosition(
ep.getElement(),
offset + ep.getStartIndex(),
offset + ep.getEndIndex()));
}
}
if (this.padRight > 0) {
if (strict && (len > this.padRight)) {
throw new IllegalArgumentException(this.padExceeded());
}
while (len < this.padRight) {
buffer.append(padChar);
len++;
}
}
} else { // padRight > 0
if (strict && (len > this.padRight)) {
throw new IllegalArgumentException(this.padExceeded());
}
buffer.append(collector);
while (printed < this.padRight) {
buffer.append(padChar);
printed++;
}
if (offset != -1) {
for (ElementPosition ep : posBuf) {
positions.add(
new ElementPosition(
ep.getElement(),
offset + ep.getStartIndex(),
offset + ep.getEndIndex()));
}
}
}
}
/**
* <p>Interpretiert den angegebenen Text. </p>
*
* @param text text to be parsed
* @param status parser information (always as new instance)
* @param attributes non-sectional control attributes
* @param parsedResult result buffer for parsed values
* @param quickPath hint for using quick path
* @since 3.15/4.12
*/
void parse(
CharSequence text,
ParseLog status,
AttributeQuery attributes,
ParsedEntity<?> parsedResult,
boolean quickPath
) {
AttributeQuery aq = (quickPath ? this.fullAttrs : this.getQuery(attributes));
if (
(this.padLeft == 0)
&& (this.padRight == 0)
) {
// Optimierung
this.doParse(text, status, aq, parsedResult, quickPath);
return;
}
boolean strict = this.isStrict(aq);
char padChar = this.getPadChar(aq);
int start = status.getPosition();
int endPos = text.length();
int index = start;
// linke Füllzeichen konsumieren
while (
(index < endPos)
&& (text.charAt(index) == padChar)
) {
index++;
}
int leftPadCount = index - start;
if (strict && (leftPadCount > this.padLeft)) {
status.setError(start, this.padExceeded());
return;
}
// Eigentliche Parser-Routine
status.setPosition(index);
this.doParse(text, status, aq, parsedResult, quickPath);
if (status.isError()) {
return;
}
index = status.getPosition();
int width = index - start - leftPadCount;
if (
strict
&& (this.padLeft > 0)
&& ((width + leftPadCount) != this.padLeft)
) {
status.setError(start, this.padMismatched());
return;
}
// rechte Füllzeichen konsumieren
int rightPadCount = 0;
while (
(index < endPos)
&& (!strict || (width + rightPadCount < this.padRight))
&& (text.charAt(index) == padChar)
) {
index++;
rightPadCount++;
}
if (
strict
&& (this.padRight > 0)
&& ((width + rightPadCount) != this.padRight)
) {
status.setError(index - rightPadCount, this.padMismatched());
return;
}
status.setPosition(index);
}
/**
* <p>Liefert die Ebene der optionalen Verarbeitung. </p>
*
* @return int
*/
int getLevel() {
return this.level;
}
/**
* <p>Identifiziert die optionale Sektion. </p>
*
* @return int
*/
int getSection() {
return this.section;
}
/**
* <p>Liegt ein fraktional oder dezimal formatiertes Element vor? </p>
*
* @return boolean
*/
boolean isDecimal() {
return (
(this.processor instanceof FractionProcessor)
|| (this.processor instanceof DecimalProcessor)
);
}
/**
* <p>Liegt ein numerisch formatiertes Element vor? </p>
*
* @return boolean
*/
boolean isNumerical() {
return this.processor.isNumerical();
}
/**
* <p>Ermittelt die Delegationsinstanz. </p>
*
* @return delegate object for formatting work
*/
FormatProcessor<?> getProcessor() {
return this.processor;
}
/**
* <p>Finaler Schritt nach dem <i>build</i> des Formatierers oder bei Attributänderungen. </p>
*
* @param formatter reference to formattter holding the default global attributes
* @return copy of this instance maybe modified
* @since 3.15/4.12
*/
FormatStep quickPath(ChronoFormatter<?> formatter) {
AttributeSet as = formatter.getAttributes0();
if (this.sectionalAttrs != null) {
Attributes attrs =
new Attributes.Builder()
.setAll(as.getAttributes())
.setAll(this.sectionalAttrs.getAttributes())
.build();
as = as.withAttributes(attrs);
}
return new FormatStep(
this.processor.quickPath(formatter, as, this.reserved),
this.level,
this.section,
this.sectionalAttrs,
as,
this.reserved,
this.padLeft,
this.padRight,
this.orMarker,
this.lastOrBlockIndex
);
}
/**
* <p>Aktualisiert diesen Formatierschritt. </p>
*
* @param element new element reference
* @return copy of this instance maybe modified
*/
FormatStep updateElement(ChronoElement<?> element) {
FormatProcessor<?> proc = update(this.processor, element);
if (this.processor == proc) {
return this;
}
return new FormatStep(
proc,
this.level,
this.section,
this.sectionalAttrs,
this.fullAttrs,
this.reserved,
this.padLeft,
this.padRight,
this.orMarker,
this.lastOrBlockIndex
);
}
/**
* <p>Rechnet die angegebene Anzahl der zu reservierenden Zeichen
* hinzu. </p>
*
* @param reserved count of chars to be reserved
* @return updated format step
*/
FormatStep reserve(int reserved) {
return new FormatStep(
this.processor,
this.level,
this.section,
this.sectionalAttrs,
null, // called before build of formatter
this.reserved + reserved,
this.padLeft,
this.padRight,
this.orMarker,
this.lastOrBlockIndex
);
}
/**
* <p>Rechnet die angegebene Anzahl von Füllzeichen hinzu. </p>
*
* @param padLeft count of left-padding chars
* @param padRight count of right-padding chars
* @return updated format step
*/
FormatStep pad(
int padLeft,
int padRight
) {
return new FormatStep(
this.processor,
this.level,
this.section,
this.sectionalAttrs,
null, // called before build of formatter
this.reserved,
this.padLeft + padLeft,
this.padRight + padRight,
this.orMarker,
this.lastOrBlockIndex
);
}
/**
* <p>Startet einen neuen oder-Block. </p>
*
* @return updated format step
* @throws IllegalStateException if a new or-block was already started
* @since 3.14/4.11
*/
FormatStep startNewOrBlock() {
if (this.orMarker) {
throw new IllegalStateException("Cannot start or-block twice.");
}
return new FormatStep(
this.processor,
this.level,
this.section,
this.sectionalAttrs,
null, // called before build of formatter
this.reserved,
this.padLeft,
this.padRight,
true,
-1
);
}
/**
* <p>Markiert den letzten oder-Block des aktuellen Abschnitts. </p>
*
* @param lastOrBlockIndex index of last or-block in current section
* @return updated format step
* @throws IllegalStateException if called for a non-starting or-block
* @since 3.16/4.13
*/
FormatStep markLastOrBlock(int lastOrBlockIndex) {
if (!this.orMarker) {
throw new IllegalStateException("This step is not starting an or-block.");
}
return new FormatStep(
this.processor,
this.level,
this.section,
this.sectionalAttrs,
this.fullAttrs,
this.reserved,
this.padLeft,
this.padRight,
true,
lastOrBlockIndex
);
}
/**
* Wird ein neuer oder-Block gestartet?
*
* @return boolean
* @since 3.14/4.11
*/
boolean isNewOrBlockStarted() {
return this.orMarker;
}
/**
* Ermittelt den Index des letzten Steps zum aktuellen oder-Abschnitt.
*
* @return int
* @since 3.16/4.13
*/
int skipTrailingOrBlocks() {
return this.lastOrBlockIndex;
}
/**
* <p>Vergleicht die internen Formatverarbeitungen und die sektionalen
* Attribute. </p>
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof FormatStep) {
FormatStep that = (FormatStep) obj;
return (
this.processor.equals(that.processor)
&& (this.level == that.level)
&& (this.section == that.section)
&& isEqual(this.sectionalAttrs, that.sectionalAttrs)
&& isEqual(this.fullAttrs, that.fullAttrs)
&& (this.reserved == that.reserved)
&& (this.padLeft == that.padLeft)
&& (this.padRight == that.padRight)
&& (this.orMarker == that.orMarker)
&& (this.lastOrBlockIndex == that.lastOrBlockIndex)
);
} else {
return false;
}
}
/**
* <p>Berechnet den Hash-Code basierend auf dem internen Zustand. </p>
*/
@Override
public int hashCode() {
return (
7 * this.processor.hashCode()
+ 31 * (
(this.sectionalAttrs == null)
? 0
: this.sectionalAttrs.hashCode())
);
}
/**
* <p>Für Debugging-Zwecke. </p>
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[processor=");
sb.append(this.processor);
sb.append(", level=");
sb.append(this.level);
sb.append(", section=");
sb.append(this.section);
if (this.sectionalAttrs != null) {
sb.append(", attributes=");
sb.append(this.sectionalAttrs);
}
sb.append(", reserved=");
sb.append(this.reserved);
sb.append(", pad-left=");
sb.append(this.padLeft);
sb.append(", pad-right=");
sb.append(this.padRight);
if (this.orMarker) {
sb.append(", or-block-started");
}
sb.append(']');
return sb.toString();
}
private AttributeQuery getQuery(AttributeQuery attributes) {
if (this.sectionalAttrs == null) {
return attributes; // Optimierung
}
return new MergedAttributes(this.sectionalAttrs, attributes);
}
@SuppressWarnings("unchecked")
private static <V> FormatProcessor<V> update(
FormatProcessor<V> fp,
ChronoElement<?> element
) {
if (fp.getElement() == null) {
return fp;
} else if (
(fp.getElement().getType() != element.getType())
&& !(element instanceof HistorizedElement)
) {
throw new IllegalArgumentException(
"Cannot change element value type: " + element.name());
}
return fp.withElement((ChronoElement<V>) element);
}
private static boolean isEqual(
Object o1, // optional
Object o2 // optional
) {
return ((o1 == null) ? (o2 == null) : o1.equals(o2));
}
private void doParse(
CharSequence text,
ParseLog status,
AttributeQuery attributes,
ParsedEntity<?> parsedResult,
boolean quickPath
) {
int current = status.getPosition();
try {
this.processor.parse(
text,
status,
attributes,
parsedResult,
quickPath
);
} catch (RuntimeException re) {
status.setError(current, re.getMessage());
}
}
private boolean isStrict(AttributeQuery attributes) {
return attributes.get(Attributes.LENIENCY, Leniency.SMART).isStrict();
}
private char getPadChar(AttributeQuery attributes) {
return attributes.get(Attributes.PAD_CHAR, Character.valueOf(' ')).charValue();
}
private String padExceeded() {
return "Pad width exceeded: " + this.processor.getElement().name();
}
private String padMismatched() {
return "Pad width mismatched: " + this.processor.getElement().name();
}
private boolean isPrinting(ChronoDisplay formattable) {
if (this.sectionalAttrs == null) {
return true;
}
ChronoCondition<ChronoDisplay> printCondition = this.sectionalAttrs.getCondition();
return ((printCondition == null) || printCondition.test(formattable));
}
}