diff options
Diffstat (limited to 'src/main/java/org/yaml')
119 files changed, 14823 insertions, 12122 deletions
diff --git a/src/main/java/org/yaml/snakeyaml/DumperOptions.java b/src/main/java/org/yaml/snakeyaml/DumperOptions.java index 07621981..48b5e096 100644 --- a/src/main/java/org/yaml/snakeyaml/DumperOptions.java +++ b/src/main/java/org/yaml/snakeyaml/DumperOptions.java @@ -1,405 +1,487 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml; import java.util.Map; import java.util.TimeZone; - import org.yaml.snakeyaml.emitter.Emitter; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.serializer.AnchorGenerator; import org.yaml.snakeyaml.serializer.NumberAnchorGenerator; public class DumperOptions { - /** - * YAML provides a rich set of scalar styles. Block scalar styles include - * the literal style and the folded style; flow scalar styles include the - * plain style and two quoted styles, the single-quoted style and the - * double-quoted style. These styles offer a range of trade-offs between - * expressive power and readability. - * - * @see <a href="http://yaml.org/spec/1.1/#id903915">Chapter 9. Scalar - * Styles</a> - * @see <a href="http://yaml.org/spec/1.1/#id858081">2.3. Scalars</a> - */ - public enum ScalarStyle { - DOUBLE_QUOTED(Character.valueOf('"')), SINGLE_QUOTED(Character.valueOf('\'')), LITERAL( - Character.valueOf('|')), FOLDED(Character.valueOf('>')), PLAIN(null); - private Character styleChar; - - private ScalarStyle(Character style) { - this.styleChar = style; - } - - public Character getChar() { - return styleChar; - } - - @Override - public String toString() { - return "Scalar style: '" + styleChar + "'"; - } - - public static ScalarStyle createStyle(Character style) { - if (style == null) { - return PLAIN; - } else { - switch (style) { - case '"': - return DOUBLE_QUOTED; - case '\'': - return SINGLE_QUOTED; - case '|': - return LITERAL; - case '>': - return FOLDED; - default: - throw new YAMLException("Unknown scalar style character: " + style); - } - } - } - } - - /** - * Block styles use indentation to denote nesting and scope within the - * document. In contrast, flow styles rely on explicit indicators to denote - * nesting and scope. - * - * @see <a href="http://www.yaml.org/spec/current.html#id2509255">3.2.3.1. - * Node Styles (http://yaml.org/spec/1.1)</a> - */ - public enum FlowStyle { - FLOW(Boolean.TRUE), BLOCK(Boolean.FALSE), AUTO(null); - - private Boolean styleBoolean; - - private FlowStyle(Boolean flowStyle) { - styleBoolean = flowStyle; - } - - public Boolean getStyleBoolean() { - return styleBoolean; - } - - @Override - public String toString() { - return "Flow style: '" + styleBoolean + "'"; - } - } - - /** - * Platform dependent line break. - */ - public enum LineBreak { - WIN("\r\n"), MAC("\r"), UNIX("\n"); - private String lineBreak; - - private LineBreak(String lineBreak) { - this.lineBreak = lineBreak; - } - - public String getString() { - return lineBreak; - } - - @Override - public String toString() { - return "Line break: " + name(); - } - - public static LineBreak getPlatformLineBreak() { - String platformLineBreak = System.getProperty("line.separator"); - for (LineBreak lb : values()) { - if (lb.lineBreak.equals(platformLineBreak)) { - return lb; - } - } - return LineBreak.UNIX; + /** + * YAML provides a rich set of scalar styles. Block scalar styles include the literal style and + * the folded style; flow scalar styles include the plain style and two quoted styles, the + * single-quoted style and the double-quoted style. These styles offer a range of trade-offs + * between expressive power and readability. + * + * @see <a href="http://yaml.org/spec/1.1/#id903915">Chapter 9. Scalar Styles</a> + * @see <a href="http://yaml.org/spec/1.1/#id858081">2.3. Scalars</a> + */ + public enum ScalarStyle { + DOUBLE_QUOTED('"'), SINGLE_QUOTED('\''), LITERAL('|'), FOLDED('>'), PLAIN(null); + + private final Character styleChar; + + ScalarStyle(Character style) { + this.styleChar = style; + } + + public Character getChar() { + return styleChar; + } + + @Override + public String toString() { + return "Scalar style: '" + styleChar + "'"; + } + + public static ScalarStyle createStyle(Character style) { + if (style == null) { + return PLAIN; + } else { + switch (style) { + case '"': + return DOUBLE_QUOTED; + case '\'': + return SINGLE_QUOTED; + case '|': + return LITERAL; + case '>': + return FOLDED; + default: + throw new YAMLException("Unknown scalar style character: " + style); } + } } + } - /** - * Specification version. Currently supported 1.0 and 1.1 - */ - public enum Version { - V1_0(new Integer[] { 1, 0 }), V1_1(new Integer[] { 1, 1 }); - - private Integer[] version; - - private Version(Integer[] version) { - this.version = version; - } - - public int major() { return version[0]; } - public int minor() { return version[1]; } - - public String getRepresentation() { - return version[0] + "." + version[1]; - } + /** + * Block styles use indentation to denote nesting and scope within the document. In contrast, flow + * styles rely on explicit indicators to denote nesting and scope. + * + * @see <a href="http://www.yaml.org/spec/current.html#id2509255">3.2.3.1. Node Styles + * (http://yaml.org/spec/1.1)</a> + */ + public enum FlowStyle { + FLOW(Boolean.TRUE), BLOCK(Boolean.FALSE), AUTO(null); - @Override - public String toString() { - return "Version: " + getRepresentation(); - } - } + private final Boolean styleBoolean; - private ScalarStyle defaultStyle = ScalarStyle.PLAIN; - private FlowStyle defaultFlowStyle = FlowStyle.AUTO; - private boolean canonical = false; - private boolean allowUnicode = true; - private boolean allowReadOnlyProperties = false; - private int indent = 2; - private int indicatorIndent = 0; - private int bestWidth = 80; - private boolean splitLines = true; - private LineBreak lineBreak = LineBreak.UNIX; - private boolean explicitStart = false; - private boolean explicitEnd = false; - private TimeZone timeZone = null; - - private Version version = null; - private Map<String, String> tags = null; - private Boolean prettyFlow = false; - private AnchorGenerator anchorGenerator = new NumberAnchorGenerator(0); - - public boolean isAllowUnicode() { - return allowUnicode; + FlowStyle(Boolean flowStyle) { + styleBoolean = flowStyle; } - /** - * Specify whether to emit non-ASCII printable Unicode characters. - * The default value is true. - * When set to false then printable non-ASCII characters (Cyrillic, Chinese etc) - * will be not printed but escaped (to support ASCII terminals) + /* + * Convenience for legacy constructors that took {@link Boolean} arguments since replaced by + * {@link FlowStyle}. Introduced in v1.22 but only to support that for backwards compatibility. * - * @param allowUnicode - * if allowUnicode is false then all non-ASCII characters are - * escaped + * @deprecated Since restored in v1.22. Use the {@link FlowStyle} constants in your code + * instead. */ - public void setAllowUnicode(boolean allowUnicode) { - this.allowUnicode = allowUnicode; + @Deprecated + public static FlowStyle fromBoolean(Boolean flowStyle) { + return flowStyle == null ? AUTO : flowStyle ? FLOW : BLOCK; } - public ScalarStyle getDefaultScalarStyle() { - return defaultStyle; + public Boolean getStyleBoolean() { + return styleBoolean; } - /** - * Set default style for scalars. See YAML 1.1 specification, 2.3 Scalars - * (http://yaml.org/spec/1.1/#id858081) - * - * @param defaultStyle - * set the style for all scalars - */ - public void setDefaultScalarStyle(ScalarStyle defaultStyle) { - if (defaultStyle == null) { - throw new NullPointerException("Use ScalarStyle enum."); - } - this.defaultStyle = defaultStyle; + @Override + public String toString() { + return "Flow style: '" + styleBoolean + "'"; } + } - public void setIndent(int indent) { - if (indent < Emitter.MIN_INDENT) { - throw new YAMLException("Indent must be at least " + Emitter.MIN_INDENT); - } - if (indent > Emitter.MAX_INDENT) { - throw new YAMLException("Indent must be at most " + Emitter.MAX_INDENT); - } - this.indent = indent; - } + /** + * Platform dependent line break. + */ + public enum LineBreak { + WIN("\r\n"), MAC("\r"), UNIX("\n"); - public int getIndent() { - return this.indent; - } + private final String lineBreak; - public void setIndicatorIndent(int indicatorIndent) { - if (indicatorIndent < 0) { - throw new YAMLException("Indicator indent must be non-negative."); - } - if (indicatorIndent > Emitter.MAX_INDENT - 1) { - throw new YAMLException("Indicator indent must be at most Emitter.MAX_INDENT-1: " + (Emitter.MAX_INDENT - 1)); - } - this.indicatorIndent = indicatorIndent; + LineBreak(String lineBreak) { + this.lineBreak = lineBreak; } - public int getIndicatorIndent() { - return this.indicatorIndent; + public String getString() { + return lineBreak; } - public void setVersion(Version version) { - this.version = version; + @Override + public String toString() { + return "Line break: " + name(); } - public Version getVersion() { - return this.version; - } - - /** - * Force the emitter to produce a canonical YAML document. - * - * @param canonical - * true produce canonical YAML document - */ - public void setCanonical(boolean canonical) { - this.canonical = canonical; - } - - public boolean isCanonical() { - return this.canonical; - } - - /** - * Force the emitter to produce a pretty YAML document when using the flow - * style. - * - * @param prettyFlow - * true produce pretty flow YAML document - */ - public void setPrettyFlow(boolean prettyFlow) { - this.prettyFlow = prettyFlow; - } - - public boolean isPrettyFlow() { - return this.prettyFlow; - } - - /** - * Specify the preferred width to emit scalars. When the scalar - * representation takes more then the preferred with the scalar will be - * split into a few lines. The default is 80. - * - * @param bestWidth - * the preferred width for scalars. - */ - public void setWidth(int bestWidth) { - this.bestWidth = bestWidth; - } - - public int getWidth() { - return this.bestWidth; - } - - /** - * Specify whether to split lines exceeding preferred width for - * scalars. The default is true. - * - * @param splitLines - * whether to split lines exceeding preferred width for scalars. - */ - public void setSplitLines(boolean splitLines) { - this.splitLines = splitLines; - } - - public boolean getSplitLines() { - return this.splitLines; - } - - public LineBreak getLineBreak() { - return lineBreak; - } - - public void setDefaultFlowStyle(FlowStyle defaultFlowStyle) { - if (defaultFlowStyle == null) { - throw new NullPointerException("Use FlowStyle enum."); + public static LineBreak getPlatformLineBreak() { + String platformLineBreak = System.getProperty("line.separator"); + for (LineBreak lb : values()) { + if (lb.lineBreak.equals(platformLineBreak)) { + return lb; } - this.defaultFlowStyle = defaultFlowStyle; + } + return LineBreak.UNIX; } + } - public FlowStyle getDefaultFlowStyle() { - return defaultFlowStyle; - } - - /** - * Specify the line break to separate the lines. It is platform specific: - * Windows - "\r\n", old MacOS - "\r", Unix - "\n". The default value is the - * one for Unix. - */ - public void setLineBreak(LineBreak lineBreak) { - if (lineBreak == null) { - throw new NullPointerException("Specify line break."); - } - this.lineBreak = lineBreak; - } + /** + * Specification version. Currently supported 1.0 and 1.1 + */ + public enum Version { + V1_0(new Integer[] {1, 0}), V1_1(new Integer[] {1, 1}); - public boolean isExplicitStart() { - return explicitStart; - } + private final Integer[] version; - public void setExplicitStart(boolean explicitStart) { - this.explicitStart = explicitStart; + Version(Integer[] version) { + this.version = version; } - public boolean isExplicitEnd() { - return explicitEnd; + public int major() { + return version[0]; } - public void setExplicitEnd(boolean explicitEnd) { - this.explicitEnd = explicitEnd; + public int minor() { + return version[1]; } - public Map<String, String> getTags() { - return tags; + public String getRepresentation() { + return version[0] + "." + version[1]; } - // TODO should use Tag ??? - public void setTags(Map<String, String> tags) { - this.tags = tags; + @Override + public String toString() { + return "Version: " + getRepresentation(); } + } + public enum NonPrintableStyle { /** - * Report whether read-only JavaBean properties (the ones without setters) - * should be included in the YAML document - * - * @return false when read-only JavaBean properties are not emitted + * Transform String to binary if it contains non-printable characters */ - public boolean isAllowReadOnlyProperties() { - return allowReadOnlyProperties; - } - + BINARY, /** - * Set to true to include read-only JavaBean properties (the ones without - * setters) in the YAML document. By default these properties are not - * included to be able to parse later the same JavaBean. - * - * @param allowReadOnlyProperties - * - true to dump read-only JavaBean properties - */ - public void setAllowReadOnlyProperties(boolean allowReadOnlyProperties) { - this.allowReadOnlyProperties = allowReadOnlyProperties; - } - - public TimeZone getTimeZone() { - return timeZone; - } - - /** - * Set the timezone to be used for Date. If set to <code>null</code> UTC is - * used. + * Escape non-printable characters */ - public void setTimeZone(TimeZone timeZone) { - this.timeZone = timeZone; - } - - - public AnchorGenerator getAnchorGenerator() { - return anchorGenerator; - } - - public void setAnchorGenerator(AnchorGenerator anchorGenerator) { - this.anchorGenerator = anchorGenerator; - } + ESCAPE + } + + private ScalarStyle defaultStyle = ScalarStyle.PLAIN; + private FlowStyle defaultFlowStyle = FlowStyle.AUTO; + private boolean canonical = false; + private boolean allowUnicode = true; + private boolean allowReadOnlyProperties = false; + private int indent = 2; + private int indicatorIndent = 0; + private boolean indentWithIndicator = false; + private int bestWidth = 80; + private boolean splitLines = true; + private LineBreak lineBreak = LineBreak.UNIX; + private boolean explicitStart = false; + private boolean explicitEnd = false; + private TimeZone timeZone = null; + private int maxSimpleKeyLength = 128; + private boolean processComments = false; + private NonPrintableStyle nonPrintableStyle = NonPrintableStyle.BINARY; + + private Version version = null; + private Map<String, String> tags = null; + private Boolean prettyFlow = false; + private AnchorGenerator anchorGenerator = new NumberAnchorGenerator(0); + + public boolean isAllowUnicode() { + return allowUnicode; + } + + /** + * Specify whether to emit non-ASCII printable Unicode characters. The default value is true. When + * set to false then printable non-ASCII characters (Cyrillic, Chinese etc) will be not printed + * but escaped (to support ASCII terminals) + * + * @param allowUnicode if allowUnicode is false then all non-ASCII characters are escaped + */ + public void setAllowUnicode(boolean allowUnicode) { + this.allowUnicode = allowUnicode; + } + + public ScalarStyle getDefaultScalarStyle() { + return defaultStyle; + } + + /** + * Set default style for scalars. See YAML 1.1 specification, 2.3 Scalars + * (http://yaml.org/spec/1.1/#id858081) + * + * @param defaultStyle set the style for all scalars + */ + public void setDefaultScalarStyle(ScalarStyle defaultStyle) { + if (defaultStyle == null) { + throw new NullPointerException("Use ScalarStyle enum."); + } + this.defaultStyle = defaultStyle; + } + + public void setIndent(int indent) { + if (indent < Emitter.MIN_INDENT) { + throw new YAMLException("Indent must be at least " + Emitter.MIN_INDENT); + } + if (indent > Emitter.MAX_INDENT) { + throw new YAMLException("Indent must be at most " + Emitter.MAX_INDENT); + } + this.indent = indent; + } + + public int getIndent() { + return this.indent; + } + + /** + * Set number of white spaces to use for the sequence indicator '-' + * + * @param indicatorIndent value to be used as indent + */ + public void setIndicatorIndent(int indicatorIndent) { + if (indicatorIndent < 0) { + throw new YAMLException("Indicator indent must be non-negative."); + } + if (indicatorIndent > Emitter.MAX_INDENT - 1) { + throw new YAMLException( + "Indicator indent must be at most Emitter.MAX_INDENT-1: " + (Emitter.MAX_INDENT - 1)); + } + this.indicatorIndent = indicatorIndent; + } + + public int getIndicatorIndent() { + return this.indicatorIndent; + } + + public boolean getIndentWithIndicator() { + return indentWithIndicator; + } + + /** + * Set to true to add the indent for sequences to the general indent + * + * @param indentWithIndicator - true when indent for sequences is added to general + */ + public void setIndentWithIndicator(boolean indentWithIndicator) { + this.indentWithIndicator = indentWithIndicator; + } + + public void setVersion(Version version) { + this.version = version; + } + + public Version getVersion() { + return this.version; + } + + /** + * Force the emitter to produce a canonical YAML document. + * + * @param canonical true produce canonical YAML document + */ + public void setCanonical(boolean canonical) { + this.canonical = canonical; + } + + public boolean isCanonical() { + return this.canonical; + } + + /** + * Force the emitter to produce a pretty YAML document when using the flow style. + * + * @param prettyFlow true produce pretty flow YAML document + */ + public void setPrettyFlow(boolean prettyFlow) { + this.prettyFlow = prettyFlow; + } + + public boolean isPrettyFlow() { + return this.prettyFlow; + } + + /** + * Specify the preferred width to emit scalars. When the scalar representation takes more then the + * preferred with the scalar will be split into a few lines. The default is 80. + * + * @param bestWidth the preferred width for scalars. + */ + public void setWidth(int bestWidth) { + this.bestWidth = bestWidth; + } + + public int getWidth() { + return this.bestWidth; + } + + /** + * Specify whether to split lines exceeding preferred width for scalars. The default is true. + * + * @param splitLines whether to split lines exceeding preferred width for scalars. + */ + public void setSplitLines(boolean splitLines) { + this.splitLines = splitLines; + } + + public boolean getSplitLines() { + return this.splitLines; + } + + public LineBreak getLineBreak() { + return lineBreak; + } + + public void setDefaultFlowStyle(FlowStyle defaultFlowStyle) { + if (defaultFlowStyle == null) { + throw new NullPointerException("Use FlowStyle enum."); + } + this.defaultFlowStyle = defaultFlowStyle; + } + + public FlowStyle getDefaultFlowStyle() { + return defaultFlowStyle; + } + + /** + * Specify the line break to separate the lines. It is platform specific: Windows - "\r\n", old + * MacOS - "\r", Unix - "\n". The default value is the one for Unix. + * + * @param lineBreak to be used for the input + */ + public void setLineBreak(LineBreak lineBreak) { + if (lineBreak == null) { + throw new NullPointerException("Specify line break."); + } + this.lineBreak = lineBreak; + } + + public boolean isExplicitStart() { + return explicitStart; + } + + public void setExplicitStart(boolean explicitStart) { + this.explicitStart = explicitStart; + } + + public boolean isExplicitEnd() { + return explicitEnd; + } + + public void setExplicitEnd(boolean explicitEnd) { + this.explicitEnd = explicitEnd; + } + + public Map<String, String> getTags() { + return tags; + } + + public void setTags(Map<String, String> tags) { + this.tags = tags; + } + + /** + * Report whether read-only JavaBean properties (the ones without setters) should be included in + * the YAML document + * + * @return false when read-only JavaBean properties are not emitted + */ + public boolean isAllowReadOnlyProperties() { + return allowReadOnlyProperties; + } + + /** + * Set to true to include read-only JavaBean properties (the ones without setters) in the YAML + * document. By default these properties are not included to be able to parse later the same + * JavaBean. + * + * @param allowReadOnlyProperties - true to dump read-only JavaBean properties + */ + public void setAllowReadOnlyProperties(boolean allowReadOnlyProperties) { + this.allowReadOnlyProperties = allowReadOnlyProperties; + } + + public TimeZone getTimeZone() { + return timeZone; + } + + /** + * Set the timezone to be used for Date. If set to <code>null</code> UTC is used. + * + * @param timeZone for created Dates or null to use UTC + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + + public AnchorGenerator getAnchorGenerator() { + return anchorGenerator; + } + + public void setAnchorGenerator(AnchorGenerator anchorGenerator) { + this.anchorGenerator = anchorGenerator; + } + + public int getMaxSimpleKeyLength() { + return maxSimpleKeyLength; + } + + /** + * Define max key length to use simple key (without '?') More info + * https://yaml.org/spec/1.1/#id934537 + * + * @param maxSimpleKeyLength - the limit after which the key gets explicit key indicator '?' + */ + public void setMaxSimpleKeyLength(int maxSimpleKeyLength) { + if (maxSimpleKeyLength > 1024) { + throw new YAMLException( + "The simple key must not span more than 1024 stream characters. See https://yaml.org/spec/1.1/#id934537"); + } + this.maxSimpleKeyLength = maxSimpleKeyLength; + } + + /** + * Set the comment processing. By default comments are ignored. + * + * @param processComments <code>true</code> to process; <code>false</code> to ignore + */ + + public void setProcessComments(boolean processComments) { + this.processComments = processComments; + } + + public boolean isProcessComments() { + return processComments; + } + + public NonPrintableStyle getNonPrintableStyle() { + return this.nonPrintableStyle; + } + + /** + * When String contains non-printable characters SnakeYAML convert it to binary data with the + * !!binary tag. Set this to ESCAPE to keep the !!str tag and escape the non-printable chars with + * \\x or \\u + * + * @param style ESCAPE to force SnakeYAML to keep !!str tag for non-printable data + */ + public void setNonPrintableStyle(NonPrintableStyle style) { + this.nonPrintableStyle = style; + } } diff --git a/src/main/java/org/yaml/snakeyaml/LoaderOptions.java b/src/main/java/org/yaml/snakeyaml/LoaderOptions.java new file mode 100644 index 00000000..78583587 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/LoaderOptions.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml; + +public class LoaderOptions { + + private boolean allowDuplicateKeys = true; + private boolean wrappedToRootException = false; + private int maxAliasesForCollections = 50; // to prevent YAML at + // https://en.wikipedia.org/wiki/Billion_laughs_attack + private boolean allowRecursiveKeys = false; + private boolean processComments = false; + private boolean enumCaseSensitive = true; + private int nestingDepthLimit = 50; + private int codePointLimit = 3 * 1024 * 1024; // 3 MB + + public final boolean isAllowDuplicateKeys() { + return allowDuplicateKeys; + } + + /** + * Allow/Reject duplicate map keys in the YAML file. + * + * Default is to allow. + * + * YAML 1.1 is slightly vague around duplicate entries in the YAML file. The best reference is + * <a href="http://www.yaml.org/spec/1.1/#id862121"> 3.2.1.3. Nodes Comparison</a> where it hints + * that a duplicate map key is an error. + * + * For future reference, YAML spec 1.2 is clear. The keys MUST be unique. + * <a href="http://www.yaml.org/spec/1.2/spec.html#id2759572">1.3. Relation to JSON</a> + * + * @param allowDuplicateKeys false to reject duplicate mapping keys + */ + public void setAllowDuplicateKeys(boolean allowDuplicateKeys) { + this.allowDuplicateKeys = allowDuplicateKeys; + } + + public final boolean isWrappedToRootException() { + return wrappedToRootException; + } + + /** + * Wrap runtime exception to YAMLException during parsing or leave them as they are + * + * Default is to leave original exceptions + * + * @param wrappedToRootException - true to convert runtime exception to YAMLException + */ + public void setWrappedToRootException(boolean wrappedToRootException) { + this.wrappedToRootException = wrappedToRootException; + } + + public final int getMaxAliasesForCollections() { + return maxAliasesForCollections; + } + + /** + * Restrict the amount of aliases for collections (sequences and mappings) to avoid + * https://en.wikipedia.org/wiki/Billion_laughs_attack + * + * @param maxAliasesForCollections set max allowed value (50 by default) + */ + public void setMaxAliasesForCollections(int maxAliasesForCollections) { + this.maxAliasesForCollections = maxAliasesForCollections; + } + + /** + * Allow recursive keys for mappings. By default, it is not allowed. This setting only prevents + * the case when the key is the value. If the key is only a part of the value (the value is a + * sequence or a mapping) then this case is not recognized and always allowed. + * + * @param allowRecursiveKeys - false to disable recursive keys + */ + public void setAllowRecursiveKeys(boolean allowRecursiveKeys) { + this.allowRecursiveKeys = allowRecursiveKeys; + } + + public final boolean getAllowRecursiveKeys() { + return allowRecursiveKeys; + } + + /** + * Set the comment processing. By default, comments are ignored. + * + * @param processComments <code>true</code> to process; <code>false</code> to ignore + */ + public LoaderOptions setProcessComments(boolean processComments) { + this.processComments = processComments; + return this; + } + + public final boolean isProcessComments() { + return processComments; + } + + public final boolean isEnumCaseSensitive() { + return enumCaseSensitive; + } + + /** + * Disables or enables case sensitivity during construct enum constant from string value Default + * is false. + * + * @param enumCaseSensitive - true to set enum case sensitive, false the reverse + */ + public void setEnumCaseSensitive(boolean enumCaseSensitive) { + this.enumCaseSensitive = enumCaseSensitive; + } + + public final int getNestingDepthLimit() { + return nestingDepthLimit; + } + + /** + * Set max depth of nested collections. When the limit is exceeded an exception is thrown. + * Aliases/Anchors are not counted. This is to prevent a DoS attack + * + * @param nestingDepthLimit - depth to be accepted (50 by default) + */ + public void setNestingDepthLimit(int nestingDepthLimit) { + this.nestingDepthLimit = nestingDepthLimit; + } + + public final int getCodePointLimit() { + return codePointLimit; + } + + /** + * The max amount of code points in the input YAML document. Please be aware that byte limit + * depends on the encoding. + * + * @param codePointLimit - the max allowed size of the YAML data + */ + public void setCodePointLimit(int codePointLimit) { + this.codePointLimit = codePointLimit; + } +} diff --git a/src/main/java/org/yaml/snakeyaml/TypeDescription.java b/src/main/java/org/yaml/snakeyaml/TypeDescription.java index 4c383076..e65308b1 100644 --- a/src/main/java/org/yaml/snakeyaml/TypeDescription.java +++ b/src/main/java/org/yaml/snakeyaml/TypeDescription.java @@ -1,148 +1,398 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml; -import java.util.HashMap; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; - +import java.util.Set; +import java.util.logging.Logger; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.introspector.PropertySubstitute; +import org.yaml.snakeyaml.introspector.PropertyUtils; +import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.Tag; /** - * Provides additional runtime information necessary to create a custom Java - * instance. + * Provides additional runtime information necessary to create a custom Java instance. + * + * In general this class is thread-safe and can be used as a singleton, the only exception being the + * PropertyUtils field. A singleton PropertyUtils should be constructed and shared between all YAML + * Constructors used if a singleton TypeDescription is used, since Constructor sets its + * propertyUtils to the TypeDescription that is passed to it, hence you may end up in a situation + * when propertyUtils in TypeDescription is from different Constructor. */ -public final class TypeDescription { - private final Class<? extends Object> type; - private Tag tag; - private Map<String, Class<? extends Object>> listProperties; - private Map<String, Class<? extends Object>> keyProperties; - private Map<String, Class<? extends Object>> valueProperties; - - public TypeDescription(Class<? extends Object> clazz, Tag tag) { - this.type = clazz; - this.tag = tag; - listProperties = new HashMap<String, Class<? extends Object>>(); - keyProperties = new HashMap<String, Class<? extends Object>>(); - valueProperties = new HashMap<String, Class<? extends Object>>(); - } +public class TypeDescription { + + final static private Logger log = Logger.getLogger(TypeDescription.class.getPackage().getName()); + + private final Class<? extends Object> type; + + // class that implements the described type; if set, will be used as a source for constructor. + // If not set - TypeDescription will leave instantiation of an entity to the YAML Constructor + private Class<?> impl; + + private Tag tag; + + private transient Set<Property> dumpProperties; + private transient PropertyUtils propertyUtils; + private transient boolean delegatesChecked; + + private Map<String, PropertySubstitute> properties = Collections.emptyMap(); + + protected Set<String> excludes = Collections.emptySet(); + protected String[] includes = null; + protected BeanAccess beanAccess; + + public TypeDescription(Class<? extends Object> clazz, Tag tag) { + this(clazz, tag, null); + } + + public TypeDescription(Class<? extends Object> clazz, Tag tag, Class<?> impl) { + this.type = clazz; + this.tag = tag; + this.impl = impl; + beanAccess = null; + } + + public TypeDescription(Class<? extends Object> clazz, String tag) { + this(clazz, new Tag(tag), null); + } + + public TypeDescription(Class<? extends Object> clazz) { + this(clazz, new Tag(clazz), null); + } + + public TypeDescription(Class<? extends Object> clazz, Class<?> impl) { + this(clazz, new Tag(clazz), impl); + } + + /** + * Get tag which shall be used to load or dump the type (class). + * + * @return tag to be used. It may be a tag for Language-Independent Types + * (http://www.yaml.org/type/) + */ + public Tag getTag() { + return tag; + } + + /** + * Set tag to be used dump the type (class). + * + * @param tag - local or global tag + * @deprecated it will be removed because it is not used + */ + @Deprecated + public void setTag(Tag tag) { + this.tag = tag; + } + + /** + * Set tag to be used to dump the type (class). + * + * @param tag - local or global tag + * @deprecated it will be removed because it is not used + */ + @Deprecated + public void setTag(String tag) { + setTag(new Tag(tag)); + } + + /** + * Get represented type (class) + * + * @return type (class) to be described. + */ + public Class<? extends Object> getType() { + return type; + } + + /** + * Specify that the property is a type-safe <code>List</code>. + * + * @param property name of the JavaBean property + * @param type class of List values + */ + @Deprecated + public void putListPropertyType(String property, Class<? extends Object> type) { + addPropertyParameters(property, type); + } - public TypeDescription(Class<? extends Object> clazz, String tag) { - this(clazz, new Tag(tag)); + /** + * Get class of List values for provided JavaBean property. + * + * @param property property name + * @return class of List values + */ + @Deprecated + public Class<? extends Object> getListPropertyType(String property) { + if (properties.containsKey(property)) { + Class<?>[] typeArguments = properties.get(property).getActualTypeArguments(); + if (typeArguments != null && typeArguments.length > 0) { + return typeArguments[0]; + } } + return null; + } - public TypeDescription(Class<? extends Object> clazz) { - this(clazz, (Tag) null); + /** + * Specify that the property is a type-safe <code>Map</code>. + * + * @param property property name of this JavaBean + * @param key class of keys in Map + * @param value class of values in Map + */ + @Deprecated + public void putMapPropertyType(String property, Class<? extends Object> key, + Class<? extends Object> value) { + addPropertyParameters(property, key, value); + } + + /** + * Get keys type info for this JavaBean + * + * @param property property name of this JavaBean + * @return class of keys in the Map + */ + @Deprecated + public Class<? extends Object> getMapKeyType(String property) { + if (properties.containsKey(property)) { + Class<?>[] typeArguments = properties.get(property).getActualTypeArguments(); + if (typeArguments != null && typeArguments.length > 0) { + return typeArguments[0]; + } } + return null; + } - /** - * Get tag which shall be used to load or dump the type (class). - * - * @return tag to be used. It may be a tag for Language-Independent Types - * (http://www.yaml.org/type/) - */ - public Tag getTag() { - return tag; + /** + * Get values type info for this JavaBean + * + * @param property property name of this JavaBean + * @return class of values in the Map + */ + @Deprecated + public Class<? extends Object> getMapValueType(String property) { + if (properties.containsKey(property)) { + Class<?>[] typeArguments = properties.get(property).getActualTypeArguments(); + if (typeArguments != null && typeArguments.length > 1) { + return typeArguments[1]; + } } + return null; + } - /** - * Set tag to be used to load or dump the type (class). - * - * @param tag - * local or global tag - */ - public void setTag(Tag tag) { - this.tag = tag; + /** + * Adds new substitute for property <code>pName</code> parameterized by <code>classes</code> to + * this <code>TypeDescription</code>. If <code>pName</code> has been added before - updates + * parameters with <code>classes</code>. + * + * @param pName - parameter name + * @param classes - parameterized by + */ + public void addPropertyParameters(String pName, Class<?>... classes) { + if (!properties.containsKey(pName)) { + substituteProperty(pName, null, null, null, classes); + } else { + PropertySubstitute pr = properties.get(pName); + pr.setActualTypeArguments(classes); } - public void setTag(String tag) { - setTag(new Tag(tag)); + } + + @Override + public String toString() { + return "TypeDescription for " + getType() + " (tag='" + getTag() + "')"; + } + + private void checkDelegates() { + Collection<PropertySubstitute> values = properties.values(); + for (PropertySubstitute p : values) { + try { + p.setDelegate(discoverProperty(p.getName())); + } catch (YAMLException e) { + } } + delegatesChecked = true; + } - /** - * Get represented type (class) - * - * @return type (class) to be described. - */ - public Class<? extends Object> getType() { - return type; + private Property discoverProperty(String name) { + if (propertyUtils != null) { + if (beanAccess == null) { + return propertyUtils.getProperty(type, name); + } + return propertyUtils.getProperty(type, name, beanAccess); } + return null; + } - /** - * Specify that the property is a type-safe <code>List</code>. - * - * @param property - * name of the JavaBean property - * @param type - * class of List values - */ - public void putListPropertyType(String property, Class<? extends Object> type) { - listProperties.put(property, type); + public Property getProperty(String name) { + if (!delegatesChecked) { + checkDelegates(); } + return properties.containsKey(name) ? properties.get(name) : discoverProperty(name); + } - /** - * Get class of List values for provided JavaBean property. - * - * @param property - * property name - * @return class of List values - */ - public Class<? extends Object> getListPropertyType(String property) { - return listProperties.get(property); + /** + * Adds property substitute for <code>pName</code> + * + * @param pName property name + * @param pType property type + * @param getter method name for getter + * @param setter method name for setter + * @param argParams actual types for parameterized type (List<?>, Map<?>) + */ + public void substituteProperty(String pName, Class<?> pType, String getter, String setter, + Class<?>... argParams) { + substituteProperty(new PropertySubstitute(pName, pType, getter, setter, argParams)); + } + + public void substituteProperty(PropertySubstitute substitute) { + if (Collections.EMPTY_MAP == properties) { + properties = new LinkedHashMap<String, PropertySubstitute>(); } + substitute.setTargetType(type); + properties.put(substitute.getName(), substitute); + } + + public void setPropertyUtils(PropertyUtils propertyUtils) { + this.propertyUtils = propertyUtils; + } - /** - * Specify that the property is a type-safe <code>Map</code>. - * - * @param property - * property name of this JavaBean - * @param key - * class of keys in Map - * @param value - * class of values in Map - */ - public void putMapPropertyType(String property, Class<? extends Object> key, - Class<? extends Object> value) { - keyProperties.put(property, key); - valueProperties.put(property, value); + /* begin: Representer */ + public void setIncludes(String... propNames) { + this.includes = (propNames != null && propNames.length > 0) ? propNames : null; + } + + public void setExcludes(String... propNames) { + if (propNames != null && propNames.length > 0) { + excludes = new HashSet<String>(); + Collections.addAll(excludes, propNames); + } else { + excludes = Collections.emptySet(); } + } - /** - * Get keys type info for this JavaBean - * - * @param property - * property name of this JavaBean - * @return class of keys in the Map - */ - public Class<? extends Object> getMapKeyType(String property) { - return keyProperties.get(property); + public Set<Property> getProperties() { + if (dumpProperties != null) { + return dumpProperties; } - /** - * Get values type info for this JavaBean - * - * @param property - * property name of this JavaBean - * @return class of values in the Map - */ - public Class<? extends Object> getMapValueType(String property) { - return valueProperties.get(property); + if (propertyUtils != null) { + if (includes != null) { + dumpProperties = new LinkedHashSet<Property>(); + for (String propertyName : includes) { + if (!excludes.contains(propertyName)) { + dumpProperties.add(getProperty(propertyName)); + } + } + return dumpProperties; + } + + final Set<Property> readableProps = (beanAccess == null) ? propertyUtils.getProperties(type) + : propertyUtils.getProperties(type, beanAccess); + + if (properties.isEmpty()) { + if (excludes.isEmpty()) { + return dumpProperties = readableProps; + } + dumpProperties = new LinkedHashSet<Property>(); + for (Property property : readableProps) { + if (!excludes.contains(property.getName())) { + dumpProperties.add(property); + } + } + return dumpProperties; + } + + if (!delegatesChecked) { + checkDelegates(); + } + + dumpProperties = new LinkedHashSet<Property>(); + + for (Property property : properties.values()) { + if (!excludes.contains(property.getName()) && property.isReadable()) { + dumpProperties.add(property); + } + } + + for (Property property : readableProps) { + if (!excludes.contains(property.getName())) { + dumpProperties.add(property); + } + } + + return dumpProperties; } + return null; + } + + /* end: Representer */ + + /*------------ Maybe something useful to override :) ---------*/ - @Override - public String toString() { - return "TypeDescription for " + getType() + " (tag='" + getTag() + "')"; + public boolean setupPropertyType(String key, Node valueNode) { + return false; + } + + public boolean setProperty(Object targetBean, String propertyName, Object value) + throws Exception { + return false; + } + + /** + * This method should be overridden for TypeDescription implementations that are supposed to + * implement instantiation logic that is different from default one as implemented in YAML + * constructors. Note that even if you override this method, default filling of fields with + * variables from parsed YAML will still occur later. + * + * @param node - node to construct the instance from + * @return new instance + */ + public Object newInstance(Node node) { + if (impl != null) { + try { + java.lang.reflect.Constructor<?> c = impl.getDeclaredConstructor(); + c.setAccessible(true); + return c.newInstance(); + } catch (Exception e) { + log.fine(e.getLocalizedMessage()); + impl = null; + } } + return null; + } + + public Object newInstance(String propertyName, Node node) { + return null; + } + + /** + * Is invoked after entity is filled with values from deserialized YAML + * + * @param obj - deserialized entity + * @return postprocessed deserialized entity + */ + public Object finalizeConstruction(Object obj) { + return obj; + } + } diff --git a/src/main/java/org/yaml/snakeyaml/Yaml.java b/src/main/java/org/yaml/snakeyaml/Yaml.java index 5c4559cb..16114cd9 100644 --- a/src/main/java/org/yaml/snakeyaml/Yaml.java +++ b/src/main/java/org/yaml/snakeyaml/Yaml.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml; @@ -24,8 +22,8 @@ import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.NoSuchElementException; import java.util.regex.Pattern; - import org.yaml.snakeyaml.DumperOptions.FlowStyle; import org.yaml.snakeyaml.composer.Composer; import org.yaml.snakeyaml.constructor.BaseConstructor; @@ -46,616 +44,675 @@ import org.yaml.snakeyaml.resolver.Resolver; import org.yaml.snakeyaml.serializer.Serializer; /** - * Public YAML interface. Each Thread must have its own instance. + * Public YAML interface. This class is not thread-safe. Which means that all the methods of the + * same instance can be called only by one thread. It is better to create an instance for every YAML + * stream. */ public class Yaml { - protected final Resolver resolver; - private String name; - protected BaseConstructor constructor; - protected Representer representer; - protected DumperOptions dumperOptions; - - /** - * Create Yaml instance. It is safe to create a few instances and use them - * in different Threads. - */ - public Yaml() { - this(new Constructor(), new Representer(), new DumperOptions(), new Resolver()); - } - - /** - * Create Yaml instance. - * - * @param dumperOptions - * DumperOptions to configure outgoing objects - */ - public Yaml(DumperOptions dumperOptions) { - this(new Constructor(), new Representer(), dumperOptions); - } - - /** - * Create Yaml instance. It is safe to create a few instances and use them - * in different Threads. - * - * @param representer - * Representer to emit outgoing objects - */ - public Yaml(Representer representer) { - this(new Constructor(), representer); - } - - /** - * Create Yaml instance. It is safe to create a few instances and use them - * in different Threads. - * - * @param constructor - * BaseConstructor to construct incoming documents - */ - public Yaml(BaseConstructor constructor) { - this(constructor, new Representer()); - } - - /** - * Create Yaml instance. It is safe to create a few instances and use them - * in different Threads. - * - * @param constructor - * BaseConstructor to construct incoming documents - * @param representer - * Representer to emit outgoing objects - */ - public Yaml(BaseConstructor constructor, Representer representer) { - this(constructor, representer, new DumperOptions()); - } - - /** - * Create Yaml instance. It is safe to create a few instances and use them - * in different Threads. - * - * @param representer - * Representer to emit outgoing objects - * @param dumperOptions - * DumperOptions to configure outgoing objects - */ - public Yaml(Representer representer, DumperOptions dumperOptions) { - this(new Constructor(), representer, dumperOptions, new Resolver()); - } - - /** - * Create Yaml instance. It is safe to create a few instances and use them - * in different Threads. - * - * @param constructor - * BaseConstructor to construct incoming documents - * @param representer - * Representer to emit outgoing objects - * @param dumperOptions - * DumperOptions to configure outgoing objects - */ - public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions) { - this(constructor, representer, dumperOptions, new Resolver()); - } - - /** - * Create Yaml instance. It is safe to create a few instances and use them - * in different Threads. - * - * @param constructor - * BaseConstructor to construct incoming documents - * @param representer - * Representer to emit outgoing objects - * @param dumperOptions - * DumperOptions to configure outgoing objects - * @param resolver - * Resolver to detect implicit type - */ - public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, - Resolver resolver) { - if (!constructor.isExplicitPropertyUtils()) { - constructor.setPropertyUtils(representer.getPropertyUtils()); - } else if (!representer.isExplicitPropertyUtils()) { - representer.setPropertyUtils(constructor.getPropertyUtils()); - } - this.constructor = constructor; - representer.setDefaultFlowStyle(dumperOptions.getDefaultFlowStyle()); - representer.setDefaultScalarStyle(dumperOptions.getDefaultScalarStyle()); - representer.getPropertyUtils().setAllowReadOnlyProperties( - dumperOptions.isAllowReadOnlyProperties()); - representer.setTimeZone(dumperOptions.getTimeZone()); - this.representer = representer; - this.dumperOptions = dumperOptions; - this.resolver = resolver; - this.name = "Yaml:" + System.identityHashCode(this); - } - - /** - * Serialize a Java object into a YAML String. - * - * @param data - * Java object to be Serialized to YAML - * @return YAML String - */ - public String dump(Object data) { - List<Object> list = new ArrayList<Object>(1); - list.add(data); - return dumpAll(list.iterator()); - } - - /** - * Produce the corresponding representation tree for a given Object. - * - * @see <a href="http://yaml.org/spec/1.1/#id859333">Figure 3.1. Processing - * Overview</a> - * @param data - * instance to build the representation tree for - * @return representation tree - */ - public Node represent(Object data) { - return representer.represent(data); - } - - /** - * Serialize a sequence of Java objects into a YAML String. - * - * @param data - * Iterator with Objects - * @return YAML String with all the objects in proper sequence - */ - public String dumpAll(Iterator<? extends Object> data) { - StringWriter buffer = new StringWriter(); - dumpAll(data, buffer, null); - return buffer.toString(); - } - - /** - * Serialize a Java object into a YAML stream. - * - * @param data - * Java object to be serialized to YAML - * @param output - * stream to write to - */ - public void dump(Object data, Writer output) { - List<Object> list = new ArrayList<Object>(1); - list.add(data); - dumpAll(list.iterator(), output, null); - } - - /** - * Serialize a sequence of Java objects into a YAML stream. - * - * @param data - * Iterator with Objects - * @param output - * stream to write to - */ - public void dumpAll(Iterator<? extends Object> data, Writer output) { - dumpAll(data, output, null); - } - - private void dumpAll(Iterator<? extends Object> data, Writer output, Tag rootTag) { - Serializer serializer = new Serializer(new Emitter(output, dumperOptions), resolver, - dumperOptions, rootTag); - try { - serializer.open(); - while (data.hasNext()) { - Node node = representer.represent(data.next()); - serializer.serialize(node); - } - serializer.close(); - } catch (IOException e) { - throw new YAMLException(e); - } - } - /** - * <p> - * Serialize a Java object into a YAML string. Override the default root tag - * with <code>rootTag</code>. - * </p> - * - * <p> - * This method is similar to <code>Yaml.dump(data)</code> except that the - * root tag for the whole document is replaced with the given tag. This has - * two main uses. - * </p> - * - * <p> - * First, if the root tag is replaced with a standard YAML tag, such as - * <code>Tag.MAP</code>, then the object will be dumped as a map. The root - * tag will appear as <code>!!map</code>, or blank (implicit !!map). - * </p> - * - * <p> - * Second, if the root tag is replaced by a different custom tag, then the - * document appears to be a different type when loaded. For example, if an - * instance of MyClass is dumped with the tag !!YourClass, then it will be - * handled as an instance of YourClass when loaded. - * </p> - * - * @param data - * Java object to be serialized to YAML - * @param rootTag - * the tag for the whole YAML document. The tag should be Tag.MAP - * for a JavaBean to make the tag disappear (to use implicit tag - * !!map). If <code>null</code> is provided then the standard tag - * with the full class name is used. - * @param flowStyle - * flow style for the whole document. See Chapter 10. Collection - * Styles http://yaml.org/spec/1.1/#id930798. If - * <code>null</code> is provided then the flow style from - * DumperOptions is used. - * - * @return YAML String - */ - public String dumpAs(Object data, Tag rootTag, FlowStyle flowStyle) { - FlowStyle oldStyle = representer.getDefaultFlowStyle(); - if (flowStyle != null) { - representer.setDefaultFlowStyle(flowStyle); - } - List<Object> list = new ArrayList<Object>(1); - list.add(data); - StringWriter buffer = new StringWriter(); - dumpAll(list.iterator(), buffer, rootTag); - representer.setDefaultFlowStyle(oldStyle); - return buffer.toString(); - } - - /** - * <p> - * Serialize a Java object into a YAML string. Override the default root tag - * with <code>Tag.MAP</code>. - * </p> - * <p> - * This method is similar to <code>Yaml.dump(data)</code> except that the - * root tag for the whole document is replaced with <code>Tag.MAP</code> tag - * (implicit !!map). - * </p> - * <p> - * Block Mapping is used as the collection style. See 10.2.2. Block Mappings - * (http://yaml.org/spec/1.1/#id934537) - * </p> - * - * @param data - * Java object to be serialized to YAML - * @return YAML String - */ - public String dumpAsMap(Object data) { - return dumpAs(data, Tag.MAP, FlowStyle.BLOCK); + protected final Resolver resolver; + private String name; + protected BaseConstructor constructor; + protected Representer representer; + protected DumperOptions dumperOptions; + protected LoaderOptions loadingConfig; + + /** + * Create Yaml instance. + */ + public Yaml() { + this(new Constructor(), new Representer(), new DumperOptions(), new LoaderOptions(), + new Resolver()); + } + + /** + * Create Yaml instance. + * + * @param dumperOptions DumperOptions to configure outgoing objects + */ + public Yaml(DumperOptions dumperOptions) { + this(new Constructor(), new Representer(dumperOptions), dumperOptions); + } + + /** + * Create Yaml instance. + * + * @param loadingConfig LoadingConfig to control load behavior + */ + public Yaml(LoaderOptions loadingConfig) { + this(new Constructor(loadingConfig), new Representer(), new DumperOptions(), loadingConfig); + } + + /** + * Create Yaml instance. + * + * @param representer Representer to emit outgoing objects + */ + public Yaml(Representer representer) { + this(new Constructor(), representer); + } + + /** + * Create Yaml instance. + * + * @param constructor BaseConstructor to construct incoming documents + */ + public Yaml(BaseConstructor constructor) { + this(constructor, new Representer()); + } + + /** + * Create Yaml instance. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + */ + public Yaml(BaseConstructor constructor, Representer representer) { + this(constructor, representer, initDumperOptions(representer)); + } + + private static DumperOptions initDumperOptions(Representer representer) { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle(representer.getDefaultFlowStyle()); + dumperOptions.setDefaultScalarStyle(representer.getDefaultScalarStyle()); + dumperOptions + .setAllowReadOnlyProperties(representer.getPropertyUtils().isAllowReadOnlyProperties()); + dumperOptions.setTimeZone(representer.getTimeZone()); + return dumperOptions; + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them in different Threads. + * + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + */ + public Yaml(Representer representer, DumperOptions dumperOptions) { + this(new Constructor(), representer, dumperOptions, new LoaderOptions(), new Resolver()); + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions) { + this(constructor, representer, dumperOptions, new LoaderOptions(), new Resolver()); + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + * @param loadingConfig LoadingConfig to control load behavior + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, + LoaderOptions loadingConfig) { + this(constructor, representer, dumperOptions, loadingConfig, new Resolver()); + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + * @param resolver Resolver to detect implicit type + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, + Resolver resolver) { + this(constructor, representer, dumperOptions, new LoaderOptions(), resolver); + } + + /** + * Create Yaml instance. It is safe to create a few instances and use them in different Threads. + * + * @param constructor BaseConstructor to construct incoming documents + * @param representer Representer to emit outgoing objects + * @param dumperOptions DumperOptions to configure outgoing objects + * @param loadingConfig LoadingConfig to control load behavior + * @param resolver Resolver to detect implicit type + */ + public Yaml(BaseConstructor constructor, Representer representer, DumperOptions dumperOptions, + LoaderOptions loadingConfig, Resolver resolver) { + if (!constructor.isExplicitPropertyUtils()) { + constructor.setPropertyUtils(representer.getPropertyUtils()); + } else if (!representer.isExplicitPropertyUtils()) { + representer.setPropertyUtils(constructor.getPropertyUtils()); + } + this.constructor = constructor; + this.constructor.setAllowDuplicateKeys(loadingConfig.isAllowDuplicateKeys()); + this.constructor.setWrappedToRootException(loadingConfig.isWrappedToRootException()); + if (!dumperOptions.getIndentWithIndicator() + && dumperOptions.getIndent() <= dumperOptions.getIndicatorIndent()) { + throw new YAMLException("Indicator indent must be smaller then indent."); + } + representer.setDefaultFlowStyle(dumperOptions.getDefaultFlowStyle()); + representer.setDefaultScalarStyle(dumperOptions.getDefaultScalarStyle()); + representer.getPropertyUtils() + .setAllowReadOnlyProperties(dumperOptions.isAllowReadOnlyProperties()); + representer.setTimeZone(dumperOptions.getTimeZone()); + this.representer = representer; + this.dumperOptions = dumperOptions; + this.loadingConfig = loadingConfig; + this.resolver = resolver; + this.name = "Yaml:" + System.identityHashCode(this); + } + + /** + * Serialize a Java object into a YAML String. + * + * @param data Java object to be Serialized to YAML + * @return YAML String + */ + public String dump(Object data) { + List<Object> list = new ArrayList<Object>(1); + list.add(data); + return dumpAll(list.iterator()); + } + + /** + * Produce the corresponding representation tree for a given Object. + * + * @param data instance to build the representation tree for + * @return representation tree + * @see <a href="http://yaml.org/spec/1.1/#id859333">Figure 3.1. Processing Overview</a> + */ + public Node represent(Object data) { + return representer.represent(data); + } + + /** + * Serialize a sequence of Java objects into a YAML String. + * + * @param data Iterator with Objects + * @return YAML String with all the objects in proper sequence + */ + public String dumpAll(Iterator<? extends Object> data) { + StringWriter buffer = new StringWriter(); + dumpAll(data, buffer, null); + return buffer.toString(); + } + + /** + * Serialize a Java object into a YAML stream. + * + * @param data Java object to be serialized to YAML + * @param output stream to write to + */ + public void dump(Object data, Writer output) { + List<Object> list = new ArrayList<Object>(1); + list.add(data); + dumpAll(list.iterator(), output, null); + } + + /** + * Serialize a sequence of Java objects into a YAML stream. + * + * @param data Iterator with Objects + * @param output stream to write to + */ + public void dumpAll(Iterator<? extends Object> data, Writer output) { + dumpAll(data, output, null); + } + + private void dumpAll(Iterator<? extends Object> data, Writer output, Tag rootTag) { + Serializer serializer = + new Serializer(new Emitter(output, dumperOptions), resolver, dumperOptions, rootTag); + try { + serializer.open(); + while (data.hasNext()) { + Node node = representer.represent(data.next()); + serializer.serialize(node); + } + serializer.close(); + } catch (IOException e) { + throw new YAMLException(e); + } + } + + /** + * <p> + * Serialize a Java object into a YAML string. Override the default root tag with + * <code>rootTag</code>. + * </p> + * + * <p> + * This method is similar to <code>Yaml.dump(data)</code> except that the root tag for the whole + * document is replaced with the given tag. This has two main uses. + * </p> + * + * <p> + * First, if the root tag is replaced with a standard YAML tag, such as <code>Tag.MAP</code>, then + * the object will be dumped as a map. The root tag will appear as <code>!!map</code>, or blank + * (implicit !!map). + * </p> + * + * <p> + * Second, if the root tag is replaced by a different custom tag, then the document appears to be + * a different type when loaded. For example, if an instance of MyClass is dumped with the tag + * !!YourClass, then it will be handled as an instance of YourClass when loaded. + * </p> + * + * @param data Java object to be serialized to YAML + * @param rootTag the tag for the whole YAML document. The tag should be Tag.MAP for a JavaBean to + * make the tag disappear (to use implicit tag !!map). If <code>null</code> is provided + * then the standard tag with the full class name is used. + * @param flowStyle flow style for the whole document. See Chapter 10. Collection Styles + * http://yaml.org/spec/1.1/#id930798. If <code>null</code> is provided then the flow style + * from DumperOptions is used. + * @return YAML String + */ + public String dumpAs(Object data, Tag rootTag, FlowStyle flowStyle) { + FlowStyle oldStyle = representer.getDefaultFlowStyle(); + if (flowStyle != null) { + representer.setDefaultFlowStyle(flowStyle); + } + List<Object> list = new ArrayList<Object>(1); + list.add(data); + StringWriter buffer = new StringWriter(); + dumpAll(list.iterator(), buffer, rootTag); + representer.setDefaultFlowStyle(oldStyle); + return buffer.toString(); + } + + /** + * <p> + * Serialize a Java object into a YAML string. Override the default root tag with + * <code>Tag.MAP</code>. + * </p> + * <p> + * This method is similar to <code>Yaml.dump(data)</code> except that the root tag for the whole + * document is replaced with <code>Tag.MAP</code> tag (implicit !!map). + * </p> + * <p> + * Block Mapping is used as the collection style. See 10.2.2. Block Mappings + * (http://yaml.org/spec/1.1/#id934537) + * </p> + * + * @param data Java object to be serialized to YAML + * @return YAML String + */ + public String dumpAsMap(Object data) { + return dumpAs(data, Tag.MAP, FlowStyle.BLOCK); + } + + /** + * Serialize (dump) a YAML node into a YAML stream. + * + * @param node YAML node to be serialized to YAML + * @param output stream to write to + */ + public void serialize(Node node, Writer output) { + Serializer serializer = + new Serializer(new Emitter(output, dumperOptions), resolver, dumperOptions, null); + try { + serializer.open(); + serializer.serialize(node); + serializer.close(); + } catch (IOException e) { + throw new YAMLException(e); + } + } + + /** + * Serialize the representation tree into Events. + * + * @param data representation tree + * @return Event list + * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> + */ + public List<Event> serialize(Node data) { + SilentEmitter emitter = new SilentEmitter(); + Serializer serializer = new Serializer(emitter, resolver, dumperOptions, null); + try { + serializer.open(); + serializer.serialize(data); + serializer.close(); + } catch (IOException e) { + throw new YAMLException(e); + } + return emitter.getEvents(); + } + + private static class SilentEmitter implements Emitable { + + private final List<Event> events = new ArrayList<Event>(100); + + public List<Event> getEvents() { + return events; } - /** - * Serialize the representation tree into Events. - * - * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> - * @param data - * representation tree - * @return Event list - */ - public List<Event> serialize(Node data) { - SilentEmitter emitter = new SilentEmitter(); - Serializer serializer = new Serializer(emitter, resolver, dumperOptions, null); - try { - serializer.open(); - serializer.serialize(data); - serializer.close(); - } catch (IOException e) { - throw new YAMLException(e); - } - return emitter.getEvents(); + @Override + public void emit(Event event) throws IOException { + events.add(event); + } + } + + /** + * Parse the only YAML document in a String and produce the corresponding Java object. (Because + * the encoding in known BOM is not respected.) + * + * @param yaml YAML data to load from (BOM must not be present) + * @param <T> the class of the instance to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T load(String yaml) { + return (T) loadFromReader(new StreamReader(yaml), Object.class); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding Java object. + * + * @param io data to load from (BOM is respected to detect encoding and removed from the data) + * @param <T> the class of the instance to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T load(InputStream io) { + return (T) loadFromReader(new StreamReader(new UnicodeReader(io)), Object.class); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding Java object. + * + * @param io data to load from (BOM must not be present) + * @param <T> the class of the instance to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T load(Reader io) { + return (T) loadFromReader(new StreamReader(io), Object.class); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding Java object. + * + * @param <T> Class is defined by the second argument + * @param io data to load from (BOM must not be present) + * @param type Class of the object to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T loadAs(Reader io, Class<T> type) { + return (T) loadFromReader(new StreamReader(io), type); + } + + /** + * Parse the only YAML document in a String and produce the corresponding Java object. (Because + * the encoding in known BOM is not respected.) + * + * @param <T> Class is defined by the second argument + * @param yaml YAML data to load from (BOM must not be present) + * @param type Class of the object to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T loadAs(String yaml, Class<T> type) { + return (T) loadFromReader(new StreamReader(yaml), type); + } + + /** + * Parse the only YAML document in a stream and produce the corresponding Java object. + * + * @param <T> Class is defined by the second argument + * @param input data to load from (BOM is respected to detect encoding and removed from the data) + * @param type Class of the object to be created + * @return parsed object + */ + @SuppressWarnings("unchecked") + public <T> T loadAs(InputStream input, Class<T> type) { + return (T) loadFromReader(new StreamReader(new UnicodeReader(input)), type); + } + + private Object loadFromReader(StreamReader sreader, Class<?> type) { + Composer composer = + new Composer(new ParserImpl(sreader, loadingConfig), resolver, loadingConfig); + constructor.setComposer(composer); + return constructor.getSingleData(type); + } + + /** + * Parse all YAML documents in the Reader and produce corresponding Java objects. The documents + * are parsed only when the iterator is invoked. + * + * @param yaml YAML data to load from (BOM must not be present) + * @return an Iterable over the parsed Java objects in this String in proper sequence + */ + public Iterable<Object> loadAll(Reader yaml) { + Composer composer = + new Composer(new ParserImpl(new StreamReader(yaml), loadingConfig.isProcessComments()), + resolver, loadingConfig); + constructor.setComposer(composer); + Iterator<Object> result = new Iterator<Object>() { + @Override + public boolean hasNext() { + return constructor.checkData(); + } + + @Override + public Object next() { + return constructor.getData(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + return new YamlIterable(result); + } + + private static class YamlIterable implements Iterable<Object> { + + private final Iterator<Object> iterator; + + public YamlIterable(Iterator<Object> iterator) { + this.iterator = iterator; } - private static class SilentEmitter implements Emitable { - private List<Event> events = new ArrayList<Event>(100); - - public List<Event> getEvents() { - return events; - } - - public void emit(Event event) throws IOException { - events.add(event); + @Override + public Iterator<Object> iterator() { + return iterator; + } + } + + /** + * Parse all YAML documents in a String and produce corresponding Java objects. (Because the + * encoding in known BOM is not respected.) The documents are parsed only when the iterator is + * invoked. + * + * @param yaml YAML data to load from (BOM must not be present) + * @return an Iterable over the parsed Java objects in this String in proper sequence + */ + public Iterable<Object> loadAll(String yaml) { + return loadAll(new StringReader(yaml)); + } + + /** + * Parse all YAML documents in a stream and produce corresponding Java objects. The documents are + * parsed only when the iterator is invoked. + * + * @param yaml YAML data to load from (BOM is respected to detect encoding and removed from the + * data) + * @return an Iterable over the parsed Java objects in this stream in proper sequence + */ + public Iterable<Object> loadAll(InputStream yaml) { + return loadAll(new UnicodeReader(yaml)); + } + + /** + * Parse the first YAML document in a stream and produce the corresponding representation tree. + * (This is the opposite of the represent() method) + * + * @param yaml YAML document + * @return parsed root Node for the specified YAML document + * @see <a href="http://yaml.org/spec/1.1/#id859333">Figure 3.1. Processing Overview</a> + */ + public Node compose(Reader yaml) { + Composer composer = + new Composer(new ParserImpl(new StreamReader(yaml), loadingConfig.isProcessComments()), + resolver, loadingConfig); + return composer.getSingleNode(); + } + + /** + * Parse all YAML documents in a stream and produce corresponding representation trees. + * + * @param yaml stream of YAML documents + * @return parsed root Nodes for all the specified YAML documents + * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> + */ + public Iterable<Node> composeAll(Reader yaml) { + final Composer composer = + new Composer(new ParserImpl(new StreamReader(yaml), loadingConfig.isProcessComments()), + resolver, loadingConfig); + Iterator<Node> result = new Iterator<Node>() { + @Override + public boolean hasNext() { + return composer.checkNode(); + } + + @Override + public Node next() { + Node node = composer.getNode(); + if (node != null) { + return node; + } else { + throw new NoSuchElementException("No Node is available."); } - } + } - /** - * Parse the only YAML document in a String and produce the corresponding - * Java object. (Because the encoding in known BOM is not respected.) - * - * @param yaml - * YAML data to load from (BOM must not be present) - * @return parsed object - */ - public Object load(String yaml) { - return loadFromReader(new StreamReader(yaml), Object.class); - } - - /** - * Parse the only YAML document in a stream and produce the corresponding - * Java object. - * - * @param io - * data to load from (BOM is respected and removed) - * @return parsed object - */ - public Object load(InputStream io) { - return loadFromReader(new StreamReader(new UnicodeReader(io)), Object.class); - } - - /** - * Parse the only YAML document in a stream and produce the corresponding - * Java object. - * - * @param io - * data to load from (BOM must not be present) - * @return parsed object - */ - public Object load(Reader io) { - return loadFromReader(new StreamReader(io), Object.class); - } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + return new NodeIterable(result); + } - /** - * Parse the only YAML document in a stream and produce the corresponding - * Java object. - * - * @param <T> - * Class is defined by the second argument - * @param io - * data to load from (BOM must not be present) - * @param type - * Class of the object to be created - * @return parsed object - */ - @SuppressWarnings("unchecked") - public <T> T loadAs(Reader io, Class<T> type) { - return (T) loadFromReader(new StreamReader(io), type); - } - - /** - * Parse the only YAML document in a String and produce the corresponding - * Java object. (Because the encoding in known BOM is not respected.) - * - * @param <T> - * Class is defined by the second argument - * @param yaml - * YAML data to load from (BOM must not be present) - * @param type - * Class of the object to be created - * @return parsed object - */ - @SuppressWarnings("unchecked") - public <T> T loadAs(String yaml, Class<T> type) { - return (T) loadFromReader(new StreamReader(yaml), type); - } - - /** - * Parse the only YAML document in a stream and produce the corresponding - * Java object. - * - * @param <T> - * Class is defined by the second argument - * @param input - * data to load from (BOM is respected and removed) - * @param type - * Class of the object to be created - * @return parsed object - */ - @SuppressWarnings("unchecked") - public <T> T loadAs(InputStream input, Class<T> type) { - return (T) loadFromReader(new StreamReader(new UnicodeReader(input)), type); - } + private static class NodeIterable implements Iterable<Node> { - private Object loadFromReader(StreamReader sreader, Class<?> type) { - Composer composer = new Composer(new ParserImpl(sreader), resolver); - constructor.setComposer(composer); - return constructor.getSingleData(type); - } + private final Iterator<Node> iterator; - /** - * Parse all YAML documents in a String and produce corresponding Java - * objects. The documents are parsed only when the iterator is invoked. - * - * @param yaml - * YAML data to load from (BOM must not be present) - * @return an iterator over the parsed Java objects in this String in proper - * sequence - */ - public Iterable<Object> loadAll(Reader yaml) { - Composer composer = new Composer(new ParserImpl(new StreamReader(yaml)), resolver); - constructor.setComposer(composer); - Iterator<Object> result = new Iterator<Object>() { - public boolean hasNext() { - return constructor.checkData(); - } - - public Object next() { - return constructor.getData(); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - return new YamlIterable(result); + public NodeIterable(Iterator<Node> iterator) { + this.iterator = iterator; } - private static class YamlIterable implements Iterable<Object> { - private Iterator<Object> iterator; - - public YamlIterable(Iterator<Object> iterator) { - this.iterator = iterator; - } - - public Iterator<Object> iterator() { - return iterator; + @Override + public Iterator<Node> iterator() { + return iterator; + } + } + + /** + * Add an implicit scalar detector. If an implicit scalar value matches the given regexp, the + * corresponding tag is assigned to the scalar. + * + * @param tag tag to assign to the node + * @param regexp regular expression to match against + * @param first a sequence of possible initial characters or null (which means any). + */ + public void addImplicitResolver(Tag tag, Pattern regexp, String first) { + resolver.addImplicitResolver(tag, regexp, first); + } + + /** + * Add an implicit scalar detector. If an implicit scalar value matches the given regexp, the + * corresponding tag is assigned to the scalar. + * + * @param tag tag to assign to the node + * @param regexp regular expression to match against + * @param first a sequence of possible initial characters or null (which means any). + * @param limit the max length of the value which may match the regular expression + */ + public void addImplicitResolver(Tag tag, Pattern regexp, String first, int limit) { + resolver.addImplicitResolver(tag, regexp, first, limit); + } + + @Override + public String toString() { + return name; + } + + /** + * Get a meaningful name. It simplifies debugging in a multi-threaded environment. If nothing is + * set explicitly the address of the instance is returned. + * + * @return human readable name + */ + public String getName() { + return name; + } + + /** + * Set a meaningful name to be shown in toString() + * + * @param name human readable name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Parse a YAML stream and produce parsing events. + * + * @param yaml YAML document(s) + * @return parsed events + * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> + */ + public Iterable<Event> parse(Reader yaml) { + final Parser parser = new ParserImpl(new StreamReader(yaml), loadingConfig.isProcessComments()); + Iterator<Event> result = new Iterator<Event>() { + @Override + public boolean hasNext() { + return parser.peekEvent() != null; + } + + @Override + public Event next() { + Event event = parser.getEvent(); + if (event != null) { + return event; + } else { + throw new NoSuchElementException("No Event is available."); } - } - - /** - * Parse all YAML documents in a String and produce corresponding Java - * objects. (Because the encoding in known BOM is not respected.) The - * documents are parsed only when the iterator is invoked. - * - * @param yaml - * YAML data to load from (BOM must not be present) - * @return an iterator over the parsed Java objects in this String in proper - * sequence - */ - public Iterable<Object> loadAll(String yaml) { - return loadAll(new StringReader(yaml)); - } - - /** - * Parse all YAML documents in a stream and produce corresponding Java - * objects. The documents are parsed only when the iterator is invoked. - * - * @param yaml - * YAML data to load from (BOM is respected and ignored) - * @return an iterator over the parsed Java objects in this stream in proper - * sequence - */ - public Iterable<Object> loadAll(InputStream yaml) { - return loadAll(new UnicodeReader(yaml)); - } + } - /** - * Parse the first YAML document in a stream and produce the corresponding - * representation tree. (This is the opposite of the represent() method) - * - * @see <a href="http://yaml.org/spec/1.1/#id859333">Figure 3.1. Processing - * Overview</a> - * @param yaml - * YAML document - * @return parsed root Node for the specified YAML document - */ - public Node compose(Reader yaml) { - Composer composer = new Composer(new ParserImpl(new StreamReader(yaml)), resolver); - constructor.setComposer(composer); - return composer.getSingleNode(); - } - - /** - * Parse all YAML documents in a stream and produce corresponding - * representation trees. - * - * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> - * @param yaml - * stream of YAML documents - * @return parsed root Nodes for all the specified YAML documents - */ - public Iterable<Node> composeAll(Reader yaml) { - final Composer composer = new Composer(new ParserImpl(new StreamReader(yaml)), resolver); - constructor.setComposer(composer); - Iterator<Node> result = new Iterator<Node>() { - public boolean hasNext() { - return composer.checkNode(); - } - - public Node next() { - return composer.getNode(); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - return new NodeIterable(result); - } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + return new EventIterable(result); + } - private static class NodeIterable implements Iterable<Node> { - private Iterator<Node> iterator; - - public NodeIterable(Iterator<Node> iterator) { - this.iterator = iterator; - } + private static class EventIterable implements Iterable<Event> { - public Iterator<Node> iterator() { - return iterator; - } - } + private final Iterator<Event> iterator; - /** - * Add an implicit scalar detector. If an implicit scalar value matches the - * given regexp, the corresponding tag is assigned to the scalar. - * - * @param tag - * tag to assign to the node - * @param regexp - * regular expression to match against - * @param first - * a sequence of possible initial characters or null (which means - * any). - */ - public void addImplicitResolver(Tag tag, Pattern regexp, String first) { - resolver.addImplicitResolver(tag, regexp, first); + public EventIterable(Iterator<Event> iterator) { + this.iterator = iterator; } @Override - public String toString() { - return name; - } - - /** - * Get a meaningful name. It simplifies debugging in a multi-threaded - * environment. If nothing is set explicitly the address of the instance is - * returned. - * - * @return human readable name - */ - public String getName() { - return name; - } - - /** - * Set a meaningful name to be shown in toString() - * - * @param name - * human readable name - */ - public void setName(String name) { - this.name = name; - } - - /** - * Parse a YAML stream and produce parsing events. - * - * @see <a href="http://yaml.org/spec/1.1/#id859333">Processing Overview</a> - * @param yaml - * YAML document(s) - * @return parsed events - */ - public Iterable<Event> parse(Reader yaml) { - final Parser parser = new ParserImpl(new StreamReader(yaml)); - Iterator<Event> result = new Iterator<Event>() { - public boolean hasNext() { - return parser.peekEvent() != null; - } - - public Event next() { - return parser.getEvent(); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - return new EventIterable(result); - } - - private static class EventIterable implements Iterable<Event> { - private Iterator<Event> iterator; - - public EventIterable(Iterator<Event> iterator) { - this.iterator = iterator; - } - - public Iterator<Event> iterator() { - return iterator; - } + public Iterator<Event> iterator() { + return iterator; } + } - public void setBeanAccess(BeanAccess beanAccess) { - constructor.getPropertyUtils().setBeanAccess(beanAccess); - representer.getPropertyUtils().setBeanAccess(beanAccess); - } + public void setBeanAccess(BeanAccess beanAccess) { + constructor.getPropertyUtils().setBeanAccess(beanAccess); + representer.getPropertyUtils().setBeanAccess(beanAccess); + } + public void addTypeDescription(TypeDescription td) { + constructor.addTypeDescription(td); + representer.addTypeDescription(td); + } } diff --git a/src/main/java/org/yaml/snakeyaml/comments/CommentEventsCollector.java b/src/main/java/org/yaml/snakeyaml/comments/CommentEventsCollector.java new file mode 100644 index 00000000..f1994217 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/comments/CommentEventsCollector.java @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.comments; + +import java.util.AbstractQueue; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import org.yaml.snakeyaml.events.CommentEvent; +import org.yaml.snakeyaml.events.Event; +import org.yaml.snakeyaml.parser.Parser; + +/** + * Used by the Composer and Emitter to collect comment events so that they can be used at a later + * point in the process. + */ +public class CommentEventsCollector { + + private List<CommentLine> commentLineList; + private final Queue<Event> eventSource; + private final CommentType[] expectedCommentTypes; + + /** + * Constructor used to collect comment events emitted by a Parser. + * + * @param parser the event source. + * @param expectedCommentTypes the comment types expected. Any comment types not included are not + * collected. + */ + public CommentEventsCollector(final Parser parser, CommentType... expectedCommentTypes) { + this.eventSource = new AbstractQueue<Event>() { + + @Override + public boolean offer(Event e) { + throw new UnsupportedOperationException(); + } + + @Override + public Event poll() { + return parser.getEvent(); + } + + @Override + public Event peek() { + return parser.peekEvent(); + } + + @Override + public Iterator<Event> iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + }; + this.expectedCommentTypes = expectedCommentTypes; + commentLineList = new ArrayList<>(); + } + + /** + * Constructor used to collect events emitted by the Serializer. + * + * @param eventSource the event source. + * + * @param expectedCommentTypes the comment types expected. Any comment types not included are not + * collected. + */ + public CommentEventsCollector(Queue<Event> eventSource, CommentType... expectedCommentTypes) { + this.eventSource = eventSource; + this.expectedCommentTypes = expectedCommentTypes; + commentLineList = new ArrayList<>(); + } + + /** + * Determine if the event is a comment of one of the expected types set during construction. + * + * @param event the event to test. + * @return <code>true</code> if the events is a comment of the expected type; Otherwise, false. + */ + private boolean isEventExpected(Event event) { + if (event == null || !event.is(Event.ID.Comment)) { + return false; + } + CommentEvent commentEvent = (CommentEvent) event; + for (CommentType type : expectedCommentTypes) { + if (commentEvent.getCommentType() == type) { + return true; + } + } + return false; + } + + /** + * Collect all events of the expected type (set during construction) starting with the top event + * on the event source. Collection stops as soon as a non comment or comment of the unexpected + * type is encountered. + * + * @return this object. + */ + public CommentEventsCollector collectEvents() { + collectEvents(null); + return this; + } + + /** + * Collect all events of the expected type (set during construction) starting with event provided + * as an argument and continuing with the top event on the event source. Collection stops as soon + * as a non comment or comment of the unexpected type is encountered. + * + * @param event the first event to attempt to collect. + * @return the event provided as an argument, if it is not collected; Otherwise, <code>null</code> + */ + public Event collectEvents(Event event) { + if (event != null) { + if (isEventExpected(event)) { + commentLineList.add(new CommentLine((CommentEvent) event)); + } else { + return event; + } + } + while (isEventExpected(eventSource.peek())) { + commentLineList.add(new CommentLine((CommentEvent) eventSource.poll())); + } + return null; + } + + /** + * Collect all events of the expected type (set during construction) starting with event provided + * as an argument and continuing with the top event on the event source. Collection stops as soon + * as a non comment or comment of the unexpected type is encountered. + * + * @param event the first event to attempt to collect. + * @return the event provided as an argument, if it is not collected; Otherwise, the first event + * that is not collected. + */ + public Event collectEventsAndPoll(Event event) { + Event nextEvent = collectEvents(event); + return nextEvent != null ? nextEvent : eventSource.poll(); + } + + /** + * Return the events collected and reset the colletor. + * + * @return the events collected. + */ + public List<CommentLine> consume() { + try { + return commentLineList; + } finally { + commentLineList = new ArrayList<>(); + } + } + + /** + * Test if the collector contains any collected events. + * + * @return <code>true</code> if it does; Otherwise, <code>false</code> + */ + public boolean isEmpty() { + return commentLineList.isEmpty(); + } +} diff --git a/src/main/java/org/yaml/snakeyaml/comments/CommentLine.java b/src/main/java/org/yaml/snakeyaml/comments/CommentLine.java new file mode 100644 index 00000000..76ecd1b4 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/comments/CommentLine.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.comments; + +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.events.CommentEvent; + +/** + * A comment line. May be a block comment, blank line, or inline comment. + */ +public class CommentLine { + + private final Mark startMark; + private final Mark endMark; + private final String value; + private final CommentType commentType; + + public CommentLine(CommentEvent event) { + this(event.getStartMark(), event.getEndMark(), event.getValue(), event.getCommentType()); + } + + public CommentLine(Mark startMark, Mark endMark, String value, CommentType commentType) { + this.startMark = startMark; + this.endMark = endMark; + this.value = value; + this.commentType = commentType; + } + + public Mark getEndMark() { + return endMark; + } + + public Mark getStartMark() { + return startMark; + } + + public CommentType getCommentType() { + return commentType; + } + + /** + * Value of this comment. + * + * @return comment's value. + */ + public String getValue() { + return value; + } + + public String toString() { + return "<" + this.getClass().getName() + " (type=" + getCommentType() + ", value=" + getValue() + + ")>"; + } +} diff --git a/src/main/java/org/yaml/snakeyaml/comments/CommentType.java b/src/main/java/org/yaml/snakeyaml/comments/CommentType.java new file mode 100644 index 00000000..5ee7cf47 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/comments/CommentType.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.comments; + +/** + * The type of a comment line. + */ +public enum CommentType { + BLANK_LINE, // + BLOCK, // + IN_LINE // +} diff --git a/src/main/java/org/yaml/snakeyaml/composer/Composer.java b/src/main/java/org/yaml/snakeyaml/composer/Composer.java index f8223c29..d89aa234 100644 --- a/src/main/java/org/yaml/snakeyaml/composer/Composer.java +++ b/src/main/java/org/yaml/snakeyaml/composer/Composer.java @@ -1,27 +1,32 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.composer; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; - +import org.yaml.snakeyaml.DumperOptions.FlowStyle; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.comments.CommentEventsCollector; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.events.AliasEvent; import org.yaml.snakeyaml.events.Event; import org.yaml.snakeyaml.events.MappingStartEvent; @@ -41,208 +46,299 @@ import org.yaml.snakeyaml.resolver.Resolver; /** * Creates a node graph from parser events. * <p> - * Corresponds to the 'Compose' step as described in chapter 3.1 of the <a - * href="http://yaml.org/spec/1.1/">YAML Specification</a>. + * Corresponds to the 'Compose' step as described in chapter 3.1 of the + * <a href="http://yaml.org/spec/1.1/">YAML Specification</a>. * </p> */ public class Composer { - protected final Parser parser; - private final Resolver resolver; - private final Map<String, Node> anchors; - private final Set<Node> recursiveNodes; - - public Composer(Parser parser, Resolver resolver) { - this.parser = parser; - this.resolver = resolver; - this.anchors = new HashMap<String, Node>(); - this.recursiveNodes = new HashSet<Node>(); - } - - /** - * Checks if further documents are available. - * - * @return <code>true</code> if there is at least one more document. - */ - public boolean checkNode() { - // Drop the STREAM-START event. - if (parser.checkEvent(Event.ID.StreamStart)) { - parser.getEvent(); - } - // If there are more documents available? - return !parser.checkEvent(Event.ID.StreamEnd); - } - - /** - * Reads and composes the next document. - * - * @return The root node of the document or <code>null</code> if no more - * documents are available. - */ - public Node getNode() { - // Get the root node of the next document. - if (!parser.checkEvent(Event.ID.StreamEnd)) { - return composeDocument(); - } else { - return null; - } + + protected final Parser parser; + private final Resolver resolver; + private final Map<String, Node> anchors; + private final Set<Node> recursiveNodes; + private int nonScalarAliasesCount = 0; + private final LoaderOptions loadingConfig; + private final CommentEventsCollector blockCommentsCollector; + private final CommentEventsCollector inlineCommentsCollector; + // keep the nesting of collections inside other collections + private int nestingDepth = 0; + private final int nestingDepthLimit; + + public Composer(Parser parser, Resolver resolver) { + this(parser, resolver, new LoaderOptions()); + } + + public Composer(Parser parser, Resolver resolver, LoaderOptions loadingConfig) { + this.parser = parser; + this.resolver = resolver; + this.anchors = new HashMap<String, Node>(); + this.recursiveNodes = new HashSet<Node>(); + this.loadingConfig = loadingConfig; + this.blockCommentsCollector = + new CommentEventsCollector(parser, CommentType.BLANK_LINE, CommentType.BLOCK); + this.inlineCommentsCollector = new CommentEventsCollector(parser, CommentType.IN_LINE); + nestingDepthLimit = loadingConfig.getNestingDepthLimit(); + } + + /** + * Checks if further documents are available. + * + * @return <code>true</code> if there is at least one more document. + */ + public boolean checkNode() { + // Drop the STREAM-START event. + if (parser.checkEvent(Event.ID.StreamStart)) { + parser.getEvent(); } + // If there are more documents available? + return !parser.checkEvent(Event.ID.StreamEnd); + } - /** - * Reads a document from a source that contains only one document. - * <p> - * If the stream contains more than one document an exception is thrown. - * </p> - * - * @return The root node of the document or <code>null</code> if no document - * is available. - */ - public Node getSingleNode() { - // Drop the STREAM-START event. - parser.getEvent(); - // Compose a document if the stream is not empty. - Node document = null; - if (!parser.checkEvent(Event.ID.StreamEnd)) { - document = composeDocument(); - } - // Ensure that the stream contains no more documents. - if (!parser.checkEvent(Event.ID.StreamEnd)) { - Event event = parser.getEvent(); - throw new ComposerException("expected a single document in the stream", - document.getStartMark(), "but found another document", event.getStartMark()); - } - // Drop the STREAM-END event. - parser.getEvent(); - return document; - } - - private Node composeDocument() { - // Drop the DOCUMENT-START event. - parser.getEvent(); - // Compose the root node. - Node node = composeNode(null); - // Drop the DOCUMENT-END event. - parser.getEvent(); - this.anchors.clear(); - recursiveNodes.clear(); - return node; - } - - private Node composeNode(Node parent) { - recursiveNodes.add(parent); - if (parser.checkEvent(Event.ID.Alias)) { - AliasEvent event = (AliasEvent) parser.getEvent(); - String anchor = event.getAnchor(); - if (!anchors.containsKey(anchor)) { - throw new ComposerException(null, null, "found undefined alias " + anchor, - event.getStartMark()); - } - Node result = anchors.get(anchor); - if (recursiveNodes.remove(result)) { - result.setTwoStepsConstruction(true); - } - return result; - } - NodeEvent event = (NodeEvent) parser.peekEvent(); - String anchor = null; - anchor = event.getAnchor(); - // the check for duplicate anchors has been removed (issue 174) - Node node = null; - if (parser.checkEvent(Event.ID.Scalar)) { - node = composeScalarNode(anchor); - } else if (parser.checkEvent(Event.ID.SequenceStart)) { - node = composeSequenceNode(anchor); - } else { - node = composeMappingNode(anchor); - } - recursiveNodes.remove(parent); - return node; - } - - protected Node composeScalarNode(String anchor) { - ScalarEvent ev = (ScalarEvent) parser.getEvent(); - String tag = ev.getTag(); - boolean resolved = false; - Tag nodeTag; - if (tag == null || tag.equals("!")) { - nodeTag = resolver.resolve(NodeId.scalar, ev.getValue(), ev.getImplicit() - .canOmitTagInPlainScalar()); - resolved = true; - } else { - nodeTag = new Tag(tag); - } - Node node = new ScalarNode(nodeTag, resolved, ev.getValue(), ev.getStartMark(), - ev.getEndMark(), ev.getStyle()); - if (anchor != null) { - anchors.put(anchor, node); - } - return node; - } - - protected Node composeSequenceNode(String anchor) { - SequenceStartEvent startEvent = (SequenceStartEvent) parser.getEvent(); - String tag = startEvent.getTag(); - Tag nodeTag; - boolean resolved = false; - if (tag == null || tag.equals("!")) { - nodeTag = resolver.resolve(NodeId.sequence, null, startEvent.getImplicit()); - resolved = true; - } else { - nodeTag = new Tag(tag); - } - final ArrayList<Node> children = new ArrayList<Node>(); - SequenceNode node = new SequenceNode(nodeTag, resolved, children, - startEvent.getStartMark(), null, startEvent.getFlowStyle()); - if (anchor != null) { - anchors.put(anchor, node); - } - while (!parser.checkEvent(Event.ID.SequenceEnd)) { - children.add(composeNode(node)); - } - Event endEvent = parser.getEvent(); - node.setEndMark(endEvent.getEndMark()); - return node; - } - - protected Node composeMappingNode(String anchor) { - MappingStartEvent startEvent = (MappingStartEvent) parser.getEvent(); - String tag = startEvent.getTag(); - Tag nodeTag; - boolean resolved = false; - if (tag == null || tag.equals("!")) { - nodeTag = resolver.resolve(NodeId.mapping, null, startEvent.getImplicit()); - resolved = true; - } else { - nodeTag = new Tag(tag); - } + /** + * Reads and composes the next document. + * + * @return The root node of the document or <code>null</code> if no more documents are available. + */ + public Node getNode() { + // Collect inter-document start comments + blockCommentsCollector.collectEvents(); + if (parser.checkEvent(Event.ID.StreamEnd)) { + List<CommentLine> commentLines = blockCommentsCollector.consume(); + Mark startMark = commentLines.get(0).getStartMark(); + List<NodeTuple> children = Collections.emptyList(); + Node node = new MappingNode(Tag.COMMENT, false, children, startMark, null, FlowStyle.BLOCK); + node.setBlockComments(commentLines); + return node; + } + // Drop the DOCUMENT-START event. + parser.getEvent(); + // Compose the root node. + Node node = composeNode(null); + // Drop the DOCUMENT-END event. + blockCommentsCollector.collectEvents(); + if (!blockCommentsCollector.isEmpty()) { + node.setEndComments(blockCommentsCollector.consume()); + } + parser.getEvent(); + this.anchors.clear(); + this.recursiveNodes.clear(); + return node; + } - final List<NodeTuple> children = new ArrayList<NodeTuple>(); - MappingNode node = new MappingNode(nodeTag, resolved, children, startEvent.getStartMark(), - null, startEvent.getFlowStyle()); - if (anchor != null) { - anchors.put(anchor, node); - } - while (!parser.checkEvent(Event.ID.MappingEnd)) { - composeMappingChildren(children, node); - } - Event endEvent = parser.getEvent(); - node.setEndMark(endEvent.getEndMark()); - return node; + /** + * Reads a document from a source that contains only one document. + * <p> + * If the stream contains more than one document an exception is thrown. + * </p> + * + * @return The root node of the document or <code>null</code> if no document is available. + */ + public Node getSingleNode() { + // Drop the STREAM-START event. + parser.getEvent(); + // Compose a document if the stream is not empty. + Node document = null; + if (!parser.checkEvent(Event.ID.StreamEnd)) { + document = getNode(); } + // Ensure that the stream contains no more documents. + if (!parser.checkEvent(Event.ID.StreamEnd)) { + Event event = parser.getEvent(); + Mark contextMark = document != null ? document.getStartMark() : null; + throw new ComposerException("expected a single document in the stream", contextMark, + "but found another document", event.getStartMark()); + } + // Drop the STREAM-END event. + parser.getEvent(); + return document; + } - protected void composeMappingChildren(List<NodeTuple> children, MappingNode node) { - Node itemKey = composeKeyNode(node); - if (itemKey.getTag().equals(Tag.MERGE)) { - node.setMerged(true); + private Node composeNode(Node parent) { + blockCommentsCollector.collectEvents(); + if (parent != null) { + recursiveNodes.add(parent); + } + final Node node; + if (parser.checkEvent(Event.ID.Alias)) { + AliasEvent event = (AliasEvent) parser.getEvent(); + String anchor = event.getAnchor(); + if (!anchors.containsKey(anchor)) { + throw new ComposerException(null, null, "found undefined alias " + anchor, + event.getStartMark()); + } + node = anchors.get(anchor); + if (!(node instanceof ScalarNode)) { + this.nonScalarAliasesCount++; + if (this.nonScalarAliasesCount > loadingConfig.getMaxAliasesForCollections()) { + throw new YAMLException( + "Number of aliases for non-scalar nodes exceeds the specified max=" + + loadingConfig.getMaxAliasesForCollections()); } - Node itemValue = composeValueNode(node); - children.add(new NodeTuple(itemKey, itemValue)); + } + if (recursiveNodes.remove(node)) { + node.setTwoStepsConstruction(true); + } + // drop comments, they can not be supported here + blockCommentsCollector.consume(); + inlineCommentsCollector.collectEvents().consume(); + } else { + NodeEvent event = (NodeEvent) parser.peekEvent(); + String anchor = event.getAnchor(); + increaseNestingDepth(); + // the check for duplicate anchors has been removed (issue 174) + if (parser.checkEvent(Event.ID.Scalar)) { + node = composeScalarNode(anchor, blockCommentsCollector.consume()); + } else if (parser.checkEvent(Event.ID.SequenceStart)) { + node = composeSequenceNode(anchor); + } else { + node = composeMappingNode(anchor); + } + decreaseNestingDepth(); } + recursiveNodes.remove(parent); + return node; + } + + protected Node composeScalarNode(String anchor, List<CommentLine> blockComments) { + ScalarEvent ev = (ScalarEvent) parser.getEvent(); + String tag = ev.getTag(); + boolean resolved = false; + Tag nodeTag; + if (tag == null || tag.equals("!")) { + nodeTag = resolver.resolve(NodeId.scalar, ev.getValue(), + ev.getImplicit().canOmitTagInPlainScalar()); + resolved = true; + } else { + nodeTag = new Tag(tag); + } + Node node = new ScalarNode(nodeTag, resolved, ev.getValue(), ev.getStartMark(), ev.getEndMark(), + ev.getScalarStyle()); + if (anchor != null) { + node.setAnchor(anchor); + anchors.put(anchor, node); + } + node.setBlockComments(blockComments); + node.setInLineComments(inlineCommentsCollector.collectEvents().consume()); + return node; + } + + protected Node composeSequenceNode(String anchor) { + SequenceStartEvent startEvent = (SequenceStartEvent) parser.getEvent(); + String tag = startEvent.getTag(); + Tag nodeTag; + + boolean resolved = false; + if (tag == null || tag.equals("!")) { + nodeTag = resolver.resolve(NodeId.sequence, null, startEvent.getImplicit()); + resolved = true; + } else { + nodeTag = new Tag(tag); + } + final ArrayList<Node> children = new ArrayList<Node>(); + SequenceNode node = new SequenceNode(nodeTag, resolved, children, startEvent.getStartMark(), + null, startEvent.getFlowStyle()); + if (startEvent.isFlow()) { + node.setBlockComments(blockCommentsCollector.consume()); + } + if (anchor != null) { + node.setAnchor(anchor); + anchors.put(anchor, node); + } + while (!parser.checkEvent(Event.ID.SequenceEnd)) { + blockCommentsCollector.collectEvents(); + if (parser.checkEvent(Event.ID.SequenceEnd)) { + break; + } + children.add(composeNode(node)); + } + if (startEvent.isFlow()) { + node.setInLineComments(inlineCommentsCollector.collectEvents().consume()); + } + Event endEvent = parser.getEvent(); + node.setEndMark(endEvent.getEndMark()); + inlineCommentsCollector.collectEvents(); + if (!inlineCommentsCollector.isEmpty()) { + node.setInLineComments(inlineCommentsCollector.consume()); + } + return node; + } + + protected Node composeMappingNode(String anchor) { + MappingStartEvent startEvent = (MappingStartEvent) parser.getEvent(); + String tag = startEvent.getTag(); + Tag nodeTag; + boolean resolved = false; + if (tag == null || tag.equals("!")) { + nodeTag = resolver.resolve(NodeId.mapping, null, startEvent.getImplicit()); + resolved = true; + } else { + nodeTag = new Tag(tag); + } + + final List<NodeTuple> children = new ArrayList<NodeTuple>(); + MappingNode node = new MappingNode(nodeTag, resolved, children, startEvent.getStartMark(), null, + startEvent.getFlowStyle()); + if (startEvent.isFlow()) { + node.setBlockComments(blockCommentsCollector.consume()); + } + if (anchor != null) { + node.setAnchor(anchor); + anchors.put(anchor, node); + } + while (!parser.checkEvent(Event.ID.MappingEnd)) { + blockCommentsCollector.collectEvents(); + if (parser.checkEvent(Event.ID.MappingEnd)) { + break; + } + composeMappingChildren(children, node); + } + if (startEvent.isFlow()) { + node.setInLineComments(inlineCommentsCollector.collectEvents().consume()); + } + Event endEvent = parser.getEvent(); + node.setEndMark(endEvent.getEndMark()); + inlineCommentsCollector.collectEvents(); + if (!inlineCommentsCollector.isEmpty()) { + node.setInLineComments(inlineCommentsCollector.consume()); + } + return node; + } + + protected void composeMappingChildren(List<NodeTuple> children, MappingNode node) { + Node itemKey = composeKeyNode(node); + if (itemKey.getTag().equals(Tag.MERGE)) { + node.setMerged(true); + } + Node itemValue = composeValueNode(node); + children.add(new NodeTuple(itemKey, itemValue)); + } + + protected Node composeKeyNode(MappingNode node) { + return composeNode(node); + } + + protected Node composeValueNode(MappingNode node) { + return composeNode(node); + } - protected Node composeKeyNode(MappingNode node) { - return composeNode(node); + /** + * Increase nesting depth and fail when it exceeds the denied limit + */ + private void increaseNestingDepth() { + if (nestingDepth > nestingDepthLimit) { + throw new YAMLException("Nesting Depth exceeded max " + nestingDepthLimit); } + nestingDepth++; + } - protected Node composeValueNode(MappingNode node) { - return composeNode(node); + /** + * Indicate that the collection is finished and the nesting is decreased + */ + private void decreaseNestingDepth() { + if (nestingDepth > 0) { + nestingDepth--; + } else { + throw new YAMLException("Nesting Depth cannot be negative"); } + } } diff --git a/src/main/java/org/yaml/snakeyaml/composer/ComposerException.java b/src/main/java/org/yaml/snakeyaml/composer/ComposerException.java index 5d20c1d4..8e6cbd7f 100644 --- a/src/main/java/org/yaml/snakeyaml/composer/ComposerException.java +++ b/src/main/java/org/yaml/snakeyaml/composer/ComposerException.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.composer; @@ -19,9 +17,10 @@ import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.MarkedYAMLException; public class ComposerException extends MarkedYAMLException { - private static final long serialVersionUID = 2146314636913113935L; - protected ComposerException(String context, Mark contextMark, String problem, Mark problemMark) { - super(context, contextMark, problem, problemMark); - } + private static final long serialVersionUID = 2146314636913113935L; + + protected ComposerException(String context, Mark contextMark, String problem, Mark problemMark) { + super(context, contextMark, problem, problemMark); + } } diff --git a/src/main/java/org/yaml/snakeyaml/constructor/AbstractConstruct.java b/src/main/java/org/yaml/snakeyaml/constructor/AbstractConstruct.java index 61d14f60..ff0e3bbd 100644 --- a/src/main/java/org/yaml/snakeyaml/constructor/AbstractConstruct.java +++ b/src/main/java/org/yaml/snakeyaml/constructor/AbstractConstruct.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.constructor; @@ -19,23 +17,22 @@ import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.nodes.Node; /** - * Because recursive structures are not very common we provide a way to save - * some typing when extending a constructor + * Because recursive structures are not very common we provide a way to save some typing when + * extending a constructor */ public abstract class AbstractConstruct implements Construct { - /** - * Fail with a reminder to provide the seconds step for a recursive - * structure - * - * @see org.yaml.snakeyaml.constructor.Construct#construct2ndStep(org.yaml.snakeyaml.nodes.Node, - * java.lang.Object) - */ - public void construct2ndStep(Node node, Object data) { - if (node.isTwoStepsConstruction()) { - throw new IllegalStateException("Not Implemented in " + getClass().getName()); - } else { - throw new YAMLException("Unexpected recursive structure for Node: " + node); - } + /** + * Fail with a reminder to provide the seconds step for a recursive structure + * + * @see org.yaml.snakeyaml.constructor.Construct#construct2ndStep(org.yaml.snakeyaml.nodes.Node, + * java.lang.Object) + */ + public void construct2ndStep(Node node, Object data) { + if (node.isTwoStepsConstruction()) { + throw new IllegalStateException("Not Implemented in " + getClass().getName()); + } else { + throw new YAMLException("Unexpected recursive structure for Node: " + node); } + } } diff --git a/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java b/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java index 3a504cbb..a4ed1f50 100644 --- a/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java +++ b/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java @@ -1,21 +1,20 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.constructor; import java.lang.reflect.Array; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; @@ -25,12 +24,19 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; - +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.TypeDescription; import org.yaml.snakeyaml.composer.Composer; import org.yaml.snakeyaml.composer.ComposerException; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.introspector.PropertyUtils; +import org.yaml.snakeyaml.nodes.CollectionNode; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeId; @@ -40,411 +46,578 @@ import org.yaml.snakeyaml.nodes.SequenceNode; import org.yaml.snakeyaml.nodes.Tag; public abstract class BaseConstructor { - /** - * It maps the node kind to the the Construct implementation. When the - * runtime class is known then the implicit tag is ignored. - */ - protected final Map<NodeId, Construct> yamlClassConstructors = new EnumMap<NodeId, Construct>( - NodeId.class); - /** - * It maps the (explicit or implicit) tag to the Construct implementation. - * It is used: <br/> - * 1) explicit tag - if present. <br/> - * 2) implicit tag - when the runtime class of the instance is unknown (the - * node has the Object.class) - */ - protected final Map<Tag, Construct> yamlConstructors = new HashMap<Tag, Construct>(); - /** - * It maps the (explicit or implicit) tag to the Construct implementation. - * It is used when no exact match found. - */ - protected final Map<String, Construct> yamlMultiConstructors = new HashMap<String, Construct>(); - - protected Composer composer; - private final Map<Node, Object> constructedObjects; - private final Set<Node> recursiveObjects; - private final ArrayList<RecursiveTuple<Map<Object, Object>, RecursiveTuple<Object, Object>>> maps2fill; - private final ArrayList<RecursiveTuple<Set<Object>, Object>> sets2fill; - - protected Tag rootTag; - private PropertyUtils propertyUtils; - private boolean explicitPropertyUtils; - - public BaseConstructor() { - constructedObjects = new HashMap<Node, Object>(); - recursiveObjects = new HashSet<Node>(); - maps2fill = new ArrayList<RecursiveTuple<Map<Object, Object>, RecursiveTuple<Object, Object>>>(); - sets2fill = new ArrayList<RecursiveTuple<Set<Object>, Object>>(); - rootTag = null; - explicitPropertyUtils = false; - } - public void setComposer(Composer composer) { - this.composer = composer; + /** + * An instance returned by newInstance methods when instantiation has not been performed. + */ + protected static final Object NOT_INSTANTIATED_OBJECT = new Object(); + + /** + * It maps the node kind to the the Construct implementation. When the runtime class is known then + * the implicit tag is ignored. + */ + protected final Map<NodeId, Construct> yamlClassConstructors = + new EnumMap<NodeId, Construct>(NodeId.class); + /** + * It maps the (explicit or implicit) tag to the Construct implementation. It is used: 1) explicit + * tag - if present. 2) implicit tag - when the runtime class of the instance is unknown (the node + * has the Object.class) + */ + protected final Map<Tag, Construct> yamlConstructors = new HashMap<Tag, Construct>(); + /** + * It maps the (explicit or implicit) tag to the Construct implementation. It is used when no + * exact match found. + */ + protected final Map<String, Construct> yamlMultiConstructors = new HashMap<String, Construct>(); + + protected Composer composer; + final Map<Node, Object> constructedObjects; + private final Set<Node> recursiveObjects; + private final ArrayList<RecursiveTuple<Map<Object, Object>, RecursiveTuple<Object, Object>>> maps2fill; + private final ArrayList<RecursiveTuple<Set<Object>, Object>> sets2fill; + + protected Tag rootTag; + private PropertyUtils propertyUtils; + private boolean explicitPropertyUtils; + private boolean allowDuplicateKeys = true; + private boolean wrappedToRootException = false; + + private boolean enumCaseSensitive = false; + + protected final Map<Class<? extends Object>, TypeDescription> typeDefinitions; + protected final Map<Tag, Class<? extends Object>> typeTags; + + protected LoaderOptions loadingConfig; + + public BaseConstructor() { + this(new LoaderOptions()); + } + + public BaseConstructor(LoaderOptions loadingConfig) { + constructedObjects = new HashMap<Node, Object>(); + recursiveObjects = new HashSet<Node>(); + maps2fill = + new ArrayList<RecursiveTuple<Map<Object, Object>, RecursiveTuple<Object, Object>>>(); + sets2fill = new ArrayList<RecursiveTuple<Set<Object>, Object>>(); + typeDefinitions = new HashMap<Class<? extends Object>, TypeDescription>(); + typeTags = new HashMap<Tag, Class<? extends Object>>(); + + rootTag = null; + explicitPropertyUtils = false; + + typeDefinitions.put(SortedMap.class, + new TypeDescription(SortedMap.class, Tag.OMAP, TreeMap.class)); + typeDefinitions.put(SortedSet.class, + new TypeDescription(SortedSet.class, Tag.SET, TreeSet.class)); + this.loadingConfig = loadingConfig; + } + + public void setComposer(Composer composer) { + this.composer = composer; + } + + /** + * Check if more documents available + * + * @return true when there are more YAML documents in the stream + */ + public boolean checkData() { + // If there are more documents available? + return composer.checkNode(); + } + + /** + * Construct and return the next document + * + * @return constructed instance + */ + public Object getData() throws NoSuchElementException { + // Construct and return the next document. + if (!composer.checkNode()) { + throw new NoSuchElementException("No document is available."); } - - /** - * Check if more documents available - * - * @return true when there are more YAML documents in the stream - */ - public boolean checkData() { - // If there are more documents available? - return composer.checkNode(); + Node node = composer.getNode(); + if (rootTag != null) { + node.setTag(rootTag); } - - /** - * Construct and return the next document - * - * @return constructed instance - */ - public Object getData() { - // Construct and return the next document. - composer.checkNode(); - Node node = composer.getNode(); - if (rootTag != null) { - node.setTag(rootTag); - } - return constructDocument(node); + return constructDocument(node); + } + + /** + * Ensure that the stream contains a single document and construct it + * + * @param type the class of the instance being created + * @return constructed instance + * @throws ComposerException in case there are more documents in the stream + */ + public Object getSingleData(Class<?> type) { + // Ensure that the stream contains a single document and construct it + final Node node = composer.getSingleNode(); + if (node != null && !Tag.NULL.equals(node.getTag())) { + if (Object.class != type) { + node.setTag(new Tag(type)); + } else if (rootTag != null) { + node.setTag(rootTag); + } + return constructDocument(node); + } else { + Construct construct = yamlConstructors.get(Tag.NULL); + return construct.construct(node); } - - /** - * Ensure that the stream contains a single document and construct it - * - * @return constructed instance - * @throws ComposerException - * in case there are more documents in the stream - */ - public Object getSingleData(Class<?> type) { - // Ensure that the stream contains a single document and construct it - Node node = composer.getSingleNode(); - if (node != null) { - if (Object.class != type) { - node.setTag(new Tag(type)); - } else if (rootTag != null) { - node.setTag(rootTag); - } - return constructDocument(node); - } - return null; + } + + /** + * Construct complete YAML document. Call the second step in case of recursive structures. At the + * end cleans all the state. + * + * @param node root Node + * @return Java instance + */ + protected final Object constructDocument(Node node) { + try { + Object data = constructObject(node); + fillRecursive(); + return data; + } catch (RuntimeException e) { + if (wrappedToRootException && !(e instanceof YAMLException)) { + throw new YAMLException(e); + } else { + throw e; + } + } finally { + // clean up resources + constructedObjects.clear(); + recursiveObjects.clear(); } - - /** - * Construct complete YAML document. Call the second step in case of - * recursive structures. At the end cleans all the state. - * - * @param node - * root Node - * @return Java instance - */ - protected final Object constructDocument(Node node) { - Object data = constructObject(node); - fillRecursive(); - constructedObjects.clear(); - recursiveObjects.clear(); - return data; + } + + /** + * Fill the recursive structures and clean the internal collections + */ + private void fillRecursive() { + if (!maps2fill.isEmpty()) { + for (RecursiveTuple<Map<Object, Object>, RecursiveTuple<Object, Object>> entry : maps2fill) { + RecursiveTuple<Object, Object> key_value = entry._2(); + entry._1().put(key_value._1(), key_value._2()); + } + maps2fill.clear(); } - - private void fillRecursive() { - if (!maps2fill.isEmpty()) { - for (RecursiveTuple<Map<Object, Object>, RecursiveTuple<Object, Object>> entry : maps2fill) { - RecursiveTuple<Object, Object> key_value = entry._2(); - entry._1().put(key_value._1(), key_value._2()); - } - maps2fill.clear(); - } - if (!sets2fill.isEmpty()) { - for (RecursiveTuple<Set<Object>, Object> value : sets2fill) { - value._1().add(value._2()); - } - sets2fill.clear(); - } + if (!sets2fill.isEmpty()) { + for (RecursiveTuple<Set<Object>, Object> value : sets2fill) { + value._1().add(value._2()); + } + sets2fill.clear(); } - - /** - * Construct object from the specified Node. Return existing instance if the - * node is already constructed. - * - * @param node - * Node to be constructed - * @return Java instance - */ - protected Object constructObject(Node node) { - if (constructedObjects.containsKey(node)) { - return constructedObjects.get(node); - } - if (recursiveObjects.contains(node)) { - throw new ConstructorException(null, null, "found unconstructable recursive node", - node.getStartMark()); - } - recursiveObjects.add(node); - Construct constructor = getConstructor(node); - Object data = constructor.construct(node); - constructedObjects.put(node, data); - recursiveObjects.remove(node); - if (node.isTwoStepsConstruction()) { - constructor.construct2ndStep(node, data); - } - return data; + } + + /** + * Construct object from the specified Node. Return existing instance if the node is already + * constructed. + * + * @param node Node to be constructed + * @return Java instance + */ + protected Object constructObject(Node node) { + if (constructedObjects.containsKey(node)) { + return constructedObjects.get(node); } + return constructObjectNoCheck(node); + } - /** - * Get the constructor to construct the Node. For implicit tags if the - * runtime class is known a dedicated Construct implementation is used. - * Otherwise the constructor is chosen by the tag. - * - * @param node - * Node to be constructed - * @return Construct implementation for the specified node - */ - protected Construct getConstructor(Node node) { - if (node.useClassConstructor()) { - return yamlClassConstructors.get(node.getNodeId()); - } else { - Construct constructor = yamlConstructors.get(node.getTag()); - if (constructor == null) { - for (String prefix : yamlMultiConstructors.keySet()) { - if (node.getTag().startsWith(prefix)) { - return yamlMultiConstructors.get(prefix); - } - } - return yamlConstructors.get(null); - } - return constructor; + protected Object constructObjectNoCheck(Node node) { + if (recursiveObjects.contains(node)) { + throw new ConstructorException(null, null, "found unconstructable recursive node", + node.getStartMark()); + } + recursiveObjects.add(node); + Construct constructor = getConstructor(node); + Object data = (constructedObjects.containsKey(node)) ? constructedObjects.get(node) + : constructor.construct(node); + + finalizeConstruction(node, data); + constructedObjects.put(node, data); + recursiveObjects.remove(node); + if (node.isTwoStepsConstruction()) { + constructor.construct2ndStep(node, data); + } + return data; + } + + /** + * Get the constructor to construct the Node. For implicit tags if the runtime class is known a + * dedicated Construct implementation is used. Otherwise the constructor is chosen by the tag. + * + * @param node {@link Node} to construct an instance from + * @return {@link Construct} implementation for the specified node + */ + protected Construct getConstructor(Node node) { + if (node.useClassConstructor()) { + return yamlClassConstructors.get(node.getNodeId()); + } else { + Construct constructor = yamlConstructors.get(node.getTag()); + if (constructor == null) { + for (String prefix : yamlMultiConstructors.keySet()) { + if (node.getTag().startsWith(prefix)) { + return yamlMultiConstructors.get(prefix); + } } + return yamlConstructors.get(null); + } + return constructor; } + } - protected Object constructScalar(ScalarNode node) { - return node.getValue(); - } + protected String constructScalar(ScalarNode node) { + return node.getValue(); + } - protected List<Object> createDefaultList(int initSize) { - return new ArrayList<Object>(initSize); - } + // >>>> DEFAULTS >>>> + protected List<Object> createDefaultList(int initSize) { + return new ArrayList<Object>(initSize); + } - protected Set<Object> createDefaultSet(int initSize) { - return new LinkedHashSet<Object>(initSize); - } + protected Set<Object> createDefaultSet(int initSize) { + return new LinkedHashSet<Object>(initSize); + } - protected Object createArray(Class<?> type, int size) { - return Array.newInstance(type.getComponentType(), size); - } + protected Map<Object, Object> createDefaultMap(int initSize) { + // respect order from YAML document + return new LinkedHashMap<Object, Object>(initSize); + } - @SuppressWarnings("unchecked") - protected List<? extends Object> constructSequence(SequenceNode node) { - List<Object> result; - if (List.class.isAssignableFrom(node.getType()) && !node.getType().isInterface()) { - // the root class may be defined (Vector for instance) - try { - result = (List<Object>) node.getType().newInstance(); - } catch (Exception e) { - throw new YAMLException(e); - } - } else { - result = createDefaultList(node.getValue().size()); - } - constructSequenceStep2(node, result); - return result; + protected Object createArray(Class<?> type, int size) { + return Array.newInstance(type.getComponentType(), size); + } - } + // <<<< DEFAULTS <<<< - @SuppressWarnings("unchecked") - protected Set<? extends Object> constructSet(SequenceNode node) { - Set<Object> result; - if (!node.getType().isInterface()) { - // the root class may be defined - try { - result = (Set<Object>) node.getType().newInstance(); - } catch (Exception e) { - throw new YAMLException(e); - } - } else { - result = createDefaultSet(node.getValue().size()); + protected Object finalizeConstruction(Node node, Object data) { + final Class<? extends Object> type = node.getType(); + if (typeDefinitions.containsKey(type)) { + return typeDefinitions.get(type).finalizeConstruction(data); + } + return data; + } + + // >>>> NEW instance + protected Object newInstance(Node node) { + return newInstance(Object.class, node); + } + + protected final Object newInstance(Class<?> ancestor, Node node) { + return newInstance(ancestor, node, true); + } + + /** + * Tries to create a new object for the node. + * + * @param ancestor expected ancestor of the {@code node.getType()} + * @param node for which to create a corresponding java object + * @param tryDefault should default constructor to be tried when there is no corresponding + * {@code TypeDescription} or {@code TypeDescription.newInstance(node)} returns + * {@code null}. + * + * @return - a new object created for {@code node.getType()} by using corresponding + * TypeDescription.newInstance or default constructor. - {@code NOT_INSTANTIATED_OBJECT} + * in case no object has been created + */ + protected Object newInstance(Class<?> ancestor, Node node, boolean tryDefault) { + try { + final Class<? extends Object> type = node.getType(); + if (typeDefinitions.containsKey(type)) { + TypeDescription td = typeDefinitions.get(type); + final Object instance = td.newInstance(node); + if (instance != null) { + return instance; } - constructSequenceStep2(node, result); - return result; - + } + + if (tryDefault) { + /* + * Removed <code> have InstantiationException in case of abstract type + */ + if (ancestor.isAssignableFrom(type) && !Modifier.isAbstract(type.getModifiers())) { + java.lang.reflect.Constructor<?> c = type.getDeclaredConstructor(); + c.setAccessible(true); + return c.newInstance(); + } + } + } catch (Exception e) { + throw new YAMLException(e); } - protected Object constructArray(SequenceNode node) { - return constructArrayStep2(node, createArray(node.getType(), node.getValue().size())); - } + return NOT_INSTANTIATED_OBJECT; + } - protected void constructSequenceStep2(SequenceNode node, Collection<Object> collection) { - for (Node child : node.getValue()) { - collection.add(constructObject(child)); - } + @SuppressWarnings("unchecked") + protected Set<Object> newSet(CollectionNode<?> node) { + Object instance = newInstance(Set.class, node); + if (instance != NOT_INSTANTIATED_OBJECT) { + return (Set<Object>) instance; + } else { + return createDefaultSet(node.getValue().size()); + } + } + + @SuppressWarnings("unchecked") + protected List<Object> newList(SequenceNode node) { + Object instance = newInstance(List.class, node); + if (instance != NOT_INSTANTIATED_OBJECT) { + return (List<Object>) instance; + } else { + return createDefaultList(node.getValue().size()); } + } + + @SuppressWarnings("unchecked") + protected Map<Object, Object> newMap(MappingNode node) { + Object instance = newInstance(Map.class, node); + if (instance != NOT_INSTANTIATED_OBJECT) { + return (Map<Object, Object>) instance; + } else { + return createDefaultMap(node.getValue().size()); + } + } + + // <<<< NEW instance + + // >>>> Construct => NEW, 2ndStep(filling) + protected List<? extends Object> constructSequence(SequenceNode node) { + List<Object> result = newList(node); + constructSequenceStep2(node, result); + return result; + } + + protected Set<? extends Object> constructSet(SequenceNode node) { + Set<Object> result = newSet(node); + constructSequenceStep2(node, result); + return result; + } + + protected Object constructArray(SequenceNode node) { + return constructArrayStep2(node, createArray(node.getType(), node.getValue().size())); + } + + protected void constructSequenceStep2(SequenceNode node, Collection<Object> collection) { + for (Node child : node.getValue()) { + collection.add(constructObject(child)); + } + } - protected Object constructArrayStep2(SequenceNode node, Object array) { - final Class<?> componentType = node.getType().getComponentType(); + protected Object constructArrayStep2(SequenceNode node, Object array) { + final Class<?> componentType = node.getType().getComponentType(); - int index = 0; - for (Node child : node.getValue()) { - // Handle multi-dimensional arrays... - if (child.getType() == Object.class) { - child.setType(componentType); - } + int index = 0; + for (Node child : node.getValue()) { + // Handle multi-dimensional arrays... + if (child.getType() == Object.class) { + child.setType(componentType); + } - final Object value = constructObject(child); + final Object value = constructObject(child); - if (componentType.isPrimitive()) { - // Null values are disallowed for primitives - if (value == null) { - throw new NullPointerException("Unable to construct element value for " + child); - } + if (componentType.isPrimitive()) { + // Null values are disallowed for primitives + if (value == null) { + throw new NullPointerException("Unable to construct element value for " + child); + } - // Primitive arrays require quite a lot of work. - if (byte.class.equals(componentType)) { - Array.setByte(array, index, ((Number) value).byteValue()); + // Primitive arrays require quite a lot of work. + if (byte.class.equals(componentType)) { + Array.setByte(array, index, ((Number) value).byteValue()); - } else if (short.class.equals(componentType)) { - Array.setShort(array, index, ((Number) value).shortValue()); + } else if (short.class.equals(componentType)) { + Array.setShort(array, index, ((Number) value).shortValue()); - } else if (int.class.equals(componentType)) { - Array.setInt(array, index, ((Number) value).intValue()); + } else if (int.class.equals(componentType)) { + Array.setInt(array, index, ((Number) value).intValue()); - } else if (long.class.equals(componentType)) { - Array.setLong(array, index, ((Number) value).longValue()); + } else if (long.class.equals(componentType)) { + Array.setLong(array, index, ((Number) value).longValue()); - } else if (float.class.equals(componentType)) { - Array.setFloat(array, index, ((Number) value).floatValue()); + } else if (float.class.equals(componentType)) { + Array.setFloat(array, index, ((Number) value).floatValue()); - } else if (double.class.equals(componentType)) { - Array.setDouble(array, index, ((Number) value).doubleValue()); + } else if (double.class.equals(componentType)) { + Array.setDouble(array, index, ((Number) value).doubleValue()); - } else if (char.class.equals(componentType)) { - Array.setChar(array, index, ((Character) value).charValue()); + } else if (char.class.equals(componentType)) { + Array.setChar(array, index, ((Character) value).charValue()); - } else if (boolean.class.equals(componentType)) { - Array.setBoolean(array, index, ((Boolean) value).booleanValue()); + } else if (boolean.class.equals(componentType)) { + Array.setBoolean(array, index, ((Boolean) value).booleanValue()); - } else { - throw new YAMLException("unexpected primitive type"); - } + } else { + throw new YAMLException("unexpected primitive type"); + } - } else { - // Non-primitive arrays can simply be assigned: - Array.set(array, index, value); - } + } else { + // Non-primitive arrays can simply be assigned: + Array.set(array, index, value); + } - ++index; + ++index; + } + return array; + } + + protected Set<Object> constructSet(MappingNode node) { + final Set<Object> set = newSet(node); + constructSet2ndStep(node, set); + return set; + } + + protected Map<Object, Object> constructMapping(MappingNode node) { + final Map<Object, Object> mapping = newMap(node); + constructMapping2ndStep(node, mapping); + return mapping; + } + + protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) { + List<NodeTuple> nodeValue = node.getValue(); + for (NodeTuple tuple : nodeValue) { + Node keyNode = tuple.getKeyNode(); + Node valueNode = tuple.getValueNode(); + Object key = constructObject(keyNode); + if (key != null) { + try { + key.hashCode();// check circular dependencies + } catch (Exception e) { + throw new ConstructorException("while constructing a mapping", node.getStartMark(), + "found unacceptable key " + key, tuple.getKeyNode().getStartMark(), e); } - return array; + } + Object value = constructObject(valueNode); + if (keyNode.isTwoStepsConstruction()) { + if (loadingConfig.getAllowRecursiveKeys()) { + postponeMapFilling(mapping, key, value); + } else { + throw new YAMLException( + "Recursive key for mapping is detected but it is not configured to be allowed."); + } + } else { + mapping.put(key, value); + } } - - protected Map<Object, Object> createDefaultMap() { - // respect order from YAML document - return new LinkedHashMap<Object, Object>(); + } + + /* + * if keyObject is created it 2 steps we should postpone putting it in map because it may have + * different hash after initialization compared to clean just created one. And map of course does + * not observe key hashCode changes. + */ + protected void postponeMapFilling(Map<Object, Object> mapping, Object key, Object value) { + maps2fill.add(0, new RecursiveTuple(mapping, new RecursiveTuple(key, value))); + } + + protected void constructSet2ndStep(MappingNode node, Set<Object> set) { + List<NodeTuple> nodeValue = node.getValue(); + for (NodeTuple tuple : nodeValue) { + Node keyNode = tuple.getKeyNode(); + Object key = constructObject(keyNode); + if (key != null) { + try { + key.hashCode();// check circular dependencies + } catch (Exception e) { + throw new ConstructorException("while constructing a Set", node.getStartMark(), + "found unacceptable key " + key, tuple.getKeyNode().getStartMark(), e); + } + } + if (keyNode.isTwoStepsConstruction()) { + postponeSetFilling(set, key); + } else { + set.add(key); + } } - - protected Set<Object> createDefaultSet() { - // respect order from YAML document - return new LinkedHashSet<Object>(); + } + + /* + * if keyObject is created it 2 steps we should postpone putting it into the set because it may + * have different hash after initialization compared to clean just created one. And set of course + * does not observe value hashCode changes. + */ + protected void postponeSetFilling(Set<Object> set, Object key) { + sets2fill.add(0, new RecursiveTuple<Set<Object>, Object>(set, key)); + } + + public void setPropertyUtils(PropertyUtils propertyUtils) { + this.propertyUtils = propertyUtils; + explicitPropertyUtils = true; + Collection<TypeDescription> tds = typeDefinitions.values(); + for (TypeDescription typeDescription : tds) { + typeDescription.setPropertyUtils(propertyUtils); } + } - protected Set<Object> constructSet(MappingNode node) { - Set<Object> set = createDefaultSet(); - constructSet2ndStep(node, set); - return set; + public final PropertyUtils getPropertyUtils() { + if (propertyUtils == null) { + propertyUtils = new PropertyUtils(); } - - protected Map<Object, Object> constructMapping(MappingNode node) { - Map<Object, Object> mapping = createDefaultMap(); - constructMapping2ndStep(node, mapping); - return mapping; + return propertyUtils; + } + + /** + * Make YAML aware how to parse a custom Class. If there is no root Class assigned in constructor + * then the 'root' property of this definition is respected. + * + * @param definition to be added to the Constructor + * @return the previous value associated with <code>definition</code>, or <code>null</code> if + * there was no mapping for <code>definition</code>. + */ + public TypeDescription addTypeDescription(TypeDescription definition) { + if (definition == null) { + throw new NullPointerException("TypeDescription is required."); } + Tag tag = definition.getTag(); + typeTags.put(tag, definition.getType()); + definition.setPropertyUtils(getPropertyUtils()); + return typeDefinitions.put(definition.getType(), definition); + } - protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) { - List<NodeTuple> nodeValue = (List<NodeTuple>) node.getValue(); - for (NodeTuple tuple : nodeValue) { - Node keyNode = tuple.getKeyNode(); - Node valueNode = tuple.getValueNode(); - Object key = constructObject(keyNode); - if (key != null) { - try { - key.hashCode();// check circular dependencies - } catch (Exception e) { - throw new ConstructorException("while constructing a mapping", - node.getStartMark(), "found unacceptable key " + key, tuple - .getKeyNode().getStartMark(), e); - } - } - Object value = constructObject(valueNode); - if (keyNode.isTwoStepsConstruction()) { - /* - * if keyObject is created it 2 steps we should postpone putting - * it in map because it may have different hash after - * initialization compared to clean just created one. And map of - * course does not observe key hashCode changes. - */ - maps2fill.add(0, - new RecursiveTuple<Map<Object, Object>, RecursiveTuple<Object, Object>>( - mapping, new RecursiveTuple<Object, Object>(key, value))); - } else { - mapping.put(key, value); - } - } - } + private static class RecursiveTuple<T, K> { - protected void constructSet2ndStep(MappingNode node, Set<Object> set) { - List<NodeTuple> nodeValue = (List<NodeTuple>) node.getValue(); - for (NodeTuple tuple : nodeValue) { - Node keyNode = tuple.getKeyNode(); - Object key = constructObject(keyNode); - if (key != null) { - try { - key.hashCode();// check circular dependencies - } catch (Exception e) { - throw new ConstructorException("while constructing a Set", node.getStartMark(), - "found unacceptable key " + key, tuple.getKeyNode().getStartMark(), e); - } - } - if (keyNode.isTwoStepsConstruction()) { - /* - * if keyObject is created it 2 steps we should postpone putting - * it into the set because it may have different hash after - * initialization compared to clean just created one. And set of - * course does not observe value hashCode changes. - */ - sets2fill.add(0, new RecursiveTuple<Set<Object>, Object>(set, key)); - } else { - set.add(key); - } - } + private final T _1; + private final K _2; + + public RecursiveTuple(T _1, K _2) { + this._1 = _1; + this._2 = _2; } - public void setPropertyUtils(PropertyUtils propertyUtils) { - this.propertyUtils = propertyUtils; - explicitPropertyUtils = true; + public K _2() { + return _2; } - public final PropertyUtils getPropertyUtils() { - if (propertyUtils == null) { - propertyUtils = new PropertyUtils(); - } - return propertyUtils; + public T _1() { + return _1; } + } - private static class RecursiveTuple<T, K> { - private final T _1; - private final K _2; + public final boolean isExplicitPropertyUtils() { + return explicitPropertyUtils; + } - public RecursiveTuple(T _1, K _2) { - this._1 = _1; - this._2 = _2; - } + public boolean isAllowDuplicateKeys() { + return allowDuplicateKeys; + } - public K _2() { - return _2; - } + public void setAllowDuplicateKeys(boolean allowDuplicateKeys) { + this.allowDuplicateKeys = allowDuplicateKeys; + } - public T _1() { - return _1; - } - } + public boolean isWrappedToRootException() { + return wrappedToRootException; + } - public final boolean isExplicitPropertyUtils() { - return explicitPropertyUtils; - } + public void setWrappedToRootException(boolean wrappedToRootException) { + this.wrappedToRootException = wrappedToRootException; + } + + public boolean isEnumCaseSensitive() { + return enumCaseSensitive; + } + + public void setEnumCaseSensitive(boolean enumCaseSensitive) { + this.enumCaseSensitive = enumCaseSensitive; + } } diff --git a/src/main/java/org/yaml/snakeyaml/constructor/Construct.java b/src/main/java/org/yaml/snakeyaml/constructor/Construct.java index 4fa91186..81ef4446 100644 --- a/src/main/java/org/yaml/snakeyaml/constructor/Construct.java +++ b/src/main/java/org/yaml/snakeyaml/constructor/Construct.java @@ -1,50 +1,43 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.constructor; import org.yaml.snakeyaml.nodes.Node; /** - * Provide a way to construct a Java instance out of the composed Node. Support - * recursive objects if it is required. (create Native Data Structure out of - * Node Graph) - * - * @see <a href="http://yaml.org/spec/1.1/#id859109">Chapter 3. Processing YAML - * Information</a> + * Provide a way to construct a Java instance out of the composed Node. Support recursive objects if + * it is required. (create Native Data Structure out of Node Graph) + * + * @see <a href="http://yaml.org/spec/1.1/#id859109">Chapter 3. Processing YAML Information</a> */ public interface Construct { - /** - * Construct a Java instance with all the properties injected when it is - * possible. - * - * @param node - * composed Node - * @return a complete Java instance - */ - Object construct(Node node); - /** - * Apply the second step when constructing recursive structures. Because the - * instance is already created it can assign a reference to itself. - * - * @param node - * composed Node - * @param object - * the instance constructed earlier by - * <code>construct(Node node)</code> for the provided Node - */ - void construct2ndStep(Node node, Object object); + /** + * Construct a Java instance with all the properties injected when it is possible. + * + * @param node composed Node + * @return a complete Java instance + */ + Object construct(Node node); + + /** + * Apply the second step when constructing recursive structures. Because the instance is already + * created it can assign a reference to itself. + * + * @param node composed Node + * @param object the instance constructed earlier by <code>construct(Node node)</code> for the + * provided Node + */ + void construct2ndStep(Node node, Object object); } diff --git a/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java b/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java index 943702f2..52dd5da8 100644 --- a/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java +++ b/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java @@ -1,38 +1,29 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.constructor; -import java.beans.IntrospectionException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; import java.util.UUID; - +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.TypeDescription; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.introspector.Property; @@ -43,640 +34,635 @@ import org.yaml.snakeyaml.nodes.NodeTuple; import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.SequenceNode; import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.util.EnumUtils; /** * Construct a custom Java instance. */ public class Constructor extends SafeConstructor { - private final Map<Tag, Class<? extends Object>> typeTags; - protected final Map<Class<? extends Object>, TypeDescription> typeDefinitions; - public Constructor() { - this(Object.class); + public Constructor() { + this(Object.class); + } + + public Constructor(LoaderOptions loadingConfig) { + this(Object.class, loadingConfig); + } + + /** + * Create Constructor for the specified class as the root. + * + * @param theRoot - the class (usually JavaBean) to be constructed + */ + public Constructor(Class<? extends Object> theRoot) { + this(new TypeDescription(checkRoot(theRoot))); + } + + public Constructor(Class<? extends Object> theRoot, LoaderOptions loadingConfig) { + this(new TypeDescription(checkRoot(theRoot)), loadingConfig); + } + + /** + * Ugly Java way to check the argument in the constructor + */ + private static Class<? extends Object> checkRoot(Class<? extends Object> theRoot) { + if (theRoot == null) { + throw new NullPointerException("Root class must be provided."); + } else { + return theRoot; } - - /** - * Create Constructor for the specified class as the root. - * - * @param theRoot - * - the class (usually JavaBean) to be constructed - */ - public Constructor(Class<? extends Object> theRoot) { - this(new TypeDescription(checkRoot(theRoot))); + } + + public Constructor(TypeDescription theRoot) { + this(theRoot, null, new LoaderOptions()); + } + + public Constructor(TypeDescription theRoot, LoaderOptions loadingConfig) { + this(theRoot, null, loadingConfig); + } + + public Constructor(TypeDescription theRoot, Collection<TypeDescription> moreTDs) { + this(theRoot, moreTDs, new LoaderOptions()); + } + + /** + * Create with all possible arguments + * + * @param theRoot - the class (usually JavaBean) to be constructed + * @param moreTDs - collection of classes used by the root class + * @param loadingConfig - configuration + */ + public Constructor(TypeDescription theRoot, Collection<TypeDescription> moreTDs, + LoaderOptions loadingConfig) { + super(loadingConfig); + if (theRoot == null) { + throw new NullPointerException("Root type must be provided."); } - - /** - * Ugly Java way to check the argument in the constructor - */ - private static Class<? extends Object> checkRoot(Class<? extends Object> theRoot) { - if (theRoot == null) { - throw new NullPointerException("Root class must be provided."); - } else - return theRoot; + this.yamlConstructors.put(null, new ConstructYamlObject()); + if (!Object.class.equals(theRoot.getType())) { + rootTag = new Tag(theRoot.getType()); } - - public Constructor(TypeDescription theRoot) { - if (theRoot == null) { - throw new NullPointerException("Root type must be provided."); - } - this.yamlConstructors.put(null, new ConstructYamlObject()); - if (!Object.class.equals(theRoot.getType())) { - rootTag = new Tag(theRoot.getType()); - } - typeTags = new HashMap<Tag, Class<? extends Object>>(); - typeDefinitions = new HashMap<Class<? extends Object>, TypeDescription>(); - yamlClassConstructors.put(NodeId.scalar, new ConstructScalar()); - yamlClassConstructors.put(NodeId.mapping, new ConstructMapping()); - yamlClassConstructors.put(NodeId.sequence, new ConstructSequence()); - addTypeDescription(theRoot); + yamlClassConstructors.put(NodeId.scalar, new ConstructScalar()); + yamlClassConstructors.put(NodeId.mapping, new ConstructMapping()); + yamlClassConstructors.put(NodeId.sequence, new ConstructSequence()); + addTypeDescription(theRoot); + if (moreTDs != null) { + for (TypeDescription td : moreTDs) { + addTypeDescription(td); + } } + } + + /** + * Create Constructor for a class which does not have to be in the classpath or for a definition + * from a Spring ApplicationContext. + * + * @param theRoot fully qualified class name of the root class (usually JavaBean) + * @throws ClassNotFoundException if cannot be loaded by the classloader + */ + public Constructor(String theRoot) throws ClassNotFoundException { + this(Class.forName(check(theRoot))); + } + + public Constructor(String theRoot, LoaderOptions loadingConfig) throws ClassNotFoundException { + this(Class.forName(check(theRoot)), loadingConfig); + } + + private static final String check(String s) { + if (s == null) { + throw new NullPointerException("Root type must be provided."); + } + if (s.trim().length() == 0) { + throw new YAMLException("Root type must be provided."); + } + return s; + } + + /** + * Construct mapping instance (Map, JavaBean) when the runtime class is known. + */ + protected class ConstructMapping implements Construct { /** - * Create Constructor for a class which does not have to be in the classpath - * or for a definition from a Spring ApplicationContext. - * - * @param theRoot - * fully qualified class name of the root class (usually - * JavaBean) - * @throws ClassNotFoundException + * Construct JavaBean. If type safe collections are used please look at + * <code>TypeDescription</code>. + * + * @param node node where the keys are property names (they can only be <code>String</code>s) + * and values are objects to be created + * @return constructed JavaBean */ - public Constructor(String theRoot) throws ClassNotFoundException { - this(Class.forName(check(theRoot))); - } - - private static final String check(String s) { - if (s == null) { - throw new NullPointerException("Root type must be provided."); + public Object construct(Node node) { + MappingNode mnode = (MappingNode) node; + if (Map.class.isAssignableFrom(node.getType())) { + if (node.isTwoStepsConstruction()) { + return newMap(mnode); + } else { + return constructMapping(mnode); } - if (s.trim().length() == 0) { - throw new YAMLException("Root type must be provided."); + } else if (Collection.class.isAssignableFrom(node.getType())) { + if (node.isTwoStepsConstruction()) { + return newSet(mnode); + } else { + return constructSet(mnode); } - return s; + } else { + Object obj = Constructor.this.newInstance(mnode); + if (obj != NOT_INSTANTIATED_OBJECT) { + if (node.isTwoStepsConstruction()) { + return obj; + } else { + return constructJavaBean2ndStep(mnode, obj); + } + } else { + throw new ConstructorException(null, null, + "Can't create an instance for " + mnode.getTag(), node.getStartMark()); + } + } } - /** - * Make YAML aware how to parse a custom Class. If there is no root Class - * assigned in constructor then the 'root' property of this definition is - * respected. - * - * @param definition - * to be added to the Constructor - * @return the previous value associated with <tt>definition</tt>, or - * <tt>null</tt> if there was no mapping for <tt>definition</tt>. - */ - public TypeDescription addTypeDescription(TypeDescription definition) { - if (definition == null) { - throw new NullPointerException("TypeDescription is required."); - } - Tag tag = definition.getTag(); - typeTags.put(tag, definition.getType()); - return typeDefinitions.put(definition.getType(), definition); + @SuppressWarnings("unchecked") + public void construct2ndStep(Node node, Object object) { + if (Map.class.isAssignableFrom(node.getType())) { + constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object); + } else if (Set.class.isAssignableFrom(node.getType())) { + constructSet2ndStep((MappingNode) node, (Set<Object>) object); + } else { + constructJavaBean2ndStep((MappingNode) node, object); + } } - /** - * Construct mapping instance (Map, JavaBean) when the runtime class is - * known. - */ - protected class ConstructMapping implements Construct { - - /** - * Construct JavaBean. If type safe collections are used please look at - * <code>TypeDescription</code>. - * - * @param node - * node where the keys are property names (they can only be - * <code>String</code>s) and values are objects to be created - * @return constructed JavaBean - */ - public Object construct(Node node) { - MappingNode mnode = (MappingNode) node; - if (Properties.class.isAssignableFrom(node.getType())) { - Properties properties = new Properties(); - if (!node.isTwoStepsConstruction()) { - constructMapping2ndStep(mnode, properties); - } else { - throw new YAMLException("Properties must not be recursive."); - } - return properties; - } else if (SortedMap.class.isAssignableFrom(node.getType())) { - SortedMap<Object, Object> map = new TreeMap<Object, Object>(); - if (!node.isTwoStepsConstruction()) { - constructMapping2ndStep(mnode, map); - } - return map; - } else if (Map.class.isAssignableFrom(node.getType())) { - if (node.isTwoStepsConstruction()) { - return createDefaultMap(); - } else { - return constructMapping(mnode); - } - } else if (SortedSet.class.isAssignableFrom(node.getType())) { - SortedSet<Object> set = new TreeSet<Object>(); - // XXX why this is not used ? - // if (!node.isTwoStepsConstruction()) { - constructSet2ndStep(mnode, set); - // } - return set; - } else if (Collection.class.isAssignableFrom(node.getType())) { - if (node.isTwoStepsConstruction()) { - return createDefaultSet(); - } else { - return constructSet(mnode); - } - } else { - if (node.isTwoStepsConstruction()) { - return createEmptyJavaBean(mnode); - } else { - return constructJavaBean2ndStep(mnode, createEmptyJavaBean(mnode)); - } + // protected Object createEmptyJavaBean(MappingNode node) { + // try { + // Object instance = Constructor.this.newInstance(node); + // if (instance != null) { + // return instance; + // } + // + // /** + // * Using only default constructor. Everything else will be + // * initialized on 2nd step. If we do here some partial + // * initialization, how do we then track what need to be done on + // * 2nd step? I think it is better to get only object here (to + // * have it as reference for recursion) and do all other thing on + // * 2nd step. + // */ + // java.lang.reflect.Constructor<?> c = + // node.getType().getDeclaredConstructor(); + // c.setAccessible(true); + // return c.newInstance(); + // } catch (Exception e) { + // throw new YAMLException(e); + // } + // } + + protected Object constructJavaBean2ndStep(MappingNode node, Object object) { + flattenMapping(node, true); + Class<? extends Object> beanType = node.getType(); + List<NodeTuple> nodeValue = node.getValue(); + for (NodeTuple tuple : nodeValue) { + Node valueNode = tuple.getValueNode(); + // flattenMapping enforces keys to be Strings + String key = (String) constructObject(tuple.getKeyNode()); + try { + TypeDescription memberDescription = typeDefinitions.get(beanType); + Property property = memberDescription == null ? getProperty(beanType, key) + : memberDescription.getProperty(key); + + if (!property.isWritable()) { + throw new YAMLException( + "No writable property '" + key + "' on class: " + beanType.getName()); + } + + valueNode.setType(property.getType()); + final boolean typeDetected = + memberDescription != null && memberDescription.setupPropertyType(key, valueNode); + if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) { + // only if there is no explicit TypeDescription + Class<?>[] arguments = property.getActualTypeArguments(); + if (arguments != null && arguments.length > 0) { + // type safe (generic) collection may contain the + // proper class + if (valueNode.getNodeId() == NodeId.sequence) { + Class<?> t = arguments[0]; + SequenceNode snode = (SequenceNode) valueNode; + snode.setListType(t); + } else if (Map.class.isAssignableFrom(valueNode.getType())) { + Class<?> keyType = arguments[0]; + Class<?> valueType = arguments[1]; + MappingNode mnode = (MappingNode) valueNode; + mnode.setTypes(keyType, valueType); + mnode.setUseClassConstructor(true); + } else if (Collection.class.isAssignableFrom(valueNode.getType())) { + Class<?> t = arguments[0]; + MappingNode mnode = (MappingNode) valueNode; + mnode.setOnlyKeyType(t); + mnode.setUseClassConstructor(true); + } } - } - - @SuppressWarnings("unchecked") - public void construct2ndStep(Node node, Object object) { - if (Map.class.isAssignableFrom(node.getType())) { - constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object); - } else if (Set.class.isAssignableFrom(node.getType())) { - constructSet2ndStep((MappingNode) node, (Set<Object>) object); - } else { - constructJavaBean2ndStep((MappingNode) node, object); + } + + Object value = + (memberDescription != null) ? newInstance(memberDescription, key, valueNode) + : constructObject(valueNode); + // Correct when the property expects float but double was + // constructed + if (property.getType() == Float.TYPE || property.getType() == Float.class) { + if (value instanceof Double) { + value = ((Double) value).floatValue(); } + } + // Correct when the property a String but the value is binary + if (property.getType() == String.class && Tag.BINARY.equals(valueNode.getTag()) + && value instanceof byte[]) { + value = new String((byte[]) value); + } + + if (memberDescription == null || !memberDescription.setProperty(object, key, value)) { + property.set(object, value); + } + } catch (DuplicateKeyException e) { + throw e; + } catch (Exception e) { + throw new ConstructorException( + "Cannot create property=" + key + " for JavaBean=" + object, node.getStartMark(), + e.getMessage(), valueNode.getStartMark(), e); } + } + return object; + } - protected Object createEmptyJavaBean(MappingNode node) { - try { - /** - * Using only default constructor. Everything else will be - * initialized on 2nd step. If we do here some partial - * initialization, how do we then track what need to be done on - * 2nd step? I think it is better to get only object here (to - * have it as reference for recursion) and do all other thing on - * 2nd step. - */ - java.lang.reflect.Constructor<?> c = node.getType().getDeclaredConstructor(); - c.setAccessible(true); - return c.newInstance(); - } catch (Exception e) { - throw new YAMLException(e); - } - } + private Object newInstance(TypeDescription memberDescription, String propertyName, Node node) { + Object newInstance = memberDescription.newInstance(propertyName, node); + if (newInstance != null) { + constructedObjects.put(node, newInstance); + return constructObjectNoCheck(node); + } + return constructObject(node); + } - protected Object constructJavaBean2ndStep(MappingNode node, Object object) { - flattenMapping(node); - Class<? extends Object> beanType = node.getType(); - List<NodeTuple> nodeValue = node.getValue(); - for (NodeTuple tuple : nodeValue) { - ScalarNode keyNode; - if (tuple.getKeyNode() instanceof ScalarNode) { - // key must be scalar - keyNode = (ScalarNode) tuple.getKeyNode(); - } else { - throw new YAMLException("Keys must be scalars but found: " + tuple.getKeyNode()); - } - Node valueNode = tuple.getValueNode(); - // keys can only be Strings - keyNode.setType(String.class); - String key = (String) constructObject(keyNode); - try { - Property property = getProperty(beanType, key); - valueNode.setType(property.getType()); - TypeDescription memberDescription = typeDefinitions.get(beanType); - boolean typeDetected = false; - if (memberDescription != null) { - switch (valueNode.getNodeId()) { - case sequence: - SequenceNode snode = (SequenceNode) valueNode; - Class<? extends Object> memberType = memberDescription - .getListPropertyType(key); - if (memberType != null) { - snode.setListType(memberType); - typeDetected = true; - } else if (property.getType().isArray()) { - snode.setListType(property.getType().getComponentType()); - typeDetected = true; - } - break; - case mapping: - MappingNode mnode = (MappingNode) valueNode; - Class<? extends Object> keyType = memberDescription.getMapKeyType(key); - if (keyType != null) { - mnode.setTypes(keyType, memberDescription.getMapValueType(key)); - typeDetected = true; - } - break; - default: // scalar - } - } - if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) { - // only if there is no explicit TypeDescription - Class<?>[] arguments = property.getActualTypeArguments(); - if (arguments != null && arguments.length > 0) { - // type safe (generic) collection may contain the - // proper class - if (valueNode.getNodeId() == NodeId.sequence) { - Class<?> t = arguments[0]; - SequenceNode snode = (SequenceNode) valueNode; - snode.setListType(t); - } else if (valueNode.getTag().equals(Tag.SET)) { - Class<?> t = arguments[0]; - MappingNode mnode = (MappingNode) valueNode; - mnode.setOnlyKeyType(t); - mnode.setUseClassConstructor(true); - } else if (property.getType().isAssignableFrom(Map.class)) { - Class<?> ketType = arguments[0]; - Class<?> valueType = arguments[1]; - MappingNode mnode = (MappingNode) valueNode; - mnode.setTypes(ketType, valueType); - mnode.setUseClassConstructor(true); - } else { - // the type for collection entries cannot be - // detected - } - } - } - - Object value = constructObject(valueNode); - // Correct when the property expects float but double was - // constructed - if (property.getType() == Float.TYPE || property.getType() == Float.class) { - if (value instanceof Double) { - value = ((Double) value).floatValue(); - } - } - // Correct when the property a String but the value is binary - if (property.getType() == String.class && Tag.BINARY.equals(valueNode.getTag()) && value instanceof byte[]) { - value = new String((byte[])value); - } - - property.set(object, value); - } catch (Exception e) { - throw new ConstructorException("Cannot create property=" + key - + " for JavaBean=" + object, node.getStartMark(), e.getMessage(), - valueNode.getStartMark(), e); - } - } - return object; - } + protected Property getProperty(Class<? extends Object> type, String name) { + return getPropertyUtils().getProperty(type, name); + } + } + + /** + * Construct an instance when the runtime class is not known but a global tag with a class name is + * defined. It delegates the construction to the appropriate constructor based on the node kind + * (scalar, sequence, mapping) + */ + protected class ConstructYamlObject implements Construct { + + private Construct getConstructor(Node node) { + Class<?> cl = getClassForNode(node); + node.setType(cl); + // call the constructor as if the runtime class is defined + Construct constructor = yamlClassConstructors.get(node.getNodeId()); + return constructor; + } - protected Property getProperty(Class<? extends Object> type, String name) - throws IntrospectionException { - return getPropertyUtils().getProperty(type, name); - } + public Object construct(Node node) { + try { + return getConstructor(node).construct(node); + } catch (ConstructorException e) { + throw e; + } catch (Exception e) { + throw new ConstructorException(null, null, + "Can't construct a java object for " + node.getTag() + "; exception=" + e.getMessage(), + node.getStartMark(), e); + } } - /** - * Construct an instance when the runtime class is not known but a global - * tag with a class name is defined. It delegates the construction to the - * appropriate constructor based on the node kind (scalar, sequence, - * mapping) - */ - protected class ConstructYamlObject implements Construct { - - private Construct getConstructor(Node node) { - Class<?> cl = getClassForNode(node); - node.setType(cl); - // call the constructor as if the runtime class is defined - Construct constructor = yamlClassConstructors.get(node.getNodeId()); - return constructor; + public void construct2ndStep(Node node, Object object) { + try { + getConstructor(node).construct2ndStep(node, object); + } catch (Exception e) { + throw new ConstructorException(null, null, + "Can't construct a second step for a java object for " + node.getTag() + "; exception=" + + e.getMessage(), + node.getStartMark(), e); + } + } + } + + /** + * Construct scalar instance when the runtime class is known. Recursive structures are not + * supported. + */ + protected class ConstructScalar extends AbstractConstruct { + + public Object construct(Node nnode) { + ScalarNode node = (ScalarNode) nnode; + Class<?> type = node.getType(); + + // In case there is TypeDefinition for the 'type' + Object instance = newInstance(type, node, false); + if (instance != NOT_INSTANTIATED_OBJECT) { + return instance; + } + + Object result; + if (type.isPrimitive() || type == String.class || Number.class.isAssignableFrom(type) + || type == Boolean.class || Date.class.isAssignableFrom(type) || type == Character.class + || type == BigInteger.class || type == BigDecimal.class + || Enum.class.isAssignableFrom(type) || Tag.BINARY.equals(node.getTag()) + || Calendar.class.isAssignableFrom(type) || type == UUID.class) { + // standard classes created directly + result = constructStandardJavaInstance(type, node); + } else { + // there must be only 1 constructor with 1 argument + java.lang.reflect.Constructor<?>[] javaConstructors = type.getDeclaredConstructors(); + int oneArgCount = 0; + java.lang.reflect.Constructor<?> javaConstructor = null; + for (java.lang.reflect.Constructor<?> c : javaConstructors) { + if (c.getParameterTypes().length == 1) { + oneArgCount++; + javaConstructor = c; + } } - - public Object construct(Node node) { - Object result = null; - try { - result = getConstructor(node).construct(node); - } catch (ConstructorException e) { - throw e; - } catch (Exception e) { - throw new ConstructorException(null, null, "Can't construct a java object for " - + node.getTag() + "; exception=" + e.getMessage(), node.getStartMark(), e); - } - return result; + Object argument; + if (javaConstructor == null) { + throw new YAMLException("No single argument constructor found for " + type); + } else if (oneArgCount == 1) { + argument = constructStandardJavaInstance(javaConstructor.getParameterTypes()[0], node); + } else { + // TODO it should be possible to use implicit types instead + // of forcing String. Resolver must be available here to + // obtain the implicit tag. Then we can set the tag and call + // callConstructor(node) to create the argument instance. + // On the other hand it may be safer to require a custom + // constructor to avoid guessing the argument class + argument = constructScalar(node); + try { + javaConstructor = type.getDeclaredConstructor(String.class); + } catch (Exception e) { + throw new YAMLException("Can't construct a java object for scalar " + node.getTag() + + "; No String constructor found. Exception=" + e.getMessage(), e); + } } - - public void construct2ndStep(Node node, Object object) { - try { - getConstructor(node).construct2ndStep(node, object); - } catch (Exception e) { - throw new ConstructorException(null, null, - "Can't construct a second step for a java object for " + node.getTag() - + "; exception=" + e.getMessage(), node.getStartMark(), e); - } + try { + javaConstructor.setAccessible(true); + result = javaConstructor.newInstance(argument); + } catch (Exception e) { + throw new ConstructorException(null, null, "Can't construct a java object for scalar " + + node.getTag() + "; exception=" + e.getMessage(), node.getStartMark(), e); } + } + return result; } - /** - * Construct scalar instance when the runtime class is known. Recursive - * structures are not supported. - */ - protected class ConstructScalar extends AbstractConstruct { - public Object construct(Node nnode) { - ScalarNode node = (ScalarNode) nnode; - Class<?> type = node.getType(); - Object result; - if (type.isPrimitive() || type == String.class || Number.class.isAssignableFrom(type) - || type == Boolean.class || Date.class.isAssignableFrom(type) - || type == Character.class || type == BigInteger.class - || type == BigDecimal.class || Enum.class.isAssignableFrom(type) - || Tag.BINARY.equals(node.getTag()) || Calendar.class.isAssignableFrom(type) || type == UUID.class) { - // standard classes created directly - result = constructStandardJavaInstance(type, node); - } else { - // there must be only 1 constructor with 1 argument - java.lang.reflect.Constructor<?>[] javaConstructors = type - .getDeclaredConstructors(); - int oneArgCount = 0; - java.lang.reflect.Constructor<?> javaConstructor = null; - for (java.lang.reflect.Constructor<?> c : javaConstructors) { - if (c.getParameterTypes().length == 1) { - oneArgCount++; - javaConstructor = c; - } - } - Object argument; - if (javaConstructor == null) { - throw new YAMLException("No single argument constructor found for " + type); - } else if (oneArgCount == 1) { - argument = constructStandardJavaInstance( - javaConstructor.getParameterTypes()[0], node); - } else { - // TODO it should be possible to use implicit types instead - // of forcing String. Resolver must be available here to - // obtain the implicit tag. Then we can set the tag and call - // callConstructor(node) to create the argument instance. - // On the other hand it may be safer to require a custom - // constructor to avoid guessing the argument class - argument = constructScalar(node); - try { - javaConstructor = type.getDeclaredConstructor(String.class); - } catch (Exception e) { - throw new YAMLException("Can't construct a java object for scalar " - + node.getTag() + "; No String constructor found. Exception=" - + e.getMessage(), e); - } - } - try { - javaConstructor.setAccessible(true); - result = javaConstructor.newInstance(argument); - } catch (Exception e) { - throw new ConstructorException(null, null, - "Can't construct a java object for scalar " + node.getTag() - + "; exception=" + e.getMessage(), node.getStartMark(), e); - } - } - return result; + @SuppressWarnings("unchecked") + private Object constructStandardJavaInstance(@SuppressWarnings("rawtypes") Class type, + ScalarNode node) { + Object result; + if (type == String.class) { + Construct stringConstructor = yamlConstructors.get(Tag.STR); + result = stringConstructor.construct(node); + } else if (type == Boolean.class || type == Boolean.TYPE) { + Construct boolConstructor = yamlConstructors.get(Tag.BOOL); + result = boolConstructor.construct(node); + } else if (type == Character.class || type == Character.TYPE) { + Construct charConstructor = yamlConstructors.get(Tag.STR); + String ch = (String) charConstructor.construct(node); + if (ch.length() == 0) { + result = null; + } else if (ch.length() != 1) { + throw new YAMLException("Invalid node Character: '" + ch + "'; length: " + ch.length()); + } else { + result = Character.valueOf(ch.charAt(0)); } - - @SuppressWarnings("unchecked") - private Object constructStandardJavaInstance(@SuppressWarnings("rawtypes") - Class type, ScalarNode node) { - Object result; - if (type == String.class) { - Construct stringConstructor = yamlConstructors.get(Tag.STR); - result = stringConstructor.construct(node); - } else if (type == Boolean.class || type == Boolean.TYPE) { - Construct boolConstructor = yamlConstructors.get(Tag.BOOL); - result = boolConstructor.construct(node); - } else if (type == Character.class || type == Character.TYPE) { - Construct charConstructor = yamlConstructors.get(Tag.STR); - String ch = (String) charConstructor.construct(node); - if (ch.length() == 0) { - result = null; - } else if (ch.length() != 1) { - throw new YAMLException("Invalid node Character: '" + ch + "'; length: " - + ch.length()); - } else { - result = Character.valueOf(ch.charAt(0)); - } - } else if (Date.class.isAssignableFrom(type)) { - Construct dateConstructor = yamlConstructors.get(Tag.TIMESTAMP); - Date date = (Date) dateConstructor.construct(node); - if (type == Date.class) { - result = date; - } else { - try { - java.lang.reflect.Constructor<?> constr = type.getConstructor(long.class); - result = constr.newInstance(date.getTime()); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new YAMLException("Cannot construct: '" + type + "'"); - } - } - } else if (type == Float.class || type == Double.class || type == Float.TYPE - || type == Double.TYPE || type == BigDecimal.class) { - if (type == BigDecimal.class) { - result = new BigDecimal(node.getValue()); - } else { - Construct doubleConstructor = yamlConstructors.get(Tag.FLOAT); - result = doubleConstructor.construct(node); - if (type == Float.class || type == Float.TYPE) { - result = new Float((Double) result); - } - } - } else if (type == Byte.class || type == Short.class || type == Integer.class - || type == Long.class || type == BigInteger.class || type == Byte.TYPE - || type == Short.TYPE || type == Integer.TYPE || type == Long.TYPE) { - Construct intConstructor = yamlConstructors.get(Tag.INT); - result = intConstructor.construct(node); - if (type == Byte.class || type == Byte.TYPE) { - result = Byte.valueOf(result.toString()); - } else if (type == Short.class || type == Short.TYPE) { - result = Short.valueOf(result.toString()); - } else if (type == Integer.class || type == Integer.TYPE) { - result = Integer.parseInt(result.toString()); - } else if (type == Long.class || type == Long.TYPE) { - result = Long.valueOf(result.toString()); - } else { - // only BigInteger left - result = new BigInteger(result.toString()); - } - } else if (Enum.class.isAssignableFrom(type)) { - String enumValueName = node.getValue(); - try { - result = Enum.valueOf(type, enumValueName); - } catch (Exception ex) { - throw new YAMLException("Unable to find enum value '" + enumValueName - + "' for enum class: " + type.getName()); - } - } else if (Calendar.class.isAssignableFrom(type)) { - ConstructYamlTimestamp contr = new ConstructYamlTimestamp(); - contr.construct(node); - result = contr.getCalendar(); - } else if (Number.class.isAssignableFrom(type)) { - ConstructYamlNumber contr = new ConstructYamlNumber(); - result = contr.construct(node); - } else if (UUID.class == type) { - result = UUID.fromString(node.getValue()); - } else { - if (yamlConstructors.containsKey(node.getTag())) { - result = yamlConstructors.get(node.getTag()).construct(node); - } else { - throw new YAMLException("Unsupported class: " + type); - } - } - return result; + } else if (Date.class.isAssignableFrom(type)) { + Construct dateConstructor = yamlConstructors.get(Tag.TIMESTAMP); + Date date = (Date) dateConstructor.construct(node); + if (type == Date.class) { + result = date; + } else { + try { + java.lang.reflect.Constructor<?> constr = type.getConstructor(long.class); + result = constr.newInstance(date.getTime()); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new YAMLException("Cannot construct: '" + type + "'"); + } + } + } else if (type == Float.class || type == Double.class || type == Float.TYPE + || type == Double.TYPE || type == BigDecimal.class) { + if (type == BigDecimal.class) { + result = new BigDecimal(node.getValue()); + } else { + Construct doubleConstructor = yamlConstructors.get(Tag.FLOAT); + result = doubleConstructor.construct(node); + if (type == Float.class || type == Float.TYPE) { + result = Float.valueOf(((Double) result).floatValue()); + } + } + } else if (type == Byte.class || type == Short.class || type == Integer.class + || type == Long.class || type == BigInteger.class || type == Byte.TYPE + || type == Short.TYPE || type == Integer.TYPE || type == Long.TYPE) { + Construct intConstructor = yamlConstructors.get(Tag.INT); + result = intConstructor.construct(node); + if (type == Byte.class || type == Byte.TYPE) { + result = Integer.valueOf(result.toString()).byteValue(); + } else if (type == Short.class || type == Short.TYPE) { + result = Integer.valueOf(result.toString()).shortValue(); + } else if (type == Integer.class || type == Integer.TYPE) { + result = Integer.parseInt(result.toString()); + } else if (type == Long.class || type == Long.TYPE) { + result = Long.valueOf(result.toString()); + } else { + // only BigInteger left + result = new BigInteger(result.toString()); } + } else if (Enum.class.isAssignableFrom(type)) { + String enumValueName = node.getValue(); + try { + if (loadingConfig.isEnumCaseSensitive()) { + result = Enum.valueOf(type, enumValueName); + } else { + result = EnumUtils.findEnumInsensitiveCase(type, enumValueName); + } + } catch (Exception ex) { + throw new YAMLException("Unable to find enum value '" + enumValueName + + "' for enum class: " + type.getName()); + } + } else if (Calendar.class.isAssignableFrom(type)) { + ConstructYamlTimestamp contr = new ConstructYamlTimestamp(); + contr.construct(node); + result = contr.getCalendar(); + } else if (Number.class.isAssignableFrom(type)) { + // since we do not know the exact type we create Float + ConstructYamlFloat contr = new ConstructYamlFloat(); + result = contr.construct(node); + } else if (UUID.class == type) { + result = UUID.fromString(node.getValue()); + } else { + if (yamlConstructors.containsKey(node.getTag())) { + result = yamlConstructors.get(node.getTag()).construct(node); + } else { + throw new YAMLException("Unsupported class: " + type); + } + } + return result; } - - /** - * Construct sequence (List, Array, or immutable object) when the runtime - * class is known. - */ - protected class ConstructSequence implements Construct { - @SuppressWarnings("unchecked") - public Object construct(Node node) { - SequenceNode snode = (SequenceNode) node; - if (Set.class.isAssignableFrom(node.getType())) { - if (node.isTwoStepsConstruction()) { - throw new YAMLException("Set cannot be recursive."); - } else { - return constructSet(snode); - } - } else if (Collection.class.isAssignableFrom(node.getType())) { - if (node.isTwoStepsConstruction()) { - return createDefaultList(snode.getValue().size()); - } else { - return constructSequence(snode); - } - } else if (node.getType().isArray()) { - if (node.isTwoStepsConstruction()) { - return createArray(node.getType(), snode.getValue().size()); - } else { - return constructArray(snode); - } - } else { - // create immutable object - List<java.lang.reflect.Constructor<?>> possibleConstructors = new ArrayList<java.lang.reflect.Constructor<?>>( - snode.getValue().size()); - for (java.lang.reflect.Constructor<?> constructor : node - .getType().getDeclaredConstructors()) { - if (snode.getValue() - .size() == constructor.getParameterTypes().length) { - possibleConstructors.add(constructor); - } - } - if (!possibleConstructors.isEmpty()) { - if (possibleConstructors.size() == 1) { - Object[] argumentList = new Object[snode.getValue().size()]; - java.lang.reflect.Constructor<?> c = possibleConstructors.get(0); - int index = 0; - for (Node argumentNode : snode.getValue()) { - Class<?> type = c.getParameterTypes()[index]; - // set runtime classes for arguments - argumentNode.setType(type); - argumentList[index++] = constructObject(argumentNode); - } - - try { - c.setAccessible(true); - return c.newInstance(argumentList); - } catch (Exception e) { - throw new YAMLException(e); - } - } - - // use BaseConstructor - List<Object> argumentList = (List<Object>) constructSequence(snode); - Class<?>[] parameterTypes = new Class[argumentList.size()]; - int index = 0; - for (Object parameter : argumentList) { - parameterTypes[index] = parameter.getClass(); - index++; - } - - for (java.lang.reflect.Constructor<?> c : possibleConstructors) { - Class<?>[] argTypes = c.getParameterTypes(); - boolean foundConstructor = true; - for (int i = 0; i < argTypes.length; i++) { - if (!wrapIfPrimitive(argTypes[i]).isAssignableFrom(parameterTypes[i])) { - foundConstructor = false; - break; - } - } - if (foundConstructor) { - try { - c.setAccessible(true); - return c.newInstance(argumentList.toArray()); - } catch (Exception e) { - throw new YAMLException(e); - } - } - } - } - throw new YAMLException("No suitable constructor with " - + String.valueOf(snode.getValue().size()) + " arguments found for " - + node.getType()); - - } + } + + /** + * Construct sequence (List, Array, or immutable object) when the runtime class is known. + */ + protected class ConstructSequence implements Construct { + + @SuppressWarnings("unchecked") + public Object construct(Node node) { + SequenceNode snode = (SequenceNode) node; + if (Set.class.isAssignableFrom(node.getType())) { + if (node.isTwoStepsConstruction()) { + throw new YAMLException("Set cannot be recursive."); + } else { + return constructSet(snode); } - - private final Class<? extends Object> wrapIfPrimitive(Class<?> clazz) { - if (!clazz.isPrimitive()) { - return clazz; - } - if (clazz == Integer.TYPE) { - return Integer.class; - } - if (clazz == Float.TYPE) { - return Float.class; - } - if (clazz == Double.TYPE) { - return Double.class; - } - if (clazz == Boolean.TYPE) { - return Boolean.class; - } - if (clazz == Long.TYPE) { - return Long.class; + } else if (Collection.class.isAssignableFrom(node.getType())) { + if (node.isTwoStepsConstruction()) { + return newList(snode); + } else { + return constructSequence(snode); + } + } else if (node.getType().isArray()) { + if (node.isTwoStepsConstruction()) { + return createArray(node.getType(), snode.getValue().size()); + } else { + return constructArray(snode); + } + } else { + // create immutable object + List<java.lang.reflect.Constructor<?>> possibleConstructors = + new ArrayList<java.lang.reflect.Constructor<?>>(snode.getValue().size()); + for (java.lang.reflect.Constructor<?> constructor : node.getType() + .getDeclaredConstructors()) { + if (snode.getValue().size() == constructor.getParameterTypes().length) { + possibleConstructors.add(constructor); + } + } + if (!possibleConstructors.isEmpty()) { + if (possibleConstructors.size() == 1) { + Object[] argumentList = new Object[snode.getValue().size()]; + java.lang.reflect.Constructor<?> c = possibleConstructors.get(0); + int index = 0; + for (Node argumentNode : snode.getValue()) { + Class<?> type = c.getParameterTypes()[index]; + // set runtime classes for arguments + argumentNode.setType(type); + argumentList[index++] = constructObject(argumentNode); } - if (clazz == Character.TYPE) { - return Character.class; + + try { + c.setAccessible(true); + return c.newInstance(argumentList); + } catch (Exception e) { + throw new YAMLException(e); } - if (clazz == Short.TYPE) { - return Short.class; + } + + // use BaseConstructor + List<Object> argumentList = (List<Object>) constructSequence(snode); + Class<?>[] parameterTypes = new Class[argumentList.size()]; + int index = 0; + for (Object parameter : argumentList) { + parameterTypes[index] = parameter.getClass(); + index++; + } + + for (java.lang.reflect.Constructor<?> c : possibleConstructors) { + Class<?>[] argTypes = c.getParameterTypes(); + boolean foundConstructor = true; + for (int i = 0; i < argTypes.length; i++) { + if (!wrapIfPrimitive(argTypes[i]).isAssignableFrom(parameterTypes[i])) { + foundConstructor = false; + break; + } } - if (clazz == Byte.TYPE) { - return Byte.class; + if (foundConstructor) { + try { + c.setAccessible(true); + return c.newInstance(argumentList.toArray()); + } catch (Exception e) { + throw new YAMLException(e); + } } - throw new YAMLException("Unexpected primitive " + clazz); + } } + throw new YAMLException("No suitable constructor with " + snode.getValue().size() + + " arguments found for " + node.getType()); - @SuppressWarnings("unchecked") - public void construct2ndStep(Node node, Object object) { - SequenceNode snode = (SequenceNode) node; - if (List.class.isAssignableFrom(node.getType())) { - List<Object> list = (List<Object>) object; - constructSequenceStep2(snode, list); - } else if (node.getType().isArray()) { - constructArrayStep2(snode, object); - } else { - throw new YAMLException("Immutable objects cannot be recursive."); - } - } + } } - protected Class<?> getClassForNode(Node node) { - Class<? extends Object> classForTag = typeTags.get(node.getTag()); - if (classForTag == null) { - String name = node.getTag().getClassName(); - Class<?> cl; - try { - cl = getClassForName(name); - } catch (ClassNotFoundException e) { - throw new YAMLException("Class not found: " + name); - } - typeTags.put(node.getTag(), cl); - return cl; - } else { - return classForTag; - } + private final Class<? extends Object> wrapIfPrimitive(Class<?> clazz) { + if (!clazz.isPrimitive()) { + return clazz; + } + if (clazz == Integer.TYPE) { + return Integer.class; + } + if (clazz == Float.TYPE) { + return Float.class; + } + if (clazz == Double.TYPE) { + return Double.class; + } + if (clazz == Boolean.TYPE) { + return Boolean.class; + } + if (clazz == Long.TYPE) { + return Long.class; + } + if (clazz == Character.TYPE) { + return Character.class; + } + if (clazz == Short.TYPE) { + return Short.class; + } + if (clazz == Byte.TYPE) { + return Byte.class; + } + throw new YAMLException("Unexpected primitive " + clazz); } - protected Class<?> getClassForName(String name) throws ClassNotFoundException { - try { - return Class.forName(name, true, Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException e) { - return Class.forName(name); - } + @SuppressWarnings("unchecked") + public void construct2ndStep(Node node, Object object) { + SequenceNode snode = (SequenceNode) node; + if (List.class.isAssignableFrom(node.getType())) { + List<Object> list = (List<Object>) object; + constructSequenceStep2(snode, list); + } else if (node.getType().isArray()) { + constructArrayStep2(snode, object); + } else { + throw new YAMLException("Immutable objects cannot be recursive."); + } + } + } + + protected Class<?> getClassForNode(Node node) { + Class<? extends Object> classForTag = typeTags.get(node.getTag()); + if (classForTag == null) { + String name = node.getTag().getClassName(); + Class<?> cl; + try { + cl = getClassForName(name); + } catch (ClassNotFoundException e) { + throw new YAMLException("Class not found: " + name); + } + typeTags.put(node.getTag(), cl); + return cl; + } else { + return classForTag; + } + } + + protected Class<?> getClassForName(String name) throws ClassNotFoundException { + try { + return Class.forName(name, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + return Class.forName(name); } + } } diff --git a/src/main/java/org/yaml/snakeyaml/constructor/ConstructorException.java b/src/main/java/org/yaml/snakeyaml/constructor/ConstructorException.java index 336e3bae..13a672f4 100644 --- a/src/main/java/org/yaml/snakeyaml/constructor/ConstructorException.java +++ b/src/main/java/org/yaml/snakeyaml/constructor/ConstructorException.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.constructor; @@ -19,15 +17,16 @@ import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.MarkedYAMLException; public class ConstructorException extends MarkedYAMLException { - private static final long serialVersionUID = -8816339931365239910L; - protected ConstructorException(String context, Mark contextMark, String problem, - Mark problemMark, Throwable cause) { - super(context, contextMark, problem, problemMark, cause); - } + private static final long serialVersionUID = -8816339931365239910L; - protected ConstructorException(String context, Mark contextMark, String problem, - Mark problemMark) { - this(context, contextMark, problem, problemMark, null); - } + protected ConstructorException(String context, Mark contextMark, String problem, Mark problemMark, + Throwable cause) { + super(context, contextMark, problem, problemMark, cause); + } + + protected ConstructorException(String context, Mark contextMark, String problem, + Mark problemMark) { + this(context, contextMark, problem, problemMark, null); + } } diff --git a/src/main/java/org/yaml/snakeyaml/constructor/CustomClassLoaderConstructor.java b/src/main/java/org/yaml/snakeyaml/constructor/CustomClassLoaderConstructor.java index edb163da..8773e3ee 100644 --- a/src/main/java/org/yaml/snakeyaml/constructor/CustomClassLoaderConstructor.java +++ b/src/main/java/org/yaml/snakeyaml/constructor/CustomClassLoaderConstructor.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.constructor; @@ -19,22 +17,23 @@ package org.yaml.snakeyaml.constructor; * Construct instances with a custom Class Loader. */ public class CustomClassLoaderConstructor extends Constructor { - private ClassLoader loader = CustomClassLoaderConstructor.class.getClassLoader(); - public CustomClassLoaderConstructor(ClassLoader cLoader) { - this(Object.class, cLoader); - } + private ClassLoader loader = CustomClassLoaderConstructor.class.getClassLoader(); - public CustomClassLoaderConstructor(Class<? extends Object> theRoot, ClassLoader theLoader) { - super(theRoot); - if (theLoader == null) { - throw new NullPointerException("Loader must be provided."); - } - this.loader = theLoader; - } + public CustomClassLoaderConstructor(ClassLoader cLoader) { + this(Object.class, cLoader); + } - @Override - protected Class<?> getClassForName(String name) throws ClassNotFoundException { - return Class.forName(name, true, loader); + public CustomClassLoaderConstructor(Class<? extends Object> theRoot, ClassLoader theLoader) { + super(theRoot); + if (theLoader == null) { + throw new NullPointerException("Loader must be provided."); } + this.loader = theLoader; + } + + @Override + protected Class<?> getClassForName(String name) throws ClassNotFoundException { + return Class.forName(name, true, loader); + } } diff --git a/src/main/java/org/yaml/snakeyaml/constructor/DuplicateKeyException.java b/src/main/java/org/yaml/snakeyaml/constructor/DuplicateKeyException.java new file mode 100644 index 00000000..3a992104 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/constructor/DuplicateKeyException.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.constructor; + +import org.yaml.snakeyaml.error.Mark; + +public class DuplicateKeyException extends ConstructorException { + + protected DuplicateKeyException(Mark contextMark, Object key, Mark problemMark) { + super("while constructing a mapping", contextMark, "found duplicate key " + key, problemMark); + } +} diff --git a/src/main/java/org/yaml/snakeyaml/constructor/SafeConstructor.java b/src/main/java/org/yaml/snakeyaml/constructor/SafeConstructor.java index b5572ea0..b34011c5 100644 --- a/src/main/java/org/yaml/snakeyaml/constructor/SafeConstructor.java +++ b/src/main/java/org/yaml/snakeyaml/constructor/SafeConstructor.java @@ -1,26 +1,21 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.constructor; import java.math.BigInteger; -import java.text.NumberFormat; -import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -28,9 +23,10 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; +import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; - +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; import org.yaml.snakeyaml.nodes.MappingNode; @@ -46,465 +42,578 @@ import org.yaml.snakeyaml.nodes.Tag; */ public class SafeConstructor extends BaseConstructor { - public static final ConstructUndefined undefinedConstructor = new ConstructUndefined(); - - public SafeConstructor() { - this.yamlConstructors.put(Tag.NULL, new ConstructYamlNull()); - this.yamlConstructors.put(Tag.BOOL, new ConstructYamlBool()); - this.yamlConstructors.put(Tag.INT, new ConstructYamlInt()); - this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlFloat()); - this.yamlConstructors.put(Tag.BINARY, new ConstructYamlBinary()); - this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlTimestamp()); - this.yamlConstructors.put(Tag.OMAP, new ConstructYamlOmap()); - this.yamlConstructors.put(Tag.PAIRS, new ConstructYamlPairs()); - this.yamlConstructors.put(Tag.SET, new ConstructYamlSet()); - this.yamlConstructors.put(Tag.STR, new ConstructYamlStr()); - this.yamlConstructors.put(Tag.SEQ, new ConstructYamlSeq()); - this.yamlConstructors.put(Tag.MAP, new ConstructYamlMap()); - this.yamlConstructors.put(null, undefinedConstructor); - this.yamlClassConstructors.put(NodeId.scalar, undefinedConstructor); - this.yamlClassConstructors.put(NodeId.sequence, undefinedConstructor); - this.yamlClassConstructors.put(NodeId.mapping, undefinedConstructor); + public static final ConstructUndefined undefinedConstructor = new ConstructUndefined(); + + public SafeConstructor() { + this(new LoaderOptions()); + } + + public SafeConstructor(LoaderOptions loadingConfig) { + super(loadingConfig); + this.yamlConstructors.put(Tag.NULL, new ConstructYamlNull()); + this.yamlConstructors.put(Tag.BOOL, new ConstructYamlBool()); + this.yamlConstructors.put(Tag.INT, new ConstructYamlInt()); + this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlFloat()); + this.yamlConstructors.put(Tag.BINARY, new ConstructYamlBinary()); + this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlTimestamp()); + this.yamlConstructors.put(Tag.OMAP, new ConstructYamlOmap()); + this.yamlConstructors.put(Tag.PAIRS, new ConstructYamlPairs()); + this.yamlConstructors.put(Tag.SET, new ConstructYamlSet()); + this.yamlConstructors.put(Tag.STR, new ConstructYamlStr()); + this.yamlConstructors.put(Tag.SEQ, new ConstructYamlSeq()); + this.yamlConstructors.put(Tag.MAP, new ConstructYamlMap()); + this.yamlConstructors.put(null, undefinedConstructor); + this.yamlClassConstructors.put(NodeId.scalar, undefinedConstructor); + this.yamlClassConstructors.put(NodeId.sequence, undefinedConstructor); + this.yamlClassConstructors.put(NodeId.mapping, undefinedConstructor); + } + + protected void flattenMapping(MappingNode node) { + flattenMapping(node, false); + } + + protected void flattenMapping(MappingNode node, boolean forceStringKeys) { + // perform merging only on nodes containing merge node(s) + processDuplicateKeys(node, forceStringKeys); + if (node.isMerged()) { + node.setValue(mergeNode(node, true, new HashMap<Object, Integer>(), + new ArrayList<NodeTuple>(), forceStringKeys)); } + } + + protected void processDuplicateKeys(MappingNode node) { + processDuplicateKeys(node, false); + } + + protected void processDuplicateKeys(MappingNode node, boolean forceStringKeys) { + List<NodeTuple> nodeValue = node.getValue(); + Map<Object, Integer> keys = new HashMap<Object, Integer>(nodeValue.size()); + TreeSet<Integer> toRemove = new TreeSet<Integer>(); + int i = 0; + for (NodeTuple tuple : nodeValue) { + Node keyNode = tuple.getKeyNode(); + if (!keyNode.getTag().equals(Tag.MERGE)) { + if (forceStringKeys) { + if (keyNode instanceof ScalarNode) { + keyNode.setType(String.class); + keyNode.setTag(Tag.STR); + } else { + throw new YAMLException("Keys must be scalars but found: " + keyNode); + } + } + Object key = constructObject(keyNode); + if (key != null && !forceStringKeys) { + if (keyNode.isTwoStepsConstruction()) { + if (!loadingConfig.getAllowRecursiveKeys()) { + throw new YAMLException( + "Recursive key for mapping is detected but it is not configured to be allowed."); + } else { + try { + key.hashCode();// check circular dependencies + } catch (Exception e) { + throw new ConstructorException("while constructing a mapping", node.getStartMark(), + "found unacceptable key " + key, tuple.getKeyNode().getStartMark(), e); + } + } + } + } - protected void flattenMapping(MappingNode node) { - // perform merging only on nodes containing merge node(s) - if (node.isMerged()) { - node.setValue(mergeNode(node, true, new HashMap<Object, Integer>(), - new ArrayList<NodeTuple>())); + Integer prevIndex = keys.put(key, i); + if (prevIndex != null) { + if (!isAllowDuplicateKeys()) { + throw new DuplicateKeyException(node.getStartMark(), key, + tuple.getKeyNode().getStartMark()); + } + toRemove.add(prevIndex); } + } + i = i + 1; } - /** - * Does merge for supplied mapping node. - * - * @param node - * where to merge - * @param isPreffered - * true if keys of node should take precedence over others... - * @param key2index - * maps already merged keys to index from values - * @param values - * collects merged NodeTuple - * @return list of the merged NodeTuple (to be set as value for the - * MappingNode) - */ - private List<NodeTuple> mergeNode(MappingNode node, boolean isPreffered, - Map<Object, Integer> key2index, List<NodeTuple> values) { - List<NodeTuple> nodeValue = node.getValue(); - // reversed for http://code.google.com/p/snakeyaml/issues/detail?id=139 - Collections.reverse(nodeValue); - for (Iterator<NodeTuple> iter = nodeValue.iterator(); iter.hasNext();) { - final NodeTuple nodeTuple = iter.next(); - final Node keyNode = nodeTuple.getKeyNode(); - final Node valueNode = nodeTuple.getValueNode(); - if (keyNode.getTag().equals(Tag.MERGE)) { - iter.remove(); - switch (valueNode.getNodeId()) { - case mapping: - MappingNode mn = (MappingNode) valueNode; - mergeNode(mn, false, key2index, values); - break; - case sequence: - SequenceNode sn = (SequenceNode) valueNode; - List<Node> vals = sn.getValue(); - for (Node subnode : vals) { - if (!(subnode instanceof MappingNode)) { - throw new ConstructorException("while constructing a mapping", - node.getStartMark(), - "expected a mapping for merging, but found " - + subnode.getNodeId(), subnode.getStartMark()); - } - MappingNode mnode = (MappingNode) subnode; - mergeNode(mnode, false, key2index, values); - } - break; - default: - throw new ConstructorException("while constructing a mapping", - node.getStartMark(), - "expected a mapping or list of mappings for merging, but found " - + valueNode.getNodeId(), valueNode.getStartMark()); - } - } else { - // we need to construct keys to avoid duplications - Object key = constructObject(keyNode); - if (!key2index.containsKey(key)) { // 1st time merging key - values.add(nodeTuple); - // keep track where tuple for the key is - key2index.put(key, values.size() - 1); - } else if (isPreffered) { // there is value for the key, but we - // need to override it - // change value for the key using saved position - values.set(key2index.get(key), nodeTuple); - } + Iterator<Integer> indices2remove = toRemove.descendingIterator(); + while (indices2remove.hasNext()) { + nodeValue.remove(indices2remove.next().intValue()); + } + } + + /** + * Does merge for supplied mapping node. + * + * @param node where to merge + * @param isPreffered true if keys of node should take precedence over others... + * @param key2index maps already merged keys to index from values + * @param values collects merged NodeTuple + * @return list of the merged NodeTuple (to be set as value for the MappingNode) + */ + private List<NodeTuple> mergeNode(MappingNode node, boolean isPreffered, + Map<Object, Integer> key2index, List<NodeTuple> values, boolean forceStringKeys) { + Iterator<NodeTuple> iter = node.getValue().iterator(); + while (iter.hasNext()) { + final NodeTuple nodeTuple = iter.next(); + final Node keyNode = nodeTuple.getKeyNode(); + final Node valueNode = nodeTuple.getValueNode(); + if (keyNode.getTag().equals(Tag.MERGE)) { + iter.remove(); + switch (valueNode.getNodeId()) { + case mapping: + MappingNode mn = (MappingNode) valueNode; + mergeNode(mn, false, key2index, values, forceStringKeys); + break; + case sequence: + SequenceNode sn = (SequenceNode) valueNode; + List<Node> vals = sn.getValue(); + for (Node subnode : vals) { + if (!(subnode instanceof MappingNode)) { + throw new ConstructorException("while constructing a mapping", node.getStartMark(), + "expected a mapping for merging, but found " + subnode.getNodeId(), + subnode.getStartMark()); + } + MappingNode mnode = (MappingNode) subnode; + mergeNode(mnode, false, key2index, values, forceStringKeys); } + break; + default: + throw new ConstructorException("while constructing a mapping", node.getStartMark(), + "expected a mapping or list of mappings for merging, but found " + + valueNode.getNodeId(), + valueNode.getStartMark()); + } + } else { + // we need to construct keys to avoid duplications + if (forceStringKeys) { + if (keyNode instanceof ScalarNode) { + keyNode.setType(String.class); + keyNode.setTag(Tag.STR); + } else { + throw new YAMLException("Keys must be scalars but found: " + keyNode); + } + } + Object key = constructObject(keyNode); + if (!key2index.containsKey(key)) { // 1st time merging key + values.add(nodeTuple); + // keep track where tuple for the key is + key2index.put(key, values.size() - 1); + } else if (isPreffered) { // there is value for the key, but we + // need to override it + // change value for the key using saved position + values.set(key2index.get(key), nodeTuple); } - return values; + } } + return values; + } + + @Override + protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) { + flattenMapping(node); + super.constructMapping2ndStep(node, mapping); + } + + @Override + protected void constructSet2ndStep(MappingNode node, Set<Object> set) { + flattenMapping(node); + super.constructSet2ndStep(node, set); + } + + public class ConstructYamlNull extends AbstractConstruct { - protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) { - flattenMapping(node); - super.constructMapping2ndStep(node, mapping); + @Override + public Object construct(Node node) { + if (node != null) { + constructScalar((ScalarNode) node); + } + return null; } + } + + private final static Map<String, Boolean> BOOL_VALUES = new HashMap<String, Boolean>(); + + static { + BOOL_VALUES.put("yes", Boolean.TRUE); + BOOL_VALUES.put("no", Boolean.FALSE); + BOOL_VALUES.put("true", Boolean.TRUE); + BOOL_VALUES.put("false", Boolean.FALSE); + BOOL_VALUES.put("on", Boolean.TRUE); + BOOL_VALUES.put("off", Boolean.FALSE); + } + + public class ConstructYamlBool extends AbstractConstruct { @Override - protected void constructSet2ndStep(MappingNode node, Set<Object> set) { - flattenMapping(node); - super.constructSet2ndStep(node, set); + public Object construct(Node node) { + String val = constructScalar((ScalarNode) node); + return BOOL_VALUES.get(val.toLowerCase()); } + } + + public class ConstructYamlInt extends AbstractConstruct { - public class ConstructYamlNull extends AbstractConstruct { - public Object construct(Node node) { - constructScalar((ScalarNode) node); - return null; + @Override + public Object construct(Node node) { + String value = constructScalar((ScalarNode) node).replaceAll("_", ""); + if (value.isEmpty()) { + throw new ConstructorException("while constructing an int", node.getStartMark(), + "found empty value", node.getStartMark()); + } + int sign = +1; + char first = value.charAt(0); + if (first == '-') { + sign = -1; + value = value.substring(1); + } else if (first == '+') { + value = value.substring(1); + } + int base = 10; + if ("0".equals(value)) { + return Integer.valueOf(0); + } else if (value.startsWith("0b")) { + value = value.substring(2); + base = 2; + } else if (value.startsWith("0x")) { + value = value.substring(2); + base = 16; + } else if (value.startsWith("0")) { + value = value.substring(1); + base = 8; + } else if (value.indexOf(':') != -1) { + String[] digits = value.split(":"); + int bes = 1; + int val = 0; + for (int i = 0, j = digits.length; i < j; i++) { + val += Long.parseLong(digits[j - i - 1]) * bes; + bes *= 60; } + return createNumber(sign, String.valueOf(val), 10); + } else { + return createNumber(sign, value, 10); + } + return createNumber(sign, value, base); } + } - private final static Map<String, Boolean> BOOL_VALUES = new HashMap<String, Boolean>(); - static { - BOOL_VALUES.put("yes", Boolean.TRUE); - BOOL_VALUES.put("no", Boolean.FALSE); - BOOL_VALUES.put("true", Boolean.TRUE); - BOOL_VALUES.put("false", Boolean.FALSE); - BOOL_VALUES.put("on", Boolean.TRUE); - BOOL_VALUES.put("off", Boolean.FALSE); + private static final int[][] RADIX_MAX = new int[17][2]; + + static { + int[] radixList = new int[] {2, 8, 10, 16}; + for (int radix : radixList) { + RADIX_MAX[radix] = + new int[] {maxLen(Integer.MAX_VALUE, radix), maxLen(Long.MAX_VALUE, radix)}; } + } + + private static int maxLen(final int max, final int radix) { + return Integer.toString(max, radix).length(); + } + + private static int maxLen(final long max, final int radix) { + return Long.toString(max, radix).length(); + } - public class ConstructYamlBool extends AbstractConstruct { - public Object construct(Node node) { - String val = (String) constructScalar((ScalarNode) node); - return BOOL_VALUES.get(val.toLowerCase()); + private Number createNumber(int sign, String number, int radix) { + final int len = number != null ? number.length() : 0; + if (sign < 0) { + number = "-" + number; + } + final int[] maxArr = radix < RADIX_MAX.length ? RADIX_MAX[radix] : null; + if (maxArr != null) { + final boolean gtInt = len > maxArr[0]; + if (gtInt) { + if (len > maxArr[1]) { + return new BigInteger(number, radix); } + return createLongOrBigInteger(number, radix); + } + } + Number result; + try { + result = Integer.valueOf(number, radix); + } catch (NumberFormatException e) { + result = createLongOrBigInteger(number, radix); + } + return result; + } + + protected static Number createLongOrBigInteger(final String number, final int radix) { + try { + return Long.valueOf(number, radix); + } catch (NumberFormatException e1) { + return new BigInteger(number, radix); } + } - public class ConstructYamlInt extends AbstractConstruct { - public Object construct(Node node) { - String value = constructScalar((ScalarNode) node).toString().replaceAll("_", ""); - int sign = +1; - char first = value.charAt(0); - if (first == '-') { - sign = -1; - value = value.substring(1); - } else if (first == '+') { - value = value.substring(1); - } - int base = 10; - if ("0".equals(value)) { - return Integer.valueOf(0); - } else if (value.startsWith("0b")) { - value = value.substring(2); - base = 2; - } else if (value.startsWith("0x")) { - value = value.substring(2); - base = 16; - } else if (value.startsWith("0")) { - value = value.substring(1); - base = 8; - } else if (value.indexOf(':') != -1) { - String[] digits = value.split(":"); - int bes = 1; - int val = 0; - for (int i = 0, j = digits.length; i < j; i++) { - val += Long.parseLong(digits[j - i - 1]) * bes; - bes *= 60; - } - return createNumber(sign, String.valueOf(val), 10); - } else { - return createNumber(sign, value, 10); - } - return createNumber(sign, value, base); + public class ConstructYamlFloat extends AbstractConstruct { + + @Override + public Object construct(Node node) { + String value = constructScalar((ScalarNode) node).replaceAll("_", ""); + if (value.isEmpty()) { + throw new ConstructorException("while constructing a float", node.getStartMark(), + "found empty value", node.getStartMark()); + } + int sign = +1; + char first = value.charAt(0); + if (first == '-') { + sign = -1; + value = value.substring(1); + } else if (first == '+') { + value = value.substring(1); + } + String valLower = value.toLowerCase(); + if (".inf".equals(valLower)) { + return Double.valueOf(sign == -1 ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY); + } else if (".nan".equals(valLower)) { + return Double.valueOf(Double.NaN); + } else if (value.indexOf(':') != -1) { + String[] digits = value.split(":"); + int bes = 1; + double val = 0.0; + for (int i = 0, j = digits.length; i < j; i++) { + val += Double.parseDouble(digits[j - i - 1]) * bes; + bes *= 60; } + return Double.valueOf(sign * val); + } else { + Double d = Double.valueOf(value); + return Double.valueOf(d.doubleValue() * sign); + } } + } - private Number createNumber(int sign, String number, int radix) { - Number result; - if (sign < 0) { - number = "-" + number; - } - try { - result = Integer.valueOf(number, radix); - } catch (NumberFormatException e) { - try { - result = Long.valueOf(number, radix); - } catch (NumberFormatException e1) { - result = new BigInteger(number, radix); - } - } - return result; + public class ConstructYamlBinary extends AbstractConstruct { + + @Override + public Object construct(Node node) { + // Ignore white spaces for base64 encoded scalar + String noWhiteSpaces = constructScalar((ScalarNode) node).replaceAll("\\s", ""); + byte[] decoded = Base64Coder.decode(noWhiteSpaces.toCharArray()); + return decoded; } + } - public class ConstructYamlFloat extends AbstractConstruct { - public Object construct(Node node) { - String value = constructScalar((ScalarNode) node).toString().replaceAll("_", ""); - int sign = +1; - char first = value.charAt(0); - if (first == '-') { - sign = -1; - value = value.substring(1); - } else if (first == '+') { - value = value.substring(1); - } - String valLower = value.toLowerCase(); - if (".inf".equals(valLower)) { - return new Double(sign == -1 ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY); - } else if (".nan".equals(valLower)) { - return new Double(Double.NaN); - } else if (value.indexOf(':') != -1) { - String[] digits = value.split(":"); - int bes = 1; - double val = 0.0; - for (int i = 0, j = digits.length; i < j; i++) { - val += Double.parseDouble(digits[j - i - 1]) * bes; - bes *= 60; - } - return new Double(sign * val); - } else { - Double d = Double.valueOf(value); - return new Double(d.doubleValue() * sign); - } - } + private final static Pattern TIMESTAMP_REGEXP = Pattern.compile( + "^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \t]*(?:Z|([-+][0-9][0-9]?)(?::([0-9][0-9])?)?))?)?$"); + private final static Pattern YMD_REGEXP = + Pattern.compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$"); + + public static class ConstructYamlTimestamp extends AbstractConstruct { + + private Calendar calendar; + + public Calendar getCalendar() { + return calendar; } - public class ConstructYamlBinary extends AbstractConstruct { - public Object construct(Node node) { - byte[] decoded = Base64Coder.decode(constructScalar((ScalarNode) node).toString() - .toCharArray()); - return decoded; + @Override + public Object construct(Node node) { + ScalarNode scalar = (ScalarNode) node; + String nodeValue = scalar.getValue(); + Matcher match = YMD_REGEXP.matcher(nodeValue); + if (match.matches()) { + String year_s = match.group(1); + String month_s = match.group(2); + String day_s = match.group(3); + calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + calendar.clear(); + calendar.set(Calendar.YEAR, Integer.parseInt(year_s)); + // Java's months are zero-based... + calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1); // x + calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s)); + return calendar.getTime(); + } else { + match = TIMESTAMP_REGEXP.matcher(nodeValue); + if (!match.matches()) { + throw new YAMLException("Unexpected timestamp: " + nodeValue); } + String year_s = match.group(1); + String month_s = match.group(2); + String day_s = match.group(3); + String hour_s = match.group(4); + String min_s = match.group(5); + // seconds and milliseconds + String seconds = match.group(6); + String millis = match.group(7); + if (millis != null) { + seconds = seconds + "." + millis; + } + double fractions = Double.parseDouble(seconds); + int sec_s = (int) Math.round(Math.floor(fractions)); + int usec = (int) Math.round((fractions - sec_s) * 1000); + // timezone + String timezoneh_s = match.group(8); + String timezonem_s = match.group(9); + TimeZone timeZone; + if (timezoneh_s != null) { + String time = timezonem_s != null ? ":" + timezonem_s : "00"; + timeZone = TimeZone.getTimeZone("GMT" + timezoneh_s + time); + } else { + // no time zone provided + timeZone = TimeZone.getTimeZone("UTC"); + } + calendar = Calendar.getInstance(timeZone); + calendar.set(Calendar.YEAR, Integer.parseInt(year_s)); + // Java's months are zero-based... + calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1); + calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s)); + calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour_s)); + calendar.set(Calendar.MINUTE, Integer.parseInt(min_s)); + calendar.set(Calendar.SECOND, sec_s); + calendar.set(Calendar.MILLISECOND, usec); + return calendar.getTime(); + } } + } - public class ConstructYamlNumber extends AbstractConstruct { - - private final NumberFormat nf = NumberFormat.getInstance(); - - public Object construct(Node node) { - ScalarNode scalar = (ScalarNode) node; - try { - return nf.parse(scalar.getValue()); - } catch (ParseException e) { - String lowerCaseValue = scalar.getValue().toLowerCase(); - if (lowerCaseValue.contains("inf") || lowerCaseValue.contains("nan")) { - /* - * Non-finites such as (+/-)infinity and NaN are not - * parseable by NumberFormat when these `Double` values are - * dumped by snakeyaml. Delegate to the `Tag.FLOAT` - * constructor when for this expected failure cause. - */ - return (Number) yamlConstructors.get(Tag.FLOAT).construct(node); - } else { - throw new IllegalArgumentException("Unable to parse as Number: " - + scalar.getValue()); - } - } + public class ConstructYamlOmap extends AbstractConstruct { + + @Override + public Object construct(Node node) { + // Note: we do not check for duplicate keys, because it's too + // CPU-expensive. + Map<Object, Object> omap = new LinkedHashMap<Object, Object>(); + if (!(node instanceof SequenceNode)) { + throw new ConstructorException("while constructing an ordered map", node.getStartMark(), + "expected a sequence, but found " + node.getNodeId(), node.getStartMark()); + } + SequenceNode snode = (SequenceNode) node; + for (Node subnode : snode.getValue()) { + if (!(subnode instanceof MappingNode)) { + throw new ConstructorException("while constructing an ordered map", node.getStartMark(), + "expected a mapping of length 1, but found " + subnode.getNodeId(), + subnode.getStartMark()); } + MappingNode mnode = (MappingNode) subnode; + if (mnode.getValue().size() != 1) { + throw new ConstructorException("while constructing an ordered map", node.getStartMark(), + "expected a single mapping item, but found " + mnode.getValue().size() + " items", + mnode.getStartMark()); + } + Node keyNode = mnode.getValue().get(0).getKeyNode(); + Node valueNode = mnode.getValue().get(0).getValueNode(); + Object key = constructObject(keyNode); + Object value = constructObject(valueNode); + omap.put(key, value); + } + return omap; } + } - private final static Pattern TIMESTAMP_REGEXP = Pattern - .compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \t]*(?:Z|([-+][0-9][0-9]?)(?::([0-9][0-9])?)?))?)?$"); - private final static Pattern YMD_REGEXP = Pattern - .compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$"); - - public static class ConstructYamlTimestamp extends AbstractConstruct { - private Calendar calendar; + public class ConstructYamlPairs extends AbstractConstruct { - public Calendar getCalendar() { - return calendar; + @Override + public Object construct(Node node) { + // Note: we do not check for duplicate keys, because it's too + // CPU-expensive. + if (!(node instanceof SequenceNode)) { + throw new ConstructorException("while constructing pairs", node.getStartMark(), + "expected a sequence, but found " + node.getNodeId(), node.getStartMark()); + } + SequenceNode snode = (SequenceNode) node; + List<Object[]> pairs = new ArrayList<Object[]>(snode.getValue().size()); + for (Node subnode : snode.getValue()) { + if (!(subnode instanceof MappingNode)) { + throw new ConstructorException("while constructingpairs", node.getStartMark(), + "expected a mapping of length 1, but found " + subnode.getNodeId(), + subnode.getStartMark()); } - - public Object construct(Node node) { - ScalarNode scalar = (ScalarNode) node; - String nodeValue = scalar.getValue(); - Matcher match = YMD_REGEXP.matcher(nodeValue); - if (match.matches()) { - String year_s = match.group(1); - String month_s = match.group(2); - String day_s = match.group(3); - calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - calendar.clear(); - calendar.set(Calendar.YEAR, Integer.parseInt(year_s)); - // Java's months are zero-based... - calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1); // x - calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s)); - return calendar.getTime(); - } else { - match = TIMESTAMP_REGEXP.matcher(nodeValue); - if (!match.matches()) { - throw new YAMLException("Unexpected timestamp: " + nodeValue); - } - String year_s = match.group(1); - String month_s = match.group(2); - String day_s = match.group(3); - String hour_s = match.group(4); - String min_s = match.group(5); - // seconds and milliseconds - String seconds = match.group(6); - String millis = match.group(7); - if (millis != null) { - seconds = seconds + "." + millis; - } - double fractions = Double.parseDouble(seconds); - int sec_s = (int) Math.round(Math.floor(fractions)); - int usec = (int) Math.round((fractions - sec_s) * 1000); - // timezone - String timezoneh_s = match.group(8); - String timezonem_s = match.group(9); - TimeZone timeZone; - if (timezoneh_s != null) { - String time = timezonem_s != null ? ":" + timezonem_s : "00"; - timeZone = TimeZone.getTimeZone("GMT" + timezoneh_s + time); - } else { - // no time zone provided - timeZone = TimeZone.getTimeZone("UTC"); - } - calendar = Calendar.getInstance(timeZone); - calendar.set(Calendar.YEAR, Integer.parseInt(year_s)); - // Java's months are zero-based... - calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1); - calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s)); - calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour_s)); - calendar.set(Calendar.MINUTE, Integer.parseInt(min_s)); - calendar.set(Calendar.SECOND, sec_s); - calendar.set(Calendar.MILLISECOND, usec); - return calendar.getTime(); - } + MappingNode mnode = (MappingNode) subnode; + if (mnode.getValue().size() != 1) { + throw new ConstructorException("while constructing pairs", node.getStartMark(), + "expected a single mapping item, but found " + mnode.getValue().size() + " items", + mnode.getStartMark()); } + Node keyNode = mnode.getValue().get(0).getKeyNode(); + Node valueNode = mnode.getValue().get(0).getValueNode(); + Object key = constructObject(keyNode); + Object value = constructObject(valueNode); + pairs.add(new Object[] {key, value}); + } + return pairs; } + } - public class ConstructYamlOmap extends AbstractConstruct { - public Object construct(Node node) { - // Note: we do not check for duplicate keys, because it's too - // CPU-expensive. - Map<Object, Object> omap = new LinkedHashMap<Object, Object>(); - if (!(node instanceof SequenceNode)) { - throw new ConstructorException("while constructing an ordered map", - node.getStartMark(), "expected a sequence, but found " + node.getNodeId(), - node.getStartMark()); - } - SequenceNode snode = (SequenceNode) node; - for (Node subnode : snode.getValue()) { - if (!(subnode instanceof MappingNode)) { - throw new ConstructorException("while constructing an ordered map", - node.getStartMark(), "expected a mapping of length 1, but found " - + subnode.getNodeId(), subnode.getStartMark()); - } - MappingNode mnode = (MappingNode) subnode; - if (mnode.getValue().size() != 1) { - throw new ConstructorException("while constructing an ordered map", - node.getStartMark(), "expected a single mapping item, but found " - + mnode.getValue().size() + " items", mnode.getStartMark()); - } - Node keyNode = mnode.getValue().get(0).getKeyNode(); - Node valueNode = mnode.getValue().get(0).getValueNode(); - Object key = constructObject(keyNode); - Object value = constructObject(valueNode); - omap.put(key, value); - } - return omap; - } + public class ConstructYamlSet implements Construct { + + @Override + public Object construct(Node node) { + if (node.isTwoStepsConstruction()) { + return (constructedObjects.containsKey(node) ? constructedObjects.get(node) + : createDefaultSet(((MappingNode) node).getValue().size())); + } else { + return constructSet((MappingNode) node); + } } - // Note: the same code as `construct_yaml_omap`. - public class ConstructYamlPairs extends AbstractConstruct { - public Object construct(Node node) { - // Note: we do not check for duplicate keys, because it's too - // CPU-expensive. - if (!(node instanceof SequenceNode)) { - throw new ConstructorException("while constructing pairs", node.getStartMark(), - "expected a sequence, but found " + node.getNodeId(), node.getStartMark()); - } - SequenceNode snode = (SequenceNode) node; - List<Object[]> pairs = new ArrayList<Object[]>(snode.getValue().size()); - for (Node subnode : snode.getValue()) { - if (!(subnode instanceof MappingNode)) { - throw new ConstructorException("while constructingpairs", node.getStartMark(), - "expected a mapping of length 1, but found " + subnode.getNodeId(), - subnode.getStartMark()); - } - MappingNode mnode = (MappingNode) subnode; - if (mnode.getValue().size() != 1) { - throw new ConstructorException("while constructing pairs", node.getStartMark(), - "expected a single mapping item, but found " + mnode.getValue().size() - + " items", mnode.getStartMark()); - } - Node keyNode = mnode.getValue().get(0).getKeyNode(); - Node valueNode = mnode.getValue().get(0).getValueNode(); - Object key = constructObject(keyNode); - Object value = constructObject(valueNode); - pairs.add(new Object[] { key, value }); - } - return pairs; - } + @Override + @SuppressWarnings("unchecked") + public void construct2ndStep(Node node, Object object) { + if (node.isTwoStepsConstruction()) { + constructSet2ndStep((MappingNode) node, (Set<Object>) object); + } else { + throw new YAMLException("Unexpected recursive set structure. Node: " + node); + } } + } - public class ConstructYamlSet implements Construct { - public Object construct(Node node) { - if (node.isTwoStepsConstruction()) { - return createDefaultSet(); - } else { - return constructSet((MappingNode) node); - } - } + public class ConstructYamlStr extends AbstractConstruct { - @SuppressWarnings("unchecked") - public void construct2ndStep(Node node, Object object) { - if (node.isTwoStepsConstruction()) { - constructSet2ndStep((MappingNode) node, (Set<Object>) object); - } else { - throw new YAMLException("Unexpected recursive set structure. Node: " + node); - } - } + @Override + public Object construct(Node node) { + return constructScalar((ScalarNode) node); } + } - public class ConstructYamlStr extends AbstractConstruct { - public Object construct(Node node) { - return constructScalar((ScalarNode) node); - } - } + public class ConstructYamlSeq implements Construct { - public class ConstructYamlSeq implements Construct { - public Object construct(Node node) { - SequenceNode seqNode = (SequenceNode) node; - if (node.isTwoStepsConstruction()) { - return createDefaultList(seqNode.getValue().size()); - } else { - return constructSequence(seqNode); - } - } + @Override + public Object construct(Node node) { + SequenceNode seqNode = (SequenceNode) node; + if (node.isTwoStepsConstruction()) { + return newList(seqNode); + } else { + return constructSequence(seqNode); + } + } - @SuppressWarnings("unchecked") - public void construct2ndStep(Node node, Object data) { - if (node.isTwoStepsConstruction()) { - constructSequenceStep2((SequenceNode) node, (List<Object>) data); - } else { - throw new YAMLException("Unexpected recursive sequence structure. Node: " + node); - } - } + @Override + @SuppressWarnings("unchecked") + public void construct2ndStep(Node node, Object data) { + if (node.isTwoStepsConstruction()) { + constructSequenceStep2((SequenceNode) node, (List<Object>) data); + } else { + throw new YAMLException("Unexpected recursive sequence structure. Node: " + node); + } } + } - public class ConstructYamlMap implements Construct { - public Object construct(Node node) { - if (node.isTwoStepsConstruction()) { - return createDefaultMap(); - } else { - return constructMapping((MappingNode) node); - } - } + public class ConstructYamlMap implements Construct { - @SuppressWarnings("unchecked") - public void construct2ndStep(Node node, Object object) { - if (node.isTwoStepsConstruction()) { - constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object); - } else { - throw new YAMLException("Unexpected recursive mapping structure. Node: " + node); - } - } + @Override + public Object construct(Node node) { + MappingNode mnode = (MappingNode) node; + if (node.isTwoStepsConstruction()) { + return createDefaultMap(mnode.getValue().size()); + } else { + return constructMapping(mnode); + } } - public static final class ConstructUndefined extends AbstractConstruct { - public Object construct(Node node) { - throw new ConstructorException(null, null, - "could not determine a constructor for the tag " + node.getTag(), - node.getStartMark()); - } + @Override + @SuppressWarnings("unchecked") + public void construct2ndStep(Node node, Object object) { + if (node.isTwoStepsConstruction()) { + constructMapping2ndStep((MappingNode) node, (Map<Object, Object>) object); + } else { + throw new YAMLException("Unexpected recursive mapping structure. Node: " + node); + } + } + } + + public static final class ConstructUndefined extends AbstractConstruct { + + @Override + public Object construct(Node node) { + throw new ConstructorException(null, null, + "could not determine a constructor for the tag " + node.getTag(), node.getStartMark()); } + } } diff --git a/src/main/java/org/yaml/snakeyaml/emitter/Emitable.java b/src/main/java/org/yaml/snakeyaml/emitter/Emitable.java index 87737280..dd72a702 100644 --- a/src/main/java/org/yaml/snakeyaml/emitter/Emitable.java +++ b/src/main/java/org/yaml/snakeyaml/emitter/Emitable.java @@ -1,24 +1,22 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.emitter; import java.io.IOException; - import org.yaml.snakeyaml.events.Event; public interface Emitable { - void emit(Event event) throws IOException; + + void emit(Event event) throws IOException; } diff --git a/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java b/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java index 2136e21c..7ba6b4bb 100644 --- a/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java +++ b/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java @@ -1,41 +1,47 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.emitter; import java.io.IOException; import java.io.Writer; +import java.util.ArrayDeque; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.ArrayBlockingQueue; +import java.util.regex.Matcher; import java.util.regex.Pattern; - import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.ScalarStyle; import org.yaml.snakeyaml.DumperOptions.Version; +import org.yaml.snakeyaml.comments.CommentEventsCollector; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.events.AliasEvent; import org.yaml.snakeyaml.events.CollectionEndEvent; import org.yaml.snakeyaml.events.CollectionStartEvent; +import org.yaml.snakeyaml.events.CommentEvent; import org.yaml.snakeyaml.events.DocumentEndEvent; import org.yaml.snakeyaml.events.DocumentStartEvent; import org.yaml.snakeyaml.events.Event; +import org.yaml.snakeyaml.events.Event.ID; import org.yaml.snakeyaml.events.MappingEndEvent; import org.yaml.snakeyaml.events.MappingStartEvent; import org.yaml.snakeyaml.events.NodeEvent; @@ -60,61 +66,141 @@ import org.yaml.snakeyaml.util.ArrayStack; * </pre> */ public final class Emitter implements Emitable { - private static final Map<Character, String> ESCAPE_REPLACEMENTS = new HashMap<Character, String>(); - public static final int MIN_INDENT = 1; - public static final int MAX_INDENT = 10; - - private static final char[] SPACE = { ' ' }; - - static { - ESCAPE_REPLACEMENTS.put('\0', "0"); - ESCAPE_REPLACEMENTS.put('\u0007', "a"); - ESCAPE_REPLACEMENTS.put('\u0008', "b"); - ESCAPE_REPLACEMENTS.put('\u0009', "t"); - ESCAPE_REPLACEMENTS.put('\n', "n"); - ESCAPE_REPLACEMENTS.put('\u000B', "v"); - ESCAPE_REPLACEMENTS.put('\u000C', "f"); - ESCAPE_REPLACEMENTS.put('\r', "r"); - ESCAPE_REPLACEMENTS.put('\u001B', "e"); - ESCAPE_REPLACEMENTS.put('"', "\""); - ESCAPE_REPLACEMENTS.put('\\', "\\"); - ESCAPE_REPLACEMENTS.put('\u0085', "N"); - ESCAPE_REPLACEMENTS.put('\u00A0', "_"); - ESCAPE_REPLACEMENTS.put('\u2028', "L"); - ESCAPE_REPLACEMENTS.put('\u2029', "P"); - } - - private final static Map<String, String> DEFAULT_TAG_PREFIXES = new LinkedHashMap<String, String>(); - static { - DEFAULT_TAG_PREFIXES.put("!", "!"); - DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!"); - } - // The stream should have the methods `write` and possibly `flush`. - private final Writer stream; - - // Encoding is defined by Writer (cannot be overriden by STREAM-START.) - // private Charset encoding; + public static final int MIN_INDENT = 1; + public static final int MAX_INDENT = 10; + private static final char[] SPACE = {' '}; + + private static final Pattern SPACES_PATTERN = Pattern.compile("\\s"); + private static final Set<Character> INVALID_ANCHOR = new HashSet(); + + static { + INVALID_ANCHOR.add('['); + INVALID_ANCHOR.add(']'); + INVALID_ANCHOR.add('{'); + INVALID_ANCHOR.add('}'); + INVALID_ANCHOR.add(','); + INVALID_ANCHOR.add('*'); + INVALID_ANCHOR.add('&'); + } + + private static final Map<Character, String> ESCAPE_REPLACEMENTS = + new HashMap<Character, String>(); + + static { + ESCAPE_REPLACEMENTS.put('\0', "0"); + ESCAPE_REPLACEMENTS.put('\u0007', "a"); + ESCAPE_REPLACEMENTS.put('\u0008', "b"); + ESCAPE_REPLACEMENTS.put('\u0009', "t"); + ESCAPE_REPLACEMENTS.put('\n', "n"); + ESCAPE_REPLACEMENTS.put('\u000B', "v"); + ESCAPE_REPLACEMENTS.put('\u000C', "f"); + ESCAPE_REPLACEMENTS.put('\r', "r"); + ESCAPE_REPLACEMENTS.put('\u001B', "e"); + ESCAPE_REPLACEMENTS.put('"', "\""); + ESCAPE_REPLACEMENTS.put('\\', "\\"); + ESCAPE_REPLACEMENTS.put('\u0085', "N"); + ESCAPE_REPLACEMENTS.put('\u00A0', "_"); + ESCAPE_REPLACEMENTS.put('\u2028', "L"); + ESCAPE_REPLACEMENTS.put('\u2029', "P"); + } + + private final static Map<String, String> DEFAULT_TAG_PREFIXES = + new LinkedHashMap<String, String>(); + + static { + DEFAULT_TAG_PREFIXES.put("!", "!"); + DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!"); + } + + // The stream should have the methods `write` and possibly `flush`. + private final Writer stream; + + // Encoding is defined by Writer (cannot be overridden by STREAM-START.) + // private Charset encoding; + + // Emitter is a state machine with a stack of states to handle nested + // structures. + private final ArrayStack<EmitterState> states; + private EmitterState state; + + // Current event and the event queue. + private final Queue<Event> events; + private Event event; + + // The current indentation level and the stack of previous indents. + private final ArrayStack<Integer> indents; + private Integer indent; + + // Flow level. + private int flowLevel; + + // Contexts. + private boolean rootContext; + private boolean mappingContext; + private boolean simpleKeyContext; + + // + // Characteristics of the last emitted character: + // - current position. + // - is it a whitespace? + // - is it an indention character + // (indentation space, '-', '?', or ':')? + // private int line; this variable is not used + private int column; + private boolean whitespace; + private boolean indention; + private boolean openEnded; + + // Formatting details. + private final Boolean canonical; + // pretty print flow by adding extra line breaks + private final Boolean prettyFlow; + + private final boolean allowUnicode; + private int bestIndent; + private final int indicatorIndent; + private final boolean indentWithIndicator; + private int bestWidth; + private final char[] bestLineBreak; + private final boolean splitLines; + private final int maxSimpleKeyLength; + private final boolean emitComments; + + // Tag prefixes. + private Map<String, String> tagPrefixes; + + // Prepared anchor and tag. + private String preparedAnchor; + private String preparedTag; + + // Scalar analysis and style. + private ScalarAnalysis analysis; + private DumperOptions.ScalarStyle style; + + // Comment processing + private final CommentEventsCollector blockCommentsCollector; + private final CommentEventsCollector inlineCommentsCollector; + + + public Emitter(Writer stream, DumperOptions opts) { + // The stream should have the methods `write` and possibly `flush`. + this.stream = stream; // Emitter is a state machine with a stack of states to handle nested // structures. - private final ArrayStack<EmitterState> states; - private EmitterState state; - + this.states = new ArrayStack<EmitterState>(100); + this.state = new ExpectStreamStart(); // Current event and the event queue. - private final Queue<Event> events; - private Event event; - + this.events = new ArrayDeque<>(100); + this.event = null; // The current indentation level and the stack of previous indents. - private final ArrayStack<Integer> indents; - private Integer indent; - + this.indents = new ArrayStack<Integer>(10); + this.indent = null; // Flow level. - private int flowLevel; - + this.flowLevel = 0; // Contexts. - private boolean rootContext; - private boolean mappingContext; - private boolean simpleKeyContext; + mappingContext = false; + simpleKeyContext = false; // // Characteristics of the last emitted character: @@ -122,1358 +208,1510 @@ public final class Emitter implements Emitable { // - is it a whitespace? // - is it an indention character // (indentation space, '-', '?', or ':')? - // private int line; this variable is not used - private int column; - private boolean whitespace; - private boolean indention; - private boolean openEnded; + column = 0; + whitespace = true; + indention = true; - // Formatting details. - private Boolean canonical; - // pretty print flow by adding extra line breaks - private Boolean prettyFlow; + // Whether the document requires an explicit document indicator + openEnded = false; - private boolean allowUnicode; - private int bestIndent; - private int indicatorIndent; - private int bestWidth; - private char[] bestLineBreak; - private boolean splitLines; + // Formatting details. + this.canonical = opts.isCanonical(); + this.prettyFlow = opts.isPrettyFlow(); + this.allowUnicode = opts.isAllowUnicode(); + this.bestIndent = 2; + if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) { + this.bestIndent = opts.getIndent(); + } + this.indicatorIndent = opts.getIndicatorIndent(); + this.indentWithIndicator = opts.getIndentWithIndicator(); + this.bestWidth = 80; + if (opts.getWidth() > this.bestIndent * 2) { + this.bestWidth = opts.getWidth(); + } + this.bestLineBreak = opts.getLineBreak().getString().toCharArray(); + this.splitLines = opts.getSplitLines(); + this.maxSimpleKeyLength = opts.getMaxSimpleKeyLength(); + this.emitComments = opts.isProcessComments(); // Tag prefixes. - private Map<String, String> tagPrefixes; + this.tagPrefixes = new LinkedHashMap<String, String>(); // Prepared anchor and tag. - private String preparedAnchor; - private String preparedTag; + this.preparedAnchor = null; + this.preparedTag = null; // Scalar analysis and style. - private ScalarAnalysis analysis; - private Character style; - - public Emitter(Writer stream, DumperOptions opts) { - // The stream should have the methods `write` and possibly `flush`. - this.stream = stream; - // Emitter is a state machine with a stack of states to handle nested - // structures. - this.states = new ArrayStack<EmitterState>(100); - this.state = new ExpectStreamStart(); - // Current event and the event queue. - this.events = new ArrayBlockingQueue<Event>(100); - this.event = null; - // The current indentation level and the stack of previous indents. - this.indents = new ArrayStack<Integer>(10); - this.indent = null; - // Flow level. - this.flowLevel = 0; - // Contexts. - mappingContext = false; - simpleKeyContext = false; - - // - // Characteristics of the last emitted character: - // - current position. - // - is it a whitespace? - // - is it an indention character - // (indentation space, '-', '?', or ':')? - column = 0; - whitespace = true; - indention = true; - - // Whether the document requires an explicit document indicator - openEnded = false; - - // Formatting details. - this.canonical = opts.isCanonical(); - this.prettyFlow = opts.isPrettyFlow(); - this.allowUnicode = opts.isAllowUnicode(); - this.bestIndent = 2; - if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) { - this.bestIndent = opts.getIndent(); - } - this.indicatorIndent = opts.getIndicatorIndent(); - this.bestWidth = 80; - if (opts.getWidth() > this.bestIndent * 2) { - this.bestWidth = opts.getWidth(); - } - this.bestLineBreak = opts.getLineBreak().getString().toCharArray(); - this.splitLines = opts.getSplitLines(); + this.analysis = null; + this.style = null; + + // Comment processing + this.blockCommentsCollector = + new CommentEventsCollector(events, CommentType.BLANK_LINE, CommentType.BLOCK); + this.inlineCommentsCollector = new CommentEventsCollector(events, CommentType.IN_LINE); + } + + public void emit(Event event) throws IOException { + this.events.add(event); + while (!needMoreEvents()) { + this.event = this.events.poll(); + this.state.expect(); + this.event = null; + } + } - // Tag prefixes. - this.tagPrefixes = new LinkedHashMap<String, String>(); + // In some cases, we wait for a few next events before emitting. - // Prepared anchor and tag. - this.preparedAnchor = null; - this.preparedTag = null; + private boolean needMoreEvents() { + if (events.isEmpty()) { + return true; + } - // Scalar analysis and style. - this.analysis = null; - this.style = null; + Iterator<Event> iter = events.iterator(); + Event event = iter.next(); // FIXME why without check ??? + while (event instanceof CommentEvent) { + if (!iter.hasNext()) { + return true; + } + event = iter.next(); } - public void emit(Event event) throws IOException { - this.events.add(event); - while (!needMoreEvents()) { - this.event = this.events.poll(); - this.state.expect(); - this.event = null; - } + if (event instanceof DocumentStartEvent) { + return needEvents(iter, 1); + } else if (event instanceof SequenceStartEvent) { + return needEvents(iter, 2); + } else if (event instanceof MappingStartEvent) { + return needEvents(iter, 3); + } else if (event instanceof StreamStartEvent) { + return needEvents(iter, 2); + } else if (event instanceof StreamEndEvent) { + return false; + } else if (emitComments) { + return needEvents(iter, 1); + } + return false; + } + + private boolean needEvents(Iterator<Event> iter, int count) { + int level = 0; + int actualCount = 0; + while (iter.hasNext()) { + Event event = iter.next(); + if (event instanceof CommentEvent) { + continue; + } + actualCount++; + if (event instanceof DocumentStartEvent || event instanceof CollectionStartEvent) { + level++; + } else if (event instanceof DocumentEndEvent || event instanceof CollectionEndEvent) { + level--; + } else if (event instanceof StreamEndEvent) { + level = -1; + } + if (level < 0) { + return false; + } + } + return actualCount < count; + } + + private void increaseIndent(boolean flow, boolean indentless) { + indents.push(indent); + if (indent == null) { + if (flow) { + indent = bestIndent; + } else { + indent = 0; + } + } else if (!indentless) { + this.indent += bestIndent; } + } - // In some cases, we wait for a few next events before emitting. + // States - private boolean needMoreEvents() { - if (events.isEmpty()) { - return true; - } - Event event = events.peek(); - if (event instanceof DocumentStartEvent) { - return needEvents(1); - } else if (event instanceof SequenceStartEvent) { - return needEvents(2); - } else if (event instanceof MappingStartEvent) { - return needEvents(3); - } else { - return false; - } - } + // Stream handlers. - private boolean needEvents(int count) { - int level = 0; - Iterator<Event> iter = events.iterator(); - iter.next(); - while (iter.hasNext()) { - Event event = iter.next(); - if (event instanceof DocumentStartEvent || event instanceof CollectionStartEvent) { - level++; - } else if (event instanceof DocumentEndEvent || event instanceof CollectionEndEvent) { - level--; - } else if (event instanceof StreamEndEvent) { - level = -1; - } - if (level < 0) { - return false; - } - } - return events.size() < count + 1; + private class ExpectStreamStart implements EmitterState { + + public void expect() throws IOException { + if (event instanceof StreamStartEvent) { + writeStreamStart(); + state = new ExpectFirstDocumentStart(); + } else { + throw new EmitterException("expected StreamStartEvent, but got " + event); + } } + } - private void increaseIndent(boolean flow, boolean indentless) { - indents.push(indent); - if (indent == null) { - if (flow) { - indent = bestIndent; - } else { - indent = 0; - } - } else if (!indentless) { - this.indent += bestIndent; - } + private class ExpectNothing implements EmitterState { + + public void expect() throws IOException { + throw new EmitterException("expecting nothing, but got " + event); } + } - // States + // Document handlers. - // Stream handlers. + private class ExpectFirstDocumentStart implements EmitterState { - private class ExpectStreamStart implements EmitterState { - public void expect() throws IOException { - if (event instanceof StreamStartEvent) { - writeStreamStart(); - state = new ExpectFirstDocumentStart(); - } else { - throw new EmitterException("expected StreamStartEvent, but got " + event); - } - } + public void expect() throws IOException { + new ExpectDocumentStart(true).expect(); } + } - private class ExpectNothing implements EmitterState { - public void expect() throws IOException { - throw new EmitterException("expecting nothing, but got " + event); - } - } + private class ExpectDocumentStart implements EmitterState { - // Document handlers. + private final boolean first; - private class ExpectFirstDocumentStart implements EmitterState { - public void expect() throws IOException { - new ExpectDocumentStart(true).expect(); - } + public ExpectDocumentStart(boolean first) { + this.first = first; } - private class ExpectDocumentStart implements EmitterState { - private boolean first; + public void expect() throws IOException { + if (event instanceof DocumentStartEvent) { + DocumentStartEvent ev = (DocumentStartEvent) event; + if ((ev.getVersion() != null || ev.getTags() != null) && openEnded) { + writeIndicator("...", true, false, false); + writeIndent(); + } + if (ev.getVersion() != null) { + String versionText = prepareVersion(ev.getVersion()); + writeVersionDirective(versionText); + } + tagPrefixes = new LinkedHashMap<String, String>(DEFAULT_TAG_PREFIXES); + if (ev.getTags() != null) { + Set<String> handles = new TreeSet<String>(ev.getTags().keySet()); + for (String handle : handles) { + String prefix = ev.getTags().get(handle); + tagPrefixes.put(prefix, handle); + String handleText = prepareTagHandle(handle); + String prefixText = prepareTagPrefix(prefix); + writeTagDirective(handleText, prefixText); + } + } + boolean implicit = first && !ev.getExplicit() && !canonical && ev.getVersion() == null + && (ev.getTags() == null || ev.getTags().isEmpty()) && !checkEmptyDocument(); + if (!implicit) { + writeIndent(); + writeIndicator("---", true, false, false); + if (canonical) { + writeIndent(); + } + } + state = new ExpectDocumentRoot(); + } else if (event instanceof StreamEndEvent) { + writeStreamEnd(); + state = new ExpectNothing(); + } else if (event instanceof CommentEvent) { + blockCommentsCollector.collectEvents(event); + writeBlockComment(); + // state = state; remains unchanged + } else { + throw new EmitterException("expected DocumentStartEvent, but got " + event); + } + } + } - public ExpectDocumentStart(boolean first) { - this.first = first; - } + private class ExpectDocumentEnd implements EmitterState { - public void expect() throws IOException { - if (event instanceof DocumentStartEvent) { - DocumentStartEvent ev = (DocumentStartEvent) event; - if ((ev.getVersion() != null || ev.getTags() != null) && openEnded) { - writeIndicator("...", true, false, false); - writeIndent(); - } - if (ev.getVersion() != null) { - String versionText = prepareVersion(ev.getVersion()); - writeVersionDirective(versionText); - } - tagPrefixes = new LinkedHashMap<String, String>(DEFAULT_TAG_PREFIXES); - if (ev.getTags() != null) { - Set<String> handles = new TreeSet<String>(ev.getTags().keySet()); - for (String handle : handles) { - String prefix = ev.getTags().get(handle); - tagPrefixes.put(prefix, handle); - String handleText = prepareTagHandle(handle); - String prefixText = prepareTagPrefix(prefix); - writeTagDirective(handleText, prefixText); - } - } - boolean implicit = first && !ev.getExplicit() && !canonical - && ev.getVersion() == null - && (ev.getTags() == null || ev.getTags().isEmpty()) - && !checkEmptyDocument(); - if (!implicit) { - writeIndent(); - writeIndicator("---", true, false, false); - if (canonical) { - writeIndent(); - } - } - state = new ExpectDocumentRoot(); - } else if (event instanceof StreamEndEvent) { - // TODO fix 313 PyYAML changeset - // if (openEnded) { - // writeIndicator("...", true, false, false); - // writeIndent(); - // } - writeStreamEnd(); - state = new ExpectNothing(); - } else { - throw new EmitterException("expected DocumentStartEvent, but got " + event); - } + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (event instanceof DocumentEndEvent) { + writeIndent(); + if (((DocumentEndEvent) event).getExplicit()) { + writeIndicator("...", true, false, false); + writeIndent(); } + flushStream(); + state = new ExpectDocumentStart(false); + } else { + throw new EmitterException("expected DocumentEndEvent, but got " + event); + } } + } - private class ExpectDocumentEnd implements EmitterState { - public void expect() throws IOException { - if (event instanceof DocumentEndEvent) { - writeIndent(); - if (((DocumentEndEvent) event).getExplicit()) { - writeIndicator("...", true, false, false); - writeIndent(); - } - flushStream(); - state = new ExpectDocumentStart(false); - } else { - throw new EmitterException("expected DocumentEndEvent, but got " + event); - } - } - } + private class ExpectDocumentRoot implements EmitterState { - private class ExpectDocumentRoot implements EmitterState { - public void expect() throws IOException { - states.push(new ExpectDocumentEnd()); - expectNode(true, false, false); + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + if (!blockCommentsCollector.isEmpty()) { + writeBlockComment(); + if (event instanceof DocumentEndEvent) { + new ExpectDocumentEnd().expect(); + return; } + } + states.push(new ExpectDocumentEnd()); + expectNode(true, false, false); } - - // Node handlers. - - private void expectNode(boolean root, boolean mapping, boolean simpleKey) throws IOException { - rootContext = root; - mappingContext = mapping; - simpleKeyContext = simpleKey; - if (event instanceof AliasEvent) { - expectAlias(); - } else if (event instanceof ScalarEvent || event instanceof CollectionStartEvent) { - processAnchor("&"); - processTag(); - if (event instanceof ScalarEvent) { - expectScalar(); - } else if (event instanceof SequenceStartEvent) { - if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).getFlowStyle() - || checkEmptySequence()) { - expectFlowSequence(); - } else { - expectBlockSequence(); - } - } else {// MappingStartEvent - if (flowLevel != 0 || canonical || ((MappingStartEvent) event).getFlowStyle() - || checkEmptyMapping()) { - expectFlowMapping(); - } else { - expectBlockMapping(); - } - } + } + + // Node handlers. + + private void expectNode(boolean root, boolean mapping, boolean simpleKey) throws IOException { + rootContext = root; + mappingContext = mapping; + simpleKeyContext = simpleKey; + if (event instanceof AliasEvent) { + expectAlias(); + } else if (event instanceof ScalarEvent || event instanceof CollectionStartEvent) { + processAnchor("&"); + processTag(); + if (event instanceof ScalarEvent) { + expectScalar(); + } else if (event instanceof SequenceStartEvent) { + if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).isFlow() + || checkEmptySequence()) { + expectFlowSequence(); + } else { + expectBlockSequence(); + } + } else {// MappingStartEvent + if (flowLevel != 0 || canonical || ((MappingStartEvent) event).isFlow() + || checkEmptyMapping()) { + expectFlowMapping(); } else { - throw new EmitterException("expected NodeEvent, but got " + event); + expectBlockMapping(); } + } + } else { + throw new EmitterException("expected NodeEvent, but got " + event); } + } - private void expectAlias() throws IOException { - if (((NodeEvent) event).getAnchor() == null) { - throw new EmitterException("anchor is not specified for alias"); - } - processAnchor("*"); - state = states.pop(); + private void expectAlias() throws IOException { + if (!(event instanceof AliasEvent)) { + throw new EmitterException("Alias must be provided"); } + processAnchor("*"); + state = states.pop(); + } + + private void expectScalar() throws IOException { + increaseIndent(true, false); + processScalar(); + indent = indents.pop(); + state = states.pop(); + } + + // Flow sequence handlers. + + private void expectFlowSequence() throws IOException { + writeIndicator("[", true, true, false); + flowLevel++; + increaseIndent(true, false); + if (prettyFlow) { + writeIndent(); + } + state = new ExpectFirstFlowSequenceItem(); + } - private void expectScalar() throws IOException { - increaseIndent(true, false); - processScalar(); + private class ExpectFirstFlowSequenceItem implements EmitterState { + + public void expect() throws IOException { + if (event instanceof SequenceEndEvent) { indent = indents.pop(); + flowLevel--; + writeIndicator("]", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); state = states.pop(); + } else if (event instanceof CommentEvent) { + blockCommentsCollector.collectEvents(event); + writeBlockComment(); + } else { + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); + } + states.push(new ExpectFlowSequenceItem()); + expectNode(false, false, false); + event = inlineCommentsCollector.collectEvents(event); + writeInlineComments(); + } } + } - // Flow sequence handlers. + private class ExpectFlowSequenceItem implements EmitterState { - private void expectFlowSequence() throws IOException { - writeIndicator("[", true, true, false); - flowLevel++; - increaseIndent(true, false); + public void expect() throws IOException { + if (event instanceof SequenceEndEvent) { + indent = indents.pop(); + flowLevel--; + if (canonical) { + writeIndicator(",", false, false, false); + writeIndent(); + } else if (prettyFlow) { + writeIndent(); + } + writeIndicator("]", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); if (prettyFlow) { - writeIndent(); + writeIndent(); } - state = new ExpectFirstFlowSequenceItem(); + state = states.pop(); + } else if (event instanceof CommentEvent) { + event = blockCommentsCollector.collectEvents(event); + } else { + writeIndicator(",", false, false, false); + writeBlockComment(); + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); + } + states.push(new ExpectFlowSequenceItem()); + expectNode(false, false, false); + event = inlineCommentsCollector.collectEvents(event); + writeInlineComments(); + } } + } - private class ExpectFirstFlowSequenceItem implements EmitterState { - public void expect() throws IOException { - if (event instanceof SequenceEndEvent) { - indent = indents.pop(); - flowLevel--; - writeIndicator("]", false, false, false); - state = states.pop(); - } else { - if (canonical || (column > bestWidth && splitLines) || prettyFlow) { - writeIndent(); - } - states.push(new ExpectFlowSequenceItem()); - expectNode(false, false, false); - } - } + // Flow mapping handlers. + + private void expectFlowMapping() throws IOException { + writeIndicator("{", true, true, false); + flowLevel++; + increaseIndent(true, false); + if (prettyFlow) { + writeIndent(); } + state = new ExpectFirstFlowMappingKey(); + } - private class ExpectFlowSequenceItem implements EmitterState { - public void expect() throws IOException { - if (event instanceof SequenceEndEvent) { - indent = indents.pop(); - flowLevel--; - if (canonical) { - writeIndicator(",", false, false, false); - writeIndent(); - } - writeIndicator("]", false, false, false); - if (prettyFlow) { - writeIndent(); - } - state = states.pop(); - } else { - writeIndicator(",", false, false, false); - if (canonical || (column > bestWidth && splitLines) || prettyFlow) { - writeIndent(); - } - states.push(new ExpectFlowSequenceItem()); - expectNode(false, false, false); - } + private class ExpectFirstFlowMappingKey implements EmitterState { + + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (event instanceof MappingEndEvent) { + indent = indents.pop(); + flowLevel--; + writeIndicator("}", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + state = states.pop(); + } else { + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); } + if (!canonical && checkSimpleKey()) { + states.push(new ExpectFlowMappingSimpleValue()); + expectNode(false, true, true); + } else { + writeIndicator("?", true, false, false); + states.push(new ExpectFlowMappingValue()); + expectNode(false, true, false); + } + } } + } - // Flow mapping handlers. + private class ExpectFlowMappingKey implements EmitterState { - private void expectFlowMapping() throws IOException { - writeIndicator("{", true, true, false); - flowLevel++; - increaseIndent(true, false); + public void expect() throws IOException { + if (event instanceof MappingEndEvent) { + indent = indents.pop(); + flowLevel--; + if (canonical) { + writeIndicator(",", false, false, false); + writeIndent(); + } if (prettyFlow) { - writeIndent(); + writeIndent(); } - state = new ExpectFirstFlowMappingKey(); - } - - private class ExpectFirstFlowMappingKey implements EmitterState { - public void expect() throws IOException { - if (event instanceof MappingEndEvent) { - indent = indents.pop(); - flowLevel--; - writeIndicator("}", false, false, false); - state = states.pop(); - } else { - if (canonical || (column > bestWidth && splitLines) || prettyFlow) { - writeIndent(); - } - if (!canonical && checkSimpleKey()) { - states.push(new ExpectFlowMappingSimpleValue()); - expectNode(false, true, true); - } else { - writeIndicator("?", true, false, false); - states.push(new ExpectFlowMappingValue()); - expectNode(false, true, false); - } - } + writeIndicator("}", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + state = states.pop(); + } else { + writeIndicator(",", false, false, false); + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); + } + if (!canonical && checkSimpleKey()) { + states.push(new ExpectFlowMappingSimpleValue()); + expectNode(false, true, true); + } else { + writeIndicator("?", true, false, false); + states.push(new ExpectFlowMappingValue()); + expectNode(false, true, false); } + } } - - private class ExpectFlowMappingKey implements EmitterState { - public void expect() throws IOException { - if (event instanceof MappingEndEvent) { - indent = indents.pop(); - flowLevel--; - if (canonical) { - writeIndicator(",", false, false, false); - writeIndent(); - } - if (prettyFlow) { - writeIndent(); - } - writeIndicator("}", false, false, false); - state = states.pop(); - } else { - writeIndicator(",", false, false, false); - if (canonical || (column > bestWidth && splitLines) || prettyFlow) { - writeIndent(); - } - if (!canonical && checkSimpleKey()) { - states.push(new ExpectFlowMappingSimpleValue()); - expectNode(false, true, true); - } else { - writeIndicator("?", true, false, false); - states.push(new ExpectFlowMappingValue()); - expectNode(false, true, false); - } - } - } + } + + private class ExpectFlowMappingSimpleValue implements EmitterState { + + public void expect() throws IOException { + writeIndicator(":", false, false, false); + event = inlineCommentsCollector.collectEventsAndPoll(event); + writeInlineComments(); + states.push(new ExpectFlowMappingKey()); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(event); + writeInlineComments(); } - - private class ExpectFlowMappingSimpleValue implements EmitterState { - public void expect() throws IOException { - writeIndicator(":", false, false, false); - states.push(new ExpectFlowMappingKey()); - expectNode(false, true, false); - } + } + + private class ExpectFlowMappingValue implements EmitterState { + + public void expect() throws IOException { + if (canonical || (column > bestWidth) || prettyFlow) { + writeIndent(); + } + writeIndicator(":", true, false, false); + event = inlineCommentsCollector.collectEventsAndPoll(event); + writeInlineComments(); + states.push(new ExpectFlowMappingKey()); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(event); + writeInlineComments(); } + } - private class ExpectFlowMappingValue implements EmitterState { - public void expect() throws IOException { - if (canonical || (column > bestWidth) || prettyFlow) { - writeIndent(); - } - writeIndicator(":", true, false, false); - states.push(new ExpectFlowMappingKey()); - expectNode(false, true, false); - } - } + // Block sequence handlers. - // Block sequence handlers. + private void expectBlockSequence() throws IOException { + boolean indentless = mappingContext && !indention; + increaseIndent(false, indentless); + state = new ExpectFirstBlockSequenceItem(); + } - private void expectBlockSequence() throws IOException { - boolean indentless = mappingContext && !indention; - increaseIndent(false, indentless); - state = new ExpectFirstBlockSequenceItem(); - } + private class ExpectFirstBlockSequenceItem implements EmitterState { - private class ExpectFirstBlockSequenceItem implements EmitterState { - public void expect() throws IOException { - new ExpectBlockSequenceItem(true).expect(); - } + public void expect() throws IOException { + new ExpectBlockSequenceItem(true).expect(); } + } - private class ExpectBlockSequenceItem implements EmitterState { - private boolean first; + private class ExpectBlockSequenceItem implements EmitterState { - public ExpectBlockSequenceItem(boolean first) { - this.first = first; - } + private final boolean first; - public void expect() throws IOException { - if (!this.first && event instanceof SequenceEndEvent) { - indent = indents.pop(); - state = states.pop(); - } else { - writeIndent(); - writeWhitespace(indicatorIndent); - writeIndicator("-", true, false, true); - states.push(new ExpectBlockSequenceItem(false)); - expectNode(false, false, false); - } - } + public ExpectBlockSequenceItem(boolean first) { + this.first = first; } - // Block mapping handlers. - private void expectBlockMapping() throws IOException { - increaseIndent(false, false); - state = new ExpectFirstBlockMappingKey(); + public void expect() throws IOException { + if (!this.first && event instanceof SequenceEndEvent) { + indent = indents.pop(); + state = states.pop(); + } else if (event instanceof CommentEvent) { + blockCommentsCollector.collectEvents(event); + } else { + writeIndent(); + if (!indentWithIndicator || this.first) { + writeWhitespace(indicatorIndent); + } + writeIndicator("-", true, false, true); + if (indentWithIndicator && this.first) { + indent += indicatorIndent; + } + if (!blockCommentsCollector.isEmpty()) { + increaseIndent(false, false); + writeBlockComment(); + if (event instanceof ScalarEvent) { + analysis = analyzeScalar(((ScalarEvent) event).getValue()); + if (!analysis.isEmpty()) { + writeIndent(); + } + } + indent = indents.pop(); + } + states.push(new ExpectBlockSequenceItem(false)); + expectNode(false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + } } + } - private class ExpectFirstBlockMappingKey implements EmitterState { - public void expect() throws IOException { - new ExpectBlockMappingKey(true).expect(); - } + // Block mapping handlers. + private void expectBlockMapping() throws IOException { + increaseIndent(false, false); + state = new ExpectFirstBlockMappingKey(); + } + + private class ExpectFirstBlockMappingKey implements EmitterState { + + public void expect() throws IOException { + new ExpectBlockMappingKey(true).expect(); } + } - private class ExpectBlockMappingKey implements EmitterState { - private boolean first; + private class ExpectBlockMappingKey implements EmitterState { - public ExpectBlockMappingKey(boolean first) { - this.first = first; - } + private final boolean first; - public void expect() throws IOException { - if (!this.first && event instanceof MappingEndEvent) { - indent = indents.pop(); - state = states.pop(); - } else { - writeIndent(); - if (checkSimpleKey()) { - states.push(new ExpectBlockMappingSimpleValue()); - expectNode(false, true, true); - } else { - writeIndicator("?", true, false, true); - states.push(new ExpectBlockMappingValue()); - expectNode(false, true, false); - } - } - } + public ExpectBlockMappingKey(boolean first) { + this.first = first; } - private class ExpectBlockMappingSimpleValue implements EmitterState { - public void expect() throws IOException { - writeIndicator(":", false, false, false); - states.push(new ExpectBlockMappingKey(false)); - expectNode(false, true, false); + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (!this.first && event instanceof MappingEndEvent) { + indent = indents.pop(); + state = states.pop(); + } else { + writeIndent(); + if (checkSimpleKey()) { + states.push(new ExpectBlockMappingSimpleValue()); + expectNode(false, true, true); + } else { + writeIndicator("?", true, false, true); + states.push(new ExpectBlockMappingValue()); + expectNode(false, true, false); } + } } + } - private class ExpectBlockMappingValue implements EmitterState { - public void expect() throws IOException { - writeIndent(); - writeIndicator(":", true, false, true); - states.push(new ExpectBlockMappingKey(false)); - expectNode(false, true, false); - } + private boolean isFoldedOrLiteral(Event event) { + if (!event.is(ID.Scalar)) { + return false; } - - // Checkers. - - private boolean checkEmptySequence() { - return event instanceof SequenceStartEvent && !events.isEmpty() && events.peek() instanceof SequenceEndEvent; + ScalarEvent scalarEvent = (ScalarEvent) event; + ScalarStyle style = scalarEvent.getScalarStyle(); + return style == ScalarStyle.FOLDED || style == ScalarStyle.LITERAL; + } + + private class ExpectBlockMappingSimpleValue implements EmitterState { + + public void expect() throws IOException { + writeIndicator(":", false, false, false); + event = inlineCommentsCollector.collectEventsAndPoll(event); + if (!isFoldedOrLiteral(event)) { + if (writeInlineComments()) { + increaseIndent(true, false); + writeIndent(); + indent = indents.pop(); + } + } + event = blockCommentsCollector.collectEventsAndPoll(event); + if (!blockCommentsCollector.isEmpty()) { + increaseIndent(true, false); + writeBlockComment(); + writeIndent(); + indent = indents.pop(); + } + states.push(new ExpectBlockMappingKey(false)); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); } - - private boolean checkEmptyMapping() { - return event instanceof MappingStartEvent && !events.isEmpty() && events.peek() instanceof MappingEndEvent; + } + + private class ExpectBlockMappingValue implements EmitterState { + + public void expect() throws IOException { + writeIndent(); + writeIndicator(":", true, false, true); + event = inlineCommentsCollector.collectEventsAndPoll(event); + writeInlineComments(); + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + states.push(new ExpectBlockMappingKey(false)); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(event); + writeInlineComments(); } + } - private boolean checkEmptyDocument() { - if (!(event instanceof DocumentStartEvent) || events.isEmpty()) { - return false; - } - Event event = events.peek(); - if (event instanceof ScalarEvent) { - ScalarEvent e = (ScalarEvent) event; - return e.getAnchor() == null && e.getTag() == null && e.getImplicit() != null && e - .getValue().length() == 0; - } - return false; - } + // Checkers. - private boolean checkSimpleKey() { - int length = 0; - if (event instanceof NodeEvent && ((NodeEvent) event).getAnchor() != null) { - if (preparedAnchor == null) { - preparedAnchor = prepareAnchor(((NodeEvent) event).getAnchor()); - } - length += preparedAnchor.length(); - } - String tag = null; - if (event instanceof ScalarEvent) { - tag = ((ScalarEvent) event).getTag(); - } else if (event instanceof CollectionStartEvent) { - tag = ((CollectionStartEvent) event).getTag(); - } - if (tag != null) { - if (preparedTag == null) { - preparedTag = prepareTag(tag); - } - length += preparedTag.length(); - } - if (event instanceof ScalarEvent) { - if (analysis == null) { - analysis = analyzeScalar(((ScalarEvent) event).getValue()); - } - length += analysis.scalar.length(); - } - return length < 128 && (event instanceof AliasEvent - || (event instanceof ScalarEvent && !analysis.empty && !analysis.multiline) - || checkEmptySequence() || checkEmptyMapping()); - } + private boolean checkEmptySequence() { + return event instanceof SequenceStartEvent && !events.isEmpty() + && events.peek() instanceof SequenceEndEvent; + } - // Anchor, Tag, and Scalar processors. + private boolean checkEmptyMapping() { + return event instanceof MappingStartEvent && !events.isEmpty() + && events.peek() instanceof MappingEndEvent; + } - private void processAnchor(String indicator) throws IOException { - NodeEvent ev = (NodeEvent) event; - if (ev.getAnchor() == null) { - preparedAnchor = null; - return; - } - if (preparedAnchor == null) { - preparedAnchor = prepareAnchor(ev.getAnchor()); - } - writeIndicator(indicator + preparedAnchor, true, false, false); - preparedAnchor = null; + private boolean checkEmptyDocument() { + if (!(event instanceof DocumentStartEvent) || events.isEmpty()) { + return false; } - - private void processTag() throws IOException { - String tag = null; - if (event instanceof ScalarEvent) { - ScalarEvent ev = (ScalarEvent) event; - tag = ev.getTag(); - if (style == null) { - style = chooseScalarStyle(); - } - if ((!canonical || tag == null) && ((style == null && ev.getImplicit() - .canOmitTagInPlainScalar()) || (style != null && ev.getImplicit() - .canOmitTagInNonPlainScalar()))) { - preparedTag = null; - return; - } - if (ev.getImplicit().canOmitTagInPlainScalar() && tag == null) { - tag = "!"; - preparedTag = null; - } - } else { - CollectionStartEvent ev = (CollectionStartEvent) event; - tag = ev.getTag(); - if ((!canonical || tag == null) && ev.getImplicit()) { - preparedTag = null; - return; - } - } - if (tag == null) { - throw new EmitterException("tag is not specified"); - } - if (preparedTag == null) { - preparedTag = prepareTag(tag); - } - writeIndicator(preparedTag, true, false, false); + Event event = events.peek(); + if (event instanceof ScalarEvent) { + ScalarEvent e = (ScalarEvent) event; + return e.getAnchor() == null && e.getTag() == null && e.getImplicit() != null + && e.getValue().length() == 0; + } + return false; + } + + private boolean checkSimpleKey() { + int length = 0; + if (event instanceof NodeEvent && ((NodeEvent) event).getAnchor() != null) { + if (preparedAnchor == null) { + preparedAnchor = prepareAnchor(((NodeEvent) event).getAnchor()); + } + length += preparedAnchor.length(); + } + String tag = null; + if (event instanceof ScalarEvent) { + tag = ((ScalarEvent) event).getTag(); + } else if (event instanceof CollectionStartEvent) { + tag = ((CollectionStartEvent) event).getTag(); + } + if (tag != null) { + if (preparedTag == null) { + preparedTag = prepareTag(tag); + } + length += preparedTag.length(); + } + if (event instanceof ScalarEvent) { + if (analysis == null) { + analysis = analyzeScalar(((ScalarEvent) event).getValue()); + } + length += analysis.getScalar().length(); + } + return length < maxSimpleKeyLength && (event instanceof AliasEvent + || (event instanceof ScalarEvent && !analysis.isEmpty() && !analysis.isMultiline()) + || checkEmptySequence() || checkEmptyMapping()); + } + + // Anchor, Tag, and Scalar processors. + + private void processAnchor(String indicator) throws IOException { + NodeEvent ev = (NodeEvent) event; + if (ev.getAnchor() == null) { + preparedAnchor = null; + return; + } + if (preparedAnchor == null) { + preparedAnchor = prepareAnchor(ev.getAnchor()); + } + writeIndicator(indicator + preparedAnchor, true, false, false); + preparedAnchor = null; + } + + private void processTag() throws IOException { + String tag = null; + if (event instanceof ScalarEvent) { + ScalarEvent ev = (ScalarEvent) event; + tag = ev.getTag(); + if (style == null) { + style = chooseScalarStyle(); + } + if ((!canonical || tag == null) + && ((style == null && ev.getImplicit().canOmitTagInPlainScalar()) + || (style != null && ev.getImplicit().canOmitTagInNonPlainScalar()))) { + preparedTag = null; + return; + } + if (ev.getImplicit().canOmitTagInPlainScalar() && tag == null) { + tag = "!"; preparedTag = null; + } + } else { + CollectionStartEvent ev = (CollectionStartEvent) event; + tag = ev.getTag(); + if ((!canonical || tag == null) && ev.getImplicit()) { + preparedTag = null; + return; + } } - - private Character chooseScalarStyle() { - ScalarEvent ev = (ScalarEvent) event; - if (analysis == null) { - analysis = analyzeScalar(ev.getValue()); - } - if (ev.getStyle() != null && ev.getStyle() == '"' || this.canonical) { - return '"'; - } - if (ev.getStyle() == null && ev.getImplicit().canOmitTagInPlainScalar()) { - if (!(simpleKeyContext && (analysis.empty || analysis.multiline)) - && ((flowLevel != 0 && analysis.allowFlowPlain) || (flowLevel == 0 && analysis.allowBlockPlain))) { - return null; - } - } - if (ev.getStyle() != null && (ev.getStyle() == '|' || ev.getStyle() == '>')) { - if (flowLevel == 0 && !simpleKeyContext && analysis.allowBlock) { - return ev.getStyle(); - } - } - if (ev.getStyle() == null || ev.getStyle() == '\'') { - if (analysis.allowSingleQuoted && !(simpleKeyContext && analysis.multiline)) { - return '\''; - } - } - return '"'; + if (tag == null) { + throw new EmitterException("tag is not specified"); } - - private void processScalar() throws IOException { - ScalarEvent ev = (ScalarEvent) event; - if (analysis == null) { - analysis = analyzeScalar(ev.getValue()); - } - if (style == null) { - style = chooseScalarStyle(); - } - boolean split = !simpleKeyContext && splitLines; - if (style == null) { - writePlain(analysis.scalar, split); - } else { - switch (style) { - case '"': - writeDoubleQuoted(analysis.scalar, split); - break; - case '\'': - writeSingleQuoted(analysis.scalar, split); - break; - case '>': - writeFolded(analysis.scalar, split); - break; - case '|': - writeLiteral(analysis.scalar); - break; - default: - throw new YAMLException("Unexpected style: " + style); - } - } - analysis = null; - style = null; + if (preparedTag == null) { + preparedTag = prepareTag(tag); } + writeIndicator(preparedTag, true, false, false); + preparedTag = null; + } + + private DumperOptions.ScalarStyle chooseScalarStyle() { + ScalarEvent ev = (ScalarEvent) event; + if (analysis == null) { + analysis = analyzeScalar(ev.getValue()); + } + if (!ev.isPlain() && ev.getScalarStyle() == DumperOptions.ScalarStyle.DOUBLE_QUOTED + || this.canonical) { + return DumperOptions.ScalarStyle.DOUBLE_QUOTED; + } + if (ev.isPlain() && ev.getImplicit().canOmitTagInPlainScalar()) { + if (!(simpleKeyContext && (analysis.isEmpty() || analysis.isMultiline())) + && ((flowLevel != 0 && analysis.isAllowFlowPlain()) + || (flowLevel == 0 && analysis.isAllowBlockPlain()))) { + return null; + } + } + if (!ev.isPlain() && (ev.getScalarStyle() == DumperOptions.ScalarStyle.LITERAL + || ev.getScalarStyle() == DumperOptions.ScalarStyle.FOLDED)) { + if (flowLevel == 0 && !simpleKeyContext && analysis.isAllowBlock()) { + return ev.getScalarStyle(); + } + } + if (ev.isPlain() || ev.getScalarStyle() == DumperOptions.ScalarStyle.SINGLE_QUOTED) { + if (analysis.isAllowSingleQuoted() && !(simpleKeyContext && analysis.isMultiline())) { + return DumperOptions.ScalarStyle.SINGLE_QUOTED; + } + } + return DumperOptions.ScalarStyle.DOUBLE_QUOTED; + } - // Analyzers. - - private String prepareVersion(Version version) { - if (version.major() != 1) { - throw new EmitterException("unsupported YAML version: " + version); - } - return version.getRepresentation(); + private void processScalar() throws IOException { + ScalarEvent ev = (ScalarEvent) event; + if (analysis == null) { + analysis = analyzeScalar(ev.getValue()); + } + if (style == null) { + style = chooseScalarStyle(); } + boolean split = !simpleKeyContext && splitLines; + if (style == null) { + writePlain(analysis.getScalar(), split); + } else { + switch (style) { + case DOUBLE_QUOTED: + writeDoubleQuoted(analysis.getScalar(), split); + break; + case SINGLE_QUOTED: + writeSingleQuoted(analysis.getScalar(), split); + break; + case FOLDED: + writeFolded(analysis.getScalar(), split); + break; + case LITERAL: + writeLiteral(analysis.getScalar()); + break; + default: + throw new YAMLException("Unexpected style: " + style); + } + } + analysis = null; + style = null; + } - private final static Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$"); + // Analyzers. - private String prepareTagHandle(String handle) { - if (handle.length() == 0) { - throw new EmitterException("tag handle must not be empty"); - } else if (handle.charAt(0) != '!' || handle.charAt(handle.length() - 1) != '!') { - throw new EmitterException("tag handle must start and end with '!': " + handle); - } else if (!"!".equals(handle) && !HANDLE_FORMAT.matcher(handle).matches()) { - throw new EmitterException("invalid character in the tag handle: " + handle); - } - return handle; + private String prepareVersion(Version version) { + if (version.major() != 1) { + throw new EmitterException("unsupported YAML version: " + version); + } + return version.getRepresentation(); + } + + private final static Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$"); + + private String prepareTagHandle(String handle) { + if (handle.length() == 0) { + throw new EmitterException("tag handle must not be empty"); + } else if (handle.charAt(0) != '!' || handle.charAt(handle.length() - 1) != '!') { + throw new EmitterException("tag handle must start and end with '!': " + handle); + } else if (!"!".equals(handle) && !HANDLE_FORMAT.matcher(handle).matches()) { + throw new EmitterException("invalid character in the tag handle: " + handle); } + return handle; + } - private String prepareTagPrefix(String prefix) { - if (prefix.length() == 0) { - throw new EmitterException("tag prefix must not be empty"); - } - StringBuilder chunks = new StringBuilder(); - int start = 0; - int end = 0; - if (prefix.charAt(0) == '!') { - end = 1; - } - while (end < prefix.length()) { - end++; - } - if (start < end) { - chunks.append(prefix.substring(start, end)); - } - return chunks.toString(); + private String prepareTagPrefix(String prefix) { + if (prefix.length() == 0) { + throw new EmitterException("tag prefix must not be empty"); + } + StringBuilder chunks = new StringBuilder(); + int start = 0; + int end = 0; + if (prefix.charAt(0) == '!') { + end = 1; + } + while (end < prefix.length()) { + end++; } + if (start < end) { + chunks.append(prefix, start, end); + } + return chunks.toString(); + } - private String prepareTag(String tag) { - if (tag.length() == 0) { - throw new EmitterException("tag must not be empty"); - } - if ("!".equals(tag)) { - return tag; - } - String handle = null; - String suffix = tag; - // shall the tag prefixes be sorted as in PyYAML? - for (String prefix : tagPrefixes.keySet()) { - if (tag.startsWith(prefix) && ("!".equals(prefix) || prefix.length() < tag.length())) { - handle = prefix; - } - } - if (handle != null) { - suffix = tag.substring(handle.length()); - handle = tagPrefixes.get(handle); - } + private String prepareTag(String tag) { + if (tag.length() == 0) { + throw new EmitterException("tag must not be empty"); + } + if ("!".equals(tag)) { + return tag; + } + String handle = null; + String suffix = tag; + // shall the tag prefixes be sorted as in PyYAML? + for (String prefix : tagPrefixes.keySet()) { + if (tag.startsWith(prefix) && ("!".equals(prefix) || prefix.length() < tag.length())) { + handle = prefix; + } + } + if (handle != null) { + suffix = tag.substring(handle.length()); + handle = tagPrefixes.get(handle); + } - int end = suffix.length(); - String suffixText = end > 0 ? suffix.substring(0, end) : ""; + int end = suffix.length(); + String suffixText = end > 0 ? suffix.substring(0, end) : ""; - if (handle != null) { - return handle + suffixText; - } - return "!<" + suffixText + ">"; + if (handle != null) { + return handle + suffixText; } + return "!<" + suffixText + ">"; + } - private final static Pattern ANCHOR_FORMAT = Pattern.compile("^[-_\\w]*$"); - - static String prepareAnchor(String anchor) { - if (anchor.length() == 0) { - throw new EmitterException("anchor must not be empty"); - } - if (!ANCHOR_FORMAT.matcher(anchor).matches()) { - throw new EmitterException("invalid character in the anchor: " + anchor); - } - return anchor; + static String prepareAnchor(String anchor) { + if (anchor.length() == 0) { + throw new EmitterException("anchor must not be empty"); } + for (Character invalid : INVALID_ANCHOR) { + if (anchor.indexOf(invalid) > -1) { + throw new EmitterException("Invalid character '" + invalid + "' in the anchor: " + anchor); + } + } + Matcher matcher = SPACES_PATTERN.matcher(anchor); + if (matcher.find()) { + throw new EmitterException("Anchor may not contain spaces: " + anchor); + } + return anchor; + } - private ScalarAnalysis analyzeScalar(String scalar) { - // Empty scalar is a special case. - if (scalar.length() == 0) { - return new ScalarAnalysis(scalar, true, false, false, true, true, false); - } - // Indicators and special characters. - boolean blockIndicators = false; - boolean flowIndicators = false; - boolean lineBreaks = false; - boolean specialCharacters = false; - - // Important whitespace combinations. - boolean leadingSpace = false; - boolean leadingBreak = false; - boolean trailingSpace = false; - boolean trailingBreak = false; - boolean breakSpace = false; - boolean spaceBreak = false; - - // Check document indicators. - if (scalar.startsWith("---") || scalar.startsWith("...")) { + private ScalarAnalysis analyzeScalar(String scalar) { + // Empty scalar is a special case. + if (scalar.length() == 0) { + return new ScalarAnalysis(scalar, true, false, false, true, true, false); + } + // Indicators and special characters. + boolean blockIndicators = false; + boolean flowIndicators = false; + boolean lineBreaks = false; + boolean specialCharacters = false; + + // Important whitespace combinations. + boolean leadingSpace = false; + boolean leadingBreak = false; + boolean trailingSpace = false; + boolean trailingBreak = false; + boolean breakSpace = false; + boolean spaceBreak = false; + + // Check document indicators. + if (scalar.startsWith("---") || scalar.startsWith("...")) { + blockIndicators = true; + flowIndicators = true; + } + // First character or preceded by a whitespace. + boolean preceededByWhitespace = true; + boolean followedByWhitespace = + scalar.length() == 1 || Constant.NULL_BL_T_LINEBR.has(scalar.codePointAt(1)); + // The previous character is a space. + boolean previousSpace = false; + + // The previous character is a break. + boolean previousBreak = false; + + int index = 0; + + while (index < scalar.length()) { + int c = scalar.codePointAt(index); + // Check for indicators. + if (index == 0) { + // Leading indicators are special characters. + if ("#,[]{}&*!|>'\"%@`".indexOf(c) != -1) { + flowIndicators = true; + blockIndicators = true; + } + if (c == '?' || c == ':') { + flowIndicators = true; + if (followedByWhitespace) { blockIndicators = true; - flowIndicators = true; + } } - // First character or preceded by a whitespace. - boolean preceededByWhitespace = true; - boolean followedByWhitespace = scalar.length() == 1 || Constant.NULL_BL_T_LINEBR.has(scalar.charAt(1)); - // The previous character is a space. - boolean previousSpace = false; - - // The previous character is a break. - boolean previousBreak = false; - - int index = 0; - - while (index < scalar.length()) { - char ch = scalar.charAt(index); - // Check for indicators. - if (index == 0) { - // Leading indicators are special characters. - if ("#,[]{}&*!|>\'\"%@`".indexOf(ch) != -1) { - flowIndicators = true; - blockIndicators = true; - } - if (ch == '?' || ch == ':') { - flowIndicators = true; - if (followedByWhitespace) { - blockIndicators = true; - } - } - if (ch == '-' && followedByWhitespace) { - flowIndicators = true; - blockIndicators = true; - } - } else { - // Some indicators cannot appear within a scalar as well. - if (",?[]{}".indexOf(ch) != -1) { - flowIndicators = true; - } - if (ch == ':') { - flowIndicators = true; - if (followedByWhitespace) { - blockIndicators = true; - } - } - if (ch == '#' && preceededByWhitespace) { - flowIndicators = true; - blockIndicators = true; - } - } - // Check for line breaks, special, and unicode characters. - boolean isLineBreak = Constant.LINEBR.has(ch); - if (isLineBreak) { - lineBreaks = true; - } - if (!(ch == '\n' || ('\u0020' <= ch && ch <= '\u007E'))) { - if ((ch == '\u0085' || ('\u00A0' <= ch && ch <= '\uD7FF') || ('\uE000' <= ch && ch <= '\uFFFD')) - && (ch != '\uFEFF')) { - // unicode is used - if (!this.allowUnicode) { - specialCharacters = true; - } - } else { - specialCharacters = true; - } - } - // Detect important whitespace combinations. - if (ch == ' ') { - if (index == 0) { - leadingSpace = true; - } - if (index == scalar.length() - 1) { - trailingSpace = true; - } - if (previousBreak) { - breakSpace = true; - } - previousSpace = true; - previousBreak = false; - } else if (isLineBreak) { - if (index == 0) { - leadingBreak = true; - } - if (index == scalar.length() - 1) { - trailingBreak = true; - } - if (previousSpace) { - spaceBreak = true; - } - previousSpace = false; - previousBreak = true; - } else { - previousSpace = false; - previousBreak = false; - } - - // Prepare for the next character. - index++; - preceededByWhitespace = Constant.NULL_BL_T.has(ch) || isLineBreak; - followedByWhitespace = index + 1 >= scalar.length() - || (Constant.NULL_BL_T.has(scalar.charAt(index + 1))) || isLineBreak; - } - // Let's decide what styles are allowed. - boolean allowFlowPlain = true; - boolean allowBlockPlain = true; - boolean allowSingleQuoted = true; - boolean allowBlock = true; - // Leading and trailing whitespaces are bad for plain scalars. - if (leadingSpace || leadingBreak || trailingSpace || trailingBreak) { - allowFlowPlain = allowBlockPlain = false; - } - // We do not permit trailing spaces for block scalars. - if (trailingSpace) { - allowBlock = false; - } - // Spaces at the beginning of a new line are only acceptable for block - // scalars. - if (breakSpace) { - allowFlowPlain = allowBlockPlain = allowSingleQuoted = false; - } - // Spaces followed by breaks, as well as special character are only - // allowed for double quoted scalars. - if (spaceBreak || specialCharacters) { - allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false; - } - // Although the plain scalar writer supports breaks, we never emit - // multiline plain scalars in the flow context. - if (lineBreaks) { - allowFlowPlain = false; - } - // Flow indicators are forbidden for flow plain scalars. - if (flowIndicators) { - allowFlowPlain = false; + if (c == '-' && followedByWhitespace) { + flowIndicators = true; + blockIndicators = true; } - // Block indicators are forbidden for block plain scalars. - if (blockIndicators) { - allowBlockPlain = false; + } else { + // Some indicators cannot appear within a scalar as well. + if (",?[]{}".indexOf(c) != -1) { + flowIndicators = true; } - - return new ScalarAnalysis(scalar, false, lineBreaks, allowFlowPlain, allowBlockPlain, - allowSingleQuoted, allowBlock); + if (c == ':') { + flowIndicators = true; + if (followedByWhitespace) { + blockIndicators = true; + } + } + if (c == '#' && preceededByWhitespace) { + flowIndicators = true; + blockIndicators = true; + } + } + // Check for line breaks, special, and unicode characters. + boolean isLineBreak = Constant.LINEBR.has(c); + if (isLineBreak) { + lineBreaks = true; + } + if (!(c == '\n' || (0x20 <= c && c <= 0x7E))) { + if (c == 0x85 || (c >= 0xA0 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD) + || (c >= 0x10000 && c <= 0x10FFFF)) { + // unicode is used + if (!this.allowUnicode) { + specialCharacters = true; + } + } else { + specialCharacters = true; + } + } + // Detect important whitespace combinations. + if (c == ' ') { + if (index == 0) { + leadingSpace = true; + } + if (index == scalar.length() - 1) { + trailingSpace = true; + } + if (previousBreak) { + breakSpace = true; + } + previousSpace = true; + previousBreak = false; + } else if (isLineBreak) { + if (index == 0) { + leadingBreak = true; + } + if (index == scalar.length() - 1) { + trailingBreak = true; + } + if (previousSpace) { + spaceBreak = true; + } + previousSpace = false; + previousBreak = true; + } else { + previousSpace = false; + previousBreak = false; + } + + // Prepare for the next character. + index += Character.charCount(c); + preceededByWhitespace = Constant.NULL_BL_T.has(c) || isLineBreak; + followedByWhitespace = true; + if (index + 1 < scalar.length()) { + int nextIndex = index + Character.charCount(scalar.codePointAt(index)); + if (nextIndex < scalar.length()) { + followedByWhitespace = + (Constant.NULL_BL_T.has(scalar.codePointAt(nextIndex))) || isLineBreak; + } + } + } + // Let's decide what styles are allowed. + boolean allowFlowPlain = true; + boolean allowBlockPlain = true; + boolean allowSingleQuoted = true; + boolean allowBlock = true; + // Leading and trailing whitespaces are bad for plain scalars. + if (leadingSpace || leadingBreak || trailingSpace || trailingBreak) { + allowFlowPlain = allowBlockPlain = false; + } + // We do not permit trailing spaces for block scalars. + if (trailingSpace) { + allowBlock = false; + } + // Spaces at the beginning of a new line are only acceptable for block + // scalars. + if (breakSpace) { + allowFlowPlain = allowBlockPlain = allowSingleQuoted = false; + } + // Spaces followed by breaks, as well as special character are only + // allowed for double quoted scalars. + if (spaceBreak || specialCharacters) { + allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false; + } + // Although the plain scalar writer supports breaks, we never emit + // multiline plain scalars in the flow context. + if (lineBreaks) { + allowFlowPlain = false; + } + // Flow indicators are forbidden for flow plain scalars. + if (flowIndicators) { + allowFlowPlain = false; + } + // Block indicators are forbidden for block plain scalars. + if (blockIndicators) { + allowBlockPlain = false; } - // Writers. + return new ScalarAnalysis(scalar, false, lineBreaks, allowFlowPlain, allowBlockPlain, + allowSingleQuoted, allowBlock); + } - void flushStream() throws IOException { - stream.flush(); - } + // Writers. - void writeStreamStart() { - // BOM is written by Writer. - } + void flushStream() throws IOException { + stream.flush(); + } - void writeStreamEnd() throws IOException { - flushStream(); + void writeStreamStart() { + // BOM is written by Writer. + } + + void writeStreamEnd() throws IOException { + flushStream(); + } + + void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace, + boolean indentation) throws IOException { + if (!this.whitespace && needWhitespace) { + this.column++; + stream.write(SPACE); + } + this.whitespace = whitespace; + this.indention = this.indention && indentation; + this.column += indicator.length(); + openEnded = false; + stream.write(indicator); + } + + void writeIndent() throws IOException { + int indent; + if (this.indent != null) { + indent = this.indent; + } else { + indent = 0; } - void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace, - boolean indentation) throws IOException { - if (!this.whitespace && needWhitespace) { - this.column++; - stream.write(SPACE); - } - this.whitespace = whitespace; - this.indention = this.indention && indentation; - this.column += indicator.length(); - openEnded = false; - stream.write(indicator); + if (!this.indention || this.column > indent || (this.column == indent && !this.whitespace)) { + writeLineBreak(null); } - void writeIndent() throws IOException { - int indent; - if (this.indent != null) { - indent = this.indent; - } else { - indent = 0; - } + writeWhitespace(indent - this.column); + } - if (!this.indention || this.column > indent || (this.column == indent && !this.whitespace)) { + private void writeWhitespace(int length) throws IOException { + if (length <= 0) { + return; + } + this.whitespace = true; + char[] data = new char[length]; + for (int i = 0; i < data.length; i++) { + data[i] = ' '; + } + this.column += length; + stream.write(data); + } + + private void writeLineBreak(String data) throws IOException { + this.whitespace = true; + this.indention = true; + this.column = 0; + if (data == null) { + stream.write(this.bestLineBreak); + } else { + stream.write(data); + } + } + + void writeVersionDirective(String versionText) throws IOException { + stream.write("%YAML "); + stream.write(versionText); + writeLineBreak(null); + } + + void writeTagDirective(String handleText, String prefixText) throws IOException { + // XXX: not sure 4 invocations better then StringBuilders created by str + // + str + stream.write("%TAG "); + stream.write(handleText); + stream.write(SPACE); + stream.write(prefixText); + writeLineBreak(null); + } + + // Scalar streams. + private void writeSingleQuoted(String text, boolean split) throws IOException { + writeIndicator("'", true, false, false); + boolean spaces = false; + boolean breaks = false; + int start = 0, end = 0; + char ch; + while (end <= text.length()) { + ch = 0; + if (end < text.length()) { + ch = text.charAt(end); + } + if (spaces) { + if (ch == 0 || ch != ' ') { + if (start + 1 == end && this.column > this.bestWidth && split && start != 0 + && end != text.length()) { + writeIndent(); + } else { + int len = end - start; + this.column += len; + stream.write(text, start, len); + } + start = end; + } + } else if (breaks) { + if (ch == 0 || Constant.LINEBR.hasNo(ch)) { + if (text.charAt(start) == '\n') { writeLineBreak(null); - } - - writeWhitespace(indent - this.column); + } + String data = text.substring(start, end); + for (char br : data.toCharArray()) { + if (br == '\n') { + writeLineBreak(null); + } else { + writeLineBreak(String.valueOf(br)); + } + } + writeIndent(); + start = end; + } + } else { + if (Constant.LINEBR.has(ch, "\0 '")) { + if (start < end) { + int len = end - start; + this.column += len; + stream.write(text, start, len); + start = end; + } + } + } + if (ch == '\'') { + this.column += 2; + stream.write("''"); + start = end + 1; + } + if (ch != 0) { + spaces = ch == ' '; + breaks = Constant.LINEBR.has(ch); + } + end++; } + writeIndicator("'", false, false, false); + } + + private void writeDoubleQuoted(String text, boolean split) throws IOException { + writeIndicator("\"", true, false, false); + int start = 0; + int end = 0; + while (end <= text.length()) { + Character ch = null; + if (end < text.length()) { + ch = text.charAt(end); + } + if (ch == null || "\"\\\u0085\u2028\u2029\uFEFF".indexOf(ch) != -1 + || !('\u0020' <= ch && ch <= '\u007E')) { + if (start < end) { + int len = end - start; + this.column += len; + stream.write(text, start, len); + start = end; + } + if (ch != null) { + String data; + + if (ESCAPE_REPLACEMENTS.containsKey(ch)) { + data = "\\" + ESCAPE_REPLACEMENTS.get(ch); + } else { + int codePoint; + + if (Character.isHighSurrogate(ch) && end + 1 < text.length()) { + char ch2 = text.charAt(end + 1); + codePoint = Character.toCodePoint(ch, ch2); + } else { + codePoint = ch; + } + + if (this.allowUnicode && StreamReader.isPrintable(codePoint)) { + data = String.valueOf(Character.toChars(codePoint)); - private void writeWhitespace(int length) throws IOException { - if (length <= 0) { - return; + if (Character.charCount(codePoint) == 2) { + end++; + } + } else { + // if !allowUnicode or the character is not printable, + // we must encode it + if (ch <= '\u00FF') { + String s = "0" + Integer.toString(ch, 16); + data = "\\x" + s.substring(s.length() - 2); + } else if (Character.charCount(codePoint) == 2) { + end++; + String s = "000" + Long.toHexString(codePoint); + data = "\\U" + s.substring(s.length() - 8); + } else { + String s = "000" + Integer.toString(ch, 16); + data = "\\u" + s.substring(s.length() - 4); + } + } + } + + this.column += data.length(); + stream.write(data); + start = end + 1; + } + } + if ((0 < end && end < (text.length() - 1)) && (ch == ' ' || start >= end) + && (this.column + (end - start)) > this.bestWidth && split) { + String data; + if (start >= end) { + data = "\\"; + } else { + data = text.substring(start, end) + "\\"; } - this.whitespace = true; - char[] data = new char[length]; - for (int i = 0; i < data.length; i++) { - data[i] = ' '; + if (start < end) { + start = end; } - this.column += length; + this.column += data.length(); stream.write(data); + writeIndent(); + this.whitespace = false; + this.indention = false; + if (text.charAt(start) == ' ') { + data = "\\"; + this.column += data.length(); + stream.write(data); + } + } + end += 1; } - - private void writeLineBreak(String data) throws IOException { - this.whitespace = true; - this.indention = true; - this.column = 0; - if (data == null) { - stream.write(this.bestLineBreak); + writeIndicator("\"", false, false, false); + } + + private boolean writeCommentLines(List<CommentLine> commentLines) throws IOException { + boolean wroteComment = false; + if (emitComments) { + int indentColumns = 0; + boolean firstComment = true; + for (CommentLine commentLine : commentLines) { + if (commentLine.getCommentType() != CommentType.BLANK_LINE) { + if (firstComment) { + firstComment = false; + writeIndicator("#", commentLine.getCommentType() == CommentType.IN_LINE, false, false); + indentColumns = this.column > 0 ? this.column - 1 : 0; + } else { + writeWhitespace(indentColumns); + writeIndicator("#", false, false, false); + } + stream.write(commentLine.getValue()); + writeLineBreak(null); } else { - stream.write(data); + writeLineBreak(null); + writeIndent(); } + wroteComment = true; + } } + return wroteComment; + } - void writeVersionDirective(String versionText) throws IOException { - stream.write("%YAML "); - stream.write(versionText); - writeLineBreak(null); - } - - void writeTagDirective(String handleText, String prefixText) throws IOException { - // XXX: not sure 4 invocations better then StringBuilders created by str - // + str - stream.write("%TAG "); - stream.write(handleText); - stream.write(SPACE); - stream.write(prefixText); - writeLineBreak(null); - } - - // Scalar streams. - private void writeSingleQuoted(String text, boolean split) throws IOException { - writeIndicator("'", true, false, false); - boolean spaces = false; - boolean breaks = false; - int start = 0, end = 0; - char ch; - while (end <= text.length()) { - ch = 0; - if (end < text.length()) { - ch = text.charAt(end); - } - if (spaces) { - if (ch == 0 || ch != ' ') { - if (start + 1 == end && this.column > this.bestWidth && split && start != 0 - && end != text.length()) { - writeIndent(); - } else { - int len = end - start; - this.column += len; - stream.write(text, start, len); - } - start = end; - } - } else if (breaks) { - if (ch == 0 || Constant.LINEBR.hasNo(ch)) { - if (text.charAt(start) == '\n') { - writeLineBreak(null); - } - String data = text.substring(start, end); - for (char br : data.toCharArray()) { - if (br == '\n') { - writeLineBreak(null); - } else { - writeLineBreak(String.valueOf(br)); - } - } - writeIndent(); - start = end; - } - } else { - if (Constant.LINEBR.has(ch, "\0 \'")) { - if (start < end) { - int len = end - start; - this.column += len; - stream.write(text, start, len); - start = end; - } - } - } - if (ch == '\'') { - this.column += 2; - stream.write("''"); - start = end + 1; - } - if (ch != 0) { - spaces = ch == ' '; - breaks = Constant.LINEBR.has(ch); - } - end++; - } - writeIndicator("'", false, false, false); + private void writeBlockComment() throws IOException { + if (!blockCommentsCollector.isEmpty()) { + writeIndent(); + writeCommentLines(blockCommentsCollector.consume()); } + } - private void writeDoubleQuoted(String text, boolean split) throws IOException { - writeIndicator("\"", true, false, false); - int start = 0; - int end = 0; - while (end <= text.length()) { - Character ch = null; - if (end < text.length()) { - ch = text.charAt(end); - } - if (ch == null || "\"\\\u0085\u2028\u2029\uFEFF".indexOf(ch) != -1 - || !('\u0020' <= ch && ch <= '\u007E')) { - if (start < end) { - int len = end - start; - this.column += len; - stream.write(text, start, len); - start = end; - } - if (ch != null) { - String data; - if (ESCAPE_REPLACEMENTS.containsKey(ch)) { - data = "\\" + ESCAPE_REPLACEMENTS.get(ch); - } else if (!this.allowUnicode || !StreamReader.isPrintable(ch)) { - // if !allowUnicode or the character is not printable, - // we must encode it - if (ch <= '\u00FF') { - String s = "0" + Integer.toString(ch, 16); - data = "\\x" + s.substring(s.length() - 2); - } else if (ch >= '\uD800' && ch <= '\uDBFF') { - if (end + 1 < text.length()) { - Character ch2 = text.charAt(++end); - String s = "000" + Long.toHexString(Character.toCodePoint(ch, ch2)); - data = "\\U" + s.substring(s.length() - 8); - } else { - String s = "000" + Integer.toString(ch, 16); - data = "\\u" + s.substring(s.length() - 4); - } - } else { - String s = "000" + Integer.toString(ch, 16); - data = "\\u" + s.substring(s.length() - 4); - } - } else { - data = String.valueOf(ch); - } - this.column += data.length(); - stream.write(data); - start = end + 1; - } - } - if ((0 < end && end < (text.length() - 1)) && (ch == ' ' || start >= end) - && (this.column + (end - start)) > this.bestWidth && split) { - String data; - if (start >= end) { - data = "\\"; - } else { - data = text.substring(start, end) + "\\"; - } - if (start < end) { - start = end; - } - this.column += data.length(); - stream.write(data); - writeIndent(); - this.whitespace = false; - this.indention = false; - if (text.charAt(start) == ' ') { - data = "\\"; - this.column += data.length(); - stream.write(data); - } - } - end += 1; - } - writeIndicator("\"", false, false, false); - } + private boolean writeInlineComments() throws IOException { + return writeCommentLines(inlineCommentsCollector.consume()); + } - private String determineBlockHints(String text) { - StringBuilder hints = new StringBuilder(); - if (Constant.LINEBR.has(text.charAt(0), " ")) { - hints.append(bestIndent); - } - char ch1 = text.charAt(text.length() - 1); - if (Constant.LINEBR.hasNo(ch1)) { - hints.append("-"); - } else if (text.length() == 1 || Constant.LINEBR.has(text.charAt(text.length() - 2))) { - hints.append("+"); - } - return hints.toString(); + private String determineBlockHints(String text) { + StringBuilder hints = new StringBuilder(); + if (Constant.LINEBR.has(text.charAt(0), " ")) { + hints.append(bestIndent); } - - void writeFolded(String text, boolean split) throws IOException { - String hints = determineBlockHints(text); - writeIndicator(">" + hints, true, false, false); - if (hints.length() > 0 && (hints.charAt(hints.length() - 1) == '+')) { - openEnded = true; - } - writeLineBreak(null); - boolean leadingSpace = true; - boolean spaces = false; - boolean breaks = true; - int start = 0, end = 0; - while (end <= text.length()) { - char ch = 0; - if (end < text.length()) { - ch = text.charAt(end); - } - if (breaks) { - if (ch == 0 || Constant.LINEBR.hasNo(ch)) { - if (!leadingSpace && ch != 0 && ch != ' ' && text.charAt(start) == '\n') { - writeLineBreak(null); - } - leadingSpace = ch == ' '; - String data = text.substring(start, end); - for (char br : data.toCharArray()) { - if (br == '\n') { - writeLineBreak(null); - } else { - writeLineBreak(String.valueOf(br)); - } - } - if (ch != 0) { - writeIndent(); - } - start = end; - } - } else if (spaces) { - if (ch != ' ') { - if (start + 1 == end && this.column > this.bestWidth && split) { - writeIndent(); - } else { - int len = end - start; - this.column += len; - stream.write(text, start, len); - } - start = end; - } + char ch1 = text.charAt(text.length() - 1); + if (Constant.LINEBR.hasNo(ch1)) { + hints.append("-"); + } else if (text.length() == 1 || Constant.LINEBR.has(text.charAt(text.length() - 2))) { + hints.append("+"); + } + return hints.toString(); + } + + void writeFolded(String text, boolean split) throws IOException { + String hints = determineBlockHints(text); + writeIndicator(">" + hints, true, false, false); + if (hints.length() > 0 && (hints.charAt(hints.length() - 1) == '+')) { + openEnded = true; + } + if (!writeInlineComments()) { + writeLineBreak(null); + } + boolean leadingSpace = true; + boolean spaces = false; + boolean breaks = true; + int start = 0, end = 0; + while (end <= text.length()) { + char ch = 0; + if (end < text.length()) { + ch = text.charAt(end); + } + if (breaks) { + if (ch == 0 || Constant.LINEBR.hasNo(ch)) { + if (!leadingSpace && ch != 0 && ch != ' ' && text.charAt(start) == '\n') { + writeLineBreak(null); + } + leadingSpace = ch == ' '; + String data = text.substring(start, end); + for (char br : data.toCharArray()) { + if (br == '\n') { + writeLineBreak(null); } else { - if (Constant.LINEBR.has(ch, "\0 ")) { - int len = end - start; - this.column += len; - stream.write(text, start, len); - if (ch == 0) { - writeLineBreak(null); - } - start = end; - } + writeLineBreak(String.valueOf(br)); } - if (ch != 0) { - breaks = Constant.LINEBR.has(ch); - spaces = ch == ' '; - } - end++; + } + if (ch != 0) { + writeIndent(); + } + start = end; } + } else if (spaces) { + if (ch != ' ') { + if (start + 1 == end && this.column > this.bestWidth && split) { + writeIndent(); + } else { + int len = end - start; + this.column += len; + stream.write(text, start, len); + } + start = end; + } + } else { + if (Constant.LINEBR.has(ch, "\0 ")) { + int len = end - start; + this.column += len; + stream.write(text, start, len); + if (ch == 0) { + writeLineBreak(null); + } + start = end; + } + } + if (ch != 0) { + breaks = Constant.LINEBR.has(ch); + spaces = ch == ' '; + } + end++; } + } - void writeLiteral(String text) throws IOException { - String hints = determineBlockHints(text); - writeIndicator("|" + hints, true, false, false); - if (hints.length() > 0 && (hints.charAt(hints.length() - 1)) == '+') { - openEnded = true; - } - writeLineBreak(null); - boolean breaks = true; - int start = 0, end = 0; - while (end <= text.length()) { - char ch = 0; - if (end < text.length()) { - ch = text.charAt(end); - } - if (breaks) { - if (ch == 0 || Constant.LINEBR.hasNo(ch)) { - String data = text.substring(start, end); - for (char br : data.toCharArray()) { - if (br == '\n') { - writeLineBreak(null); - } else { - writeLineBreak(String.valueOf(br)); - } - } - if (ch != 0) { - writeIndent(); - } - start = end; - } + void writeLiteral(String text) throws IOException { + String hints = determineBlockHints(text); + writeIndicator("|" + hints, true, false, false); + if (hints.length() > 0 && (hints.charAt(hints.length() - 1)) == '+') { + openEnded = true; + } + if (!writeInlineComments()) { + writeLineBreak(null); + } + boolean breaks = true; + int start = 0, end = 0; + while (end <= text.length()) { + char ch = 0; + if (end < text.length()) { + ch = text.charAt(end); + } + if (breaks) { + if (ch == 0 || Constant.LINEBR.hasNo(ch)) { + String data = text.substring(start, end); + for (char br : data.toCharArray()) { + if (br == '\n') { + writeLineBreak(null); } else { - if (ch == 0 || Constant.LINEBR.has(ch)) { - stream.write(text, start, end - start); - if (ch == 0) { - writeLineBreak(null); - } - start = end; - } - } - if (ch != 0) { - breaks = Constant.LINEBR.has(ch); + writeLineBreak(String.valueOf(br)); } - end++; + } + if (ch != 0) { + writeIndent(); + } + start = end; } + } else { + if (ch == 0 || Constant.LINEBR.has(ch)) { + stream.write(text, start, end - start); + if (ch == 0) { + writeLineBreak(null); + } + start = end; + } + } + if (ch != 0) { + breaks = Constant.LINEBR.has(ch); + } + end++; } + } - void writePlain(String text, boolean split) throws IOException { - if (rootContext) { - openEnded = true; - } - if (text.length() == 0) { - return; - } - if (!this.whitespace) { - this.column++; - stream.write(SPACE); - } - this.whitespace = false; - this.indention = false; - boolean spaces = false; - boolean breaks = false; - int start = 0, end = 0; - while (end <= text.length()) { - char ch = 0; - if (end < text.length()) { - ch = text.charAt(end); - } - if (spaces) { - if (ch != ' ') { - if (start + 1 == end && this.column > this.bestWidth && split) { - writeIndent(); - this.whitespace = false; - this.indention = false; - } else { - int len = end - start; - this.column += len; - stream.write(text, start, len); - } - start = end; - } - } else if (breaks) { - if (Constant.LINEBR.hasNo(ch)) { - if (text.charAt(start) == '\n') { - writeLineBreak(null); - } - String data = text.substring(start, end); - for (char br : data.toCharArray()) { - if (br == '\n') { - writeLineBreak(null); - } else { - writeLineBreak(String.valueOf(br)); - } - } - writeIndent(); - this.whitespace = false; - this.indention = false; - start = end; - } + void writePlain(String text, boolean split) throws IOException { + if (rootContext) { + openEnded = true; + } + if (text.length() == 0) { + return; + } + if (!this.whitespace) { + this.column++; + stream.write(SPACE); + } + this.whitespace = false; + this.indention = false; + boolean spaces = false; + boolean breaks = false; + int start = 0, end = 0; + while (end <= text.length()) { + char ch = 0; + if (end < text.length()) { + ch = text.charAt(end); + } + if (spaces) { + if (ch != ' ') { + if (start + 1 == end && this.column > this.bestWidth && split) { + writeIndent(); + this.whitespace = false; + this.indention = false; + } else { + int len = end - start; + this.column += len; + stream.write(text, start, len); + } + start = end; + } + } else if (breaks) { + if (Constant.LINEBR.hasNo(ch)) { + if (text.charAt(start) == '\n') { + writeLineBreak(null); + } + String data = text.substring(start, end); + for (char br : data.toCharArray()) { + if (br == '\n') { + writeLineBreak(null); } else { - if (ch == 0 || Constant.LINEBR.has(ch)) { - int len = end - start; - this.column += len; - stream.write(text, start, len); - start = end; - } + writeLineBreak(String.valueOf(br)); } - if (ch != 0) { - spaces = ch == ' '; - breaks = Constant.LINEBR.has(ch); - } - end++; - } + } + writeIndent(); + this.whitespace = false; + this.indention = false; + start = end; + } + } else { + if (Constant.LINEBR.has(ch, "\0 ")) { + int len = end - start; + this.column += len; + stream.write(text, start, len); + start = end; + } + } + if (ch != 0) { + spaces = ch == ' '; + breaks = Constant.LINEBR.has(ch); + } + end++; } + } } diff --git a/src/main/java/org/yaml/snakeyaml/emitter/EmitterException.java b/src/main/java/org/yaml/snakeyaml/emitter/EmitterException.java index ed83fee5..b029f3be 100644 --- a/src/main/java/org/yaml/snakeyaml/emitter/EmitterException.java +++ b/src/main/java/org/yaml/snakeyaml/emitter/EmitterException.java @@ -1,26 +1,25 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.emitter; import org.yaml.snakeyaml.error.YAMLException; public class EmitterException extends YAMLException { - private static final long serialVersionUID = -8280070025452995908L; - public EmitterException(String msg) { - super(msg); - } + private static final long serialVersionUID = -8280070025452995908L; + + public EmitterException(String msg) { + super(msg); + } } diff --git a/src/main/java/org/yaml/snakeyaml/emitter/EmitterState.java b/src/main/java/org/yaml/snakeyaml/emitter/EmitterState.java index 8e6622e0..82ca1349 100755 --- a/src/main/java/org/yaml/snakeyaml/emitter/EmitterState.java +++ b/src/main/java/org/yaml/snakeyaml/emitter/EmitterState.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.emitter; @@ -21,5 +19,6 @@ import java.io.IOException; * Python's methods are first class object. Java needs a class. */ interface EmitterState { - void expect() throws IOException; -}
\ No newline at end of file + + void expect() throws IOException; +} diff --git a/src/main/java/org/yaml/snakeyaml/emitter/ScalarAnalysis.java b/src/main/java/org/yaml/snakeyaml/emitter/ScalarAnalysis.java index 99a0829a..f9a84614 100644 --- a/src/main/java/org/yaml/snakeyaml/emitter/ScalarAnalysis.java +++ b/src/main/java/org/yaml/snakeyaml/emitter/ScalarAnalysis.java @@ -1,37 +1,68 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.emitter; + +/** + * Accumulate information to choose the scalar style + */ public final class ScalarAnalysis { - public String scalar; - public boolean empty; - public boolean multiline; - public boolean allowFlowPlain; - public boolean allowBlockPlain; - public boolean allowSingleQuoted; - public boolean allowBlock; - - public ScalarAnalysis(String scalar, boolean empty, boolean multiline, boolean allowFlowPlain, - boolean allowBlockPlain, boolean allowSingleQuoted, boolean allowBlock) { - this.scalar = scalar; - this.empty = empty; - this.multiline = multiline; - this.allowFlowPlain = allowFlowPlain; - this.allowBlockPlain = allowBlockPlain; - this.allowSingleQuoted = allowSingleQuoted; - this.allowBlock = allowBlock; - } -}
\ No newline at end of file + + private final String scalar; + private final boolean empty; + private final boolean multiline; + private final boolean allowFlowPlain; + private final boolean allowBlockPlain; + private final boolean allowSingleQuoted; + private final boolean allowBlock; + + public ScalarAnalysis(String scalar, boolean empty, boolean multiline, boolean allowFlowPlain, + boolean allowBlockPlain, boolean allowSingleQuoted, boolean allowBlock) { + this.scalar = scalar; + this.empty = empty; + this.multiline = multiline; + this.allowFlowPlain = allowFlowPlain; + this.allowBlockPlain = allowBlockPlain; + this.allowSingleQuoted = allowSingleQuoted; + this.allowBlock = allowBlock; + } + + public String getScalar() { + return scalar; + } + + public boolean isEmpty() { + return empty; + } + + public boolean isMultiline() { + return multiline; + } + + public boolean isAllowFlowPlain() { + return allowFlowPlain; + } + + public boolean isAllowBlockPlain() { + return allowBlockPlain; + } + + public boolean isAllowSingleQuoted() { + return allowSingleQuoted; + } + + public boolean isAllowBlock() { + return allowBlock; + } +} diff --git a/src/main/java/org/yaml/snakeyaml/env/EnvScalarConstructor.java b/src/main/java/org/yaml/snakeyaml/env/EnvScalarConstructor.java new file mode 100644 index 00000000..566f6c51 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/env/EnvScalarConstructor.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.env; + +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.TypeDescription; +import org.yaml.snakeyaml.constructor.AbstractConstruct; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.error.MissingEnvironmentVariableException; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.Tag; + +/** + * Construct scalar for format ${VARIABLE} replacing the template with the value from environment. + * It can also be used to create JavaBeans when the all the arguments are provided. + * + * @see <a href="https://bitbucket.org/snakeyaml/snakeyaml/wiki/Variable%20substitution">Variable + * substitution</a> + * @see <a href="https://docs.docker.com/compose/compose-file/#variable-substitution">Variable + * substitution</a> + */ +public class EnvScalarConstructor extends Constructor { + + public static final Tag ENV_TAG = new Tag("!ENV"); + // name must be a word -> \w+ + // value can be any non-space -> \S+ + public static final Pattern ENV_FORMAT = Pattern + .compile("^\\$\\{\\s*((?<name>\\w+)((?<separator>:?(-|\\?))(?<value>\\S+)?)?)\\s*\\}$"); + + /** + * For simple cases when no JavaBeans are needed + */ + public EnvScalarConstructor() { + this.yamlConstructors.put(ENV_TAG, new ConstructEnv()); + } + + /** + * Create EnvScalarConstructor which can create JavaBeans with variable substitution + * + * @param theRoot - the class (usually JavaBean) to be constructed + * @param moreTDs - collection of classes used by the root class + * @param loadingConfig - configuration + */ + public EnvScalarConstructor(TypeDescription theRoot, Collection<TypeDescription> moreTDs, + LoaderOptions loadingConfig) { + super(theRoot, moreTDs, loadingConfig); + this.yamlConstructors.put(ENV_TAG, new ConstructEnv()); + } + + private class ConstructEnv extends AbstractConstruct { + + public Object construct(Node node) { + String val = constructScalar((ScalarNode) node); + Matcher matcher = ENV_FORMAT.matcher(val); + matcher.matches(); + String name = matcher.group("name"); + String value = matcher.group("value"); + String separator = matcher.group("separator"); + return apply(name, separator, value != null ? value : "", getEnv(name)); + } + } + + /** + * Implement the logic for missing and unset variables + * + * @param name - variable name in the template + * @param separator - separator in the template, can be :-, -, :?, ? + * @param value - default value or the error in the template + * @param environment - the value from environment for the provided variable + * @return the value to apply in the template + */ + public String apply(String name, String separator, String value, String environment) { + if (environment != null && !environment.isEmpty()) { + return environment; + } + // variable is either unset or empty + if (separator != null) { + // there is a default value or error + if (separator.equals("?")) { + if (environment == null) { + throw new MissingEnvironmentVariableException( + "Missing mandatory variable " + name + ": " + value); + } + } + if (separator.equals(":?")) { + if (environment == null) { + throw new MissingEnvironmentVariableException( + "Missing mandatory variable " + name + ": " + value); + } + if (environment.isEmpty()) { + throw new MissingEnvironmentVariableException( + "Empty mandatory variable " + name + ": " + value); + } + } + if (separator.startsWith(":")) { + if (environment == null || environment.isEmpty()) { + return value; + } + } else { + if (environment == null) { + return value; + } + } + } + return ""; + } + + /** + * Get value of the environment variable + * + * @param key - the name of the variable + * @return value or null if not set + */ + public String getEnv(String key) { + return System.getenv(key); + } +} diff --git a/src/main/java/org/yaml/snakeyaml/error/Mark.java b/src/main/java/org/yaml/snakeyaml/error/Mark.java index 7e1d546e..367ee6bb 100644 --- a/src/main/java/org/yaml/snakeyaml/error/Mark.java +++ b/src/main/java/org/yaml/snakeyaml/error/Mark.java @@ -1,132 +1,161 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.error; +import java.io.Serializable; import org.yaml.snakeyaml.scanner.Constant; /** - * It's just a record and its only use is producing nice error messages. Parser - * does not use it for any other purposes. + * It's just a record and its only use is producing nice error messages. Parser does not use it for + * any other purposes. */ -public final class Mark { - private String name; - private int index; - private int line; - private int column; - private String buffer; - private int pointer; - - public Mark(String name, int index, int line, int column, String buffer, int pointer) { - super(); - this.name = name; - this.index = index; - this.line = line; - this.column = column; - this.buffer = buffer; - this.pointer = pointer; - } +public final class Mark implements Serializable { - private boolean isLineBreak(char ch) { - return Constant.NULL_OR_LINEBR.has(ch); - } + private final String name; + private final int index; + private final int line; + private final int column; + private final int[] buffer; + private final int pointer; - public String get_snippet(int indent, int max_length) { - if (buffer == null) { - return null; - } - float half = max_length / 2 - 1; - int start = pointer; - String head = ""; - while ((start > 0) && !isLineBreak(buffer.charAt(start - 1))) { - start -= 1; - if (pointer - start > half) { - head = " ... "; - start += 5; - break; - } - } - String tail = ""; - int end = pointer; - while ((end < buffer.length()) && !isLineBreak(buffer.charAt(end))) { - end += 1; - if (end - pointer > half) { - tail = " ... "; - end -= 5; - break; - } - } - String snippet = buffer.substring(start, end); - StringBuilder result = new StringBuilder(); - for (int i = 0; i < indent; i++) { - result.append(" "); - } - result.append(head); - result.append(snippet); - result.append(tail); - result.append("\n"); - for (int i = 0; i < indent + pointer - start + head.length(); i++) { - result.append(" "); - } - result.append("^"); - return result.toString(); + private static int[] toCodePoints(char[] str) { + int[] codePoints = new int[Character.codePointCount(str, 0, str.length)]; + for (int i = 0, c = 0; i < str.length; c++) { + int cp = Character.codePointAt(str, i); + codePoints[c] = cp; + i += Character.charCount(cp); } + return codePoints; + } - public String get_snippet() { - return get_snippet(4, 75); - } + public Mark(String name, int index, int line, int column, char[] str, int pointer) { + this(name, index, line, column, toCodePoints(str), pointer); + } - @Override - public String toString() { - String snippet = get_snippet(); - StringBuilder where = new StringBuilder(" in "); - where.append(name); - where.append(", line "); - where.append(line + 1); - where.append(", column "); - where.append(column + 1); - if (snippet != null) { - where.append(":\n"); - where.append(snippet); - } - return where.toString(); - } + /* + * Existed in older versions but replaced with {@code char[]}-based constructor. Restored in v1.22 + * for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link Mark#Mark(String, int, int, int, char[], int)}. + */ + @Deprecated + public Mark(String name, int index, int line, int column, String buffer, int pointer) { + this(name, index, line, column, buffer.toCharArray(), pointer); + } - public String getName() { - return name; - } + public Mark(String name, int index, int line, int column, int[] buffer, int pointer) { + super(); + this.name = name; + this.index = index; + this.line = line; + this.column = column; + this.buffer = buffer; + this.pointer = pointer; + } - /** - * starts with 0 - */ - public int getLine() { - return line; - } + private boolean isLineBreak(int c) { + return Constant.NULL_OR_LINEBR.has(c); + } - /** - * starts with 0 - */ - public int getColumn() { - return column; + public String get_snippet(int indent, int max_length) { + float half = max_length / 2f - 1f; + int start = pointer; + String head = ""; + while ((start > 0) && !isLineBreak(buffer[start - 1])) { + start -= 1; + if (pointer - start > half) { + head = " ... "; + start += 5; + break; + } + } + String tail = ""; + int end = pointer; + while ((end < buffer.length) && !isLineBreak(buffer[end])) { + end += 1; + if (end - pointer > half) { + tail = " ... "; + end -= 5; + break; + } } - /** - * starts with 0 - */ - public int getIndex() { - return index; + StringBuilder result = new StringBuilder(); + for (int i = 0; i < indent; i++) { + result.append(" "); + } + result.append(head); + for (int i = start; i < end; i++) { + result.appendCodePoint(buffer[i]); } + result.append(tail); + result.append("\n"); + for (int i = 0; i < indent + pointer - start + head.length(); i++) { + result.append(" "); + } + result.append("^"); + return result.toString(); + } + + public String get_snippet() { + return get_snippet(4, 75); + } + + @Override + public String toString() { + String snippet = get_snippet(); + String builder = + " in " + name + ", line " + (line + 1) + ", column " + (column + 1) + ":\n" + snippet; + return builder; + } + + public String getName() { + return name; + } + + /** + * starts with 0 + * + * @return line number + */ + public int getLine() { + return line; + } + + /** + * starts with 0 + * + * @return column number + */ + public int getColumn() { + return column; + } + + /** + * starts with 0 + * + * @return character number + */ + public int getIndex() { + return index; + } + + public int[] getBuffer() { + return buffer; + } + public int getPointer() { + return pointer; + } } diff --git a/src/main/java/org/yaml/snakeyaml/error/MarkedYAMLException.java b/src/main/java/org/yaml/snakeyaml/error/MarkedYAMLException.java index 4e44ab9b..1e94902e 100644 --- a/src/main/java/org/yaml/snakeyaml/error/MarkedYAMLException.java +++ b/src/main/java/org/yaml/snakeyaml/error/MarkedYAMLException.java @@ -1,101 +1,99 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.error; public class MarkedYAMLException extends YAMLException { - private static final long serialVersionUID = -9119388488683035101L; - private String context; - private Mark contextMark; - private String problem; - private Mark problemMark; - private String note; + private static final long serialVersionUID = -9119388488683035101L; + private final String context; + private final Mark contextMark; + private final String problem; + private final Mark problemMark; + private final String note; - protected MarkedYAMLException(String context, Mark contextMark, String problem, - Mark problemMark, String note) { - this(context, contextMark, problem, problemMark, note, null); - } + protected MarkedYAMLException(String context, Mark contextMark, String problem, Mark problemMark, + String note) { + this(context, contextMark, problem, problemMark, note, null); + } - protected MarkedYAMLException(String context, Mark contextMark, String problem, - Mark problemMark, String note, Throwable cause) { - super(context + "; " + problem + "; " + problemMark, cause); - this.context = context; - this.contextMark = contextMark; - this.problem = problem; - this.problemMark = problemMark; - this.note = note; - } + protected MarkedYAMLException(String context, Mark contextMark, String problem, Mark problemMark, + String note, Throwable cause) { + super(context + "; " + problem + "; " + problemMark, cause); + this.context = context; + this.contextMark = contextMark; + this.problem = problem; + this.problemMark = problemMark; + this.note = note; + } - protected MarkedYAMLException(String context, Mark contextMark, String problem, Mark problemMark) { - this(context, contextMark, problem, problemMark, null, null); - } + protected MarkedYAMLException(String context, Mark contextMark, String problem, + Mark problemMark) { + this(context, contextMark, problem, problemMark, null, null); + } - protected MarkedYAMLException(String context, Mark contextMark, String problem, - Mark problemMark, Throwable cause) { - this(context, contextMark, problem, problemMark, null, cause); - } + protected MarkedYAMLException(String context, Mark contextMark, String problem, Mark problemMark, + Throwable cause) { + this(context, contextMark, problem, problemMark, null, cause); + } - @Override - public String getMessage() { - return toString(); - } + @Override + public String getMessage() { + return toString(); + } - @Override - public String toString() { - StringBuilder lines = new StringBuilder(); - if (context != null) { - lines.append(context); - lines.append("\n"); - } - if (contextMark != null - && (problem == null || problemMark == null - || contextMark.getName().equals(problemMark.getName()) - || (contextMark.getLine() != problemMark.getLine()) || (contextMark - .getColumn() != problemMark.getColumn()))) { - lines.append(contextMark.toString()); - lines.append("\n"); - } - if (problem != null) { - lines.append(problem); - lines.append("\n"); - } - if (problemMark != null) { - lines.append(problemMark.toString()); - lines.append("\n"); - } - if (note != null) { - lines.append(note); - lines.append("\n"); - } - return lines.toString(); + @Override + public String toString() { + StringBuilder lines = new StringBuilder(); + if (context != null) { + lines.append(context); + lines.append("\n"); } - - public String getContext() { - return context; + if (contextMark != null && (problem == null || problemMark == null + || contextMark.getName().equals(problemMark.getName()) + || (contextMark.getLine() != problemMark.getLine()) + || (contextMark.getColumn() != problemMark.getColumn()))) { + lines.append(contextMark); + lines.append("\n"); } - - public Mark getContextMark() { - return contextMark; + if (problem != null) { + lines.append(problem); + lines.append("\n"); } - - public String getProblem() { - return problem; + if (problemMark != null) { + lines.append(problemMark); + lines.append("\n"); } - - public Mark getProblemMark() { - return problemMark; + if (note != null) { + lines.append(note); + lines.append("\n"); } + return lines.toString(); + } + + public String getContext() { + return context; + } + + public Mark getContextMark() { + return contextMark; + } + + public String getProblem() { + return problem; + } + + public Mark getProblemMark() { + return problemMark; + } } diff --git a/src/main/java/org/yaml/snakeyaml/error/MissingEnvironmentVariableException.java b/src/main/java/org/yaml/snakeyaml/error/MissingEnvironmentVariableException.java new file mode 100644 index 00000000..382c949d --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/error/MissingEnvironmentVariableException.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.error; + +/** + * Indicate missing mandatory environment variable in the template Used by EnvScalarConstructor + */ +public class MissingEnvironmentVariableException extends YAMLException { + + public MissingEnvironmentVariableException(String message) { + super(message); + } +} diff --git a/src/main/java/org/yaml/snakeyaml/error/YAMLException.java b/src/main/java/org/yaml/snakeyaml/error/YAMLException.java index af7189b3..f712f229 100644 --- a/src/main/java/org/yaml/snakeyaml/error/YAMLException.java +++ b/src/main/java/org/yaml/snakeyaml/error/YAMLException.java @@ -1,32 +1,31 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.error; public class YAMLException extends RuntimeException { - private static final long serialVersionUID = -4738336175050337570L; - public YAMLException(String message) { - super(message); - } + private static final long serialVersionUID = -4738336175050337570L; - public YAMLException(Throwable cause) { - super(cause); - } + public YAMLException(String message) { + super(message); + } - public YAMLException(String message, Throwable cause) { - super(message, cause); - } + public YAMLException(Throwable cause) { + super(cause); + } + + public YAMLException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/AliasEvent.java b/src/main/java/org/yaml/snakeyaml/events/AliasEvent.java index e0dcf7f8..75f2fb4c 100644 --- a/src/main/java/org/yaml/snakeyaml/events/AliasEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/AliasEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -21,12 +19,16 @@ import org.yaml.snakeyaml.error.Mark; * Marks the inclusion of a previously anchored node. */ public final class AliasEvent extends NodeEvent { - public AliasEvent(String anchor, Mark startMark, Mark endMark) { - super(anchor, startMark, endMark); - } - @Override - public boolean is(Event.ID id) { - return ID.Alias == id; + public AliasEvent(String anchor, Mark startMark, Mark endMark) { + super(anchor, startMark, endMark); + if (anchor == null) { + throw new NullPointerException("anchor is not specified for alias"); } + } + + @Override + public Event.ID getEventId() { + return ID.Alias; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/CollectionEndEvent.java b/src/main/java/org/yaml/snakeyaml/events/CollectionEndEvent.java index b36f32c3..d9135b5e 100644 --- a/src/main/java/org/yaml/snakeyaml/events/CollectionEndEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/CollectionEndEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -22,7 +20,7 @@ import org.yaml.snakeyaml.error.Mark; */ public abstract class CollectionEndEvent extends Event { - public CollectionEndEvent(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public CollectionEndEvent(Mark startMark, Mark endMark) { + super(startMark, endMark); + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java b/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java index 9a772996..e03e514a 100644 --- a/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java @@ -1,73 +1,91 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** * Base class for the start events of the collection nodes. */ public abstract class CollectionStartEvent extends NodeEvent { - private final String tag; - // The implicit flag of a collection start event indicates if the tag may be - // omitted when the collection is emitted - private final boolean implicit; - // flag indicates if a collection is block or flow - private final Boolean flowStyle; - public CollectionStartEvent(String anchor, String tag, boolean implicit, Mark startMark, - Mark endMark, Boolean flowStyle) { - super(anchor, startMark, endMark); - this.tag = tag; - this.implicit = implicit; - this.flowStyle = flowStyle; - } + private final String tag; + // The implicit flag of a collection start event indicates if the tag may be + // omitted when the collection is emitted + private final boolean implicit; + // flag indicates if a collection is block or flow + private final DumperOptions.FlowStyle flowStyle; - /** - * Tag of this collection. - * - * @return The tag of this collection, or <code>null</code> if no explicit - * tag is available. - */ - public String getTag() { - return this.tag; + public CollectionStartEvent(String anchor, String tag, boolean implicit, Mark startMark, + Mark endMark, DumperOptions.FlowStyle flowStyle) { + super(anchor, startMark, endMark); + this.tag = tag; + this.implicit = implicit; + if (flowStyle == null) { + throw new NullPointerException("Flow style must be provided."); } + this.flowStyle = flowStyle; + } - /** - * <code>true</code> if the tag can be omitted while this collection is - * emitted. - * - * @return True if the tag can be omitted while this collection is emitted. - */ - public boolean getImplicit() { - return this.implicit; - } + /* + * Existed in older versions but replaced with {@link DumperOptions.FlowStyle}-based constructor. + * Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link + * CollectionStartEvent#CollectionStartEvent(String, String, boolean, Mark, Mark, + * org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public CollectionStartEvent(String anchor, String tag, boolean implicit, Mark startMark, + Mark endMark, Boolean flowStyle) { + this(anchor, tag, implicit, startMark, endMark, DumperOptions.FlowStyle.fromBoolean(flowStyle)); + } - /** - * <code>true</code> if this collection is in flow style, <code>false</code> - * for block style. - * - * @return If this collection is in flow style. - */ - public Boolean getFlowStyle() { - return this.flowStyle; - } + /** + * Tag of this collection. + * + * @return The tag of this collection, or <code>null</code> if no explicit tag is available. + */ + public String getTag() { + return this.tag; + } - @Override - protected String getArguments() { - return super.getArguments() + ", tag=" + tag + ", implicit=" + implicit; - } + /** + * <code>true</code> if the tag can be omitted while this collection is emitted. + * + * @return True if the tag can be omitted while this collection is emitted. + */ + public boolean getImplicit() { + return this.implicit; + } + + /** + * <code>true</code> if this collection is in flow style, <code>false</code> for block style. + * + * @return If this collection is in flow style. + */ + public DumperOptions.FlowStyle getFlowStyle() { + return this.flowStyle; + } + + @Override + protected String getArguments() { + return super.getArguments() + ", tag=" + tag + ", implicit=" + implicit; + } + + public boolean isFlow() { + return DumperOptions.FlowStyle.FLOW == flowStyle; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/CommentEvent.java b/src/main/java/org/yaml/snakeyaml/events/CommentEvent.java new file mode 100644 index 00000000..3b16e387 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/events/CommentEvent.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.events; + +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.error.Mark; + +/** + * Marks a comment block value. + */ +public final class CommentEvent extends Event { + + private final CommentType type; + private final String value; + + public CommentEvent(CommentType type, String value, Mark startMark, Mark endMark) { + super(startMark, endMark); + if (type == null) { + throw new NullPointerException("Event Type must be provided."); + } + this.type = type; + if (value == null) { + throw new NullPointerException("Value must be provided."); + } + this.value = value; + } + + /** + * String representation of the value. + * <p> + * Without quotes and escaping. + * </p> + * + * @return Value a comment line string without the leading '#' or a blank line. + */ + public String getValue() { + return this.value; + } + + /** + * The comment type. + * + * @return the commentType. + */ + public CommentType getCommentType() { + return this.type; + } + + @Override + protected String getArguments() { + return super.getArguments() + "type=" + type + ", value=" + value; + } + + @Override + public Event.ID getEventId() { + return ID.Comment; + } + +} diff --git a/src/main/java/org/yaml/snakeyaml/events/DocumentEndEvent.java b/src/main/java/org/yaml/snakeyaml/events/DocumentEndEvent.java index 30fe439f..17cdf716 100644 --- a/src/main/java/org/yaml/snakeyaml/events/DocumentEndEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/DocumentEndEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -24,19 +22,20 @@ import org.yaml.snakeyaml.error.Mark; * </p> */ public final class DocumentEndEvent extends Event { - private final boolean explicit; - public DocumentEndEvent(Mark startMark, Mark endMark, boolean explicit) { - super(startMark, endMark); - this.explicit = explicit; - } + private final boolean explicit; - public boolean getExplicit() { - return explicit; - } + public DocumentEndEvent(Mark startMark, Mark endMark, boolean explicit) { + super(startMark, endMark); + this.explicit = explicit; + } - @Override - public boolean is(Event.ID id) { - return ID.DocumentEnd == id; - } + public boolean getExplicit() { + return explicit; + } + + @Override + public Event.ID getEventId() { + return ID.DocumentEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/DocumentStartEvent.java b/src/main/java/org/yaml/snakeyaml/events/DocumentStartEvent.java index fa24cdfb..0eab4322 100644 --- a/src/main/java/org/yaml/snakeyaml/events/DocumentStartEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/DocumentStartEvent.java @@ -1,22 +1,19 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; import java.util.Map; - import org.yaml.snakeyaml.DumperOptions.Version; import org.yaml.snakeyaml.error.Mark; @@ -27,50 +24,45 @@ import org.yaml.snakeyaml.error.Mark; * </p> */ public final class DocumentStartEvent extends Event { - private final boolean explicit; - private final Version version; - private final Map<String, String> tags; - public DocumentStartEvent(Mark startMark, Mark endMark, boolean explicit, Version version, - Map<String, String> tags) { - super(startMark, endMark); - this.explicit = explicit; - this.version = version; - // TODO enforce not null - // if (tags == null) { - // throw new NullPointerException("Tags must be provided."); - // } - this.tags = tags; - } + private final boolean explicit; + private final Version version; + private final Map<String, String> tags; + + public DocumentStartEvent(Mark startMark, Mark endMark, boolean explicit, Version version, + Map<String, String> tags) { + super(startMark, endMark); + this.explicit = explicit; + this.version = version; + this.tags = tags; + } - public boolean getExplicit() { - return explicit; - } + public boolean getExplicit() { + return explicit; + } - /** - * YAML version the document conforms to. - * - * @return <code>null</code>if the document has no explicit - * <code>%YAML</code> directive. Otherwise an array with two - * components, the major and minor part of the version (in this - * order). - */ - public Version getVersion() { - return version; - } + /** + * YAML version the document conforms to. + * + * @return <code>null</code>if the document has no explicit <code>%YAML</code> directive. + * Otherwise an array with two components, the major and minor part of the version (in + * this order). + */ + public Version getVersion() { + return version; + } - /** - * Tag shorthands as defined by the <code>%TAG</code> directive. - * - * @return Mapping of 'handles' to 'prefixes' (the handles include the '!' - * characters). - */ - public Map<String, String> getTags() { - return tags; - } + /** + * Tag shorthands as defined by the <code>%TAG</code> directive. + * + * @return Mapping of 'handles' to 'prefixes' (the handles include the '!' characters). + */ + public Map<String, String> getTags() { + return tags; + } - @Override - public boolean is(Event.ID id) { - return ID.DocumentStart == id; - } + @Override + public Event.ID getEventId() { + return ID.DocumentStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/Event.java b/src/main/java/org/yaml/snakeyaml/events/Event.java index 4ebfa921..62c85b6e 100644 --- a/src/main/java/org/yaml/snakeyaml/events/Event.java +++ b/src/main/java/org/yaml/snakeyaml/events/Event.java @@ -1,77 +1,94 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; import org.yaml.snakeyaml.error.Mark; /** - * Basic unit of output from a {@link org.yaml.snakeyaml.parser.Parser} or input - * of a {@link org.yaml.snakeyaml.emitter.Emitter}. + * Basic unit of output from a {@link org.yaml.snakeyaml.parser.Parser} or input of a + * {@link org.yaml.snakeyaml.emitter.Emitter}. */ public abstract class Event { - public enum ID { - Alias, DocumentEnd, DocumentStart, MappingEnd, MappingStart, Scalar, SequenceEnd, SequenceStart, StreamEnd, StreamStart - } - private final Mark startMark; - private final Mark endMark; + public enum ID { + Alias, Comment, DocumentEnd, DocumentStart, MappingEnd, MappingStart, Scalar, SequenceEnd, SequenceStart, StreamEnd, StreamStart + } - public Event(Mark startMark, Mark endMark) { - this.startMark = startMark; - this.endMark = endMark; - } + private final Mark startMark; + private final Mark endMark; - public String toString() { - return "<" + this.getClass().getName() + "(" + getArguments() + ")>"; - } + public Event(Mark startMark, Mark endMark) { + this.startMark = startMark; + this.endMark = endMark; + } - public Mark getStartMark() { - return startMark; - } + public String toString() { + return "<" + this.getClass().getName() + "(" + getArguments() + ")>"; + } - public Mark getEndMark() { - return endMark; - } + public Mark getStartMark() { + return startMark; + } - /** - * @see "__repr__ for Event in PyYAML" - */ - protected String getArguments() { - return ""; - } + public Mark getEndMark() { + return endMark; + } - public abstract boolean is(Event.ID id); + /** + * Generate human readable representation of the Event + * + * @see "__repr__ for Event in PyYAML" + * @return representation fore humans + */ + protected String getArguments() { + return ""; + } - /* - * for tests only - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof Event) { - return toString().equals(obj.toString()); - } else { - return false; - } - } + /** + * Check if the Event is of the provided kind + * + * @param id - the Event.ID enum + * @return true then this Event of the provided type + */ + public boolean is(Event.ID id) { + return getEventId() == id; + } - /* - * for tests only - */ - @Override - public int hashCode() { - return toString().hashCode(); + /** + * Get the type (kind) if this Event + * + * @return the ID of this Event + */ + public abstract Event.ID getEventId(); + + /* + * for tests only + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Event) { + return toString().equals(obj.toString()); + } else { + return false; } + } + + /* + * for tests only + */ + @Override + public int hashCode() { + return toString().hashCode(); + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/ImplicitTuple.java b/src/main/java/org/yaml/snakeyaml/events/ImplicitTuple.java index 86a99acc..55b7dfc9 100644 --- a/src/main/java/org/yaml/snakeyaml/events/ImplicitTuple.java +++ b/src/main/java/org/yaml/snakeyaml/events/ImplicitTuple.java @@ -1,58 +1,54 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; /** - * The implicit flag of a scalar event is a pair of boolean values that indicate - * if the tag may be omitted when the scalar is emitted in a plain and non-plain - * style correspondingly. - * + * The implicit flag of a scalar event is a pair of boolean values that indicate if the tag may be + * omitted when the scalar is emitted in a plain and non-plain style correspondingly. + * * @see <a href="http://pyyaml.org/wiki/PyYAMLDocumentation#Events">Events</a> */ public class ImplicitTuple { - private final boolean plain; - private final boolean nonPlain; - public ImplicitTuple(boolean plain, boolean nonplain) { - this.plain = plain; - this.nonPlain = nonplain; - } + private final boolean plain; + private final boolean nonPlain; + + public ImplicitTuple(boolean plain, boolean nonplain) { + this.plain = plain; + this.nonPlain = nonplain; + } - /** - * @return true when tag may be omitted when the scalar is emitted in a - * plain style. - */ - public boolean canOmitTagInPlainScalar() { - return plain; - } + /** + * @return true when tag may be omitted when the scalar is emitted in a plain style. + */ + public boolean canOmitTagInPlainScalar() { + return plain; + } - /** - * @return true when tag may be omitted when the scalar is emitted in a - * non-plain style. - */ - public boolean canOmitTagInNonPlainScalar() { - return nonPlain; - } + /** + * @return true when tag may be omitted when the scalar is emitted in a non-plain style. + */ + public boolean canOmitTagInNonPlainScalar() { + return nonPlain; + } - public boolean bothFalse() { - return !plain && !nonPlain; - } + public boolean bothFalse() { + return !plain && !nonPlain; + } - @Override - public String toString() { - return "implicit=[" + plain + ", " + nonPlain + "]"; - } + @Override + public String toString() { + return "implicit=[" + plain + ", " + nonPlain + "]"; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/MappingEndEvent.java b/src/main/java/org/yaml/snakeyaml/events/MappingEndEvent.java index 618c9163..bfa684fb 100644 --- a/src/main/java/org/yaml/snakeyaml/events/MappingEndEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/MappingEndEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -19,17 +17,17 @@ import org.yaml.snakeyaml.error.Mark; /** * Marks the end of a mapping node. - * + * * @see MappingStartEvent */ public final class MappingEndEvent extends CollectionEndEvent { - public MappingEndEvent(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public MappingEndEvent(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public boolean is(Event.ID id) { - return ID.MappingEnd == id; - } + @Override + public Event.ID getEventId() { + return ID.MappingEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/MappingStartEvent.java b/src/main/java/org/yaml/snakeyaml/events/MappingStartEvent.java index 412e4d54..fab822ef 100644 --- a/src/main/java/org/yaml/snakeyaml/events/MappingStartEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/MappingStartEvent.java @@ -1,44 +1,57 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** * Marks the beginning of a mapping node. * <p> * This event is followed by a number of key value pairs. <br> - * The pairs are not in any particular order. However, the value always directly - * follows the corresponding key. <br> + * The pairs are not in any particular order. However, the value always directly follows the + * corresponding key. <br> * After the key value pairs follows a {@link MappingEndEvent}. * </p> * <p> * There must be an even number of node events between the start and end event. * </p> - * + * * @see MappingEndEvent */ public final class MappingStartEvent extends CollectionStartEvent { - public MappingStartEvent(String anchor, String tag, boolean implicit, Mark startMark, - Mark endMark, Boolean flowStyle) { - super(anchor, tag, implicit, startMark, endMark, flowStyle); - } - @Override - public boolean is(Event.ID id) { - return ID.MappingStart == id; - } + public MappingStartEvent(String anchor, String tag, boolean implicit, Mark startMark, + Mark endMark, DumperOptions.FlowStyle flowStyle) { + super(anchor, tag, implicit, startMark, endMark, flowStyle); + } + + /* + * Existed in older versions but replaced with {@link DumperOptions.FlowStyle}-based constructor. + * Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link MappingStartEvent#CollectionStartEvent(String, + * String, boolean, Mark, Mark, org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public MappingStartEvent(String anchor, String tag, boolean implicit, Mark startMark, + Mark endMark, Boolean flowStyle) { + this(anchor, tag, implicit, startMark, endMark, DumperOptions.FlowStyle.fromBoolean(flowStyle)); + } + + @Override + public Event.ID getEventId() { + return ID.MappingStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/NodeEvent.java b/src/main/java/org/yaml/snakeyaml/events/NodeEvent.java index f0af48b9..4db308e6 100644 --- a/src/main/java/org/yaml/snakeyaml/events/NodeEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/NodeEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -22,28 +20,27 @@ import org.yaml.snakeyaml.error.Mark; */ public abstract class NodeEvent extends Event { - private final String anchor; + private final String anchor; - public NodeEvent(String anchor, Mark startMark, Mark endMark) { - super(startMark, endMark); - this.anchor = anchor; - } + public NodeEvent(String anchor, Mark startMark, Mark endMark) { + super(startMark, endMark); + this.anchor = anchor; + } - /** - * Node anchor by which this node might later be referenced by a - * {@link AliasEvent}. - * <p> - * Note that {@link AliasEvent}s are by it self <code>NodeEvent</code>s and - * use this property to indicate the referenced anchor. - * - * @return Anchor of this node or <code>null</code> if no anchor is defined. - */ - public String getAnchor() { - return this.anchor; - } + /** + * Node anchor by which this node might later be referenced by a {@link AliasEvent}. + * <p> + * Note that {@link AliasEvent}s are by it self <code>NodeEvent</code>s and use this property to + * indicate the referenced anchor. + * + * @return Anchor of this node or <code>null</code> if no anchor is defined. + */ + public String getAnchor() { + return this.anchor; + } - @Override - protected String getArguments() { - return "anchor=" + anchor; - } + @Override + protected String getArguments() { + return "anchor=" + anchor; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java b/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java index 7f07a62b..ed2ea0cc 100644 --- a/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java @@ -1,101 +1,132 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** * Marks a scalar value. */ public final class ScalarEvent extends NodeEvent { - private final String tag; - // style flag of a scalar event indicates the style of the scalar. Possible - // values are None, '', '\'', '"', '|', '>' - private final Character style; - private final String value; - // The implicit flag of a scalar event is a pair of boolean values that - // indicate if the tag may be omitted when the scalar is emitted in a plain - // and non-plain style correspondingly. - private final ImplicitTuple implicit; - public ScalarEvent(String anchor, String tag, ImplicitTuple implicit, String value, - Mark startMark, Mark endMark, Character style) { - super(anchor, startMark, endMark); - this.tag = tag; - this.implicit = implicit; - this.value = value; - this.style = style; - } + private final String tag; + // style flag of a scalar event indicates the style of the scalar. Possible + // values are None, '', '\'', '"', '|', '>' + private final DumperOptions.ScalarStyle style; + private final String value; + // The implicit flag of a scalar event is a pair of boolean values that + // indicate if the tag may be omitted when the scalar is emitted in a plain + // and non-plain style correspondingly. + private final ImplicitTuple implicit; - /** - * Tag of this scalar. - * - * @return The tag of this scalar, or <code>null</code> if no explicit tag - * is available. - */ - public String getTag() { - return this.tag; + public ScalarEvent(String anchor, String tag, ImplicitTuple implicit, String value, + Mark startMark, Mark endMark, DumperOptions.ScalarStyle style) { + super(anchor, startMark, endMark); + this.tag = tag; + this.implicit = implicit; + if (value == null) { + throw new NullPointerException("Value must be provided."); } - - /** - * Style of the scalar. - * <dl> - * <dt>null</dt> - * <dd>Flow Style - Plain</dd> - * <dt>'\''</dt> - * <dd>Flow Style - Single-Quoted</dd> - * <dt>'"'</dt> - * <dd>Flow Style - Double-Quoted</dd> - * <dt>'|'</dt> - * <dd>Block Style - Literal</dd> - * <dt>'>'</dt> - * <dd>Block Style - Folded</dd> - * </dl> - * - * @see <a href="http://yaml.org/spec/1.1/#id864487">Kind/Style - * Combinations</a> - * @return Style of the scalar. - */ - public Character getStyle() { - return this.style; + this.value = value; + if (style == null) { + throw new NullPointerException("Style must be provided."); } + this.style = style; + } - /** - * String representation of the value. - * <p> - * Without quotes and escaping. - * </p> - * - * @return Value as Unicode string. - */ - public String getValue() { - return this.value; - } + /* + * Existed in older versions but replaced with {@link DumperOptions.ScalarStyle}-based + * constructor. Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link ScalarEvent#ScalarEvent(String, String, + * ImplicitTuple, String, Mark, Mark, org.yaml.snakeyaml.DumperOptions.ScalarStyle) }. + */ + @Deprecated + public ScalarEvent(String anchor, String tag, ImplicitTuple implicit, String value, + Mark startMark, Mark endMark, Character style) { + this(anchor, tag, implicit, value, startMark, endMark, + DumperOptions.ScalarStyle.createStyle(style)); + } - public ImplicitTuple getImplicit() { - return this.implicit; - } + /** + * Tag of this scalar. + * + * @return The tag of this scalar, or <code>null</code> if no explicit tag is available. + */ + public String getTag() { + return this.tag; + } - @Override - protected String getArguments() { - return super.getArguments() + ", tag=" + tag + ", " + implicit + ", value=" + value; - } + /** + * Style of the scalar. + * <dl> + * <dt>null</dt> + * <dd>Flow Style - Plain</dd> + * <dt>'\''</dt> + * <dd>Flow Style - Single-Quoted</dd> + * <dt>'"'</dt> + * <dd>Flow Style - Double-Quoted</dd> + * <dt>'|'</dt> + * <dd>Block Style - Literal</dd> + * <dt>'>'</dt> + * <dd>Block Style - Folded</dd> + * </dl> + * + * @see <a href="http://yaml.org/spec/1.1/#id864487">Kind/Style Combinations</a> + * @return Style of the scalar. + */ + public DumperOptions.ScalarStyle getScalarStyle() { + return this.style; + } - @Override - public boolean is(Event.ID id) { - return ID.Scalar == id; - } + /** + * @deprecated use getScalarStyle() instead + * @return char which is a value behind ScalarStyle + */ + @Deprecated + public Character getStyle() { + return this.style.getChar(); + } + + /** + * String representation of the value. + * <p> + * Without quotes and escaping. + * </p> + * + * @return Value as Unicode string. + */ + public String getValue() { + return this.value; + } + + public ImplicitTuple getImplicit() { + return this.implicit; + } + + @Override + protected String getArguments() { + return super.getArguments() + ", tag=" + tag + ", " + implicit + ", value=" + value; + } + + @Override + public Event.ID getEventId() { + return ID.Scalar; + } + + public boolean isPlain() { + return style == DumperOptions.ScalarStyle.PLAIN; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/SequenceEndEvent.java b/src/main/java/org/yaml/snakeyaml/events/SequenceEndEvent.java index a6a61275..dc1b610b 100644 --- a/src/main/java/org/yaml/snakeyaml/events/SequenceEndEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/SequenceEndEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -19,17 +17,17 @@ import org.yaml.snakeyaml.error.Mark; /** * Marks the end of a sequence. - * + * * @see SequenceStartEvent */ public final class SequenceEndEvent extends CollectionEndEvent { - public SequenceEndEvent(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public SequenceEndEvent(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public boolean is(Event.ID id) { - return ID.SequenceEnd == id; - } + @Override + public Event.ID getEventId() { + return ID.SequenceEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/SequenceStartEvent.java b/src/main/java/org/yaml/snakeyaml/events/SequenceStartEvent.java index eb7b910f..944f2e46 100644 --- a/src/main/java/org/yaml/snakeyaml/events/SequenceStartEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/SequenceStartEvent.java @@ -1,39 +1,51 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** * Marks the beginning of a sequence node. * <p> - * This event is followed by the elements contained in the sequence, and a - * {@link SequenceEndEvent}. + * This event is followed by the elements contained in the sequence, and a {@link SequenceEndEvent}. * </p> - * + * * @see SequenceEndEvent */ public final class SequenceStartEvent extends CollectionStartEvent { - public SequenceStartEvent(String anchor, String tag, boolean implicit, Mark startMark, - Mark endMark, Boolean flowStyle) { - super(anchor, tag, implicit, startMark, endMark, flowStyle); - } - @Override - public boolean is(Event.ID id) { - return ID.SequenceStart == id; - } + public SequenceStartEvent(String anchor, String tag, boolean implicit, Mark startMark, + Mark endMark, DumperOptions.FlowStyle flowStyle) { + super(anchor, tag, implicit, startMark, endMark, flowStyle); + } + + /* + * Existed in older versions but replaced with {@link DumperOptions.SequenceStyle}-based + * constructor. Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link SequenceStartEvent#SequenceStartEvent(String, + * String, boolean, Mark, Mark, org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public SequenceStartEvent(String anchor, String tag, boolean implicit, Mark startMark, + Mark endMark, Boolean flowStyle) { + this(anchor, tag, implicit, startMark, endMark, DumperOptions.FlowStyle.fromBoolean(flowStyle)); + } + + @Override + public Event.ID getEventId() { + return ID.SequenceStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/StreamEndEvent.java b/src/main/java/org/yaml/snakeyaml/events/StreamEndEvent.java index 1389c6b6..6dbb02a4 100644 --- a/src/main/java/org/yaml/snakeyaml/events/StreamEndEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/StreamEndEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -20,21 +18,21 @@ import org.yaml.snakeyaml.error.Mark; /** * Marks the end of a stream that might have contained multiple documents. * <p> - * This event is the last event that a parser emits. Together with - * {@link StreamStartEvent} (which is the first event a parser emits) they mark - * the beginning and the end of a stream of documents. + * This event is the last event that a parser emits. Together with {@link StreamStartEvent} (which + * is the first event a parser emits) they mark the beginning and the end of a stream of documents. * </p> * <p> * See {@link Event} for an exemplary output. * </p> */ public final class StreamEndEvent extends Event { - public StreamEndEvent(Mark startMark, Mark endMark) { - super(startMark, endMark); - } - @Override - public boolean is(Event.ID id) { - return ID.StreamEnd == id; - } + public StreamEndEvent(Mark startMark, Mark endMark) { + super(startMark, endMark); + } + + @Override + public Event.ID getEventId() { + return ID.StreamEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/events/StreamStartEvent.java b/src/main/java/org/yaml/snakeyaml/events/StreamStartEvent.java index 42e6c763..08dededf 100644 --- a/src/main/java/org/yaml/snakeyaml/events/StreamStartEvent.java +++ b/src/main/java/org/yaml/snakeyaml/events/StreamStartEvent.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.events; @@ -20,9 +18,8 @@ import org.yaml.snakeyaml.error.Mark; /** * Marks the start of a stream that might contain multiple documents. * <p> - * This event is the first event that a parser emits. Together with - * {@link StreamEndEvent} (which is the last event a parser emits) they mark the - * beginning and the end of a stream of documents. + * This event is the first event that a parser emits. Together with {@link StreamEndEvent} (which is + * the last event a parser emits) they mark the beginning and the end of a stream of documents. * </p> * <p> * See {@link Event} for an exemplary output. @@ -30,12 +27,12 @@ import org.yaml.snakeyaml.error.Mark; */ public final class StreamStartEvent extends Event { - public StreamStartEvent(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public StreamStartEvent(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public boolean is(Event.ID id) { - return ID.StreamStart == id; - } + @Override + public Event.ID getEventId() { + return ID.StreamStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactConstructor.java b/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactConstructor.java index ec8f1b69..1a722bd1 100644 --- a/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactConstructor.java +++ b/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactConstructor.java @@ -1,21 +1,18 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.extensions.compactnotation; -import java.beans.IntrospectionException; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -23,7 +20,6 @@ import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; - import org.yaml.snakeyaml.constructor.Construct; import org.yaml.snakeyaml.constructor.Constructor; import org.yaml.snakeyaml.error.YAMLException; @@ -38,199 +34,200 @@ import org.yaml.snakeyaml.nodes.SequenceNode; * Construct a custom Java instance out of a compact object notation format. */ public class CompactConstructor extends Constructor { - private static final Pattern GUESS_COMPACT = Pattern - .compile("\\p{Alpha}.*\\s*\\((?:,?\\s*(?:(?:\\w*)|(?:\\p{Alpha}\\w*\\s*=.+))\\s*)+\\)"); - private static final Pattern FIRST_PATTERN = Pattern.compile("(\\p{Alpha}.*)(\\s*)\\((.*?)\\)"); - private static final Pattern PROPERTY_NAME_PATTERN = Pattern - .compile("\\s*(\\p{Alpha}\\w*)\\s*=(.+)"); - private Construct compactConstruct; - - protected Object constructCompactFormat(ScalarNode node, CompactData data) { - try { - Object obj = createInstance(node, data); - Map<String, Object> properties = new HashMap<String, Object>(data.getProperties()); - setProperties(obj, properties); - return obj; - } catch (Exception e) { - throw new YAMLException(e); - } + + private static final Pattern GUESS_COMPACT = Pattern + .compile("\\p{Alpha}.*\\s*\\((?:,?\\s*(?:(?:\\w*)|(?:\\p{Alpha}\\w*\\s*=.+))\\s*)+\\)"); + private static final Pattern FIRST_PATTERN = Pattern.compile("(\\p{Alpha}.*)(\\s*)\\((.*?)\\)"); + private static final Pattern PROPERTY_NAME_PATTERN = + Pattern.compile("\\s*(\\p{Alpha}\\w*)\\s*=(.+)"); + private Construct compactConstruct; + + protected Object constructCompactFormat(ScalarNode node, CompactData data) { + try { + Object obj = createInstance(node, data); + Map<String, Object> properties = new HashMap<String, Object>(data.getProperties()); + setProperties(obj, properties); + return obj; + } catch (Exception e) { + throw new YAMLException(e); + } + } + + protected Object createInstance(ScalarNode node, CompactData data) throws Exception { + Class<?> clazz = getClassForName(data.getPrefix()); + Class<?>[] args = new Class[data.getArguments().size()]; + for (int i = 0; i < args.length; i++) { + // assume all the arguments are Strings + args[i] = String.class; } + java.lang.reflect.Constructor<?> c = clazz.getDeclaredConstructor(args); + c.setAccessible(true); + return c.newInstance(data.getArguments().toArray()); - protected Object createInstance(ScalarNode node, CompactData data) throws Exception { - Class<?> clazz = getClassForName(data.getPrefix()); - Class<?>[] args = new Class[data.getArguments().size()]; - for (int i = 0; i < args.length; i++) { - // assume all the arguments are Strings - args[i] = String.class; - } - java.lang.reflect.Constructor<?> c = clazz.getDeclaredConstructor(args); - c.setAccessible(true); - return c.newInstance(data.getArguments().toArray()); + } + protected void setProperties(Object bean, Map<String, Object> data) throws Exception { + if (data == null) { + throw new NullPointerException("Data for Compact Object Notation cannot be null."); } - - protected void setProperties(Object bean, Map<String, Object> data) throws Exception { - if (data == null) { - throw new NullPointerException("Data for Compact Object Notation cannot be null."); - } - for (Map.Entry<String, Object> entry : data.entrySet()) { - String key = entry.getKey(); - Property property = getPropertyUtils().getProperty(bean.getClass(), key); - try { - property.set(bean, entry.getValue()); - } catch (IllegalArgumentException e) { - throw new YAMLException("Cannot set property='" + key + "' with value='" - + data.get(key) + "' (" + data.get(key).getClass() + ") in " + bean); - } - } + for (Map.Entry<String, Object> entry : data.entrySet()) { + String key = entry.getKey(); + Property property = getPropertyUtils().getProperty(bean.getClass(), key); + try { + property.set(bean, entry.getValue()); + } catch (IllegalArgumentException e) { + throw new YAMLException("Cannot set property='" + key + "' with value='" + data.get(key) + + "' (" + data.get(key).getClass() + ") in " + bean); + } } + } - public CompactData getCompactData(String scalar) { - if (!scalar.endsWith(")")) { - return null; - } - if (scalar.indexOf('(') < 0) { + public CompactData getCompactData(String scalar) { + if (!scalar.endsWith(")")) { + return null; + } + if (scalar.indexOf('(') < 0) { + return null; + } + Matcher m = FIRST_PATTERN.matcher(scalar); + if (m.matches()) { + String tag = m.group(1).trim(); + String content = m.group(3); + CompactData data = new CompactData(tag); + if (content.length() == 0) { + return data; + } + String[] names = content.split("\\s*,\\s*"); + for (int i = 0; i < names.length; i++) { + String section = names[i]; + if (section.indexOf('=') < 0) { + data.getArguments().add(section); + } else { + Matcher sm = PROPERTY_NAME_PATTERN.matcher(section); + if (sm.matches()) { + String name = sm.group(1); + String value = sm.group(2).trim(); + data.getProperties().put(name, value); + } else { return null; + } } - Matcher m = FIRST_PATTERN.matcher(scalar); - if (m.matches()) { - String tag = m.group(1).trim(); - String content = m.group(3); - CompactData data = new CompactData(tag); - if (content.length() == 0) - return data; - String[] names = content.split("\\s*,\\s*"); - for (int i = 0; i < names.length; i++) { - String section = names[i]; - if (section.indexOf('=') < 0) { - data.getArguments().add(section); - } else { - Matcher sm = PROPERTY_NAME_PATTERN.matcher(section); - if (sm.matches()) { - String name = sm.group(1); - String value = sm.group(2).trim(); - data.getProperties().put(name, value); - } else { - return null; - } - } - } - return data; - } - return null; + } + return data; } + return null; + } - private Construct getCompactConstruct() { - if (compactConstruct == null) { - compactConstruct = createCompactConstruct(); + private Construct getCompactConstruct() { + if (compactConstruct == null) { + compactConstruct = createCompactConstruct(); + } + return compactConstruct; + } + + protected Construct createCompactConstruct() { + return new ConstructCompactObject(); + } + + @Override + protected Construct getConstructor(Node node) { + if (node instanceof MappingNode) { + MappingNode mnode = (MappingNode) node; + List<NodeTuple> list = mnode.getValue(); + if (list.size() == 1) { + NodeTuple tuple = list.get(0); + Node key = tuple.getKeyNode(); + if (key instanceof ScalarNode) { + ScalarNode scalar = (ScalarNode) key; + if (GUESS_COMPACT.matcher(scalar.getValue()).matches()) { + return getCompactConstruct(); + } } - return compactConstruct; + } + } else if (node instanceof ScalarNode) { + ScalarNode scalar = (ScalarNode) node; + if (GUESS_COMPACT.matcher(scalar.getValue()).matches()) { + return getCompactConstruct(); + } } + return super.getConstructor(node); + } - protected Construct createCompactConstruct() { - return new ConstructCompactObject(); - } + public class ConstructCompactObject extends ConstructMapping { @Override - protected Construct getConstructor(Node node) { - if (node instanceof MappingNode) { - MappingNode mnode = (MappingNode) node; - List<NodeTuple> list = mnode.getValue(); - if (list.size() == 1) { - NodeTuple tuple = list.get(0); - Node key = tuple.getKeyNode(); - if (key instanceof ScalarNode) { - ScalarNode scalar = (ScalarNode) key; - if (GUESS_COMPACT.matcher(scalar.getValue()).matches()) { - return getCompactConstruct(); - } - } - } - } else if (node instanceof ScalarNode) { - ScalarNode scalar = (ScalarNode) node; - if (GUESS_COMPACT.matcher(scalar.getValue()).matches()) { - return getCompactConstruct(); - } - } - return super.getConstructor(node); + public void construct2ndStep(Node node, Object object) { + // Compact Object Notation may contain only one entry + MappingNode mnode = (MappingNode) node; + NodeTuple nodeTuple = mnode.getValue().iterator().next(); + + Node valueNode = nodeTuple.getValueNode(); + + if (valueNode instanceof MappingNode) { + valueNode.setType(object.getClass()); + constructJavaBean2ndStep((MappingNode) valueNode, object); + } else { + // value is a list + applySequence(object, constructSequence((SequenceNode) valueNode)); + } } - public class ConstructCompactObject extends ConstructMapping { - - @Override - public void construct2ndStep(Node node, Object object) { - // Compact Object Notation may contain only one entry - MappingNode mnode = (MappingNode) node; - NodeTuple nodeTuple = mnode.getValue().iterator().next(); - - Node valueNode = nodeTuple.getValueNode(); - - if (valueNode instanceof MappingNode) { - valueNode.setType(object.getClass()); - constructJavaBean2ndStep((MappingNode) valueNode, object); - } else { - // value is a list - applySequence(object, constructSequence((SequenceNode) valueNode)); - } - } - - /* - * MappingNode and ScalarNode end up here only they assumed to be a - * compact object's representation (@see getConstructor(Node) above) - */ - public Object construct(Node node) { - ScalarNode tmpNode = null; - if (node instanceof MappingNode) { - // Compact Object Notation may contain only one entry - MappingNode mnode = (MappingNode) node; - NodeTuple nodeTuple = mnode.getValue().iterator().next(); - node.setTwoStepsConstruction(true); - tmpNode = (ScalarNode) nodeTuple.getKeyNode(); - // return constructScalar((ScalarNode) keyNode); - } else { - tmpNode = (ScalarNode) node; - } - - CompactData data = getCompactData(tmpNode.getValue()); - if (data == null) { // TODO: Should we throw an exception here ? - return constructScalar(tmpNode); - } - return constructCompactFormat(tmpNode, data); - } + /* + * MappingNode and ScalarNode end up here only they assumed to be a compact object's + * representation (@see getConstructor(Node) above) + */ + public Object construct(Node node) { + ScalarNode tmpNode; + if (node instanceof MappingNode) { + // Compact Object Notation may contain only one entry + MappingNode mnode = (MappingNode) node; + NodeTuple nodeTuple = mnode.getValue().iterator().next(); + node.setTwoStepsConstruction(true); + tmpNode = (ScalarNode) nodeTuple.getKeyNode(); + // return constructScalar((ScalarNode) keyNode); + } else { + tmpNode = (ScalarNode) node; + } + + CompactData data = getCompactData(tmpNode.getValue()); + if (data == null) { // TODO: Should we throw an exception here ? + return constructScalar(tmpNode); + } + return constructCompactFormat(tmpNode, data); } - - protected void applySequence(Object bean, List<?> value) { - try { - Property property = getPropertyUtils().getProperty(bean.getClass(), - getSequencePropertyName(bean.getClass())); - property.set(bean, value); - } catch (Exception e) { - throw new YAMLException(e); - } + } + + protected void applySequence(Object bean, List<?> value) { + try { + Property property = + getPropertyUtils().getProperty(bean.getClass(), getSequencePropertyName(bean.getClass())); + property.set(bean, value); + } catch (Exception e) { + throw new YAMLException(e); } - - /** - * Provide the name of the property which is used when the entries form a - * sequence. The property must be a List. - * - * @throws IntrospectionException - */ - protected String getSequencePropertyName(Class<?> bean) throws IntrospectionException { - Set<Property> properties = getPropertyUtils().getProperties(bean); - for (Iterator<Property> iterator = properties.iterator(); iterator.hasNext();) { - Property property = iterator.next(); - if (!List.class.isAssignableFrom(property.getType())) { - iterator.remove(); - } - } - if (properties.size() == 0) { - throw new YAMLException("No list property found in " + bean); - } else if (properties.size() > 1) { - throw new YAMLException( - "Many list properties found in " - + bean - + "; Please override getSequencePropertyName() to specify which property to use."); - } - return properties.iterator().next().getName(); + } + + /** + * Provide the name of the property which is used when the entries form a sequence. The property + * must be a List. + * + * @param bean the class to provide exactly one List property + * @return name of the List property + */ + protected String getSequencePropertyName(Class<?> bean) { + Set<Property> properties = getPropertyUtils().getProperties(bean); + for (Iterator<Property> iterator = properties.iterator(); iterator.hasNext();) { + Property property = iterator.next(); + if (!List.class.isAssignableFrom(property.getType())) { + iterator.remove(); + } + } + if (properties.size() == 0) { + throw new YAMLException("No list property found in " + bean); + } else if (properties.size() > 1) { + throw new YAMLException("Many list properties found in " + bean + + "; Please override getSequencePropertyName() to specify which property to use."); } + return properties.iterator().next().getName(); + } } diff --git a/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactData.java b/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactData.java index c2da7c46..13126d80 100644 --- a/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactData.java +++ b/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/CompactData.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.extensions.compactnotation; @@ -21,28 +19,29 @@ import java.util.List; import java.util.Map; public class CompactData { - private String prefix; - private List<String> arguments = new ArrayList<String>(); - private Map<String, String> properties = new HashMap<String, String>(); - - public CompactData(String prefix) { - this.prefix = prefix; - } - - public String getPrefix() { - return prefix; - } - - public Map<String, String> getProperties() { - return properties; - } - - public List<String> getArguments() { - return arguments; - } - - @Override - public String toString() { - return "CompactData: " + prefix + " " + properties; - } -}
\ No newline at end of file + + private final String prefix; + private final List<String> arguments = new ArrayList<String>(); + private final Map<String, String> properties = new HashMap<String, String>(); + + public CompactData(String prefix) { + this.prefix = prefix; + } + + public String getPrefix() { + return prefix; + } + + public Map<String, String> getProperties() { + return properties; + } + + public List<String> getArguments() { + return arguments; + } + + @Override + public String toString() { + return "CompactData: " + prefix + " " + properties; + } +} diff --git a/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/PackageCompactConstructor.java b/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/PackageCompactConstructor.java index e58c3add..e6b1a31a 100644 --- a/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/PackageCompactConstructor.java +++ b/src/main/java/org/yaml/snakeyaml/extensions/compactnotation/PackageCompactConstructor.java @@ -1,37 +1,36 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.extensions.compactnotation; public class PackageCompactConstructor extends CompactConstructor { - private String packageName; - public PackageCompactConstructor(String packageName) { - this.packageName = packageName; - } + private final String packageName; + + public PackageCompactConstructor(String packageName) { + this.packageName = packageName; + } - @Override - protected Class<?> getClassForName(String name) throws ClassNotFoundException { - if (name.indexOf('.') < 0) { - try { - Class<?> clazz = Class.forName(packageName + "." + name); - return clazz; - } catch (ClassNotFoundException e) { - // use super implementation - } - } - return super.getClassForName(name); + @Override + protected Class<?> getClassForName(String name) throws ClassNotFoundException { + if (name.indexOf('.') < 0) { + try { + Class<?> clazz = Class.forName(packageName + "." + name); + return clazz; + } catch (ClassNotFoundException e) { + // use super implementation + } } + return super.getClassForName(name); + } } diff --git a/src/main/java/org/yaml/snakeyaml/external/biz/base64Coder/Base64Coder.java b/src/main/java/org/yaml/snakeyaml/external/biz/base64Coder/Base64Coder.java index 65923b65..db43b474 100644 --- a/src/main/java/org/yaml/snakeyaml/external/biz/base64Coder/Base64Coder.java +++ b/src/main/java/org/yaml/snakeyaml/external/biz/base64Coder/Base64Coder.java @@ -4,11 +4,11 @@ // This module is multi-licensed and may be used under the terms // of any of the following licenses: // -// EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal -// LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html -// GPL, GNU General Public License, V2 or later, http://www.gnu.org/licenses/gpl.html -// AL, Apache License, V2.0 or later, http://www.apache.org/licenses -// BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php +// EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal +// LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html +// GPL, GNU General Public License, V2 or later, http://www.gnu.org/licenses/gpl.html +// AL, Apache License, V2.0 or later, http://www.apache.org/licenses +// BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php // // Please contact the author if you need another license. // This module is provided "as is", without warranties of any kind. @@ -17,289 +17,265 @@ package org.yaml.snakeyaml.external.biz.base64Coder; /** * A Base64 encoder/decoder. - * + * * <p> - * This class is used to encode and decode data in Base64 format as described in - * RFC 1521. - * + * This class is used to encode and decode data in Base64 format as described in RFC 1521. + * * <p> - * Project home page: <a - * href="http://www.source-code.biz/base64coder/java/">www. + * Project home page: <a href="http://www.source-code.biz/base64coder/java/">www. * source-code.biz/base64coder/java</a><br> * Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br> * Multi-licensed: EPL / LGPL / GPL / AL / BSD. */ public class Base64Coder { - // The line separator string of the operating system. - private static final String systemLineSeparator = System.getProperty("line.separator"); + // The line separator string of the operating system. + private static final String systemLineSeparator = System.getProperty("line.separator"); - // Mapping table from 6-bit nibbles to Base64 characters. - private static char[] map1 = new char[64]; - static { - int i = 0; - for (char c = 'A'; c <= 'Z'; c++) - map1[i++] = c; - for (char c = 'a'; c <= 'z'; c++) - map1[i++] = c; - for (char c = '0'; c <= '9'; c++) - map1[i++] = c; - map1[i++] = '+'; - map1[i++] = '/'; - } + // Mapping table from 6-bit nibbles to Base64 characters. + private static final char[] map1 = new char[64]; - // Mapping table from Base64 characters to 6-bit nibbles. - private static byte[] map2 = new byte[128]; - static { - for (int i = 0; i < map2.length; i++) - map2[i] = -1; - for (int i = 0; i < 64; i++) - map2[map1[i]] = (byte) i; + static { + int i = 0; + for (char c = 'A'; c <= 'Z'; c++) { + map1[i++] = c; } - - /** - * Encodes a string into Base64 format. No blanks or line breaks are - * inserted. - * - * @param s - * A String to be encoded. - * @return A String containing the Base64 encoded data. - */ - public static String encodeString(String s) { - return new String(encode(s.getBytes())); + for (char c = 'a'; c <= 'z'; c++) { + map1[i++] = c; } - - /** - * Encodes a byte array into Base 64 format and breaks the output into lines - * of 76 characters. This method is compatible with - * <code>sun.misc.BASE64Encoder.encodeBuffer(byte[])</code>. - * - * @param in - * An array containing the data bytes to be encoded. - * @return A String containing the Base64 encoded data, broken into lines. - */ - public static String encodeLines(byte[] in) { - return encodeLines(in, 0, in.length, 76, systemLineSeparator); + for (char c = '0'; c <= '9'; c++) { + map1[i++] = c; } + map1[i++] = '+'; + map1[i++] = '/'; + } - /** - * Encodes a byte array into Base 64 format and breaks the output into - * lines. - * - * @param in - * An array containing the data bytes to be encoded. - * @param iOff - * Offset of the first byte in <code>in</code> to be processed. - * @param iLen - * Number of bytes to be processed in <code>in</code>, starting - * at <code>iOff</code>. - * @param lineLen - * Line length for the output data. Should be a multiple of 4. - * @param lineSeparator - * The line separator to be used to separate the output lines. - * @return A String containing the Base64 encoded data, broken into lines. - */ - public static String encodeLines(byte[] in, int iOff, int iLen, int lineLen, - String lineSeparator) { - int blockLen = (lineLen * 3) / 4; - if (blockLen <= 0) - throw new IllegalArgumentException(); - int lines = (iLen + blockLen - 1) / blockLen; - int bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length(); - StringBuilder buf = new StringBuilder(bufLen); - int ip = 0; - while (ip < iLen) { - int l = Math.min(iLen - ip, blockLen); - buf.append(encode(in, iOff + ip, l)); - buf.append(lineSeparator); - ip += l; - } - return buf.toString(); - } + // Mapping table from Base64 characters to 6-bit nibbles. + private static final byte[] map2 = new byte[128]; - /** - * Encodes a byte array into Base64 format. No blanks or line breaks are - * inserted in the output. - * - * @param in - * An array containing the data bytes to be encoded. - * @return A character array containing the Base64 encoded data. - */ - public static char[] encode(byte[] in) { - return encode(in, 0, in.length); + static { + for (int i = 0; i < map2.length; i++) { + map2[i] = -1; } - - /** - * Encodes a byte array into Base64 format. No blanks or line breaks are - * inserted in the output. - * - * @param in - * An array containing the data bytes to be encoded. - * @param iLen - * Number of bytes to process in <code>in</code>. - * @return A character array containing the Base64 encoded data. - */ - public static char[] encode(byte[] in, int iLen) { - return encode(in, 0, iLen); + for (int i = 0; i < 64; i++) { + map2[map1[i]] = (byte) i; } + } - /** - * Encodes a byte array into Base64 format. No blanks or line breaks are - * inserted in the output. - * - * @param in - * An array containing the data bytes to be encoded. - * @param iOff - * Offset of the first byte in <code>in</code> to be processed. - * @param iLen - * Number of bytes to process in <code>in</code>, starting at - * <code>iOff</code>. - * @return A character array containing the Base64 encoded data. - */ - public static char[] encode(byte[] in, int iOff, int iLen) { - int oDataLen = (iLen * 4 + 2) / 3; // output length without padding - int oLen = ((iLen + 2) / 3) * 4; // output length including padding - char[] out = new char[oLen]; - int ip = iOff; - int iEnd = iOff + iLen; - int op = 0; - while (ip < iEnd) { - int i0 = in[ip++] & 0xff; - int i1 = ip < iEnd ? in[ip++] & 0xff : 0; - int i2 = ip < iEnd ? in[ip++] & 0xff : 0; - int o0 = i0 >>> 2; - int o1 = ((i0 & 3) << 4) | (i1 >>> 4); - int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); - int o3 = i2 & 0x3F; - out[op++] = map1[o0]; - out[op++] = map1[o1]; - out[op] = op < oDataLen ? map1[o2] : '='; - op++; - out[op] = op < oDataLen ? map1[o3] : '='; - op++; - } - return out; - } + /** + * Encodes a string into Base64 format. No blanks or line breaks are inserted. + * + * @param s A String to be encoded. + * @return A String containing the Base64 encoded data. + */ + public static String encodeString(String s) { + return new String(encode(s.getBytes())); + } - /** - * Decodes a string from Base64 format. No blanks or line breaks are allowed - * within the Base64 encoded input data. - * - * @param s - * A Base64 String to be decoded. - * @return A String containing the decoded data. - * @throws IllegalArgumentException - * If the input is not valid Base64 encoded data. - */ - public static String decodeString(String s) { - return new String(decode(s)); - } + /** + * Encodes a byte array into Base 64 format and breaks the output into lines of 76 characters. + * This method is compatible with <code>sun.misc.BASE64Encoder.encodeBuffer(byte[])</code>. + * + * @param in An array containing the data bytes to be encoded. + * @return A String containing the Base64 encoded data, broken into lines. + */ + public static String encodeLines(byte[] in) { + return encodeLines(in, 0, in.length, 76, systemLineSeparator); + } - /** - * Decodes a byte array from Base64 format and ignores line separators, tabs - * and blanks. CR, LF, Tab and Space characters are ignored in the input - * data. This method is compatible with - * <code>sun.misc.BASE64Decoder.decodeBuffer(String)</code>. - * - * @param s - * A Base64 String to be decoded. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException - * If the input is not valid Base64 encoded data. - */ - public static byte[] decodeLines(String s) { - char[] buf = new char[s.length()]; - int p = 0; - for (int ip = 0; ip < s.length(); ip++) { - char c = s.charAt(ip); - if (c != ' ' && c != '\r' && c != '\n' && c != '\t') - buf[p++] = c; - } - return decode(buf, 0, p); + /** + * Encodes a byte array into Base 64 format and breaks the output into lines. + * + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in <code>in</code> to be processed. + * @param iLen Number of bytes to be processed in <code>in</code>, starting at <code>iOff</code>. + * @param lineLen Line length for the output data. Should be a multiple of 4. + * @param lineSeparator The line separator to be used to separate the output lines. + * @return A String containing the Base64 encoded data, broken into lines. + */ + public static String encodeLines(byte[] in, int iOff, int iLen, int lineLen, + String lineSeparator) { + int blockLen = (lineLen * 3) / 4; + if (blockLen <= 0) { + throw new IllegalArgumentException(); } - - /** - * Decodes a byte array from Base64 format. No blanks or line breaks are - * allowed within the Base64 encoded input data. - * - * @param s - * A Base64 String to be decoded. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException - * If the input is not valid Base64 encoded data. - */ - public static byte[] decode(String s) { - return decode(s.toCharArray()); + int lines = (iLen + blockLen - 1) / blockLen; + int bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length(); + StringBuilder buf = new StringBuilder(bufLen); + int ip = 0; + while (ip < iLen) { + int l = Math.min(iLen - ip, blockLen); + buf.append(encode(in, iOff + ip, l)); + buf.append(lineSeparator); + ip += l; } + return buf.toString(); + } - /** - * Decodes a byte array from Base64 format. No blanks or line breaks are - * allowed within the Base64 encoded input data. - * - * @param in - * A character array containing the Base64 encoded data. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException - * If the input is not valid Base64 encoded data. - */ - public static byte[] decode(char[] in) { - return decode(in, 0, in.length); + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output. + * + * @param in An array containing the data bytes to be encoded. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in) { + return encode(in, 0, in.length); + } + + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output. + * + * @param in An array containing the data bytes to be encoded. + * @param iLen Number of bytes to process in <code>in</code>. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in, int iLen) { + return encode(in, 0, iLen); + } + + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output. + * + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in <code>in</code> to be processed. + * @param iLen Number of bytes to process in <code>in</code>, starting at <code>iOff</code>. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in, int iOff, int iLen) { + int oDataLen = (iLen * 4 + 2) / 3; // output length without padding + int oLen = ((iLen + 2) / 3) * 4; // output length including padding + char[] out = new char[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++] & 0xff; + int i1 = ip < iEnd ? in[ip++] & 0xff : 0; + int i2 = ip < iEnd ? in[ip++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[op++] = map1[o0]; + out[op++] = map1[o1]; + out[op] = op < oDataLen ? map1[o2] : '='; + op++; + out[op] = op < oDataLen ? map1[o3] : '='; + op++; } + return out; + } - /** - * Decodes a byte array from Base64 format. No blanks or line breaks are - * allowed within the Base64 encoded input data. - * - * @param in - * A character array containing the Base64 encoded data. - * @param iOff - * Offset of the first character in <code>in</code> to be - * processed. - * @param iLen - * Number of characters to process in <code>in</code>, starting - * at <code>iOff</code>. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException - * If the input is not valid Base64 encoded data. - */ - public static byte[] decode(char[] in, int iOff, int iLen) { - if (iLen % 4 != 0) - throw new IllegalArgumentException( - "Length of Base64 encoded input string is not a multiple of 4."); - while (iLen > 0 && in[iOff + iLen - 1] == '=') - iLen--; - int oLen = (iLen * 3) / 4; - byte[] out = new byte[oLen]; - int ip = iOff; - int iEnd = iOff + iLen; - int op = 0; - while (ip < iEnd) { - int i0 = in[ip++]; - int i1 = in[ip++]; - int i2 = ip < iEnd ? in[ip++] : 'A'; - int i3 = ip < iEnd ? in[ip++] : 'A'; - if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) - throw new IllegalArgumentException("Illegal character in Base64 encoded data."); - int b0 = map2[i0]; - int b1 = map2[i1]; - int b2 = map2[i2]; - int b3 = map2[i3]; - if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) - throw new IllegalArgumentException("Illegal character in Base64 encoded data."); - int o0 = (b0 << 2) | (b1 >>> 4); - int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); - int o2 = ((b2 & 3) << 6) | b3; - out[op++] = (byte) o0; - if (op < oLen) - out[op++] = (byte) o1; - if (op < oLen) - out[op++] = (byte) o2; - } - return out; + /** + * Decodes a string from Base64 format. No blanks or line breaks are allowed within the Base64 + * encoded input data. + * + * @param s A Base64 String to be decoded. + * @return A String containing the decoded data. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static String decodeString(String s) { + return new String(decode(s)); + } + + /** + * Decodes a byte array from Base64 format and ignores line separators, tabs and blanks. CR, LF, + * Tab and Space characters are ignored in the input data. This method is compatible with + * <code>sun.misc.BASE64Decoder.decodeBuffer(String)</code>. + * + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decodeLines(String s) { + char[] buf = new char[s.length()]; + int p = 0; + for (int ip = 0; ip < s.length(); ip++) { + char c = s.charAt(ip); + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') { + buf[p++] = c; + } } + return decode(buf, 0, p); + } + + /** + * Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 + * encoded input data. + * + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(String s) { + return decode(s.toCharArray()); + } - // Dummy constructor. - private Base64Coder() { + /** + * Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 + * encoded input data. + * + * @param in A character array containing the Base64 encoded data. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(char[] in) { + return decode(in, 0, in.length); + } + + /** + * Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 + * encoded input data. + * + * @param in A character array containing the Base64 encoded data. + * @param iOff Offset of the first character in <code>in</code> to be processed. + * @param iLen Number of characters to process in <code>in</code>, starting at <code>iOff</code>. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(char[] in, int iOff, int iLen) { + if (iLen % 4 != 0) { + throw new IllegalArgumentException( + "Length of Base64 encoded input string is not a multiple of 4."); + } + while (iLen > 0 && in[iOff + iLen - 1] == '=') { + iLen--; } + int oLen = (iLen * 3) / 4; + byte[] out = new byte[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iEnd ? in[ip++] : 'A'; + int i3 = ip < iEnd ? in[ip++] : 'A'; + if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) { + throw new IllegalArgumentException("Illegal character in Base64 encoded data."); + } + int b0 = map2[i0]; + int b1 = map2[i1]; + int b2 = map2[i2]; + int b3 = map2[i3]; + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) { + throw new IllegalArgumentException("Illegal character in Base64 encoded data."); + } + int o0 = (b0 << 2) | (b1 >>> 4); + int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); + int o2 = ((b2 & 3) << 6) | b3; + out[op++] = (byte) o0; + if (op < oLen) { + out[op++] = (byte) o1; + } + if (op < oLen) { + out[op++] = (byte) o2; + } + } + return out; + } + + // Dummy constructor. + private Base64Coder() {} } // end class Base64Coder diff --git a/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/Escaper.java b/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/Escaper.java index c26e3cb6..a94f6154 100644 --- a/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/Escaper.java +++ b/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/Escaper.java @@ -1,97 +1,82 @@ -/* Copyright (c) 2008 Google Inc. +/* + * Copyright (c) 2008 Google Inc. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.external.com.google.gdata.util.common.base; /** - * An object that converts literal text into a format safe for inclusion in a - * particular context (such as an XML document). Typically (but not always), the - * inverse process of "unescaping" the text is performed automatically by the - * relevant parser. - * + * An object that converts literal text into a format safe for inclusion in a particular context + * (such as an XML document). Typically (but not always), the inverse process of "unescaping" the + * text is performed automatically by the relevant parser. + * * <p> - * For example, an XML escaper would convert the literal string - * {@code "Foo<Bar>"} into {@code "Foo<Bar>"} to prevent {@code "<Bar>"} - * from being confused with an XML tag. When the resulting XML document is - * parsed, the parser API will return this text as the original literal string - * {@code "Foo<Bar>"}. - * + * For example, an XML escaper would convert the literal string {@code "Foo<Bar>"} into + * {@code "Foo<Bar>"} to prevent {@code "<Bar>"} from being confused with an XML tag. When the + * resulting XML document is parsed, the parser API will return this text as the original literal + * string {@code "Foo<Bar>"}. + * * <p> - * An {@code Escaper} instance is required to be stateless, and safe when used - * concurrently by multiple threads. - * + * An {@code Escaper} instance is required to be stateless, and safe when used concurrently by + * multiple threads. + * * <p> - * Several popular escapers are defined as constants in the class - * {@link CharEscapers}. To create your own escapers, use - * {@link CharEscaperBuilder}, or extend {@link CharEscaper} or + * Several popular escapers are defined as constants in the class {@link CharEscapers}. To create + * your own escapers, use {@link CharEscaperBuilder}, or extend {@link CharEscaper} or * {@code UnicodeEscaper}. - * - * */ public interface Escaper { - /** - * Returns the escaped form of a given literal string. - * - * <p> - * Note that this method may treat input characters differently depending on - * the specific escaper implementation. - * <ul> - * <li>{@link UnicodeEscaper} handles <a - * href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> correctly, - * including surrogate character pairs. If the input is badly formed the - * escaper should throw {@link IllegalArgumentException}. - * <li>{@link CharEscaper} handles Java characters independently and does - * not verify the input for well formed characters. A CharEscaper should not - * be used in situations where input is not guaranteed to be restricted to - * the Basic Multilingual Plane (BMP). - * </ul> - * - * @param string - * the literal string to be escaped - * @return the escaped form of {@code string} - * @throws NullPointerException - * if {@code string} is null - * @throws IllegalArgumentException - * if {@code string} contains badly formed UTF-16 or cannot be - * escaped for any other reason - */ - public String escape(String string); - /** - * Returns an {@code Appendable} instance which automatically escapes all - * text appended to it before passing the resulting text to an underlying - * {@code Appendable}. - * - * <p> - * Note that this method may treat input characters differently depending on - * the specific escaper implementation. - * <ul> - * <li>{@link UnicodeEscaper} handles <a - * href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> correctly, - * including surrogate character pairs. If the input is badly formed the - * escaper should throw {@link IllegalArgumentException}. - * <li>{@link CharEscaper} handles Java characters independently and does - * not verify the input for well formed characters. A CharEscaper should not - * be used in situations where input is not guaranteed to be restricted to - * the Basic Multilingual Plane (BMP). - * </ul> - * - * @param out - * the underlying {@code Appendable} to append escaped output to - * @return an {@code Appendable} which passes text to {@code out} after - * escaping it. - */ - public Appendable escape(Appendable out); + /** + * Returns the escaped form of a given literal string. + * + * <p> + * Note that this method may treat input characters differently depending on the specific escaper + * implementation. + * <ul> + * <li>{@link UnicodeEscaper} handles <a href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> + * correctly, including surrogate character pairs. If the input is badly formed the escaper should + * throw {@link IllegalArgumentException}. + * <li>{@link CharEscaper} handles Java characters independently and does not verify the input for + * well formed characters. A CharEscaper should not be used in situations where input is not + * guaranteed to be restricted to the Basic Multilingual Plane (BMP). + * </ul> + * + * @param string the literal string to be escaped + * @return the escaped form of {@code string} + * @throws NullPointerException if {@code string} is null + * @throws IllegalArgumentException if {@code string} contains badly formed UTF-16 or cannot be + * escaped for any other reason + */ + String escape(String string); + + /** + * Returns an {@code Appendable} instance which automatically escapes all text appended to it + * before passing the resulting text to an underlying {@code Appendable}. + * + * <p> + * Note that this method may treat input characters differently depending on the specific escaper + * implementation. + * <ul> + * <li>{@link UnicodeEscaper} handles <a href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> + * correctly, including surrogate character pairs. If the input is badly formed the escaper should + * throw {@link IllegalArgumentException}. + * <li>{@link CharEscaper} handles Java characters independently and does not verify the input for + * well formed characters. A CharEscaper should not be used in situations where input is not + * guaranteed to be restricted to the Basic Multilingual Plane (BMP). + * </ul> + * + * @param out the underlying {@code Appendable} to append escaped output to + * @return an {@code Appendable} which passes text to {@code out} after escaping it. + */ + Appendable escape(Appendable out); } diff --git a/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/PercentEscaper.java b/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/PercentEscaper.java index 5e2f902d..f115a2cc 100644 --- a/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/PercentEscaper.java +++ b/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/PercentEscaper.java @@ -1,281 +1,267 @@ -/* Copyright (c) 2008 Google Inc. +/* + * Copyright (c) 2008 Google Inc. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.external.com.google.gdata.util.common.base; /** - * A {@code UnicodeEscaper} that escapes some set of Java characters using the - * URI percent encoding scheme. The set of safe characters (those which remain - * unescaped) can be specified on construction. - * + * A {@code UnicodeEscaper} that escapes some set of Java characters using the URI percent encoding + * scheme. The set of safe characters (those which remain unescaped) can be specified on + * construction. + * * <p> - * For details on escaping URIs for use in web pages, see section 2.4 of <a - * href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>. - * + * For details on escaping URIs for use in web pages, see section 2.4 of + * <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>. + * * <p> - * In most cases this class should not need to be used directly. If you have no - * special requirements for escaping your URIs, you should use either - * {@link CharEscapers#uriEscaper()} or {@link CharEscapers#uriEscaper(boolean)}. - * + * In most cases this class should not need to be used directly. If you have no special requirements + * for escaping your URIs, you should use either {@link CharEscapers#uriEscaper()} or + * {@link CharEscapers#uriEscaper(boolean)}. + * * <p> * When encoding a String, the following rules apply: * <ul> - * <li>The alphanumeric characters "a" through "z", "A" through "Z" and "0" - * through "9" remain the same. + * <li>The alphanumeric characters "a" through "z", "A" through "Z" and "0" through "9" remain the + * same. * <li>Any additionally specified safe characters remain the same. - * <li>If {@code plusForSpace} was specified, the space character " " is - * converted into a plus sign "+". - * <li>All other characters are converted into one or more bytes using UTF-8 - * encoding and each byte is then represented by the 3-character string "%XY", - * where "XY" is the two-digit, uppercase, hexadecimal representation of the - * byte value. + * <li>If {@code plusForSpace} was specified, the space character " " is converted into a plus sign + * "+". + * <li>All other characters are converted into one or more bytes using UTF-8 encoding and each byte + * is then represented by the 3-character string "%XY", where "XY" is the two-digit, uppercase, + * hexadecimal representation of the byte value. * </ul> - * + * * <p> - * RFC 2396 specifies the set of unreserved characters as "-", "_", ".", "!", - * "~", "*", "'", "(" and ")". It goes on to state: - * + * RFC 2396 specifies the set of unreserved characters as "-", "_", ".", "!", "~", "*", "'", "(" and + * ")". It goes on to state: + * * <p> - * <i>Unreserved characters can be escaped without changing the semantics of the - * URI, but this should not be done unless the URI is being used in a context - * that does not allow the unescaped character to appear.</i> - * + * <i>Unreserved characters can be escaped without changing the semantics of the URI, but this + * should not be done unless the URI is being used in a context that does not allow the unescaped + * character to appear.</i> + * * <p> - * For performance reasons the only currently supported character encoding of - * this class is UTF-8. - * + * For performance reasons the only currently supported character encoding of this class is UTF-8. + * * <p> - * <b>Note</b>: This escaper produces uppercase hexidecimal sequences. From <a - * href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>:<br> - * <i>"URI producers and normalizers should use uppercase hexadecimal digits for - * all percent-encodings."</i> - * - * + * <b>Note</b>: This escaper produces uppercase hexidecimal sequences. From + * <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>:<br> + * <i>"URI producers and normalizers should use uppercase hexadecimal digits for all + * percent-encodings."</i> */ public class PercentEscaper extends UnicodeEscaper { - /** - * A string of safe characters that mimics the behavior of - * {@link java.net.URLEncoder}. - * - */ - public static final String SAFECHARS_URLENCODER = "-_.*"; - /** - * A string of characters that do not need to be encoded when used in URI - * path segments, as specified in RFC 3986. Note that some of these - * characters do need to be escaped when used in other parts of the URI. - */ - public static final String SAFEPATHCHARS_URLENCODER = "-_.!~*'()@:$&,;="; + /** + * A string of safe characters that mimics the behavior of {@link java.net.URLEncoder}. + */ + public static final String SAFECHARS_URLENCODER = "-_.*"; - /** - * A string of characters that do not need to be encoded when used in URI - * query strings, as specified in RFC 3986. Note that some of these - * characters do need to be escaped when used in other parts of the URI. - */ - public static final String SAFEQUERYSTRINGCHARS_URLENCODER = "-_.!~*'()@:$,;/?:"; + /** + * A string of characters that do not need to be encoded when used in URI path segments, as + * specified in RFC 3986. Note that some of these characters do need to be escaped when used in + * other parts of the URI. + */ + public static final String SAFEPATHCHARS_URLENCODER = "-_.!~*'()@:$&,;="; - // In some uri escapers spaces are escaped to '+' - private static final char[] URI_ESCAPED_SPACE = { '+' }; + /** + * A string of characters that do not need to be encoded when used in URI query strings, as + * specified in RFC 3986. Note that some of these characters do need to be escaped when used in + * other parts of the URI. + */ + public static final String SAFEQUERYSTRINGCHARS_URLENCODER = "-_.!~*'()@:$,;/?:"; - private static final char[] UPPER_HEX_DIGITS = "0123456789ABCDEF".toCharArray(); + // In some uri escapers spaces are escaped to '+' + private static final char[] URI_ESCAPED_SPACE = {'+'}; - /** - * If true we should convert space to the {@code +} character. - */ - private final boolean plusForSpace; + private static final char[] UPPER_HEX_DIGITS = "0123456789ABCDEF".toCharArray(); - /** - * An array of flags where for any {@code char c} if {@code safeOctets[c]} - * is true then {@code c} should remain unmodified in the output. If - * {@code c > safeOctets.length} then it should be escaped. - */ - private final boolean[] safeOctets; + /** + * If true we should convert space to the {@code +} character. + */ + private final boolean plusForSpace; - /** - * Constructs a URI escaper with the specified safe characters and optional - * handling of the space character. - * - * @param safeChars - * a non null string specifying additional safe characters for - * this escaper (the ranges 0..9, a..z and A..Z are always safe - * and should not be specified here) - * @param plusForSpace - * true if ASCII space should be escaped to {@code +} rather than - * {@code %20} - * @throws IllegalArgumentException - * if any of the parameters were invalid - */ - public PercentEscaper(String safeChars, boolean plusForSpace) { - // Avoid any misunderstandings about the behavior of this escaper - if (safeChars.matches(".*[0-9A-Za-z].*")) { - throw new IllegalArgumentException( - "Alphanumeric characters are always 'safe' and should not be " - + "explicitly specified"); - } - // Avoid ambiguous parameters. Safe characters are never modified so if - // space is a safe character then setting plusForSpace is meaningless. - if (plusForSpace && safeChars.contains(" ")) { - throw new IllegalArgumentException( - "plusForSpace cannot be specified when space is a 'safe' character"); - } - if (safeChars.contains("%")) { - throw new IllegalArgumentException("The '%' character cannot be specified as 'safe'"); - } - this.plusForSpace = plusForSpace; - this.safeOctets = createSafeOctets(safeChars); + /** + * An array of flags where for any {@code char c} if {@code safeOctets[c]} is true then {@code c} + * should remain unmodified in the output. If {@code c > safeOctets.length} then it should be + * escaped. + */ + private final boolean[] safeOctets; + + /** + * Constructs a URI escaper with the specified safe characters and optional handling of the space + * character. + * + * @param safeChars a non null string specifying additional safe characters for this escaper (the + * ranges 0..9, a..z and A..Z are always safe and should not be specified here) + * @param plusForSpace true if ASCII space should be escaped to {@code +} rather than {@code %20} + * @throws IllegalArgumentException if any of the parameters were invalid + */ + public PercentEscaper(String safeChars, boolean plusForSpace) { + // Avoid any misunderstandings about the behavior of this escaper + if (safeChars.matches(".*[0-9A-Za-z].*")) { + throw new IllegalArgumentException( + "Alphanumeric characters are always 'safe' and should not be " + "explicitly specified"); + } + // Avoid ambiguous parameters. Safe characters are never modified so if + // space is a safe character then setting plusForSpace is meaningless. + if (plusForSpace && safeChars.contains(" ")) { + throw new IllegalArgumentException( + "plusForSpace cannot be specified when space is a 'safe' character"); } + if (safeChars.contains("%")) { + throw new IllegalArgumentException("The '%' character cannot be specified as 'safe'"); + } + this.plusForSpace = plusForSpace; + this.safeOctets = createSafeOctets(safeChars); + } - /** - * Creates a boolean[] with entries corresponding to the character values - * for 0-9, A-Z, a-z and those specified in safeChars set to true. The array - * is as small as is required to hold the given character information. - */ - private static boolean[] createSafeOctets(String safeChars) { - int maxChar = 'z'; - char[] safeCharArray = safeChars.toCharArray(); - for (char c : safeCharArray) { - maxChar = Math.max(c, maxChar); - } - boolean[] octets = new boolean[maxChar + 1]; - for (int c = '0'; c <= '9'; c++) { - octets[c] = true; - } - for (int c = 'A'; c <= 'Z'; c++) { - octets[c] = true; - } - for (int c = 'a'; c <= 'z'; c++) { - octets[c] = true; - } - for (char c : safeCharArray) { - octets[c] = true; - } - return octets; + /** + * Creates a boolean[] with entries corresponding to the character values for 0-9, A-Z, a-z and + * those specified in safeChars set to true. The array is as small as is required to hold the + * given character information. + */ + private static boolean[] createSafeOctets(String safeChars) { + int maxChar = 'z'; + char[] safeCharArray = safeChars.toCharArray(); + for (char c : safeCharArray) { + maxChar = Math.max(c, maxChar); + } + boolean[] octets = new boolean[maxChar + 1]; + for (int c = '0'; c <= '9'; c++) { + octets[c] = true; + } + for (int c = 'A'; c <= 'Z'; c++) { + octets[c] = true; + } + for (int c = 'a'; c <= 'z'; c++) { + octets[c] = true; + } + for (char c : safeCharArray) { + octets[c] = true; } + return octets; + } - /* - * Overridden for performance. For unescaped strings this improved the - * performance of the uri escaper from ~760ns to ~400ns as measured by - * {@link CharEscapersBenchmark}. - */ - @Override - protected int nextEscapeIndex(CharSequence csq, int index, int end) { - for (; index < end; index++) { - char c = csq.charAt(index); - if (c >= safeOctets.length || !safeOctets[c]) { - break; - } - } - return index; + /* + * Overridden for performance. For unescaped strings this improved the performance of the uri + * escaper from ~760ns to ~400ns as measured by {@link CharEscapersBenchmark}. + */ + @Override + protected int nextEscapeIndex(CharSequence csq, int index, int end) { + for (; index < end; index++) { + char c = csq.charAt(index); + if (c >= safeOctets.length || !safeOctets[c]) { + break; + } } + return index; + } - /* - * Overridden for performance. For unescaped strings this improved the - * performance of the uri escaper from ~400ns to ~170ns as measured by - * {@link CharEscapersBenchmark}. - */ - @Override - public String escape(String s) { - int slen = s.length(); - for (int index = 0; index < slen; index++) { - char c = s.charAt(index); - if (c >= safeOctets.length || !safeOctets[c]) { - return escapeSlow(s, index); - } - } - return s; + /* + * Overridden for performance. For unescaped strings this improved the performance of the uri + * escaper from ~400ns to ~170ns as measured by {@link CharEscapersBenchmark}. + */ + @Override + public String escape(String s) { + int slen = s.length(); + for (int index = 0; index < slen; index++) { + char c = s.charAt(index); + if (c >= safeOctets.length || !safeOctets[c]) { + return escapeSlow(s, index); + } } + return s; + } - /** - * Escapes the given Unicode code point in UTF-8. - */ - @Override - protected char[] escape(int cp) { - // We should never get negative values here but if we do it will throw - // an - // IndexOutOfBoundsException, so at least it will get spotted. - if (cp < safeOctets.length && safeOctets[cp]) { - return null; - } else if (cp == ' ' && plusForSpace) { - return URI_ESCAPED_SPACE; - } else if (cp <= 0x7F) { - // Single byte UTF-8 characters - // Start with "%--" and fill in the blanks - char[] dest = new char[3]; - dest[0] = '%'; - dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; - dest[1] = UPPER_HEX_DIGITS[cp >>> 4]; - return dest; - } else if (cp <= 0x7ff) { - // Two byte UTF-8 characters [cp >= 0x80 && cp <= 0x7ff] - // Start with "%--%--" and fill in the blanks - char[] dest = new char[6]; - dest[0] = '%'; - dest[3] = '%'; - dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; - cp >>>= 4; - dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; - cp >>>= 2; - dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; - cp >>>= 4; - dest[1] = UPPER_HEX_DIGITS[0xC | cp]; - return dest; - } else if (cp <= 0xffff) { - // Three byte UTF-8 characters [cp >= 0x800 && cp <= 0xffff] - // Start with "%E-%--%--" and fill in the blanks - char[] dest = new char[9]; - dest[0] = '%'; - dest[1] = 'E'; - dest[3] = '%'; - dest[6] = '%'; - dest[8] = UPPER_HEX_DIGITS[cp & 0xF]; - cp >>>= 4; - dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; - cp >>>= 2; - dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; - cp >>>= 4; - dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; - cp >>>= 2; - dest[2] = UPPER_HEX_DIGITS[cp]; - return dest; - } else if (cp <= 0x10ffff) { - char[] dest = new char[12]; - // Four byte UTF-8 characters [cp >= 0xffff && cp <= 0x10ffff] - // Start with "%F-%--%--%--" and fill in the blanks - dest[0] = '%'; - dest[1] = 'F'; - dest[3] = '%'; - dest[6] = '%'; - dest[9] = '%'; - dest[11] = UPPER_HEX_DIGITS[cp & 0xF]; - cp >>>= 4; - dest[10] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; - cp >>>= 2; - dest[8] = UPPER_HEX_DIGITS[cp & 0xF]; - cp >>>= 4; - dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; - cp >>>= 2; - dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; - cp >>>= 4; - dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; - cp >>>= 2; - dest[2] = UPPER_HEX_DIGITS[cp & 0x7]; - return dest; - } else { - // If this ever happens it is due to bug in UnicodeEscaper, not bad - // input. - throw new IllegalArgumentException("Invalid unicode character value " + cp); - } + /** + * Escapes the given Unicode code point in UTF-8. + */ + @Override + protected char[] escape(int cp) { + // We should never get negative values here but if we do it will throw + // an + // IndexOutOfBoundsException, so at least it will get spotted. + if (cp < safeOctets.length && safeOctets[cp]) { + return null; + } else if (cp == ' ' && plusForSpace) { + return URI_ESCAPED_SPACE; + } else if (cp <= 0x7F) { + // Single byte UTF-8 characters + // Start with "%--" and fill in the blanks + char[] dest = new char[3]; + dest[0] = '%'; + dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; + dest[1] = UPPER_HEX_DIGITS[cp >>> 4]; + return dest; + } else if (cp <= 0x7ff) { + // Two byte UTF-8 characters [cp >= 0x80 && cp <= 0x7ff] + // Start with "%--%--" and fill in the blanks + char[] dest = new char[6]; + dest[0] = '%'; + dest[3] = '%'; + dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[1] = UPPER_HEX_DIGITS[0xC | cp]; + return dest; + } else if (cp <= 0xffff) { + // Three byte UTF-8 characters [cp >= 0x800 && cp <= 0xffff] + // Start with "%E-%--%--" and fill in the blanks + char[] dest = new char[9]; + dest[0] = '%'; + dest[1] = 'E'; + dest[3] = '%'; + dest[6] = '%'; + dest[8] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[2] = UPPER_HEX_DIGITS[cp]; + return dest; + } else if (cp <= 0x10ffff) { + char[] dest = new char[12]; + // Four byte UTF-8 characters [cp >= 0xffff && cp <= 0x10ffff] + // Start with "%F-%--%--%--" and fill in the blanks + dest[0] = '%'; + dest[1] = 'F'; + dest[3] = '%'; + dest[6] = '%'; + dest[9] = '%'; + dest[11] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[10] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[8] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[2] = UPPER_HEX_DIGITS[cp & 0x7]; + return dest; + } else { + // If this ever happens it is due to bug in UnicodeEscaper, not bad + // input. + throw new IllegalArgumentException("Invalid unicode character value " + cp); } + } } diff --git a/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/UnicodeEscaper.java b/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/UnicodeEscaper.java index 54031850..00230df8 100644 --- a/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/UnicodeEscaper.java +++ b/src/main/java/org/yaml/snakeyaml/external/com/google/gdata/util/common/base/UnicodeEscaper.java @@ -1,16 +1,15 @@ -/* Copyright (c) 2008 Google Inc. +/* + * Copyright (c) 2008 Google Inc. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.external.com.google.gdata.util.common.base; @@ -18,489 +17,446 @@ package org.yaml.snakeyaml.external.com.google.gdata.util.common.base; import java.io.IOException; /** - * An {@link Escaper} that converts literal text into a format safe for - * inclusion in a particular context (such as an XML document). Typically (but - * not always), the inverse process of "unescaping" the text is performed - * automatically by the relevant parser. - * + * An {@link Escaper} that converts literal text into a format safe for inclusion in a particular + * context (such as an XML document). Typically (but not always), the inverse process of + * "unescaping" the text is performed automatically by the relevant parser. + * * <p> - * For example, an XML escaper would convert the literal string - * {@code "Foo<Bar>"} into {@code "Foo<Bar>"} to prevent {@code "<Bar>"} - * from being confused with an XML tag. When the resulting XML document is - * parsed, the parser API will return this text as the original literal string - * {@code "Foo<Bar>"}. - * + * For example, an XML escaper would convert the literal string {@code "Foo<Bar>"} into + * {@code "Foo<Bar>"} to prevent {@code "<Bar>"} from being confused with an XML tag. When the + * resulting XML document is parsed, the parser API will return this text as the original literal + * string {@code "Foo<Bar>"}. + * * <p> - * <b>Note:</b> This class is similar to {@link CharEscaper} but with one very - * important difference. A CharEscaper can only process Java <a - * href="http://en.wikipedia.org/wiki/UTF-16">UTF16</a> characters in isolation - * and may not cope when it encounters surrogate pairs. This class facilitates - * the correct escaping of all Unicode characters. - * + * <b>Note:</b> This class is similar to {@link CharEscaper} but with one very important difference. + * A CharEscaper can only process Java <a href="http://en.wikipedia.org/wiki/UTF-16">UTF16</a> + * characters in isolation and may not cope when it encounters surrogate pairs. This class + * facilitates the correct escaping of all Unicode characters. + * * <p> - * As there are important reasons, including potential security issues, to - * handle Unicode correctly if you are considering implementing a new escaper - * you should favor using UnicodeEscaper wherever possible. - * + * As there are important reasons, including potential security issues, to handle Unicode correctly + * if you are considering implementing a new escaper you should favor using UnicodeEscaper wherever + * possible. + * * <p> - * A {@code UnicodeEscaper} instance is required to be stateless, and safe when - * used concurrently by multiple threads. - * + * A {@code UnicodeEscaper} instance is required to be stateless, and safe when used concurrently by + * multiple threads. + * * <p> - * Several popular escapers are defined as constants in the class - * {@link CharEscapers}. To create your own escapers extend this class and - * implement the {@link #escape(int)} method. - * - * + * Several popular escapers are defined as constants in the class {@link CharEscapers}. To create + * your own escapers extend this class and implement the {@link #escape(int)} method. */ public abstract class UnicodeEscaper implements Escaper { - /** The amount of padding (chars) to use when growing the escape buffer. */ - private static final int DEST_PAD = 32; - /** - * Returns the escaped form of the given Unicode code point, or {@code null} - * if this code point does not need to be escaped. When called as part of an - * escaping operation, the given code point is guaranteed to be in the range - * {@code 0 <= cp <= Character#MAX_CODE_POINT}. - * - * <p> - * If an empty array is returned, this effectively strips the input - * character from the resulting text. - * - * <p> - * If the character does not need to be escaped, this method should return - * {@code null}, rather than an array containing the character - * representation of the code point. This enables the escaping algorithm to - * perform more efficiently. - * - * <p> - * If the implementation of this method cannot correctly handle a particular - * code point then it should either throw an appropriate runtime exception - * or return a suitable replacement character. It must never silently - * discard invalid input as this may constitute a security risk. - * - * @param cp - * the Unicode code point to escape if necessary - * @return the replacement characters, or {@code null} if no escaping was - * needed - */ - protected abstract char[] escape(int cp); + /** + * The amount of padding (chars) to use when growing the escape buffer. + */ + private static final int DEST_PAD = 32; - /** - * Scans a sub-sequence of characters from a given {@link CharSequence}, - * returning the index of the next character that requires escaping. - * - * <p> - * <b>Note:</b> When implementing an escaper, it is a good idea to override - * this method for efficiency. The base class implementation determines - * successive Unicode code points and invokes {@link #escape(int)} for each - * of them. If the semantics of your escaper are such that code points in - * the supplementary range are either all escaped or all unescaped, this - * method can be implemented more efficiently using - * {@link CharSequence#charAt(int)}. - * - * <p> - * Note however that if your escaper does not escape characters in the - * supplementary range, you should either continue to validate the - * correctness of any surrogate characters encountered or provide a clear - * warning to users that your escaper does not validate its input. - * - * <p> - * See {@link PercentEscaper} for an example. - * - * @param csq - * a sequence of characters - * @param start - * the index of the first character to be scanned - * @param end - * the index immediately after the last character to be scanned - * @throws IllegalArgumentException - * if the scanned sub-sequence of {@code csq} contains invalid - * surrogate pairs - */ - protected int nextEscapeIndex(CharSequence csq, int start, int end) { - int index = start; - while (index < end) { - int cp = codePointAt(csq, index, end); - if (cp < 0 || escape(cp) != null) { - break; - } - index += Character.isSupplementaryCodePoint(cp) ? 2 : 1; - } - return index; - } + /** + * Returns the escaped form of the given Unicode code point, or {@code null} if this code point + * does not need to be escaped. When called as part of an escaping operation, the given code point + * is guaranteed to be in the range {@code 0 <= cp <= Character#MAX_CODE_POINT}. + * + * <p> + * If an empty array is returned, this effectively strips the input character from the resulting + * text. + * + * <p> + * If the character does not need to be escaped, this method should return {@code null}, rather + * than an array containing the character representation of the code point. This enables the + * escaping algorithm to perform more efficiently. + * + * <p> + * If the implementation of this method cannot correctly handle a particular code point then it + * should either throw an appropriate runtime exception or return a suitable replacement + * character. It must never silently discard invalid input as this may constitute a security risk. + * + * @param cp the Unicode code point to escape if necessary + * @return the replacement characters, or {@code null} if no escaping was needed + */ + protected abstract char[] escape(int cp); - /** - * Returns the escaped form of a given literal string. - * - * <p> - * If you are escaping input in arbitrary successive chunks, then it is not - * generally safe to use this method. If an input string ends with an - * unmatched high surrogate character, then this method will throw - * {@link IllegalArgumentException}. You should either ensure your input is - * valid <a href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> before - * calling this method or use an escaped {@link Appendable} (as returned by - * {@link #escape(Appendable)}) which can cope with arbitrarily split input. - * - * <p> - * <b>Note:</b> When implementing an escaper it is a good idea to override - * this method for efficiency by inlining the implementation of - * {@link #nextEscapeIndex(CharSequence, int, int)} directly. Doing this for - * {@link PercentEscaper} more than doubled the performance for unescaped - * strings (as measured by {@link CharEscapersBenchmark}). - * - * @param string - * the literal string to be escaped - * @return the escaped form of {@code string} - * @throws NullPointerException - * if {@code string} is null - * @throws IllegalArgumentException - * if invalid surrogate characters are encountered - */ - public String escape(String string) { - int end = string.length(); - int index = nextEscapeIndex(string, 0, end); - return index == end ? string : escapeSlow(string, index); + /** + * Scans a sub-sequence of characters from a given {@link CharSequence}, returning the index of + * the next character that requires escaping. + * + * <p> + * <b>Note:</b> When implementing an escaper, it is a good idea to override this method for + * efficiency. The base class implementation determines successive Unicode code points and invokes + * {@link #escape(int)} for each of them. If the semantics of your escaper are such that code + * points in the supplementary range are either all escaped or all unescaped, this method can be + * implemented more efficiently using {@link CharSequence#charAt(int)}. + * + * <p> + * Note however that if your escaper does not escape characters in the supplementary range, you + * should either continue to validate the correctness of any surrogate characters encountered or + * provide a clear warning to users that your escaper does not validate its input. + * + * <p> + * See {@link PercentEscaper} for an example. + * + * @param csq a sequence of characters + * @param start the index of the first character to be scanned + * @param end the index immediately after the last character to be scanned + * @throws IllegalArgumentException if the scanned sub-sequence of {@code csq} contains invalid + * surrogate pairs + */ + protected int nextEscapeIndex(CharSequence csq, int start, int end) { + int index = start; + while (index < end) { + int cp = codePointAt(csq, index, end); + if (cp < 0 || escape(cp) != null) { + break; + } + index += Character.isSupplementaryCodePoint(cp) ? 2 : 1; } + return index; + } - /** - * Returns the escaped form of a given literal string, starting at the given - * index. This method is called by the {@link #escape(String)} method when - * it discovers that escaping is required. It is protected to allow - * subclasses to override the fastpath escaping function to inline their - * escaping test. See {@link CharEscaperBuilder} for an example usage. - * - * <p> - * This method is not reentrant and may only be invoked by the top level - * {@link #escape(String)} method. - * - * @param s - * the literal string to be escaped - * @param index - * the index to start escaping from - * @return the escaped form of {@code string} - * @throws NullPointerException - * if {@code string} is null - * @throws IllegalArgumentException - * if invalid surrogate characters are encountered - */ - protected final String escapeSlow(String s, int index) { - int end = s.length(); + /** + * Returns the escaped form of a given literal string. + * + * <p> + * If you are escaping input in arbitrary successive chunks, then it is not generally safe to use + * this method. If an input string ends with an unmatched high surrogate character, then this + * method will throw {@link IllegalArgumentException}. You should either ensure your input is + * valid <a href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> before calling this method or + * use an escaped {@link Appendable} (as returned by {@link #escape(Appendable)}) which can cope + * with arbitrarily split input. + * + * <p> + * <b>Note:</b> When implementing an escaper it is a good idea to override this method for + * efficiency by inlining the implementation of {@link #nextEscapeIndex(CharSequence, int, int)} + * directly. Doing this for {@link PercentEscaper} more than doubled the performance for unescaped + * strings (as measured by {@link CharEscapersBenchmark}). + * + * @param string the literal string to be escaped + * @return the escaped form of {@code string} + * @throws NullPointerException if {@code string} is null + * @throws IllegalArgumentException if invalid surrogate characters are encountered + */ + public String escape(String string) { + int end = string.length(); + int index = nextEscapeIndex(string, 0, end); + return index == end ? string : escapeSlow(string, index); + } - // Get a destination buffer and setup some loop variables. - char[] dest = DEST_TL.get(); - int destIndex = 0; - int unescapedChunkStart = 0; + /** + * Returns the escaped form of a given literal string, starting at the given index. This method is + * called by the {@link #escape(String)} method when it discovers that escaping is required. It is + * protected to allow subclasses to override the fastpath escaping function to inline their + * escaping test. See {@link CharEscaperBuilder} for an example usage. + * + * <p> + * This method is not reentrant and may only be invoked by the top level {@link #escape(String)} + * method. + * + * @param s the literal string to be escaped + * @param index the index to start escaping from + * @return the escaped form of {@code string} + * @throws NullPointerException if {@code string} is null + * @throws IllegalArgumentException if invalid surrogate characters are encountered + */ + protected final String escapeSlow(String s, int index) { + int end = s.length(); - while (index < end) { - int cp = codePointAt(s, index, end); - if (cp < 0) { - throw new IllegalArgumentException("Trailing high surrogate at end of input"); - } - char[] escaped = escape(cp); - if (escaped != null) { - int charsSkipped = index - unescapedChunkStart; + // Get a destination buffer and setup some loop variables. + char[] dest = DEST_TL.get(); + int destIndex = 0; + int unescapedChunkStart = 0; - // This is the size needed to add the replacement, not the full - // size needed by the string. We only regrow when we absolutely - // must. - int sizeNeeded = destIndex + charsSkipped + escaped.length; - if (dest.length < sizeNeeded) { - int destLength = sizeNeeded + (end - index) + DEST_PAD; - dest = growBuffer(dest, destIndex, destLength); - } - // If we have skipped any characters, we need to copy them now. - if (charsSkipped > 0) { - s.getChars(unescapedChunkStart, index, dest, destIndex); - destIndex += charsSkipped; - } - if (escaped.length > 0) { - System.arraycopy(escaped, 0, dest, destIndex, escaped.length); - destIndex += escaped.length; - } - } - unescapedChunkStart = index + (Character.isSupplementaryCodePoint(cp) ? 2 : 1); - index = nextEscapeIndex(s, unescapedChunkStart, end); - } + while (index < end) { + int cp = codePointAt(s, index, end); + if (cp < 0) { + throw new IllegalArgumentException("Trailing high surrogate at end of input"); + } + char[] escaped = escape(cp); + if (escaped != null) { + int charsSkipped = index - unescapedChunkStart; - // Process trailing unescaped characters - no need to account for - // escaped - // length or padding the allocation. - int charsSkipped = end - unescapedChunkStart; + // This is the size needed to add the replacement, not the full + // size needed by the string. We only regrow when we absolutely + // must. + int sizeNeeded = destIndex + charsSkipped + escaped.length; + if (dest.length < sizeNeeded) { + int destLength = sizeNeeded + (end - index) + DEST_PAD; + dest = growBuffer(dest, destIndex, destLength); + } + // If we have skipped any characters, we need to copy them now. if (charsSkipped > 0) { - int endIndex = destIndex + charsSkipped; - if (dest.length < endIndex) { - dest = growBuffer(dest, destIndex, endIndex); - } - s.getChars(unescapedChunkStart, end, dest, destIndex); - destIndex = endIndex; + s.getChars(unescapedChunkStart, index, dest, destIndex); + destIndex += charsSkipped; } - return new String(dest, 0, destIndex); + if (escaped.length > 0) { + System.arraycopy(escaped, 0, dest, destIndex, escaped.length); + destIndex += escaped.length; + } + } + unescapedChunkStart = index + (Character.isSupplementaryCodePoint(cp) ? 2 : 1); + index = nextEscapeIndex(s, unescapedChunkStart, end); } - /** - * Returns an {@code Appendable} instance which automatically escapes all - * text appended to it before passing the resulting text to an underlying - * {@code Appendable}. - * - * <p> - * Unlike {@link #escape(String)} it is permitted to append arbitrarily - * split input to this Appendable, including input that is split over a - * surrogate pair. In this case the pending high surrogate character will - * not be processed until the corresponding low surrogate is appended. This - * means that a trailing high surrogate character at the end of the input - * cannot be detected and will be silently ignored. This is unavoidable - * since the Appendable interface has no {@code close()} method, and it is - * impossible to determine when the last characters have been appended. - * - * <p> - * The methods of the returned object will propagate any exceptions thrown - * by the underlying {@code Appendable}. - * - * <p> - * For well formed <a href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> - * the escaping behavior is identical to that of {@link #escape(String)} and - * the following code is equivalent to (but much slower than) - * {@code escaper.escape(string)}: - * - * <pre> - * { - * @code - * StringBuilder sb = new StringBuilder(); - * escaper.escape(sb).append(string); - * return sb.toString(); - * } - * </pre> - * - * @param out - * the underlying {@code Appendable} to append escaped output to - * @return an {@code Appendable} which passes text to {@code out} after - * escaping it - * @throws NullPointerException - * if {@code out} is null - * @throws IllegalArgumentException - * if invalid surrogate characters are encountered - * - */ - public Appendable escape(final Appendable out) { - assert out != null; + // Process trailing unescaped characters - no need to account for + // escaped + // length or padding the allocation. + int charsSkipped = end - unescapedChunkStart; + if (charsSkipped > 0) { + int endIndex = destIndex + charsSkipped; + if (dest.length < endIndex) { + dest = growBuffer(dest, destIndex, endIndex); + } + s.getChars(unescapedChunkStart, end, dest, destIndex); + destIndex = endIndex; + } + return new String(dest, 0, destIndex); + } - return new Appendable() { - int pendingHighSurrogate = -1; - char[] decodedChars = new char[2]; + /** + * Returns an {@code Appendable} instance which automatically escapes all text appended to it + * before passing the resulting text to an underlying {@code Appendable}. + * + * <p> + * Unlike {@link #escape(String)} it is permitted to append arbitrarily split input to this + * Appendable, including input that is split over a surrogate pair. In this case the pending high + * surrogate character will not be processed until the corresponding low surrogate is appended. + * This means that a trailing high surrogate character at the end of the input cannot be detected + * and will be silently ignored. This is unavoidable since the Appendable interface has no + * {@code close()} method, and it is impossible to determine when the last characters have been + * appended. + * + * <p> + * The methods of the returned object will propagate any exceptions thrown by the underlying + * {@code Appendable}. + * + * <p> + * For well formed <a href="http://en.wikipedia.org/wiki/UTF-16">UTF-16</a> the escaping behavior + * is identical to that of {@link #escape(String)} and the following code is equivalent to (but + * much slower than) {@code escaper.escape(string)}: + * + * <pre> + * { + * @code + * StringBuilder sb = new StringBuilder(); + * escaper.escape(sb).append(string); + * return sb.toString(); + * } + * </pre> + * + * @param out the underlying {@code Appendable} to append escaped output to + * @return an {@code Appendable} which passes text to {@code out} after escaping it + * @throws NullPointerException if {@code out} is null + * @throws IllegalArgumentException if invalid surrogate characters are encountered + */ + public Appendable escape(final Appendable out) { + assert out != null; - public Appendable append(CharSequence csq) throws IOException { - return append(csq, 0, csq.length()); - } + return new Appendable() { + int pendingHighSurrogate = -1; + final char[] decodedChars = new char[2]; - public Appendable append(CharSequence csq, int start, int end) throws IOException { - int index = start; - if (index < end) { - // This is a little subtle: index must never reference the - // middle of a - // surrogate pair but unescapedChunkStart can. The first - // time we enter - // the loop below it is possible that index != - // unescapedChunkStart. - int unescapedChunkStart = index; - if (pendingHighSurrogate != -1) { - // Our last append operation ended halfway through a - // surrogate pair - // so we have to do some extra work first. - char c = csq.charAt(index++); - if (!Character.isLowSurrogate(c)) { - throw new IllegalArgumentException( - "Expected low surrogate character but got " + c); - } - char[] escaped = escape(Character.toCodePoint((char) pendingHighSurrogate, - c)); - if (escaped != null) { - // Emit the escaped character and adjust - // unescapedChunkStart to - // skip the low surrogate we have consumed. - outputChars(escaped, escaped.length); - unescapedChunkStart += 1; - } else { - // Emit pending high surrogate (unescaped) but do - // not modify - // unescapedChunkStart as we must still emit the low - // surrogate. - out.append((char) pendingHighSurrogate); - } - pendingHighSurrogate = -1; - } - while (true) { - // Find and append the next subsequence of unescaped - // characters. - index = nextEscapeIndex(csq, index, end); - if (index > unescapedChunkStart) { - out.append(csq, unescapedChunkStart, index); - } - if (index == end) { - break; - } - // If we are not finished, calculate the next code - // point. - int cp = codePointAt(csq, index, end); - if (cp < 0) { - // Our sequence ended half way through a surrogate - // pair so just - // record the state and exit. - pendingHighSurrogate = -cp; - break; - } - // Escape the code point and output the characters. - char[] escaped = escape(cp); - if (escaped != null) { - outputChars(escaped, escaped.length); - } else { - // This shouldn't really happen if nextEscapeIndex - // is correct but - // we should cope with false positives. - int len = Character.toChars(cp, decodedChars, 0); - outputChars(decodedChars, len); - } - // Update our index past the escaped character and - // continue. - index += (Character.isSupplementaryCodePoint(cp) ? 2 : 1); - unescapedChunkStart = index; - } - } - return this; - } + public Appendable append(CharSequence csq) throws IOException { + return append(csq, 0, csq.length()); + } - public Appendable append(char c) throws IOException { - if (pendingHighSurrogate != -1) { - // Our last append operation ended halfway through a - // surrogate pair - // so we have to do some extra work first. - if (!Character.isLowSurrogate(c)) { - throw new IllegalArgumentException( - "Expected low surrogate character but got '" + c + "' with value " - + (int) c); - } - char[] escaped = escape(Character.toCodePoint((char) pendingHighSurrogate, c)); - if (escaped != null) { - outputChars(escaped, escaped.length); - } else { - out.append((char) pendingHighSurrogate); - out.append(c); - } - pendingHighSurrogate = -1; - } else if (Character.isHighSurrogate(c)) { - // This is the start of a (split) surrogate pair. - pendingHighSurrogate = c; - } else { - if (Character.isLowSurrogate(c)) { - throw new IllegalArgumentException("Unexpected low surrogate character '" - + c + "' with value " + (int) c); - } - // This is a normal (non surrogate) char. - char[] escaped = escape(c); - if (escaped != null) { - outputChars(escaped, escaped.length); - } else { - out.append(c); - } - } - return this; + public Appendable append(CharSequence csq, int start, int end) throws IOException { + int index = start; + if (index < end) { + // This is a little subtle: index must never reference the + // middle of a + // surrogate pair but unescapedChunkStart can. The first + // time we enter + // the loop below it is possible that index != + // unescapedChunkStart. + int unescapedChunkStart = index; + if (pendingHighSurrogate != -1) { + // Our last append operation ended halfway through a + // surrogate pair + // so we have to do some extra work first. + char c = csq.charAt(index++); + if (!Character.isLowSurrogate(c)) { + throw new IllegalArgumentException("Expected low surrogate character but got " + c); } - - private void outputChars(char[] chars, int len) throws IOException { - for (int n = 0; n < len; n++) { - out.append(chars[n]); - } + char[] escaped = escape(Character.toCodePoint((char) pendingHighSurrogate, c)); + if (escaped != null) { + // Emit the escaped character and adjust + // unescapedChunkStart to + // skip the low surrogate we have consumed. + outputChars(escaped, escaped.length); + unescapedChunkStart += 1; + } else { + // Emit pending high surrogate (unescaped) but do + // not modify + // unescapedChunkStart as we must still emit the low + // surrogate. + out.append((char) pendingHighSurrogate); } - }; - } - - /** - * Returns the Unicode code point of the character at the given index. - * - * <p> - * Unlike {@link Character#codePointAt(CharSequence, int)} or - * {@link String#codePointAt(int)} this method will never fail silently when - * encountering an invalid surrogate pair. - * - * <p> - * The behaviour of this method is as follows: - * <ol> - * <li>If {@code index >= end}, {@link IndexOutOfBoundsException} is thrown. - * <li><b>If the character at the specified index is not a surrogate, it is - * returned.</b> - * <li>If the first character was a high surrogate value, then an attempt is - * made to read the next character. - * <ol> - * <li><b>If the end of the sequence was reached, the negated value of the - * trailing high surrogate is returned.</b> - * <li><b>If the next character was a valid low surrogate, the code point - * value of the high/low surrogate pair is returned.</b> - * <li>If the next character was not a low surrogate value, then - * {@link IllegalArgumentException} is thrown. - * </ol> - * <li>If the first character was a low surrogate value, - * {@link IllegalArgumentException} is thrown. - * </ol> - * - * @param seq - * the sequence of characters from which to decode the code point - * @param index - * the index of the first character to decode - * @param end - * the index beyond the last valid character to decode - * @return the Unicode code point for the given index or the negated value - * of the trailing high surrogate character at the end of the - * sequence - */ - protected static final int codePointAt(CharSequence seq, int index, int end) { - if (index < end) { - char c1 = seq.charAt(index++); - if (c1 < Character.MIN_HIGH_SURROGATE || c1 > Character.MAX_LOW_SURROGATE) { - // Fast path (first test is probably all we need to do) - return c1; - } else if (c1 <= Character.MAX_HIGH_SURROGATE) { - // If the high surrogate was the last character, return its - // inverse - if (index == end) { - return -c1; - } - // Otherwise look for the low surrogate following it - char c2 = seq.charAt(index); - if (Character.isLowSurrogate(c2)) { - return Character.toCodePoint(c1, c2); - } - throw new IllegalArgumentException("Expected low surrogate but got char '" + c2 - + "' with value " + (int) c2 + " at index " + index); + pendingHighSurrogate = -1; + } + while (true) { + // Find and append the next subsequence of unescaped + // characters. + index = nextEscapeIndex(csq, index, end); + if (index > unescapedChunkStart) { + out.append(csq, unescapedChunkStart, index); + } + if (index == end) { + break; + } + // If we are not finished, calculate the next code + // point. + int cp = codePointAt(csq, index, end); + if (cp < 0) { + // Our sequence ended half way through a surrogate + // pair so just + // record the state and exit. + pendingHighSurrogate = -cp; + break; + } + // Escape the code point and output the characters. + char[] escaped = escape(cp); + if (escaped != null) { + outputChars(escaped, escaped.length); } else { - throw new IllegalArgumentException("Unexpected low surrogate character '" + c1 - + "' with value " + (int) c1 + " at index " + (index - 1)); + // This shouldn't really happen if nextEscapeIndex + // is correct but + // we should cope with false positives. + int len = Character.toChars(cp, decodedChars, 0); + outputChars(decodedChars, len); } + // Update our index past the escaped character and + // continue. + index += (Character.isSupplementaryCodePoint(cp) ? 2 : 1); + unescapedChunkStart = index; + } } - throw new IndexOutOfBoundsException("Index exceeds specified range"); - } + return this; + } - /** - * Helper method to grow the character buffer as needed, this only happens - * once in a while so it's ok if it's in a method call. If the index passed - * in is 0 then no copying will be done. - */ - private static final char[] growBuffer(char[] dest, int index, int size) { - char[] copy = new char[size]; - if (index > 0) { - System.arraycopy(dest, 0, copy, 0, index); + public Appendable append(char c) throws IOException { + if (pendingHighSurrogate != -1) { + // Our last append operation ended halfway through a + // surrogate pair + // so we have to do some extra work first. + if (!Character.isLowSurrogate(c)) { + throw new IllegalArgumentException( + "Expected low surrogate character but got '" + c + "' with value " + (int) c); + } + char[] escaped = escape(Character.toCodePoint((char) pendingHighSurrogate, c)); + if (escaped != null) { + outputChars(escaped, escaped.length); + } else { + out.append((char) pendingHighSurrogate); + out.append(c); + } + pendingHighSurrogate = -1; + } else if (Character.isHighSurrogate(c)) { + // This is the start of a (split) surrogate pair. + pendingHighSurrogate = c; + } else { + if (Character.isLowSurrogate(c)) { + throw new IllegalArgumentException( + "Unexpected low surrogate character '" + c + "' with value " + (int) c); + } + // This is a normal (non surrogate) char. + char[] escaped = escape(c); + if (escaped != null) { + outputChars(escaped, escaped.length); + } else { + out.append(c); + } } - return copy; - } + return this; + } - /** - * A thread-local destination buffer to keep us from creating new buffers. - * The starting size is 1024 characters. If we grow past this we don't put - * it back in the threadlocal, we just keep going and grow as needed. - */ - private static final ThreadLocal<char[]> DEST_TL = new ThreadLocal<char[]>() { - @Override - protected char[] initialValue() { - return new char[1024]; + private void outputChars(char[] chars, int len) throws IOException { + for (int n = 0; n < len; n++) { + out.append(chars[n]); } + } }; + } + + /** + * Returns the Unicode code point of the character at the given index. + * + * <p> + * Unlike {@link Character#codePointAt(CharSequence, int)} or {@link String#codePointAt(int)} this + * method will never fail silently when encountering an invalid surrogate pair. + * + * <p> + * The behaviour of this method is as follows: + * <ol> + * <li>If {@code index >= end}, {@link IndexOutOfBoundsException} is thrown. + * <li><b>If the character at the specified index is not a surrogate, it is returned.</b> + * <li>If the first character was a high surrogate value, then an attempt is made to read the next + * character. + * <ol> + * <li><b>If the end of the sequence was reached, the negated value of the trailing high surrogate + * is returned.</b> + * <li><b>If the next character was a valid low surrogate, the code point value of the high/low + * surrogate pair is returned.</b> + * <li>If the next character was not a low surrogate value, then {@link IllegalArgumentException} + * is thrown. + * </ol> + * <li>If the first character was a low surrogate value, {@link IllegalArgumentException} is + * thrown. + * </ol> + * + * @param seq the sequence of characters from which to decode the code point + * @param index the index of the first character to decode + * @param end the index beyond the last valid character to decode + * @return the Unicode code point for the given index or the negated value of the trailing high + * surrogate character at the end of the sequence + */ + protected static final int codePointAt(CharSequence seq, int index, int end) { + if (index < end) { + char c1 = seq.charAt(index++); + if (c1 < Character.MIN_HIGH_SURROGATE || c1 > Character.MAX_LOW_SURROGATE) { + // Fast path (first test is probably all we need to do) + return c1; + } else if (c1 <= Character.MAX_HIGH_SURROGATE) { + // If the high surrogate was the last character, return its + // inverse + if (index == end) { + return -c1; + } + // Otherwise look for the low surrogate following it + char c2 = seq.charAt(index); + if (Character.isLowSurrogate(c2)) { + return Character.toCodePoint(c1, c2); + } + throw new IllegalArgumentException("Expected low surrogate but got char '" + c2 + + "' with value " + (int) c2 + " at index " + index); + } else { + throw new IllegalArgumentException("Unexpected low surrogate character '" + c1 + + "' with value " + (int) c1 + " at index " + (index - 1)); + } + } + throw new IndexOutOfBoundsException("Index exceeds specified range"); + } + + /** + * Helper method to grow the character buffer as needed, this only happens once in a while so it's + * ok if it's in a method call. If the index passed in is 0 then no copying will be done. + */ + private static final char[] growBuffer(char[] dest, int index, int size) { + char[] copy = new char[size]; + if (index > 0) { + System.arraycopy(dest, 0, copy, 0, index); + } + return copy; + } + + /** + * A thread-local destination buffer to keep us from creating new buffers. The starting size is + * 1024 characters. If we grow past this we don't put it back in the threadlocal, we just keep + * going and grow as needed. + */ + private static final ThreadLocal<char[]> DEST_TL = new ThreadLocal<char[]>() { + @Override + protected char[] initialValue() { + return new char[1024]; + } + }; } diff --git a/src/main/java/org/yaml/snakeyaml/introspector/BeanAccess.java b/src/main/java/org/yaml/snakeyaml/introspector/BeanAccess.java index 696fec73..04454673 100644 --- a/src/main/java/org/yaml/snakeyaml/introspector/BeanAccess.java +++ b/src/main/java/org/yaml/snakeyaml/introspector/BeanAccess.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.introspector; @@ -19,12 +17,12 @@ package org.yaml.snakeyaml.introspector; * Control instance variables. */ public enum BeanAccess { - /** use JavaBean properties and public fields */ - DEFAULT, + /** use JavaBean properties and public fields */ + DEFAULT, - /** use all declared fields (including inherited) */ - FIELD, + /** use all declared fields (including inherited) */ + FIELD, - /** reserved */ - PROPERTY; -}
\ No newline at end of file + /** reserved */ + PROPERTY +} diff --git a/src/main/java/org/yaml/snakeyaml/introspector/FieldProperty.java b/src/main/java/org/yaml/snakeyaml/introspector/FieldProperty.java index 9bc38aac..6d064174 100644 --- a/src/main/java/org/yaml/snakeyaml/introspector/FieldProperty.java +++ b/src/main/java/org/yaml/snakeyaml/introspector/FieldProperty.java @@ -1,52 +1,64 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.introspector; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; - +import java.util.List; import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.util.ArrayUtils; /** * <p> - * A <code>FieldProperty</code> is a <code>Property</code> which is accessed as - * a field, without going through accessor methods (setX, getX). The field may - * have any scope (public, package, protected, private). + * A <code>FieldProperty</code> is a <code>Property</code> which is accessed as a field, without + * going through accessor methods (setX, getX). The field may have any scope (public, package, + * protected, private). * </p> */ public class FieldProperty extends GenericProperty { - private final Field field; - public FieldProperty(Field field) { - super(field.getName(), field.getType(), field.getGenericType()); - this.field = field; - field.setAccessible(true); - } + private final Field field; - @Override - public void set(Object object, Object value) throws Exception { - field.set(object, value); - } + public FieldProperty(Field field) { + super(field.getName(), field.getType(), field.getGenericType()); + this.field = field; + field.setAccessible(true); + } + + @Override + public void set(Object object, Object value) throws Exception { + field.set(object, value); + } - @Override - public Object get(Object object) { - try { - return field.get(object); - } catch (Exception e) { - throw new YAMLException("Unable to access field " + field.getName() + " on object " - + object + " : " + e); - } + @Override + public Object get(Object object) { + try { + return field.get(object); + } catch (Exception e) { + throw new YAMLException( + "Unable to access field " + field.getName() + " on object " + object + " : " + e); } -}
\ No newline at end of file + } + + @Override + public List<Annotation> getAnnotations() { + return ArrayUtils.toUnmodifiableList(field.getAnnotations()); + } + + @Override + public <A extends Annotation> A getAnnotation(Class<A> annotationType) { + return field.getAnnotation(annotationType); + } + +} diff --git a/src/main/java/org/yaml/snakeyaml/introspector/GenericProperty.java b/src/main/java/org/yaml/snakeyaml/introspector/GenericProperty.java index 30981601..7fff2b5e 100644 --- a/src/main/java/org/yaml/snakeyaml/introspector/GenericProperty.java +++ b/src/main/java/org/yaml/snakeyaml/introspector/GenericProperty.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.introspector; @@ -20,63 +18,62 @@ import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -abstract public class GenericProperty extends Property { +public abstract class GenericProperty extends Property { - private Type genType; + private final Type genType; - public GenericProperty(String name, Class<?> aClass, Type aType) { - super(name, aClass); - genType = aType; - actualClassesChecked = aType == null; - } + public GenericProperty(String name, Class<?> aClass, Type aType) { + super(name, aClass); + genType = aType; + actualClassesChecked = aType == null; + } - private boolean actualClassesChecked; - private Class<?>[] actualClasses; + private boolean actualClassesChecked; + private Class<?>[] actualClasses; - public Class<?>[] getActualTypeArguments() { // should we synchronize here ? - if (!actualClassesChecked) { - if (genType instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) genType; - Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); - if (actualTypeArguments.length > 0) { - actualClasses = new Class<?>[actualTypeArguments.length]; - for (int i = 0; i < actualTypeArguments.length; i++) { - if (actualTypeArguments[i] instanceof Class<?>) { - actualClasses[i] = (Class<?>) actualTypeArguments[i]; - } else if (actualTypeArguments[i] instanceof ParameterizedType) { - actualClasses[i] = (Class<?>) ((ParameterizedType) actualTypeArguments[i]) - .getRawType(); - } else if (actualTypeArguments[i] instanceof GenericArrayType) { - Type componentType = ((GenericArrayType) actualTypeArguments[i]) - .getGenericComponentType(); - if (componentType instanceof Class<?>) { - actualClasses[i] = Array.newInstance((Class<?>) componentType, 0) - .getClass(); - } else { - actualClasses = null; - break; - } - } else { - actualClasses = null; - break; - } - } - } - } else if (genType instanceof GenericArrayType) { - Type componentType = ((GenericArrayType) genType).getGenericComponentType(); - if (componentType instanceof Class<?>) { - actualClasses = new Class<?>[] { (Class<?>) componentType }; - } - } else if (genType instanceof Class<?>) {// XXX this check is only - // required for IcedTea6 - Class<?> classType = (Class<?>) genType; - if (classType.isArray()) { - actualClasses = new Class<?>[1]; - actualClasses[0] = getType().getComponentType(); - } + public Class<?>[] getActualTypeArguments() { // should we synchronize here ? + if (!actualClassesChecked) { + if (genType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genType; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length > 0) { + actualClasses = new Class<?>[actualTypeArguments.length]; + for (int i = 0; i < actualTypeArguments.length; i++) { + if (actualTypeArguments[i] instanceof Class<?>) { + actualClasses[i] = (Class<?>) actualTypeArguments[i]; + } else if (actualTypeArguments[i] instanceof ParameterizedType) { + actualClasses[i] = + (Class<?>) ((ParameterizedType) actualTypeArguments[i]).getRawType(); + } else if (actualTypeArguments[i] instanceof GenericArrayType) { + Type componentType = + ((GenericArrayType) actualTypeArguments[i]).getGenericComponentType(); + if (componentType instanceof Class<?>) { + actualClasses[i] = Array.newInstance((Class<?>) componentType, 0).getClass(); + } else { + actualClasses = null; + break; + } + } else { + actualClasses = null; + break; } - actualClassesChecked = true; + } + } + } else if (genType instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) genType).getGenericComponentType(); + if (componentType instanceof Class<?>) { + actualClasses = new Class<?>[] {(Class<?>) componentType}; + } + } else if (genType instanceof Class<?>) {// XXX this check is only + // required for IcedTea6 + Class<?> classType = (Class<?>) genType; + if (classType.isArray()) { + actualClasses = new Class<?>[1]; + actualClasses[0] = getType().getComponentType(); } - return actualClasses; + } + actualClassesChecked = true; } + return actualClasses; + } } diff --git a/src/main/java/org/yaml/snakeyaml/introspector/MethodProperty.java b/src/main/java/org/yaml/snakeyaml/introspector/MethodProperty.java index fc6e00d5..c27fa139 100644 --- a/src/main/java/org/yaml/snakeyaml/introspector/MethodProperty.java +++ b/src/main/java/org/yaml/snakeyaml/introspector/MethodProperty.java @@ -1,71 +1,137 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.introspector; import java.beans.PropertyDescriptor; - +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.List; import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.util.ArrayUtils; /** * <p> - * A <code>MethodProperty</code> is a <code>Property</code> which is accessed - * through accessor methods (setX, getX). It is possible to have a - * <code>MethodProperty</code> which has only setter, only getter, or both. It - * is not possible to have a <code>MethodProperty</code> which has neither + * A <code>MethodProperty</code> is a <code>Property</code> which is accessed through accessor + * methods (setX, getX). It is possible to have a <code>MethodProperty</code> which has only setter, + * only getter, or both. It is not possible to have a <code>MethodProperty</code> which has neither * setter nor getter. * </p> */ public class MethodProperty extends GenericProperty { - private final PropertyDescriptor property; - private final boolean readable; - private final boolean writable; + private final PropertyDescriptor property; + private final boolean readable; + private final boolean writable; + + private static Type discoverGenericType(PropertyDescriptor property) { + Method readMethod = property.getReadMethod(); + if (readMethod != null) { + return readMethod.getGenericReturnType(); + } - public MethodProperty(PropertyDescriptor property) { - super(property.getName(), property.getPropertyType(), - property.getReadMethod() == null ? null : property.getReadMethod() - .getGenericReturnType()); - this.property = property; - this.readable = property.getReadMethod() != null; - this.writable = property.getWriteMethod() != null; + Method writeMethod = property.getWriteMethod(); + if (writeMethod != null) { + Type[] paramTypes = writeMethod.getGenericParameterTypes(); + if (paramTypes.length > 0) { + return paramTypes[0]; + } } + /* + * This actually may happen if PropertyDescriptor is of type IndexedPropertyDescriptor and it + * has only IndexedGetter/Setter. ATM we simply skip type discovery. + */ + return null; + } + + public MethodProperty(PropertyDescriptor property) { + super(property.getName(), property.getPropertyType(), + MethodProperty.discoverGenericType(property)); + this.property = property; + this.readable = property.getReadMethod() != null; + this.writable = property.getWriteMethod() != null; + } - @Override - public void set(Object object, Object value) throws Exception { - property.getWriteMethod().invoke(object, value); + @Override + public void set(Object object, Object value) throws Exception { + if (!writable) { + throw new YAMLException( + "No writable property '" + getName() + "' on class: " + object.getClass().getName()); } + property.getWriteMethod().invoke(object, value); + } - @Override - public Object get(Object object) { - try { - property.getReadMethod().setAccessible(true);// issue 50 - return property.getReadMethod().invoke(object); - } catch (Exception e) { - throw new YAMLException("Unable to find getter for property '" + property.getName() - + "' on object " + object + ":" + e); - } + @Override + public Object get(Object object) { + try { + property.getReadMethod().setAccessible(true);// issue 50 + return property.getReadMethod().invoke(object); + } catch (Exception e) { + throw new YAMLException("Unable to find getter for property '" + property.getName() + + "' on object " + object + ":" + e); } + } - @Override - public boolean isWritable() { - return writable; + /** + * Returns the annotations that are present on read and write methods of this property or empty + * {@code List} if there're no annotations. + * + * @return the annotations that are present on this property or empty {@code List} if there're no + * annotations + */ + @Override + public List<Annotation> getAnnotations() { + List<Annotation> annotations; + if (isReadable() && isWritable()) { + annotations = ArrayUtils.toUnmodifiableCompositeList( + property.getReadMethod().getAnnotations(), property.getWriteMethod().getAnnotations()); + } else if (isReadable()) { + annotations = ArrayUtils.toUnmodifiableList(property.getReadMethod().getAnnotations()); + } else { + annotations = ArrayUtils.toUnmodifiableList(property.getWriteMethod().getAnnotations()); } + return annotations; + } - @Override - public boolean isReadable() { - return readable; + /** + * Returns property's annotation for the given type or {@code null} if it's not present. If the + * annotation is present on both read and write methods, the annotation on read method takes + * precedence. + * + * @param annotationType the type of the annotation to be returned + * @return property's annotation for the given type or {@code null} if it's not present + */ + @Override + public <A extends Annotation> A getAnnotation(Class<A> annotationType) { + A annotation = null; + if (isReadable()) { + annotation = property.getReadMethod().getAnnotation(annotationType); } -}
\ No newline at end of file + if (annotation == null && isWritable()) { + annotation = property.getWriteMethod().getAnnotation(annotationType); + } + return annotation; + } + + @Override + public boolean isWritable() { + return writable; + } + + @Override + public boolean isReadable() { + return readable; + } + +} diff --git a/src/main/java/org/yaml/snakeyaml/introspector/MissingProperty.java b/src/main/java/org/yaml/snakeyaml/introspector/MissingProperty.java index b16a1c27..97d7233c 100644 --- a/src/main/java/org/yaml/snakeyaml/introspector/MissingProperty.java +++ b/src/main/java/org/yaml/snakeyaml/introspector/MissingProperty.java @@ -1,44 +1,56 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.introspector; +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; + /** - * A property that does not map to a real property; this is used when {@link - * PropertyUtils.setSkipMissingProperties(boolean)} is set to true. + * A property that does not map to a real property; this is used when + * {@link PropertyUtils}.setSkipMissingProperties(boolean) is set to true. */ public class MissingProperty extends Property { - public MissingProperty(String name) { - super(name, Object.class); - } - - @Override - public Class<?>[] getActualTypeArguments() { - return new Class[0]; - } - - /** - * Setter does nothing. - */ - @Override - public void set(Object object, Object value) throws Exception { - } - - @Override - public Object get(Object object) { - return object; - } + public MissingProperty(String name) { + super(name, Object.class); + } + + @Override + public Class<?>[] getActualTypeArguments() { + return new Class[0]; + } + + /** + * Setter does nothing. + */ + @Override + public void set(Object object, Object value) throws Exception {} + + @Override + public Object get(Object object) { + return object; + } + + @Override + public List<Annotation> getAnnotations() { + return Collections.emptyList(); + } + + @Override + public <A extends Annotation> A getAnnotation(Class<A> annotationType) { + return null; + } + } diff --git a/src/main/java/org/yaml/snakeyaml/introspector/Property.java b/src/main/java/org/yaml/snakeyaml/introspector/Property.java index 286ca0e3..a65d8505 100644 --- a/src/main/java/org/yaml/snakeyaml/introspector/Property.java +++ b/src/main/java/org/yaml/snakeyaml/introspector/Property.java @@ -1,85 +1,104 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.introspector; +import java.lang.annotation.Annotation; +import java.util.List; + /** * <p> - * A <code>Property</code> represents a single member variable of a class, - * possibly including its accessor methods (getX, setX). The name stored in this - * class is the actual name of the property as given for the class, not an - * alias. + * A <code>Property</code> represents a single member variable of a class, possibly including its + * accessor methods (getX, setX). The name stored in this class is the actual name of the property + * as given for the class, not an alias. * </p> - * + * * <p> - * Objects of this class have a total ordering which defaults to ordering based - * on the name of the property. + * Objects of this class have a total ordering which defaults to ordering based on the name of the + * property. * </p> */ public abstract class Property implements Comparable<Property> { - private final String name; - private final Class<?> type; + private final String name; + private final Class<?> type; - public Property(String name, Class<?> type) { - this.name = name; - this.type = type; - } + public Property(String name, Class<?> type) { + this.name = name; + this.type = type; + } - public Class<?> getType() { - return type; - } + public Class<?> getType() { + return type; + } - abstract public Class<?>[] getActualTypeArguments(); + public abstract Class<?>[] getActualTypeArguments(); - public String getName() { - return name; - } + public String getName() { + return name; + } - @Override - public String toString() { - return getName() + " of " + getType(); - } + @Override + public String toString() { + return getName() + " of " + getType(); + } - public int compareTo(Property o) { - return name.compareTo(o.name); - } + public int compareTo(Property o) { + return getName().compareTo(o.getName()); + } - public boolean isWritable() { - return true; - } + public boolean isWritable() { + return true; + } - public boolean isReadable() { - return true; - } + public boolean isReadable() { + return true; + } - abstract public void set(Object object, Object value) throws Exception; + public abstract void set(Object object, Object value) throws Exception; - abstract public Object get(Object object); + public abstract Object get(Object object); - @Override - public int hashCode() { - return name.hashCode() + type.hashCode(); - } + /** + * Returns the annotations that are present on this property or empty {@code List} if there're no + * annotations. + * + * @return the annotations that are present on this property or empty {@code List} if there're no + * annotations + */ + public abstract List<Annotation> getAnnotations(); + + /** + * Returns property's annotation for the given type or {@code null} if it's not present. + * + * @param annotationType the type of the annotation to be returned + * @param <A> class of the annotation + * + * @return property's annotation for the given type or {@code null} if it's not present + */ + public abstract <A extends Annotation> A getAnnotation(Class<A> annotationType); + + @Override + public int hashCode() { + return getName().hashCode() + getType().hashCode(); + } - @Override - public boolean equals(Object other) { - if (other instanceof Property) { - Property p = (Property) other; - return name.equals(p.getName()) && type.equals(p.getType()); - } - return false; + @Override + public boolean equals(Object other) { + if (other instanceof Property) { + Property p = (Property) other; + return getName().equals(p.getName()) && getType().equals(p.getType()); } -}
\ No newline at end of file + return false; + } +} diff --git a/src/main/java/org/yaml/snakeyaml/introspector/PropertySubstitute.java b/src/main/java/org/yaml/snakeyaml/introspector/PropertySubstitute.java new file mode 100644 index 00000000..162df62a --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/introspector/PropertySubstitute.java @@ -0,0 +1,253 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.introspector; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.yaml.snakeyaml.error.YAMLException; + +// TODO: decide priorities for get/set Read/Field/Delegate Write/Field/Delegate - is FIELD on the +// correct place ? +public class PropertySubstitute extends Property { + + private static final Logger log = + Logger.getLogger(PropertySubstitute.class.getPackage().getName()); + + protected Class<?> targetType; + private final String readMethod; + private final String writeMethod; + private transient Method read; + private transient Method write; + private Field field; + protected Class<?>[] parameters; + private Property delegate; + private boolean filler; + + public PropertySubstitute(String name, Class<?> type, String readMethod, String writeMethod, + Class<?>... params) { + super(name, type); + this.readMethod = readMethod; + this.writeMethod = writeMethod; + setActualTypeArguments(params); + this.filler = false; + } + + public PropertySubstitute(String name, Class<?> type, Class<?>... params) { + this(name, type, null, null, params); + } + + @Override + public Class<?>[] getActualTypeArguments() { + if (parameters == null && delegate != null) { + return delegate.getActualTypeArguments(); + } + return parameters; + } + + public void setActualTypeArguments(Class<?>... args) { + if (args != null && args.length > 0) { + parameters = args; + } else { + parameters = null; + } + } + + @Override + public void set(Object object, Object value) throws Exception { + if (write != null) { + if (!filler) { + write.invoke(object, value); + } else if (value != null) { + if (value instanceof Collection<?>) { + Collection<?> collection = (Collection<?>) value; + for (Object val : collection) { + write.invoke(object, val); + } + } else if (value instanceof Map<?, ?>) { + Map<?, ?> map = (Map<?, ?>) value; + for (Entry<?, ?> entry : map.entrySet()) { + write.invoke(object, entry.getKey(), entry.getValue()); + } + } else if (value.getClass().isArray()) { + // TODO: maybe arrays need 2 fillers like SET(index, value) add ADD(value) + int len = Array.getLength(value); + for (int i = 0; i < len; i++) { + write.invoke(object, Array.get(value, i)); + } + } + } + } else if (field != null) { + field.set(object, value); + } else if (delegate != null) { + delegate.set(object, value); + } else { + log.warning("No setter/delegate for '" + getName() + "' on object " + object); + } + // TODO: maybe throw YAMLException here + } + + @Override + public Object get(Object object) { + try { + if (read != null) { + return read.invoke(object); + } else if (field != null) { + return field.get(object); + } + } catch (Exception e) { + throw new YAMLException( + "Unable to find getter for property '" + getName() + "' on object " + object + ":" + e); + } + + if (delegate != null) { + return delegate.get(object); + } + throw new YAMLException( + "No getter or delegate for property '" + getName() + "' on object " + object); + } + + @Override + public List<Annotation> getAnnotations() { + Annotation[] annotations = null; + if (read != null) { + annotations = read.getAnnotations(); + } else if (field != null) { + annotations = field.getAnnotations(); + } + return annotations != null ? Arrays.asList(annotations) : delegate.getAnnotations(); + } + + @Override + public <A extends Annotation> A getAnnotation(Class<A> annotationType) { + A annotation; + if (read != null) { + annotation = read.getAnnotation(annotationType); + } else if (field != null) { + annotation = field.getAnnotation(annotationType); + } else { + annotation = delegate.getAnnotation(annotationType); + } + return annotation; + } + + public void setTargetType(Class<?> targetType) { + if (this.targetType != targetType) { + this.targetType = targetType; + + final String name = getName(); + for (Class<?> c = targetType; c != null; c = c.getSuperclass()) { + for (Field f : c.getDeclaredFields()) { + if (f.getName().equals(name)) { + int modifiers = f.getModifiers(); + if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) { + f.setAccessible(true); + field = f; + } + break; + } + } + } + if (field == null && log.isLoggable(Level.FINE)) { + log.fine(String.format("Failed to find field for %s.%s", targetType.getName(), getName())); + } + + // Retrieve needed info + if (readMethod != null) { + read = discoverMethod(targetType, readMethod); + } + if (writeMethod != null) { + filler = false; + write = discoverMethod(targetType, writeMethod, getType()); + if (write == null && parameters != null) { + filler = true; + write = discoverMethod(targetType, writeMethod, parameters); + } + } + } + } + + private Method discoverMethod(Class<?> type, String name, Class<?>... params) { + for (Class<?> c = type; c != null; c = c.getSuperclass()) { + for (Method method : c.getDeclaredMethods()) { + if (name.equals(method.getName())) { + Class<?>[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != params.length) { + continue; + } + boolean found = true; + for (int i = 0; i < parameterTypes.length; i++) { + if (!parameterTypes[i].isAssignableFrom(params[i])) { + found = false; + } + } + if (found) { + method.setAccessible(true); + return method; + } + } + } + } + if (log.isLoggable(Level.FINE)) { + log.fine(String.format("Failed to find [%s(%d args)] for %s.%s", name, params.length, + targetType.getName(), getName())); + } + return null; + } + + @Override + public String getName() { + final String n = super.getName(); + if (n != null) { + return n; + } + return delegate != null ? delegate.getName() : null; + } + + @Override + public Class<?> getType() { + final Class<?> t = super.getType(); + if (t != null) { + return t; + } + return delegate != null ? delegate.getType() : null; + } + + @Override + public boolean isReadable() { + return (read != null) || (field != null) || (delegate != null && delegate.isReadable()); + } + + @Override + public boolean isWritable() { + return (write != null) || (field != null) || (delegate != null && delegate.isWritable()); + } + + public void setDelegate(Property delegate) { + this.delegate = delegate; + if (writeMethod != null && write == null && !filler) { + filler = true; + write = discoverMethod(targetType, writeMethod, getActualTypeArguments()); + } + } +} diff --git a/src/main/java/org/yaml/snakeyaml/introspector/PropertyUtils.java b/src/main/java/org/yaml/snakeyaml/introspector/PropertyUtils.java index 92519d90..8afc5761 100644 --- a/src/main/java/org/yaml/snakeyaml/introspector/PropertyUtils.java +++ b/src/main/java/org/yaml/snakeyaml/introspector/PropertyUtils.java @@ -1,20 +1,19 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.introspector; +import java.beans.FeatureDescriptor; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; @@ -27,140 +26,174 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; - import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.util.PlatformFeatureDetector; public class PropertyUtils { - private final Map<Class<?>, Map<String, Property>> propertiesCache = new HashMap<Class<?>, Map<String, Property>>(); - private final Map<Class<?>, Set<Property>> readableProperties = new HashMap<Class<?>, Set<Property>>(); - private BeanAccess beanAccess = BeanAccess.DEFAULT; - private boolean allowReadOnlyProperties = false; - private boolean skipMissingProperties = false; + private final Map<Class<?>, Map<String, Property>> propertiesCache = + new HashMap<Class<?>, Map<String, Property>>(); + private final Map<Class<?>, Set<Property>> readableProperties = + new HashMap<Class<?>, Set<Property>>(); + private BeanAccess beanAccess = BeanAccess.DEFAULT; + private boolean allowReadOnlyProperties = false; + private boolean skipMissingProperties = false; - protected Map<String, Property> getPropertiesMap(Class<?> type, BeanAccess bAccess) - throws IntrospectionException { - if (propertiesCache.containsKey(type)) { - return propertiesCache.get(type); - } + private final PlatformFeatureDetector platformFeatureDetector; - Map<String, Property> properties = new LinkedHashMap<String, Property>(); - boolean inaccessableFieldsExist = false; - switch (bAccess) { - case FIELD: - for (Class<?> c = type; c != null; c = c.getSuperclass()) { - for (Field field : c.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers) - && !properties.containsKey(field.getName())) { - properties.put(field.getName(), new FieldProperty(field)); - } - } - } - break; - default: - // add JavaBean properties - for (PropertyDescriptor property : Introspector.getBeanInfo(type) - .getPropertyDescriptors()) { - Method readMethod = property.getReadMethod(); - if (readMethod == null || !readMethod.getName().equals("getClass")) { - properties.put(property.getName(), new MethodProperty(property)); - } - } + public PropertyUtils() { + this(new PlatformFeatureDetector()); + } - // add public fields - for (Class<?> c = type; c != null; c = c.getSuperclass()) { - for (Field field : c.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) { - if (Modifier.isPublic(modifiers)) { - properties.put(field.getName(), new FieldProperty(field)); - } else { - inaccessableFieldsExist = true; - } - } - } - } - break; - } - if (properties.isEmpty() && inaccessableFieldsExist) { - throw new YAMLException("No JavaBean properties found in " + type.getName()); - } - propertiesCache.put(type, properties); - return properties; + PropertyUtils(PlatformFeatureDetector platformFeatureDetector) { + this.platformFeatureDetector = platformFeatureDetector; + + /* + * Android lacks much of java.beans (including the Introspector class, used here), because + * java.beans classes tend to rely on java.awt, which isn't supported in the Android SDK. That + * means we have to fall back on FIELD access only when SnakeYAML is running on the Android + * Runtime. + */ + if (platformFeatureDetector.isRunningOnAndroid()) { + beanAccess = BeanAccess.FIELD; } + } - public Set<Property> getProperties(Class<? extends Object> type) throws IntrospectionException { - return getProperties(type, beanAccess); + protected Map<String, Property> getPropertiesMap(Class<?> type, BeanAccess bAccess) { + if (propertiesCache.containsKey(type)) { + return propertiesCache.get(type); } - public Set<Property> getProperties(Class<? extends Object> type, BeanAccess bAccess) - throws IntrospectionException { - if (readableProperties.containsKey(type)) { - return readableProperties.get(type); + Map<String, Property> properties = new LinkedHashMap<String, Property>(); + boolean inaccessableFieldsExist = false; + if (bAccess == BeanAccess.FIELD) { + for (Class<?> c = type; c != null; c = c.getSuperclass()) { + for (Field field : c.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers) + && !properties.containsKey(field.getName())) { + properties.put(field.getName(), new FieldProperty(field)); + } } - Set<Property> properties = createPropertySet(type, bAccess); - readableProperties.put(type, properties); - return properties; - } + } + } else {// add JavaBean properties + try { + for (PropertyDescriptor property : Introspector.getBeanInfo(type) + .getPropertyDescriptors()) { + Method readMethod = property.getReadMethod(); + if ((readMethod == null || !readMethod.getName().equals("getClass")) + && !isTransient(property)) { + properties.put(property.getName(), new MethodProperty(property)); + } + } + } catch (IntrospectionException e) { + throw new YAMLException(e); + } - protected Set<Property> createPropertySet(Class<? extends Object> type, BeanAccess bAccess) - throws IntrospectionException { - Set<Property> properties = new TreeSet<Property>(); - Collection<Property> props = getPropertiesMap(type, bAccess).values(); - for (Property property : props) { - if (property.isReadable() && (allowReadOnlyProperties || property.isWritable())) { - properties.add(property); + // add public fields + for (Class<?> c = type; c != null; c = c.getSuperclass()) { + for (Field field : c.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) { + if (Modifier.isPublic(modifiers)) { + properties.put(field.getName(), new FieldProperty(field)); + } else { + inaccessableFieldsExist = true; } + } } - return properties; + } + } + if (properties.isEmpty() && inaccessableFieldsExist) { + throw new YAMLException("No JavaBean properties found in " + type.getName()); } + propertiesCache.put(type, properties); + return properties; + } + + private static final String TRANSIENT = "transient"; + + private boolean isTransient(FeatureDescriptor fd) { + return Boolean.TRUE.equals(fd.getValue(TRANSIENT)); + } + + public Set<Property> getProperties(Class<? extends Object> type) { + return getProperties(type, beanAccess); + } - public Property getProperty(Class<? extends Object> type, String name) - throws IntrospectionException { - return getProperty(type, name, beanAccess); + public Set<Property> getProperties(Class<? extends Object> type, BeanAccess bAccess) { + if (readableProperties.containsKey(type)) { + return readableProperties.get(type); } + Set<Property> properties = createPropertySet(type, bAccess); + readableProperties.put(type, properties); + return properties; + } - public Property getProperty(Class<? extends Object> type, String name, BeanAccess bAccess) - throws IntrospectionException { - Map<String, Property> properties = getPropertiesMap(type, bAccess); - Property property = properties.get(name); - if (property == null && skipMissingProperties) { - property = new MissingProperty(name); - } - if (property == null || !property.isWritable()) { - throw new YAMLException("Unable to find property '" + name + "' on class: " - + type.getName()); - } - return property; + protected Set<Property> createPropertySet(Class<? extends Object> type, BeanAccess bAccess) { + Set<Property> properties = new TreeSet<Property>(); + Collection<Property> props = getPropertiesMap(type, bAccess).values(); + for (Property property : props) { + if (property.isReadable() && (allowReadOnlyProperties || property.isWritable())) { + properties.add(property); + } } + return properties; + } - public void setBeanAccess(BeanAccess beanAccess) { - if (this.beanAccess != beanAccess) { - this.beanAccess = beanAccess; - propertiesCache.clear(); - readableProperties.clear(); - } + public Property getProperty(Class<? extends Object> type, String name) { + return getProperty(type, name, beanAccess); + } + + public Property getProperty(Class<? extends Object> type, String name, BeanAccess bAccess) { + Map<String, Property> properties = getPropertiesMap(type, bAccess); + Property property = properties.get(name); + if (property == null && skipMissingProperties) { + property = new MissingProperty(name); } + if (property == null) { + throw new YAMLException("Unable to find property '" + name + "' on class: " + type.getName()); + } + return property; + } - public void setAllowReadOnlyProperties(boolean allowReadOnlyProperties) { - if (this.allowReadOnlyProperties != allowReadOnlyProperties) { - this.allowReadOnlyProperties = allowReadOnlyProperties; - readableProperties.clear(); - } + public void setBeanAccess(BeanAccess beanAccess) { + if (platformFeatureDetector.isRunningOnAndroid() && beanAccess != BeanAccess.FIELD) { + throw new IllegalArgumentException("JVM is Android - only BeanAccess.FIELD is available"); } - /** - * Skip properties that are missing during deserialization of YAML to a Java - * object. The default is false. - * - * @param skipMissingProperties - * true if missing properties should be skipped, false otherwise. - */ - public void setSkipMissingProperties(boolean skipMissingProperties) { - if (this.skipMissingProperties != skipMissingProperties) { - this.skipMissingProperties = skipMissingProperties; - readableProperties.clear(); - } + if (this.beanAccess != beanAccess) { + this.beanAccess = beanAccess; + propertiesCache.clear(); + readableProperties.clear(); + } + } + + public void setAllowReadOnlyProperties(boolean allowReadOnlyProperties) { + if (this.allowReadOnlyProperties != allowReadOnlyProperties) { + this.allowReadOnlyProperties = allowReadOnlyProperties; + readableProperties.clear(); } + } + + public boolean isAllowReadOnlyProperties() { + return allowReadOnlyProperties; + } + + /** + * Skip properties that are missing during deserialization of YAML to a Java object. The default + * is false. + * + * @param skipMissingProperties true if missing properties should be skipped, false otherwise. + */ + public void setSkipMissingProperties(boolean skipMissingProperties) { + if (this.skipMissingProperties != skipMissingProperties) { + this.skipMissingProperties = skipMissingProperties; + readableProperties.clear(); + } + } + + public boolean isSkipMissingProperties() { + return skipMissingProperties; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/AnchorNode.java b/src/main/java/org/yaml/snakeyaml/nodes/AnchorNode.java index 8a901317..52834120 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/AnchorNode.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/AnchorNode.java @@ -1,35 +1,36 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; +/** + * This class is only used during representation (dumping) + */ public class AnchorNode extends Node { - private Node realNode; + private final Node realNode; - public AnchorNode(Node realNode) { - super(realNode.getTag(), realNode.getStartMark(), realNode.getEndMark()); - this.realNode = realNode; - } + public AnchorNode(Node realNode) { + super(realNode.getTag(), realNode.getStartMark(), realNode.getEndMark()); + this.realNode = realNode; + } - @Override - public NodeId getNodeId() { - return NodeId.anchor; - } + @Override + public NodeId getNodeId() { + return NodeId.anchor; + } - public Node getRealNode() { - return realNode; - } + public Node getRealNode() { + return realNode; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/CollectionNode.java b/src/main/java/org/yaml/snakeyaml/nodes/CollectionNode.java index aa443686..09ad80f5 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/CollectionNode.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/CollectionNode.java @@ -1,49 +1,83 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; +import java.util.List; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** - * Base class for the two collection types {@link MappingNode mapping} and - * {@link SequenceNode collection}. + * Base class for the two collection types {@link MappingNode mapping} and {@link SequenceNode + * collection}. */ -public abstract class CollectionNode extends Node { - private Boolean flowStyle; +public abstract class CollectionNode<T> extends Node { - public CollectionNode(Tag tag, Mark startMark, Mark endMark, Boolean flowStyle) { - super(tag, startMark, endMark); - this.flowStyle = flowStyle; - } + private DumperOptions.FlowStyle flowStyle; - /** - * Serialization style of this collection. - * - * @return <code>true</code> for flow style, <code>false</code> for block - * style. - */ - public Boolean getFlowStyle() { - return flowStyle; - } + public CollectionNode(Tag tag, Mark startMark, Mark endMark, DumperOptions.FlowStyle flowStyle) { + super(tag, startMark, endMark); + setFlowStyle(flowStyle); + } - public void setFlowStyle(Boolean flowStyle) { - this.flowStyle = flowStyle; - } + /* + * Existed in older versions but replaced with {@link DumperOptions.FlowStyle}-based constructor. + * Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link CollectionNode#CollectionNode(Tag, Mark, Mark, + * org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public CollectionNode(Tag tag, Mark startMark, Mark endMark, Boolean flowStyle) { + this(tag, startMark, endMark, DumperOptions.FlowStyle.fromBoolean(flowStyle)); + } + + /** + * Returns the elements in this sequence. + * + * @return Nodes in the specified order. + */ + public abstract List<T> getValue(); - public void setEndMark(Mark endMark) { - this.endMark = endMark; + /** + * Serialization style of this collection. + * + * @return <code>true</code> for flow style, <code>false</code> for block style. + */ + public DumperOptions.FlowStyle getFlowStyle() { + return flowStyle; + } + + public void setFlowStyle(DumperOptions.FlowStyle flowStyle) { + if (flowStyle == null) { + throw new NullPointerException("Flow style must be provided."); } + this.flowStyle = flowStyle; + } + + /* + * Existed in older versions but replaced with {@link DumperOptions.FlowStyle}-based method. + * Restored in v1.26 for backwards compatibility. + * + * @deprecated Since restored in v1.26. Use {@link + * CollectionNode#setFlowStyle(org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public void setFlowStyle(Boolean flowStyle) { + setFlowStyle(DumperOptions.FlowStyle.fromBoolean(flowStyle)); + } + + public void setEndMark(Mark endMark) { + this.endMark = endMark; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/MappingNode.java b/src/main/java/org/yaml/snakeyaml/nodes/MappingNode.java index ed3dcff5..f0f7a84d 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/MappingNode.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/MappingNode.java @@ -1,22 +1,20 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; import java.util.List; - +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** @@ -25,87 +23,113 @@ import org.yaml.snakeyaml.error.Mark; * A map is a collection of unsorted key-value pairs. * </p> */ -public class MappingNode extends CollectionNode { - private List<NodeTuple> value; - private boolean merged = false; - - public MappingNode(Tag tag, boolean resolved, List<NodeTuple> value, Mark startMark, - Mark endMark, Boolean flowStyle) { - super(tag, startMark, endMark, flowStyle); - if (value == null) { - throw new NullPointerException("value in a Node is required."); - } - this.value = value; - this.resolved = resolved; - } +public class MappingNode extends CollectionNode<NodeTuple> { - public MappingNode(Tag tag, List<NodeTuple> value, Boolean flowStyle) { - this(tag, true, value, null, null, flowStyle); - } + private List<NodeTuple> value; + private boolean merged = false; - @Override - public NodeId getNodeId() { - return NodeId.mapping; + public MappingNode(Tag tag, boolean resolved, List<NodeTuple> value, Mark startMark, Mark endMark, + DumperOptions.FlowStyle flowStyle) { + super(tag, startMark, endMark, flowStyle); + if (value == null) { + throw new NullPointerException("value in a Node is required."); } + this.value = value; + this.resolved = resolved; + } - /** - * Returns the entries of this map. - * - * @return List of entries. - */ - public List<NodeTuple> getValue() { - return value; - } + public MappingNode(Tag tag, List<NodeTuple> value, DumperOptions.FlowStyle flowStyle) { + this(tag, true, value, null, null, flowStyle); + } - public void setValue(List<NodeTuple> merge) { - value = merge; - } + /* + * Existed in older versions but replaced with {@link DumperOptions.FlowStyle}-based constructor. + * Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link MappingNode#MappingNode(Tag, boolean, List, + * Mark, Mark, org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public MappingNode(Tag tag, boolean resolved, List<NodeTuple> value, Mark startMark, Mark endMark, + Boolean flowStyle) { + this(tag, resolved, value, startMark, endMark, DumperOptions.FlowStyle.fromBoolean(flowStyle)); + } - public void setOnlyKeyType(Class<? extends Object> keyType) { - for (NodeTuple nodes : value) { - nodes.getKeyNode().setType(keyType); - } - } + /* + * Existed in older versions but replaced with {@link DumperOptions.FlowStyle}-based constructor. + * Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link MappingNode#MappingNode(Tag, List, + * org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public MappingNode(Tag tag, List<NodeTuple> value, Boolean flowStyle) { + this(tag, value, DumperOptions.FlowStyle.fromBoolean(flowStyle)); - public void setTypes(Class<? extends Object> keyType, Class<? extends Object> valueType) { - for (NodeTuple nodes : value) { - nodes.getValueNode().setType(valueType); - nodes.getKeyNode().setType(keyType); - } - } + } + + @Override + public NodeId getNodeId() { + return NodeId.mapping; + } + + /** + * Returns the entries of this map. + * + * @return List of entries. + */ + public List<NodeTuple> getValue() { + return value; + } - @Override - public String toString() { - String values; - StringBuilder buf = new StringBuilder(); - for (NodeTuple node : getValue()) { - buf.append("{ key="); - buf.append(node.getKeyNode()); - buf.append("; value="); - if (node.getValueNode() instanceof CollectionNode) { - // to avoid overflow in case of recursive structures - buf.append(System.identityHashCode(node.getValueNode())); - } else { - buf.append(node.toString()); - } - buf.append(" }"); - } - values = buf.toString(); - return "<" + this.getClass().getName() + " (tag=" + getTag() + ", values=" + values + ")>"; + public void setValue(List<NodeTuple> mergedValue) { + value = mergedValue; + } + + public void setOnlyKeyType(Class<? extends Object> keyType) { + for (NodeTuple nodes : value) { + nodes.getKeyNode().setType(keyType); } + } - /** - * @param merged - * - true if map contains merge node - */ - public void setMerged(boolean merged) { - this.merged = merged; + public void setTypes(Class<? extends Object> keyType, Class<? extends Object> valueType) { + for (NodeTuple nodes : value) { + nodes.getValueNode().setType(valueType); + nodes.getKeyNode().setType(keyType); } + } - /** - * @return true if map contains merge node - */ - public boolean isMerged() { - return merged; + @Override + public String toString() { + String values; + StringBuilder buf = new StringBuilder(); + for (NodeTuple node : getValue()) { + buf.append("{ key="); + buf.append(node.getKeyNode()); + buf.append("; value="); + if (node.getValueNode() instanceof CollectionNode) { + // to avoid overflow in case of recursive structures + buf.append(System.identityHashCode(node.getValueNode())); + } else { + buf.append(node); + } + buf.append(" }"); } + values = buf.toString(); + return "<" + this.getClass().getName() + " (tag=" + getTag() + ", values=" + values + ")>"; + } + + /** + * @param merged - true if map contains merge node + */ + public void setMerged(boolean merged) { + this.merged = merged; + } + + /** + * @return true if map contains merge node + */ + public boolean isMerged() { + return merged; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/Node.java b/src/main/java/org/yaml/snakeyaml/nodes/Node.java index 1037e41a..45b9b9b8 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/Node.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/Node.java @@ -1,166 +1,225 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; +import java.util.List; +import org.yaml.snakeyaml.comments.CommentLine; import org.yaml.snakeyaml.error.Mark; /** * Base class for all nodes. * <p> - * The nodes form the node-graph described in the <a - * href="http://yaml.org/spec/1.1/">YAML Specification</a>. + * The nodes form the node-graph described in the <a href="http://yaml.org/spec/1.1/">YAML + * Specification</a>. * </p> * <p> * While loading, the node graph is usually created by the - * {@link org.yaml.snakeyaml.composer.Composer}, and later transformed into - * application specific Java classes by the classes from the - * {@link org.yaml.snakeyaml.constructor} package. + * {@link org.yaml.snakeyaml.composer.Composer}, and later transformed into application specific + * Java classes by the classes from the {@link org.yaml.snakeyaml.constructor} package. * </p> */ public abstract class Node { - private Tag tag; - private Mark startMark; - protected Mark endMark; - private Class<? extends Object> type; - private boolean twoStepsConstruction; - /** - * true when the tag is assigned by the resolver - */ - protected boolean resolved; - protected Boolean useClassConstructor; - - public Node(Tag tag, Mark startMark, Mark endMark) { - setTag(tag); - this.startMark = startMark; - this.endMark = endMark; - this.type = Object.class; - this.twoStepsConstruction = false; - this.resolved = true; - this.useClassConstructor = null; - } - - /** - * Tag of this node. - * <p> - * Every node has a tag assigned. The tag is either local or global. - * - * @return Tag of this node. - */ - public Tag getTag() { - return this.tag; - } - - public Mark getEndMark() { - return endMark; - } - - /** - * For error reporting. - * - * @see "class variable 'id' in PyYAML" - * @return scalar, sequence, mapping - */ - public abstract NodeId getNodeId(); - - public Mark getStartMark() { - return startMark; - } - - public void setTag(Tag tag) { - if (tag == null) { - throw new NullPointerException("tag in a Node is required."); - } - this.tag = tag; - } - - /** - * Two Nodes are never equal. - */ - @Override - public final boolean equals(Object obj) { - return super.equals(obj); - } - public Class<? extends Object> getType() { - return type; + private Tag tag; + private final Mark startMark; + protected Mark endMark; + private Class<? extends Object> type; + private boolean twoStepsConstruction; + private String anchor; + private List<CommentLine> inLineComments; + private List<CommentLine> blockComments; + // End Comments are only on the last node in a document + private List<CommentLine> endComments; + + /** + * true when the tag is assigned by the resolver + */ + protected boolean resolved; + protected Boolean useClassConstructor; + + public Node(Tag tag, Mark startMark, Mark endMark) { + setTag(tag); + this.startMark = startMark; + this.endMark = endMark; + this.type = Object.class; + this.twoStepsConstruction = false; + this.resolved = true; + this.useClassConstructor = null; + this.inLineComments = null; + this.blockComments = null; + this.endComments = null; + } + + /** + * Tag of this node. + * <p> + * Every node has a tag assigned. The tag is either local or global. + * + * @return Tag of this node. + */ + public Tag getTag() { + return this.tag; + } + + public Mark getEndMark() { + return endMark; + } + + /** + * For error reporting. + * + * @see "class variable 'id' in PyYAML" + * @return scalar, sequence, mapping + */ + public abstract NodeId getNodeId(); + + public Mark getStartMark() { + return startMark; + } + + public void setTag(Tag tag) { + if (tag == null) { + throw new NullPointerException("tag in a Node is required."); } - - public void setType(Class<? extends Object> type) { - if (!type.isAssignableFrom(this.type)) { - this.type = type; - } - } - - public void setTwoStepsConstruction(boolean twoStepsConstruction) { - this.twoStepsConstruction = twoStepsConstruction; - } - - /** - * Indicates if this node must be constructed in two steps. - * <p> - * Two-step construction is required whenever a node is a child (direct or - * indirect) of it self. That is, if a recursive structure is build using - * anchors and aliases. - * </p> - * <p> - * Set by {@link org.yaml.snakeyaml.composer.Composer}, used during the - * construction process. - * </p> - * <p> - * Only relevant during loading. - * </p> - * - * @return <code>true</code> if the node is self referenced. - */ - public boolean isTwoStepsConstruction() { - return twoStepsConstruction; - } - - @Override - public final int hashCode() { - return super.hashCode(); - } - - public boolean useClassConstructor() { - if (useClassConstructor == null) { - if (!tag.isSecondary() && isResolved() && !Object.class.equals(type) - && !tag.equals(Tag.NULL)) { - return true; - } else if (tag.isCompatible(getType())) { - // the tag is compatible with the runtime class - // the tag will be ignored - return true; - } else { - return false; - } - } - return useClassConstructor.booleanValue(); + this.tag = tag; + } + + /** + * Node is only equal to itself + */ + @Override + public final boolean equals(Object obj) { + return super.equals(obj); + } + + public Class<? extends Object> getType() { + return type; + } + + public void setType(Class<? extends Object> type) { + if (!type.isAssignableFrom(this.type)) { + this.type = type; } - - public void setUseClassConstructor(Boolean useClassConstructor) { - this.useClassConstructor = useClassConstructor; - } - - /** - * Indicates if the tag was added by - * {@link org.yaml.snakeyaml.resolver.Resolver}. - * - * @return <code>true</code> if the tag of this node was resolved</code> - */ - public boolean isResolved() { - return resolved; + } + + public void setTwoStepsConstruction(boolean twoStepsConstruction) { + this.twoStepsConstruction = twoStepsConstruction; + } + + /** + * Indicates if this node must be constructed in two steps. + * <p> + * Two-step construction is required whenever a node is a child (direct or indirect) of it self. + * That is, if a recursive structure is build using anchors and aliases. + * </p> + * <p> + * Set by {@link org.yaml.snakeyaml.composer.Composer}, used during the construction process. + * </p> + * <p> + * Only relevant during loading. + * </p> + * + * @return <code>true</code> if the node is self referenced. + */ + public boolean isTwoStepsConstruction() { + return twoStepsConstruction; + } + + @Override + public final int hashCode() { + return super.hashCode(); + } + + public boolean useClassConstructor() { + if (useClassConstructor == null) { + // the tag is compatible with the runtime class + // the tag will be ignored + if (!tag.isSecondary() && resolved && !Object.class.equals(type) && !tag.equals(Tag.NULL)) { + return true; + } else { + return tag.isCompatible(getType()); + } } + return useClassConstructor.booleanValue(); + } + + public void setUseClassConstructor(Boolean useClassConstructor) { + this.useClassConstructor = useClassConstructor; + } + + /** + * Indicates if the tag was added by {@link org.yaml.snakeyaml.resolver.Resolver}. + * + * @return true if the tag of this node was resolved + * + * @deprecated Since v1.22. Absent in immediately prior versions, but present previously. Restored + * deprecated for backwards compatibility. + */ + @Deprecated + public boolean isResolved() { + return resolved; + } + + public String getAnchor() { + return anchor; + } + + public void setAnchor(String anchor) { + this.anchor = anchor; + } + + /** + * The ordered list of in-line comments. The first of which appears at the end of the line + * respresent by this node. The rest are in the following lines, indented per the Spec to indicate + * they are continuation of the inline comment. + * + * @return the comment line list. + */ + public List<CommentLine> getInLineComments() { + return inLineComments; + } + + public void setInLineComments(List<CommentLine> inLineComments) { + this.inLineComments = inLineComments; + } + + /** + * The ordered list of blank lines and block comments (full line) that appear before this node. + * + * @return the comment line list. + */ + public List<CommentLine> getBlockComments() { + return blockComments; + } + + public void setBlockComments(List<CommentLine> blockComments) { + this.blockComments = blockComments; + } + + /** + * The ordered list of blank lines and block comments (full line) that appear AFTER this node. + * <p> + * NOTE: these comment should occur only in the last node in a document, when walking the node + * tree "in order" + * + * @return the comment line list. + */ + public List<CommentLine> getEndComments() { + return endComments; + } + + public void setEndComments(List<CommentLine> endComments) { + this.endComments = endComments; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/NodeId.java b/src/main/java/org/yaml/snakeyaml/nodes/NodeId.java index 178e26d0..f9bcef7b 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/NodeId.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/NodeId.java @@ -1,23 +1,21 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; /** - * Enum for the three basic YAML types: scalar, sequence and mapping. + * Enum for the basic YAML types: scalar, sequence, mapping or anchor. */ public enum NodeId { - scalar, sequence, mapping, anchor; + scalar, sequence, mapping, anchor } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/NodeTuple.java b/src/main/java/org/yaml/snakeyaml/nodes/NodeTuple.java index 3e3cb36e..1380a5f6 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/NodeTuple.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/NodeTuple.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; @@ -20,36 +18,37 @@ package org.yaml.snakeyaml.nodes; */ public final class NodeTuple { - private Node keyNode; - private Node valueNode; + private final Node keyNode; + private final Node valueNode; - public NodeTuple(Node keyNode, Node valueNode) { - if (keyNode == null || valueNode == null) { - throw new NullPointerException("Nodes must be provided."); - } - this.keyNode = keyNode; - this.valueNode = valueNode; + public NodeTuple(Node keyNode, Node valueNode) { + if (keyNode == null || valueNode == null) { + throw new NullPointerException("Nodes must be provided."); } + this.keyNode = keyNode; + this.valueNode = valueNode; + } - /** - * Key node. - */ - public Node getKeyNode() { - return keyNode; - } + /** + * Key node. + * + * @return the node used as key + */ + public Node getKeyNode() { + return keyNode; + } - /** - * Value node. - * - * @return value - */ - public Node getValueNode() { - return valueNode; - } + /** + * Value node. + * + * @return node used as value + */ + public Node getValueNode() { + return valueNode; + } - @Override - public String toString() { - return "<NodeTuple keyNode=" + keyNode.toString() + "; valueNode=" + valueNode.toString() - + ">"; - } + @Override + public String toString() { + return "<NodeTuple keyNode=" + keyNode + "; valueNode=" + valueNode + ">"; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java b/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java index 9120d0ee..61212df0 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java @@ -1,20 +1,19 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** @@ -24,51 +23,97 @@ import org.yaml.snakeyaml.error.Mark; * </p> */ public class ScalarNode extends Node { - private Character style; - private String value; - public ScalarNode(Tag tag, String value, Mark startMark, Mark endMark, Character style) { - this(tag, true, value, startMark, endMark, style); - } + private final DumperOptions.ScalarStyle style; + private final String value; - public ScalarNode(Tag tag, boolean resolved, String value, Mark startMark, Mark endMark, - Character style) { - super(tag, startMark, endMark); - if (value == null) { - throw new NullPointerException("value in a Node is required."); - } - this.value = value; - this.style = style; - this.resolved = resolved; - } + public ScalarNode(Tag tag, String value, Mark startMark, Mark endMark, + DumperOptions.ScalarStyle style) { + this(tag, true, value, startMark, endMark, style); + } - /** - * Get scalar style of this node. - * - * @see org.yaml.snakeyaml.events.ScalarEvent - * @see <a href="http://yaml.org/spec/1.1/#id903915">Chapter 9. Scalar - * Styles</a> - */ - public Character getStyle() { - return style; + public ScalarNode(Tag tag, boolean resolved, String value, Mark startMark, Mark endMark, + DumperOptions.ScalarStyle style) { + super(tag, startMark, endMark); + if (value == null) { + throw new NullPointerException("value in a Node is required."); } - - @Override - public NodeId getNodeId() { - return NodeId.scalar; + this.value = value; + if (style == null) { + throw new NullPointerException("Scalar style must be provided."); } + this.style = style; + this.resolved = resolved; + } - /** - * Value of this scalar. - * - * @return Scalar's value. - */ - public String getValue() { - return value; - } + /* + * Existed in older versions but replaced with {@link DumperOptions.ScalarStyle}-based + * constructor. Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link ScalarNode#ScalarNode(Tag, String, Mark, Mark, + * org.yaml.snakeyaml.DumperOptions.ScalarStyle) }. + */ + @Deprecated + public ScalarNode(Tag tag, String value, Mark startMark, Mark endMark, Character style) { + this(tag, value, startMark, endMark, DumperOptions.ScalarStyle.createStyle(style)); + } - public String toString() { - return "<" + this.getClass().getName() + " (tag=" + getTag() + ", value=" + getValue() - + ")>"; - } + /* + * Existed in older versions but replaced with {@link DumperOptions.ScalarStyle}-based + * constructor. Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link ScalarNode#ScalarNode(Tag, boolean, String, + * Mark, Mark, org.yaml.snakeyaml.DumperOptions.ScalarStyle) }. + */ + @Deprecated + public ScalarNode(Tag tag, boolean resolved, String value, Mark startMark, Mark endMark, + Character style) { + this(tag, resolved, value, startMark, endMark, DumperOptions.ScalarStyle.createStyle(style)); + } + + /** + * Get scalar style of this node. + * + * @see org.yaml.snakeyaml.events.ScalarEvent + * @see <a href="http://yaml.org/spec/1.1/#id903915">Chapter 9. Scalar Styles</a> + * @return style of this scalar node + * @deprecated use getScalarStyle instead + */ + @Deprecated + public Character getStyle() { + return style.getChar(); + } + + /** + * Get scalar style of this node. + * + * @see org.yaml.snakeyaml.events.ScalarEvent + * @see <a href="http://yaml.org/spec/1.1/#id903915">Chapter 9. Scalar Styles</a> + * @return style of this scalar node + */ + public DumperOptions.ScalarStyle getScalarStyle() { + return style; + } + + @Override + public NodeId getNodeId() { + return NodeId.scalar; + } + + /** + * Value of this scalar. + * + * @return Scalar's value. + */ + public String getValue() { + return value; + } + + public String toString() { + return "<" + this.getClass().getName() + " (tag=" + getTag() + ", value=" + getValue() + ")>"; + } + + public boolean isPlain() { + return style == DumperOptions.ScalarStyle.PLAIN; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/SequenceNode.java b/src/main/java/org/yaml/snakeyaml/nodes/SequenceNode.java index 5e2f6b40..745268da 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/SequenceNode.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/SequenceNode.java @@ -1,22 +1,20 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; import java.util.List; - +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; /** @@ -25,45 +23,70 @@ import org.yaml.snakeyaml.error.Mark; * A sequence is a ordered collection of nodes. * </p> */ -public class SequenceNode extends CollectionNode { - final private List<Node> value; +public class SequenceNode extends CollectionNode<Node> { - public SequenceNode(Tag tag, boolean resolved, List<Node> value, Mark startMark, Mark endMark, - Boolean flowStyle) { - super(tag, startMark, endMark, flowStyle); - if (value == null) { - throw new NullPointerException("value in a Node is required."); - } - this.value = value; - this.resolved = resolved; - } + private final List<Node> value; - public SequenceNode(Tag tag, List<Node> value, Boolean flowStyle) { - this(tag, true, value, null, null, flowStyle); + public SequenceNode(Tag tag, boolean resolved, List<Node> value, Mark startMark, Mark endMark, + DumperOptions.FlowStyle flowStyle) { + super(tag, startMark, endMark, flowStyle); + if (value == null) { + throw new NullPointerException("value in a Node is required."); } + this.value = value; + this.resolved = resolved; + } - @Override - public NodeId getNodeId() { - return NodeId.sequence; - } + public SequenceNode(Tag tag, List<Node> value, DumperOptions.FlowStyle flowStyle) { + this(tag, true, value, null, null, flowStyle); + } - /** - * Returns the elements in this sequence. - * - * @return Nodes in the specified order. - */ - public List<Node> getValue() { - return value; - } + /* + * Existed in older versions but replaced with {@link DumperOptions.SequenceStyle}-based + * constructor. Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link SequenceNode#SequenceNode(Tag, List<Node>, + * org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public SequenceNode(Tag tag, List<Node> value, Boolean style) { + this(tag, value, DumperOptions.FlowStyle.fromBoolean(style)); + } - public void setListType(Class<? extends Object> listType) { - for (Node node : value) { - node.setType(listType); - } - } + /* + * Existed in older versions but replaced with {@link DumperOptions.SequenceStyle}-based + * constructor. Restored in v1.22 for backwards compatibility. + * + * @deprecated Since restored in v1.22. Use {@link SequenceNode#SequenceNode(Tag, boolean, + * List<Node>, Mark, Mark, org.yaml.snakeyaml.DumperOptions.FlowStyle) }. + */ + @Deprecated + public SequenceNode(Tag tag, boolean resolved, List<Node> value, Mark startMark, Mark endMark, + Boolean style) { + this(tag, resolved, value, startMark, endMark, DumperOptions.FlowStyle.fromBoolean(style)); + } + + @Override + public NodeId getNodeId() { + return NodeId.sequence; + } - public String toString() { - return "<" + this.getClass().getName() + " (tag=" + getTag() + ", value=" + getValue() - + ")>"; + /** + * Returns the elements in this sequence. + * + * @return Nodes in the specified order. + */ + public List<Node> getValue() { + return value; + } + + public void setListType(Class<? extends Object> listType) { + for (Node node : value) { + node.setType(listType); } + } + + public String toString() { + return "<" + this.getClass().getName() + " (tag=" + getTag() + ", value=" + getValue() + ")>"; + } } diff --git a/src/main/java/org/yaml/snakeyaml/nodes/Tag.java b/src/main/java/org/yaml/snakeyaml/nodes/Tag.java index 913e1f5b..dbb6f5a8 100644 --- a/src/main/java/org/yaml/snakeyaml/nodes/Tag.java +++ b/src/main/java/org/yaml/snakeyaml/nodes/Tag.java @@ -1,172 +1,175 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.nodes; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; -import java.sql.Timestamp; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; - import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.util.UriEncoder; -public final class Tag implements Comparable<Tag> { - public static final String PREFIX = "tag:yaml.org,2002:"; - public static final Tag YAML = new Tag(PREFIX + "yaml"); - public static final Tag MERGE = new Tag(PREFIX + "merge"); - public static final Tag SET = new Tag(PREFIX + "set"); - public static final Tag PAIRS = new Tag(PREFIX + "pairs"); - public static final Tag OMAP = new Tag(PREFIX + "omap"); - public static final Tag BINARY = new Tag(PREFIX + "binary"); - public static final Tag INT = new Tag(PREFIX + "int"); - public static final Tag FLOAT = new Tag(PREFIX + "float"); - public static final Tag TIMESTAMP = new Tag(PREFIX + "timestamp"); - public static final Tag BOOL = new Tag(PREFIX + "bool"); - public static final Tag NULL = new Tag(PREFIX + "null"); - public static final Tag STR = new Tag(PREFIX + "str"); - public static final Tag SEQ = new Tag(PREFIX + "seq"); - public static final Tag MAP = new Tag(PREFIX + "map"); - public static final Map<Tag, Set<Class<?>>> COMPATIBILITY_MAP; - static { - COMPATIBILITY_MAP = new HashMap<Tag, Set<Class<?>>>(); - Set<Class<?>> floatSet = new HashSet<Class<?>>(); - floatSet.add(Double.class); - floatSet.add(Float.class); - floatSet.add(BigDecimal.class); - COMPATIBILITY_MAP.put(FLOAT, floatSet); - // - Set<Class<?>> intSet = new HashSet<Class<?>>(); - intSet.add(Integer.class); - intSet.add(Long.class); - intSet.add(BigInteger.class); - COMPATIBILITY_MAP.put(INT, intSet); - // - Set<Class<?>> timestampSet = new HashSet<Class<?>>(); - timestampSet.add(Date.class); - timestampSet.add(java.sql.Date.class); - timestampSet.add(Timestamp.class); - COMPATIBILITY_MAP.put(TIMESTAMP, timestampSet); - } - - private final String value; - private boolean secondary = false; // see http://www.yaml.org/refcard.html - - public Tag(String tag) { - if (tag == null) { - throw new NullPointerException("Tag must be provided."); - } else if (tag.length() == 0) { - throw new IllegalArgumentException("Tag must not be empty."); - } else if (tag.trim().length() != tag.length()) { - throw new IllegalArgumentException("Tag must not contain leading or trailing spaces."); - } - this.value = UriEncoder.encode(tag); - this.secondary = !tag.startsWith(PREFIX); +public final class Tag { + + public static final String PREFIX = "tag:yaml.org,2002:"; + public static final Tag YAML = new Tag(PREFIX + "yaml"); + public static final Tag MERGE = new Tag(PREFIX + "merge"); + public static final Tag SET = new Tag(PREFIX + "set"); + public static final Tag PAIRS = new Tag(PREFIX + "pairs"); + public static final Tag OMAP = new Tag(PREFIX + "omap"); + public static final Tag BINARY = new Tag(PREFIX + "binary"); + public static final Tag INT = new Tag(PREFIX + "int"); + public static final Tag FLOAT = new Tag(PREFIX + "float"); + public static final Tag TIMESTAMP = new Tag(PREFIX + "timestamp"); + public static final Tag BOOL = new Tag(PREFIX + "bool"); + public static final Tag NULL = new Tag(PREFIX + "null"); + public static final Tag STR = new Tag(PREFIX + "str"); + public static final Tag SEQ = new Tag(PREFIX + "seq"); + public static final Tag MAP = new Tag(PREFIX + "map"); + // For use to indicate a DUMMY node that contains comments, when there is no other (empty + // document) + public static final Tag COMMENT = new Tag(PREFIX + "comment"); + private static final Map<Tag, Set<Class<?>>> COMPATIBILITY_MAP; + + static { + COMPATIBILITY_MAP = new HashMap<Tag, Set<Class<?>>>(); + Set<Class<?>> floatSet = new HashSet<Class<?>>(); + floatSet.add(Double.class); + floatSet.add(Float.class); + floatSet.add(BigDecimal.class); + COMPATIBILITY_MAP.put(FLOAT, floatSet); + // + Set<Class<?>> intSet = new HashSet<Class<?>>(); + intSet.add(Integer.class); + intSet.add(Long.class); + intSet.add(BigInteger.class); + COMPATIBILITY_MAP.put(INT, intSet); + // + Set<Class<?>> timestampSet = new HashSet<Class<?>>(); + timestampSet.add(Date.class); + + // java.sql is a separate module since jigsaw was introduced in java9 + try { + timestampSet.add(Class.forName("java.sql.Date")); + timestampSet.add(Class.forName("java.sql.Timestamp")); + } catch (ClassNotFoundException ignored) { + // ignore - we are running in a module path without java.sql } - public Tag(Class<? extends Object> clazz) { - if (clazz == null) { - throw new NullPointerException("Class for tag must be provided."); - } - this.value = Tag.PREFIX + UriEncoder.encode(clazz.getName()); - } + COMPATIBILITY_MAP.put(TIMESTAMP, timestampSet); + } - //TODO to be removed ? - public Tag(URI uri) { - if (uri == null) { - throw new NullPointerException("URI for tag must be provided."); - } - this.value = uri.toASCIIString(); - } - - public boolean isSecondary() { - return secondary; - } + private final String value; + private boolean secondary = false; // see http://www.yaml.org/refcard.html - public String getValue() { - return value; + public Tag(String tag) { + if (tag == null) { + throw new NullPointerException("Tag must be provided."); + } else if (tag.length() == 0) { + throw new IllegalArgumentException("Tag must not be empty."); + } else if (tag.trim().length() != tag.length()) { + throw new IllegalArgumentException("Tag must not contain leading or trailing spaces."); } + this.value = UriEncoder.encode(tag); + this.secondary = !tag.startsWith(PREFIX); + } - public boolean startsWith(String prefix) { - return value.startsWith(prefix); + public Tag(Class<? extends Object> clazz) { + if (clazz == null) { + throw new NullPointerException("Class for tag must be provided."); } - - public String getClassName() { - if (!value.startsWith(Tag.PREFIX)) { - throw new YAMLException("Invalid tag: " + value); - } - return UriEncoder.decode(value.substring(Tag.PREFIX.length())); + this.value = Tag.PREFIX + UriEncoder.encode(clazz.getName()); + } + + /** + * @deprecated - it will be removed + * @param uri - URI to be encoded as tag value + */ + @Deprecated + public Tag(URI uri) { + if (uri == null) { + throw new NullPointerException("URI for tag must be provided."); } + this.value = uri.toASCIIString(); + } - public int getLength() { - return value.length(); - } + public boolean isSecondary() { + return secondary; + } - @Override - public String toString() { - return value; - } + public String getValue() { + return value; + } - @Override - public boolean equals(Object obj) { - if (obj instanceof Tag) { - return value.equals(((Tag) obj).getValue()); - } else - return false; - } + public boolean startsWith(String prefix) { + return value.startsWith(prefix); + } - @Override - public int hashCode() { - return value.hashCode(); + public String getClassName() { + if (!value.startsWith(Tag.PREFIX)) { + throw new YAMLException("Invalid tag: " + value); } - - /** - * Java has more then 1 class compatible with a language-independent tag - * (!!int, !!float, !!timestamp etc) - * - * @param clazz - * - Class to check compatibility - * @return true when the Class can be represented by this - * language-independent tag - */ - public boolean isCompatible(Class<?> clazz) { - Set<Class<?>> set = COMPATIBILITY_MAP.get(this); - if (set != null) { - return set.contains(clazz); - } else { - return false; - } + return UriEncoder.decode(value.substring(Tag.PREFIX.length())); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Tag) { + return value.equals(((Tag) obj).getValue()); + } else { + return false; } - - /** - * Check whether this tag matches the global tag for the Class - * - * @param clazz - * - Class to check - * @return true when the this tag can be used as a global tag for the Class - */ - public boolean matches(Class<? extends Object> clazz) { - return value.equals(Tag.PREFIX + clazz.getName()); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Java has more then 1 class compatible with a language-independent tag (!!int, !!float, + * !!timestamp etc) + * + * @param clazz - Class to check compatibility + * @return true when the Class can be represented by this language-independent tag + */ + public boolean isCompatible(Class<?> clazz) { + Set<Class<?>> set = COMPATIBILITY_MAP.get(this); + if (set != null) { + return set.contains(clazz); + } else { + return false; } + } + + /** + * Check whether this tag matches the global tag for the Class + * + * @param clazz - Class to check + * @return true when the this tag can be used as a global tag for the Class + */ + public boolean matches(Class<? extends Object> clazz) { + return value.equals(Tag.PREFIX + clazz.getName()); + } - public int compareTo(Tag o) { - return value.compareTo(o.getValue()); - } } diff --git a/src/main/java/org/yaml/snakeyaml/parser/Parser.java b/src/main/java/org/yaml/snakeyaml/parser/Parser.java index 8c1bf169..b0e96ea4 100644 --- a/src/main/java/org/yaml/snakeyaml/parser/Parser.java +++ b/src/main/java/org/yaml/snakeyaml/parser/Parser.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.parser; @@ -20,46 +18,40 @@ import org.yaml.snakeyaml.events.Event; /** * This interface represents an input stream of {@link Event Events}. * <p> - * The parser and the scanner form together the 'Parse' step in the loading - * process (see chapter 3.1 of the <a href="http://yaml.org/spec/1.1/">YAML - * Specification</a>). + * The parser and the scanner form together the 'Parse' step in the loading process (see chapter 3.1 + * of the <a href="http://yaml.org/spec/1.1/">YAML Specification</a>). * </p> - * + * * @see org.yaml.snakeyaml.events.Event */ public interface Parser { - /** - * Check if the next event is one of the given type. - * - * @param choice - * Event ID. - * @return <code>true</code> if the next event can be assigned to a variable - * of the given type. Returns <code>false</code> if no more events - * are available. - * @throws ParserException - * Thrown in case of malformed input. - */ - public boolean checkEvent(Event.ID choice); + /** + * Check if the next event is one of the given type. + * + * @param choice Event ID. + * @return <code>true</code> if the next event can be assigned to a variable of the given type. + * Returns <code>false</code> if no more events are available. + * @throws ParserException Thrown in case of malformed input. + */ + boolean checkEvent(Event.ID choice); - /** - * Return the next event, but do not delete it from the stream. - * - * @return The event that will be returned on the next call to - * {@link #getEvent} - * @throws ParserException - * Thrown in case of malformed input. - */ - public Event peekEvent(); + /** + * Return the next event, but do not delete it from the stream. + * + * @return The event that will be returned on the next call to {@link #getEvent} + * @throws ParserException Thrown in case of malformed input. + */ + Event peekEvent(); - /** - * Returns the next event. - * <p> - * The event will be removed from the stream. - * </p> - * - * @throws ParserException - * Thrown in case of malformed input. - */ - public Event getEvent(); + /** + * Returns the next event. + * <p> + * The event will be removed from the stream. + * </p> + * + * @return the next parsed event + * @throws ParserException Thrown in case of malformed input. + */ + Event getEvent(); } diff --git a/src/main/java/org/yaml/snakeyaml/parser/ParserException.java b/src/main/java/org/yaml/snakeyaml/parser/ParserException.java index fd4b1f1c..90e49c08 100644 --- a/src/main/java/org/yaml/snakeyaml/parser/ParserException.java +++ b/src/main/java/org/yaml/snakeyaml/parser/ParserException.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.parser; @@ -19,26 +17,21 @@ import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.MarkedYAMLException; /** - * Exception thrown by the {@link Parser} implementations in case of malformed - * input. + * Exception thrown by the {@link Parser} implementations in case of malformed input. */ public class ParserException extends MarkedYAMLException { - private static final long serialVersionUID = -2349253802798398038L; - /** - * Constructs an instance. - * - * @param context - * Part of the input document in which vicinity the problem - * occurred. - * @param contextMark - * Position of the <code>context</code> within the document. - * @param problem - * Part of the input document that caused the problem. - * @param problemMark - * Position of the <code>problem</code>. within the document. - */ - public ParserException(String context, Mark contextMark, String problem, Mark problemMark) { - super(context, contextMark, problem, problemMark, null, null); - } + private static final long serialVersionUID = -2349253802798398038L; + + /** + * Constructs an instance. + * + * @param context Part of the input document in which vicinity the problem occurred. + * @param contextMark Position of the <code>context</code> within the document. + * @param problem Part of the input document that caused the problem. + * @param problemMark Position of the <code>problem</code>. within the document. + */ + public ParserException(String context, Mark contextMark, String problem, Mark problemMark) { + super(context, contextMark, problem, problemMark, null, null); + } } diff --git a/src/main/java/org/yaml/snakeyaml/parser/ParserImpl.java b/src/main/java/org/yaml/snakeyaml/parser/ParserImpl.java index 7fea2eec..7c66434f 100644 --- a/src/main/java/org/yaml/snakeyaml/parser/ParserImpl.java +++ b/src/main/java/org/yaml/snakeyaml/parser/ParserImpl.java @@ -1,28 +1,30 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.parser; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; - +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions.Version; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.comments.CommentType; import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.events.AliasEvent; +import org.yaml.snakeyaml.events.CommentEvent; import org.yaml.snakeyaml.events.DocumentEndEvent; import org.yaml.snakeyaml.events.DocumentStartEvent; import org.yaml.snakeyaml.events.Event; @@ -41,6 +43,7 @@ import org.yaml.snakeyaml.scanner.ScannerImpl; import org.yaml.snakeyaml.tokens.AliasToken; import org.yaml.snakeyaml.tokens.AnchorToken; import org.yaml.snakeyaml.tokens.BlockEntryToken; +import org.yaml.snakeyaml.tokens.CommentToken; import org.yaml.snakeyaml.tokens.DirectiveToken; import org.yaml.snakeyaml.tokens.ScalarToken; import org.yaml.snakeyaml.tokens.StreamEndToken; @@ -109,686 +112,863 @@ import org.yaml.snakeyaml.util.ArrayStack; * flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } * flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } * </pre> - * - * Since writing a recursive-descendant parser is a straightforward task, we do - * not give many comments here. + * + * Since writing a recursive-descendant parser is a straightforward task, we do not give many + * comments here. */ public class ParserImpl implements Parser { - private static final Map<String, String> DEFAULT_TAGS = new HashMap<String, String>(); - static { - DEFAULT_TAGS.put("!", "!"); - DEFAULT_TAGS.put("!!", Tag.PREFIX); - } - - protected final Scanner scanner; - private Event currentEvent; - private final ArrayStack<Production> states; - private final ArrayStack<Mark> marks; - private Production state; - private VersionTagsTuple directives; - - public ParserImpl(StreamReader reader) { - this(new ScannerImpl(reader)); - } - - public ParserImpl(Scanner scanner) { - this.scanner = scanner; - currentEvent = null; - directives = new VersionTagsTuple(null, new HashMap<String, String>(DEFAULT_TAGS)); - states = new ArrayStack<Production>(100); - marks = new ArrayStack<Mark>(10); - state = new ParseStreamStart(); - } - - /** - * Check the type of the next event. - */ - public boolean checkEvent(Event.ID choice) { - peekEvent(); - return currentEvent != null && currentEvent.is(choice); - } - - /** - * Get the next event. - */ - public Event peekEvent() { - if (currentEvent == null) { - if (state != null) { - currentEvent = state.produce(); - } - } - return currentEvent; - } - - /** - * Get the next event and proceed further. - */ - public Event getEvent() { - peekEvent(); - Event value = currentEvent; - currentEvent = null; - return value; - } - - /** - * <pre> - * stream ::= STREAM-START implicit_document? explicit_document* STREAM-END - * implicit_document ::= block_node DOCUMENT-END* - * explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* - * </pre> - */ - private class ParseStreamStart implements Production { - public Event produce() { - // Parse the stream start. - StreamStartToken token = (StreamStartToken) scanner.getToken(); - Event event = new StreamStartEvent(token.getStartMark(), token.getEndMark()); - // Prepare the next state. - state = new ParseImplicitDocumentStart(); - return event; - } - } - - private class ParseImplicitDocumentStart implements Production { - public Event produce() { - // Parse an implicit document. - if (!scanner.checkToken(Token.ID.Directive, Token.ID.DocumentStart, Token.ID.StreamEnd)) { - directives = new VersionTagsTuple(null, DEFAULT_TAGS); - Token token = scanner.peekToken(); - Mark startMark = token.getStartMark(); - Mark endMark = startMark; - Event event = new DocumentStartEvent(startMark, endMark, false, null, null); - // Prepare the next state. - states.push(new ParseDocumentEnd()); - state = new ParseBlockNode(); - return event; - } else { - Production p = new ParseDocumentStart(); - return p.produce(); - } - } - } - - private class ParseDocumentStart implements Production { - public Event produce() { - // Parse any extra document end indicators. - while (scanner.checkToken(Token.ID.DocumentEnd)) { - scanner.getToken(); - } - // Parse an explicit document. - Event event; - if (!scanner.checkToken(Token.ID.StreamEnd)) { - Token token = scanner.peekToken(); - Mark startMark = token.getStartMark(); - VersionTagsTuple tuple = processDirectives(); - if (!scanner.checkToken(Token.ID.DocumentStart)) { - throw new ParserException(null, null, "expected '<document start>', but found " - + scanner.peekToken().getTokenId(), scanner.peekToken().getStartMark()); - } - token = scanner.getToken(); - Mark endMark = token.getEndMark(); - event = new DocumentStartEvent(startMark, endMark, true, tuple.getVersion(), - tuple.getTags()); - states.push(new ParseDocumentEnd()); - state = new ParseDocumentContent(); - } else { - // Parse the end of the stream. - StreamEndToken token = (StreamEndToken) scanner.getToken(); - event = new StreamEndEvent(token.getStartMark(), token.getEndMark()); - if (!states.isEmpty()) { - throw new YAMLException("Unexpected end of stream. States left: " + states); - } - if (!marks.isEmpty()) { - throw new YAMLException("Unexpected end of stream. Marks left: " + marks); - } - state = null; - } - return event; - } - } - - private class ParseDocumentEnd implements Production { - public Event produce() { - // Parse the document end. - Token token = scanner.peekToken(); - Mark startMark = token.getStartMark(); - Mark endMark = startMark; - boolean explicit = false; - if (scanner.checkToken(Token.ID.DocumentEnd)) { - token = scanner.getToken(); - endMark = token.getEndMark(); - explicit = true; - } - Event event = new DocumentEndEvent(startMark, endMark, explicit); - // Prepare the next state. - state = new ParseDocumentStart(); - return event; - } - } - private class ParseDocumentContent implements Production { - public Event produce() { - Event event; - if (scanner.checkToken(Token.ID.Directive, Token.ID.DocumentStart, - Token.ID.DocumentEnd, Token.ID.StreamEnd)) { - event = processEmptyScalar(scanner.peekToken().getStartMark()); - state = states.pop(); - return event; - } else { - Production p = new ParseBlockNode(); - return p.produce(); - } + private static final Map<String, String> DEFAULT_TAGS = new HashMap<String, String>(); + + static { + DEFAULT_TAGS.put("!", "!"); + DEFAULT_TAGS.put("!!", Tag.PREFIX); + } + + protected final Scanner scanner; + private Event currentEvent; + private final ArrayStack<Production> states; + private final ArrayStack<Mark> marks; + private Production state; + private VersionTagsTuple directives; + + public ParserImpl(StreamReader reader) { + this(new ScannerImpl(reader)); + } + + @Deprecated + public ParserImpl(StreamReader reader, boolean parseComments) { + this(new ScannerImpl(reader, new LoaderOptions().setProcessComments(parseComments))); + } + + public ParserImpl(StreamReader reader, LoaderOptions options) { + this(new ScannerImpl(reader, options)); + } + + public ParserImpl(Scanner scanner) { + this.scanner = scanner; + currentEvent = null; + directives = new VersionTagsTuple(null, new HashMap<String, String>(DEFAULT_TAGS)); + states = new ArrayStack<Production>(100); + marks = new ArrayStack<Mark>(10); + state = new ParseStreamStart(); + } + + /** + * Check the type of the next event. + */ + public boolean checkEvent(Event.ID choice) { + peekEvent(); + return currentEvent != null && currentEvent.is(choice); + } + + /** + * Get the next event. + */ + public Event peekEvent() { + if (currentEvent == null) { + if (state != null) { + currentEvent = state.produce(); + } + } + return currentEvent; + } + + /** + * Get the next event and proceed further. + */ + public Event getEvent() { + peekEvent(); + Event value = currentEvent; + currentEvent = null; + return value; + } + + private CommentEvent produceCommentEvent(CommentToken token) { + Mark startMark = token.getStartMark(); + Mark endMark = token.getEndMark(); + String value = token.getValue(); + CommentType type = token.getCommentType(); + + // state = state, that no change in state + + return new CommentEvent(type, value, startMark, endMark); + } + + /** + * <pre> + * stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + * implicit_document ::= block_node DOCUMENT-END* + * explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + * </pre> + */ + private class ParseStreamStart implements Production { + + public Event produce() { + // Parse the stream start. + StreamStartToken token = (StreamStartToken) scanner.getToken(); + Event event = new StreamStartEvent(token.getStartMark(), token.getEndMark()); + // Prepare the next state. + state = new ParseImplicitDocumentStart(); + return event; + } + } + + private class ParseImplicitDocumentStart implements Production { + + public Event produce() { + // Parse an implicit document. + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseImplicitDocumentStart(); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (!scanner.checkToken(Token.ID.Directive, Token.ID.DocumentStart, Token.ID.StreamEnd)) { + Token token = scanner.peekToken(); + Mark startMark = token.getStartMark(); + Mark endMark = startMark; + Event event = new DocumentStartEvent(startMark, endMark, false, null, null); + // Prepare the next state. + states.push(new ParseDocumentEnd()); + state = new ParseBlockNode(); + return event; + } + return new ParseDocumentStart().produce(); + } + } + + private class ParseDocumentStart implements Production { + + public Event produce() { + // Parse any extra document end indicators. + while (scanner.checkToken(Token.ID.DocumentEnd)) { + scanner.getToken(); + } + // Parse an explicit document. + Event event; + if (!scanner.checkToken(Token.ID.StreamEnd)) { + Token token = scanner.peekToken(); + Mark startMark = token.getStartMark(); + VersionTagsTuple tuple = processDirectives(); + while (scanner.checkToken(Token.ID.Comment)) { + // TODO: till we figure out what todo with the comments + scanner.getToken(); } - } - - @SuppressWarnings("unchecked") - private VersionTagsTuple processDirectives() { - Version yamlVersion = null; - HashMap<String, String> tagHandles = new HashMap<String, String>(); - while (scanner.checkToken(Token.ID.Directive)) { - @SuppressWarnings("rawtypes") - DirectiveToken token = (DirectiveToken) scanner.getToken(); - if (token.getName().equals("YAML")) { - if (yamlVersion != null) { - throw new ParserException(null, null, "found duplicate YAML directive", - token.getStartMark()); - } - List<Integer> value = (List<Integer>) token.getValue(); - Integer major = value.get(0); - if (major != 1) { - throw new ParserException(null, null, - "found incompatible YAML document (version 1.* is required)", - token.getStartMark()); - } - Integer minor = value.get(1); - switch (minor) { - case 0: - yamlVersion = Version.V1_0; - break; - - default: - yamlVersion = Version.V1_1; - break; - } - } else if (token.getName().equals("TAG")) { - List<String> value = (List<String>) token.getValue(); - String handle = value.get(0); - String prefix = value.get(1); - if (tagHandles.containsKey(handle)) { - throw new ParserException(null, null, "duplicate tag handle " + handle, - token.getStartMark()); - } - tagHandles.put(handle, prefix); - } + if (!scanner.checkToken(Token.ID.StreamEnd)) { + if (!scanner.checkToken(Token.ID.DocumentStart)) { + throw new ParserException(null, null, + "expected '<document start>', but found '" + scanner.peekToken().getTokenId() + "'", + scanner.peekToken().getStartMark()); + } + token = scanner.getToken(); + Mark endMark = token.getEndMark(); + event = + new DocumentStartEvent(startMark, endMark, true, tuple.getVersion(), tuple.getTags()); + states.push(new ParseDocumentEnd()); + state = new ParseDocumentContent(); + return event; } - if (yamlVersion != null || !tagHandles.isEmpty()) { - // directives in the document found - drop the previous - for (String key : DEFAULT_TAGS.keySet()) { - // do not overwrite re-defined tags - if (!tagHandles.containsKey(key)) { - tagHandles.put(key, DEFAULT_TAGS.get(key)); - } - } - directives = new VersionTagsTuple(yamlVersion, tagHandles); + } + // Parse the end of the stream. + StreamEndToken token = (StreamEndToken) scanner.getToken(); + event = new StreamEndEvent(token.getStartMark(), token.getEndMark()); + if (!states.isEmpty()) { + throw new YAMLException("Unexpected end of stream. States left: " + states); + } + if (!marks.isEmpty()) { + throw new YAMLException("Unexpected end of stream. Marks left: " + marks); + } + state = null; + return event; + } + } + + private class ParseDocumentEnd implements Production { + + public Event produce() { + // Parse the document end. + Token token = scanner.peekToken(); + Mark startMark = token.getStartMark(); + Mark endMark = startMark; + boolean explicit = false; + if (scanner.checkToken(Token.ID.DocumentEnd)) { + token = scanner.getToken(); + endMark = token.getEndMark(); + explicit = true; + } + Event event = new DocumentEndEvent(startMark, endMark, explicit); + // Prepare the next state. + state = new ParseDocumentStart(); + return event; + } + } + + private class ParseDocumentContent implements Production { + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseDocumentContent(); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (scanner.checkToken(Token.ID.Directive, Token.ID.DocumentStart, Token.ID.DocumentEnd, + Token.ID.StreamEnd)) { + Event event = processEmptyScalar(scanner.peekToken().getStartMark()); + state = states.pop(); + return event; + } + return new ParseBlockNode().produce(); + } + } + + /** + * https://yaml.org/spec/1.1/#id898785 says "If the document specifies no directives, it is parsed + * using the same settings as the previous document. If the document does specify any directives, + * all directives of previous documents, if any, are ignored." TODO the last statement is not + * respected (as in PyYAML, to work the same) + * + * @return directives to be applied for the current document + */ + @SuppressWarnings("unchecked") + private VersionTagsTuple processDirectives() { + HashMap<String, String> tagHandles = new HashMap<String, String>(directives.getTags()); + for (String key : DEFAULT_TAGS.keySet()) { + tagHandles.remove(key); + } + // keep only added tag handlers + directives = new VersionTagsTuple(null, tagHandles); + while (scanner.checkToken(Token.ID.Directive)) { + @SuppressWarnings("rawtypes") + DirectiveToken token = (DirectiveToken) scanner.getToken(); + if (token.getName().equals("YAML")) { + if (directives.getVersion() != null) { + throw new ParserException(null, null, "found duplicate YAML directive", + token.getStartMark()); } - return directives; - } - - /** - * <pre> - * block_node_or_indentless_sequence ::= ALIAS - * | properties (block_content | indentless_block_sequence)? - * | block_content - * | indentless_block_sequence - * block_node ::= ALIAS - * | properties block_content? - * | block_content - * flow_node ::= ALIAS - * | properties flow_content? - * | flow_content - * properties ::= TAG ANCHOR? | ANCHOR TAG? - * block_content ::= block_collection | flow_collection | SCALAR - * flow_content ::= flow_collection | SCALAR - * block_collection ::= block_sequence | block_mapping - * flow_collection ::= flow_sequence | flow_mapping - * </pre> - */ - - private class ParseBlockNode implements Production { - public Event produce() { - return parseNode(true, false); + List<Integer> value = (List<Integer>) token.getValue(); + Integer major = value.get(0); + if (major != 1) { + throw new ParserException(null, null, + "found incompatible YAML document (version 1.* is required)", token.getStartMark()); } - } - - private Event parseFlowNode() { - return parseNode(false, false); - } - - private Event parseBlockNodeOrIndentlessSequence() { - return parseNode(true, true); - } - - private Event parseNode(boolean block, boolean indentlessSequence) { - Event event; - Mark startMark = null; - Mark endMark = null; - Mark tagMark = null; - if (scanner.checkToken(Token.ID.Alias)) { - AliasToken token = (AliasToken) scanner.getToken(); - event = new AliasEvent(token.getValue(), token.getStartMark(), token.getEndMark()); - state = states.pop(); + Integer minor = value.get(1); + // TODO refactor with ternary + if (minor == 0) { + directives = new VersionTagsTuple(Version.V1_0, tagHandles); } else { - String anchor = null; - TagTuple tagTokenTag = null; - if (scanner.checkToken(Token.ID.Anchor)) { - AnchorToken token = (AnchorToken) scanner.getToken(); - startMark = token.getStartMark(); - endMark = token.getEndMark(); - anchor = token.getValue(); - if (scanner.checkToken(Token.ID.Tag)) { - TagToken tagToken = (TagToken) scanner.getToken(); - tagMark = tagToken.getStartMark(); - endMark = tagToken.getEndMark(); - tagTokenTag = tagToken.getValue(); - } - } else if (scanner.checkToken(Token.ID.Tag)) { - TagToken tagToken = (TagToken) scanner.getToken(); - startMark = tagToken.getStartMark(); - tagMark = startMark; - endMark = tagToken.getEndMark(); - tagTokenTag = tagToken.getValue(); - if (scanner.checkToken(Token.ID.Anchor)) { - AnchorToken token = (AnchorToken) scanner.getToken(); - endMark = token.getEndMark(); - anchor = token.getValue(); - } - } - String tag = null; - if (tagTokenTag != null) { - String handle = tagTokenTag.getHandle(); - String suffix = tagTokenTag.getSuffix(); - if (handle != null) { - if (!directives.getTags().containsKey(handle)) { - throw new ParserException("while parsing a node", startMark, - "found undefined tag handle " + handle, tagMark); - } - tag = directives.getTags().get(handle) + suffix; - } else { - tag = suffix; - } - } - if (startMark == null) { - startMark = scanner.peekToken().getStartMark(); - endMark = startMark; - } - event = null; - boolean implicit = tag == null || tag.equals("!"); - if (indentlessSequence && scanner.checkToken(Token.ID.BlockEntry)) { - endMark = scanner.peekToken().getEndMark(); - event = new SequenceStartEvent(anchor, tag, implicit, startMark, endMark, - Boolean.FALSE); - state = new ParseIndentlessSequenceEntry(); - } else { - if (scanner.checkToken(Token.ID.Scalar)) { - ScalarToken token = (ScalarToken) scanner.getToken(); - endMark = token.getEndMark(); - ImplicitTuple implicitValues; - if ((token.getPlain() && tag == null) || "!".equals(tag)) { - implicitValues = new ImplicitTuple(true, false); - } else if (tag == null) { - implicitValues = new ImplicitTuple(false, true); - } else { - implicitValues = new ImplicitTuple(false, false); - } - event = new ScalarEvent(anchor, tag, implicitValues, token.getValue(), - startMark, endMark, token.getStyle()); - state = states.pop(); - } else if (scanner.checkToken(Token.ID.FlowSequenceStart)) { - endMark = scanner.peekToken().getEndMark(); - event = new SequenceStartEvent(anchor, tag, implicit, startMark, endMark, - Boolean.TRUE); - state = new ParseFlowSequenceFirstEntry(); - } else if (scanner.checkToken(Token.ID.FlowMappingStart)) { - endMark = scanner.peekToken().getEndMark(); - event = new MappingStartEvent(anchor, tag, implicit, startMark, endMark, - Boolean.TRUE); - state = new ParseFlowMappingFirstKey(); - } else if (block && scanner.checkToken(Token.ID.BlockSequenceStart)) { - endMark = scanner.peekToken().getStartMark(); - event = new SequenceStartEvent(anchor, tag, implicit, startMark, endMark, - Boolean.FALSE); - state = new ParseBlockSequenceFirstEntry(); - } else if (block && scanner.checkToken(Token.ID.BlockMappingStart)) { - endMark = scanner.peekToken().getStartMark(); - event = new MappingStartEvent(anchor, tag, implicit, startMark, endMark, - Boolean.FALSE); - state = new ParseBlockMappingFirstKey(); - } else if (anchor != null || tag != null) { - // Empty scalars are allowed even if a tag or an anchor is - // specified. - event = new ScalarEvent(anchor, tag, new ImplicitTuple(implicit, false), "", - startMark, endMark, (char) 0); - state = states.pop(); - } else { - String node; - if (block) { - node = "block"; - } else { - node = "flow"; - } - Token token = scanner.peekToken(); - throw new ParserException("while parsing a " + node + " node", startMark, - "expected the node content, but found " + token.getTokenId(), - token.getStartMark()); - } - } - } - return event; - } - - // block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* - // BLOCK-END - - private class ParseBlockSequenceFirstEntry implements Production { - public Event produce() { - Token token = scanner.getToken(); - marks.push(token.getStartMark()); - return new ParseBlockSequenceEntry().produce(); + directives = new VersionTagsTuple(Version.V1_1, tagHandles); } - } - - private class ParseBlockSequenceEntry implements Production { - public Event produce() { - if (scanner.checkToken(Token.ID.BlockEntry)) { - BlockEntryToken token = (BlockEntryToken) scanner.getToken(); - if (!scanner.checkToken(Token.ID.BlockEntry, Token.ID.BlockEnd)) { - states.push(new ParseBlockSequenceEntry()); - return new ParseBlockNode().produce(); - } else { - state = new ParseBlockSequenceEntry(); - return processEmptyScalar(token.getEndMark()); - } - } - if (!scanner.checkToken(Token.ID.BlockEnd)) { - Token token = scanner.peekToken(); - throw new ParserException("while parsing a block collection", marks.pop(), - "expected <block end>, but found " + token.getTokenId(), - token.getStartMark()); - } - Token token = scanner.getToken(); - Event event = new SequenceEndEvent(token.getStartMark(), token.getEndMark()); - state = states.pop(); - marks.pop(); - return event; - } - } - - // indentless_sequence ::= (BLOCK-ENTRY block_node?)+ - - private class ParseIndentlessSequenceEntry implements Production { - public Event produce() { - if (scanner.checkToken(Token.ID.BlockEntry)) { - Token token = scanner.getToken(); - if (!scanner.checkToken(Token.ID.BlockEntry, Token.ID.Key, Token.ID.Value, - Token.ID.BlockEnd)) { - states.push(new ParseIndentlessSequenceEntry()); - return new ParseBlockNode().produce(); - } else { - state = new ParseIndentlessSequenceEntry(); - return processEmptyScalar(token.getEndMark()); - } - } - Token token = scanner.peekToken(); - Event event = new SequenceEndEvent(token.getStartMark(), token.getEndMark()); - state = states.pop(); - return event; + } else if (token.getName().equals("TAG")) { + List<String> value = (List<String>) token.getValue(); + String handle = value.get(0); + String prefix = value.get(1); + if (tagHandles.containsKey(handle)) { + throw new ParserException(null, null, "duplicate tag handle " + handle, + token.getStartMark()); } - } - - private class ParseBlockMappingFirstKey implements Production { - public Event produce() { - Token token = scanner.getToken(); - marks.push(token.getStartMark()); - return new ParseBlockMappingKey().produce(); + tagHandles.put(handle, prefix); + } + } + HashMap<String, String> detectedTagHandles = new HashMap<String, String>(); + if (!tagHandles.isEmpty()) { + // copy from tagHandles + detectedTagHandles = new HashMap<String, String>(tagHandles); + } + // add default tag handlers to resolve tags + for (String key : DEFAULT_TAGS.keySet()) { + // do not overwrite re-defined tags + if (!tagHandles.containsKey(key)) { + tagHandles.put(key, DEFAULT_TAGS.get(key)); + } + } + // data for the events (no default tags added) + return new VersionTagsTuple(directives.getVersion(), detectedTagHandles); + } + + /** + * <pre> + * block_node_or_indentless_sequence ::= ALIAS + * | properties (block_content | indentless_block_sequence)? + * | block_content + * | indentless_block_sequence + * block_node ::= ALIAS + * | properties block_content? + * | block_content + * flow_node ::= ALIAS + * | properties flow_content? + * | flow_content + * properties ::= TAG ANCHOR? | ANCHOR TAG? + * block_content ::= block_collection | flow_collection | SCALAR + * flow_content ::= flow_collection | SCALAR + * block_collection ::= block_sequence | block_mapping + * flow_collection ::= flow_sequence | flow_mapping + * </pre> + */ + + private class ParseBlockNode implements Production { + + public Event produce() { + return parseNode(true, false); + } + } + + private Event parseFlowNode() { + return parseNode(false, false); + } + + private Event parseBlockNodeOrIndentlessSequence() { + return parseNode(true, true); + } + + private Event parseNode(boolean block, boolean indentlessSequence) { + Event event; + Mark startMark = null; + Mark endMark = null; + Mark tagMark = null; + if (scanner.checkToken(Token.ID.Alias)) { + AliasToken token = (AliasToken) scanner.getToken(); + event = new AliasEvent(token.getValue(), token.getStartMark(), token.getEndMark()); + state = states.pop(); + } else { + String anchor = null; + TagTuple tagTokenTag = null; + if (scanner.checkToken(Token.ID.Anchor)) { + AnchorToken token = (AnchorToken) scanner.getToken(); + startMark = token.getStartMark(); + endMark = token.getEndMark(); + anchor = token.getValue(); + if (scanner.checkToken(Token.ID.Tag)) { + TagToken tagToken = (TagToken) scanner.getToken(); + tagMark = tagToken.getStartMark(); + endMark = tagToken.getEndMark(); + tagTokenTag = tagToken.getValue(); } - } - - private class ParseBlockMappingKey implements Production { - public Event produce() { - if (scanner.checkToken(Token.ID.Key)) { - Token token = scanner.getToken(); - if (!scanner.checkToken(Token.ID.Key, Token.ID.Value, Token.ID.BlockEnd)) { - states.push(new ParseBlockMappingValue()); - return parseBlockNodeOrIndentlessSequence(); - } else { - state = new ParseBlockMappingValue(); - return processEmptyScalar(token.getEndMark()); - } - } - if (!scanner.checkToken(Token.ID.BlockEnd)) { - Token token = scanner.peekToken(); - throw new ParserException("while parsing a block mapping", marks.pop(), - "expected <block end>, but found " + token.getTokenId(), - token.getStartMark()); - } - Token token = scanner.getToken(); - Event event = new MappingEndEvent(token.getStartMark(), token.getEndMark()); - state = states.pop(); - marks.pop(); - return event; + } else if (scanner.checkToken(Token.ID.Tag)) { + TagToken tagToken = (TagToken) scanner.getToken(); + startMark = tagToken.getStartMark(); + tagMark = startMark; + endMark = tagToken.getEndMark(); + tagTokenTag = tagToken.getValue(); + if (scanner.checkToken(Token.ID.Anchor)) { + AnchorToken token = (AnchorToken) scanner.getToken(); + endMark = token.getEndMark(); + anchor = token.getValue(); } - } - - private class ParseBlockMappingValue implements Production { - public Event produce() { - if (scanner.checkToken(Token.ID.Value)) { - Token token = scanner.getToken(); - if (!scanner.checkToken(Token.ID.Key, Token.ID.Value, Token.ID.BlockEnd)) { - states.push(new ParseBlockMappingKey()); - return parseBlockNodeOrIndentlessSequence(); - } else { - state = new ParseBlockMappingKey(); - return processEmptyScalar(token.getEndMark()); - } - } - state = new ParseBlockMappingKey(); - Token token = scanner.peekToken(); - return processEmptyScalar(token.getStartMark()); + } + String tag = null; + if (tagTokenTag != null) { + String handle = tagTokenTag.getHandle(); + String suffix = tagTokenTag.getSuffix(); + if (handle != null) { + if (!directives.getTags().containsKey(handle)) { + throw new ParserException("while parsing a node", startMark, + "found undefined tag handle " + handle, tagMark); + } + tag = directives.getTags().get(handle) + suffix; + } else { + tag = suffix; } - } - - /** - * <pre> - * flow_sequence ::= FLOW-SEQUENCE-START - * (flow_sequence_entry FLOW-ENTRY)* - * flow_sequence_entry? - * FLOW-SEQUENCE-END - * flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? - * Note that while production rules for both flow_sequence_entry and - * flow_mapping_entry are equal, their interpretations are different. - * For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` - * generate an inline mapping (set syntax). - * </pre> - */ - private class ParseFlowSequenceFirstEntry implements Production { - public Event produce() { - Token token = scanner.getToken(); - marks.push(token.getStartMark()); - return new ParseFlowSequenceEntry(true).produce(); + } + if (startMark == null) { + startMark = scanner.peekToken().getStartMark(); + endMark = startMark; + } + event = null; + boolean implicit = tag == null || tag.equals("!"); + if (indentlessSequence && scanner.checkToken(Token.ID.BlockEntry)) { + endMark = scanner.peekToken().getEndMark(); + event = new SequenceStartEvent(anchor, tag, implicit, startMark, endMark, + DumperOptions.FlowStyle.BLOCK); + state = new ParseIndentlessSequenceEntryKey(); + } else { + if (scanner.checkToken(Token.ID.Scalar)) { + ScalarToken token = (ScalarToken) scanner.getToken(); + endMark = token.getEndMark(); + ImplicitTuple implicitValues; + if ((token.getPlain() && tag == null) || "!".equals(tag)) { + implicitValues = new ImplicitTuple(true, false); + } else if (tag == null) { + implicitValues = new ImplicitTuple(false, true); + } else { + implicitValues = new ImplicitTuple(false, false); + } + event = new ScalarEvent(anchor, tag, implicitValues, token.getValue(), startMark, endMark, + token.getStyle()); + state = states.pop(); + } else if (scanner.checkToken(Token.ID.FlowSequenceStart)) { + endMark = scanner.peekToken().getEndMark(); + event = new SequenceStartEvent(anchor, tag, implicit, startMark, endMark, + DumperOptions.FlowStyle.FLOW); + state = new ParseFlowSequenceFirstEntry(); + } else if (scanner.checkToken(Token.ID.FlowMappingStart)) { + endMark = scanner.peekToken().getEndMark(); + event = new MappingStartEvent(anchor, tag, implicit, startMark, endMark, + DumperOptions.FlowStyle.FLOW); + state = new ParseFlowMappingFirstKey(); + } else if (block && scanner.checkToken(Token.ID.BlockSequenceStart)) { + endMark = scanner.peekToken().getStartMark(); + event = new SequenceStartEvent(anchor, tag, implicit, startMark, endMark, + DumperOptions.FlowStyle.BLOCK); + state = new ParseBlockSequenceFirstEntry(); + } else if (block && scanner.checkToken(Token.ID.BlockMappingStart)) { + endMark = scanner.peekToken().getStartMark(); + event = new MappingStartEvent(anchor, tag, implicit, startMark, endMark, + DumperOptions.FlowStyle.BLOCK); + state = new ParseBlockMappingFirstKey(); + } else if (anchor != null || tag != null) { + // Empty scalars are allowed even if a tag or an anchor is + // specified. + event = new ScalarEvent(anchor, tag, new ImplicitTuple(implicit, false), "", startMark, + endMark, DumperOptions.ScalarStyle.PLAIN); + state = states.pop(); + } else { + Token token = scanner.peekToken(); + throw new ParserException("while parsing a " + (block ? "block" : "flow") + " node", + startMark, "expected the node content, but found '" + token.getTokenId() + "'", + token.getStartMark()); } - } + } + } + return event; + } + + // block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* + // BLOCK-END + + private class ParseBlockSequenceFirstEntry implements Production { + + public Event produce() { + Token token = scanner.getToken(); + marks.push(token.getStartMark()); + return new ParseBlockSequenceEntryKey().produce(); + } + } + + private class ParseBlockSequenceEntryKey implements Production { - private class ParseFlowSequenceEntry implements Production { - private boolean first = false; - - public ParseFlowSequenceEntry(boolean first) { - this.first = first; + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseBlockSequenceEntryKey(); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (scanner.checkToken(Token.ID.BlockEntry)) { + BlockEntryToken token = (BlockEntryToken) scanner.getToken(); + return new ParseBlockSequenceEntryValue(token).produce(); + } + if (!scanner.checkToken(Token.ID.BlockEnd)) { + Token token = scanner.peekToken(); + throw new ParserException("while parsing a block collection", marks.pop(), + "expected <block end>, but found '" + token.getTokenId() + "'", token.getStartMark()); + } + Token token = scanner.getToken(); + Event event = new SequenceEndEvent(token.getStartMark(), token.getEndMark()); + state = states.pop(); + marks.pop(); + return event; + } + } + + private class ParseBlockSequenceEntryValue implements Production { + + BlockEntryToken token; + + public ParseBlockSequenceEntryValue(final BlockEntryToken token) { + this.token = token; + } + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseBlockSequenceEntryValue(token); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (!scanner.checkToken(Token.ID.BlockEntry, Token.ID.BlockEnd)) { + states.push(new ParseBlockSequenceEntryKey()); + return new ParseBlockNode().produce(); + } else { + state = new ParseBlockSequenceEntryKey(); + return processEmptyScalar(token.getEndMark()); + } + } + } + + // indentless_sequence ::= (BLOCK-ENTRY block_node?)+ + + private class ParseIndentlessSequenceEntryKey implements Production { + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseIndentlessSequenceEntryKey(); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (scanner.checkToken(Token.ID.BlockEntry)) { + BlockEntryToken token = (BlockEntryToken) scanner.getToken(); + return new ParseIndentlessSequenceEntryValue(token).produce(); + } + Token token = scanner.peekToken(); + Event event = new SequenceEndEvent(token.getStartMark(), token.getEndMark()); + state = states.pop(); + return event; + } + } + + private class ParseIndentlessSequenceEntryValue implements Production { + + BlockEntryToken token; + + public ParseIndentlessSequenceEntryValue(final BlockEntryToken token) { + this.token = token; + } + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseIndentlessSequenceEntryValue(token); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (!scanner.checkToken(Token.ID.BlockEntry, Token.ID.Key, Token.ID.Value, + Token.ID.BlockEnd)) { + states.push(new ParseIndentlessSequenceEntryKey()); + return new ParseBlockNode().produce(); + } else { + state = new ParseIndentlessSequenceEntryKey(); + return processEmptyScalar(token.getEndMark()); + } + } + } + + private class ParseBlockMappingFirstKey implements Production { + + public Event produce() { + Token token = scanner.getToken(); + marks.push(token.getStartMark()); + return new ParseBlockMappingKey().produce(); + } + } + + private class ParseBlockMappingKey implements Production { + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseBlockMappingKey(); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (scanner.checkToken(Token.ID.Key)) { + Token token = scanner.getToken(); + if (!scanner.checkToken(Token.ID.Key, Token.ID.Value, Token.ID.BlockEnd)) { + states.push(new ParseBlockMappingValue()); + return parseBlockNodeOrIndentlessSequence(); + } else { + state = new ParseBlockMappingValue(); + return processEmptyScalar(token.getEndMark()); } - - public Event produce() { - if (!scanner.checkToken(Token.ID.FlowSequenceEnd)) { - if (!first) { - if (scanner.checkToken(Token.ID.FlowEntry)) { - scanner.getToken(); - } else { - Token token = scanner.peekToken(); - throw new ParserException("while parsing a flow sequence", marks.pop(), - "expected ',' or ']', but got " + token.getTokenId(), - token.getStartMark()); - } - } - if (scanner.checkToken(Token.ID.Key)) { - Token token = scanner.peekToken(); - Event event = new MappingStartEvent(null, null, true, token.getStartMark(), - token.getEndMark(), Boolean.TRUE); - state = new ParseFlowSequenceEntryMappingKey(); - return event; - } else if (!scanner.checkToken(Token.ID.FlowSequenceEnd)) { - states.push(new ParseFlowSequenceEntry(false)); - return parseFlowNode(); - } - } - Token token = scanner.getToken(); - Event event = new SequenceEndEvent(token.getStartMark(), token.getEndMark()); - state = states.pop(); - marks.pop(); - return event; + } + if (!scanner.checkToken(Token.ID.BlockEnd)) { + Token token = scanner.peekToken(); + throw new ParserException("while parsing a block mapping", marks.pop(), + "expected <block end>, but found '" + token.getTokenId() + "'", token.getStartMark()); + } + Token token = scanner.getToken(); + Event event = new MappingEndEvent(token.getStartMark(), token.getEndMark()); + state = states.pop(); + marks.pop(); + return event; + } + } + + private class ParseBlockMappingValue implements Production { + + public Event produce() { + if (scanner.checkToken(Token.ID.Value)) { + Token token = scanner.getToken(); + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseBlockMappingValueComment(); + return state.produce(); + } else if (!scanner.checkToken(Token.ID.Key, Token.ID.Value, Token.ID.BlockEnd)) { + states.push(new ParseBlockMappingKey()); + return parseBlockNodeOrIndentlessSequence(); + } else { + state = new ParseBlockMappingKey(); + return processEmptyScalar(token.getEndMark()); } - } - - private class ParseFlowSequenceEntryMappingKey implements Production { - public Event produce() { - Token token = scanner.getToken(); - if (!scanner.checkToken(Token.ID.Value, Token.ID.FlowEntry, Token.ID.FlowSequenceEnd)) { - states.push(new ParseFlowSequenceEntryMappingValue()); - return parseFlowNode(); - } else { - state = new ParseFlowSequenceEntryMappingValue(); - return processEmptyScalar(token.getEndMark()); - } + } else if (scanner.checkToken(Token.ID.Scalar)) { + states.push(new ParseBlockMappingKey()); + return parseBlockNodeOrIndentlessSequence(); + } + state = new ParseBlockMappingKey(); + Token token = scanner.peekToken(); + return processEmptyScalar(token.getStartMark()); + } + } + + private class ParseBlockMappingValueComment implements Production { + + List<CommentToken> tokens = new LinkedList<>(); + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + tokens.add((CommentToken) scanner.getToken()); + return produce(); + } else if (!scanner.checkToken(Token.ID.Key, Token.ID.Value, Token.ID.BlockEnd)) { + if (!tokens.isEmpty()) { + return produceCommentEvent(tokens.remove(0)); } - } - - private class ParseFlowSequenceEntryMappingValue implements Production { - public Event produce() { - if (scanner.checkToken(Token.ID.Value)) { - Token token = scanner.getToken(); - if (!scanner.checkToken(Token.ID.FlowEntry, Token.ID.FlowSequenceEnd)) { - states.push(new ParseFlowSequenceEntryMappingEnd()); - return parseFlowNode(); - } else { - state = new ParseFlowSequenceEntryMappingEnd(); - return processEmptyScalar(token.getEndMark()); - } - } else { - state = new ParseFlowSequenceEntryMappingEnd(); - Token token = scanner.peekToken(); - return processEmptyScalar(token.getStartMark()); + states.push(new ParseBlockMappingKey()); + return parseBlockNodeOrIndentlessSequence(); + } else { + state = new ParseBlockMappingValueCommentList(tokens); + return processEmptyScalar(scanner.peekToken().getStartMark()); + } + } + } + + private class ParseBlockMappingValueCommentList implements Production { + + List<CommentToken> tokens; + + public ParseBlockMappingValueCommentList(final List<CommentToken> tokens) { + this.tokens = tokens; + } + + public Event produce() { + if (!tokens.isEmpty()) { + return produceCommentEvent(tokens.remove(0)); + } + return new ParseBlockMappingKey().produce(); + } + } + + /** + * <pre> + * flow_sequence ::= FLOW-SEQUENCE-START + * (flow_sequence_entry FLOW-ENTRY)* + * flow_sequence_entry? + * FLOW-SEQUENCE-END + * flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * Note that while production rules for both flow_sequence_entry and + * flow_mapping_entry are equal, their interpretations are different. + * For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` + * generate an inline mapping (set syntax). + * </pre> + */ + private class ParseFlowSequenceFirstEntry implements Production { + + public Event produce() { + Token token = scanner.getToken(); + marks.push(token.getStartMark()); + return new ParseFlowSequenceEntry(true).produce(); + } + } + + private class ParseFlowSequenceEntry implements Production { + + private final boolean first; + + public ParseFlowSequenceEntry(boolean first) { + this.first = first; + } + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseFlowSequenceEntry(first); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (!scanner.checkToken(Token.ID.FlowSequenceEnd)) { + if (!first) { + if (scanner.checkToken(Token.ID.FlowEntry)) { + scanner.getToken(); + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseFlowSequenceEntry(true); + return produceCommentEvent((CommentToken) scanner.getToken()); } - } - } - - private class ParseFlowSequenceEntryMappingEnd implements Production { - public Event produce() { - state = new ParseFlowSequenceEntry(false); + } else { Token token = scanner.peekToken(); - return new MappingEndEvent(token.getStartMark(), token.getEndMark()); + throw new ParserException("while parsing a flow sequence", marks.pop(), + "expected ',' or ']', but got " + token.getTokenId(), token.getStartMark()); + } } - } - - /** - * <pre> - * flow_mapping ::= FLOW-MAPPING-START - * (flow_mapping_entry FLOW-ENTRY)* - * flow_mapping_entry? - * FLOW-MAPPING-END - * flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? - * </pre> - */ - private class ParseFlowMappingFirstKey implements Production { - public Event produce() { - Token token = scanner.getToken(); - marks.push(token.getStartMark()); - return new ParseFlowMappingKey(true).produce(); + if (scanner.checkToken(Token.ID.Key)) { + Token token = scanner.peekToken(); + Event event = new MappingStartEvent(null, null, true, token.getStartMark(), + token.getEndMark(), DumperOptions.FlowStyle.FLOW); + state = new ParseFlowSequenceEntryMappingKey(); + return event; + } else if (!scanner.checkToken(Token.ID.FlowSequenceEnd)) { + states.push(new ParseFlowSequenceEntry(false)); + return parseFlowNode(); } - } - - private class ParseFlowMappingKey implements Production { - private boolean first = false; - - public ParseFlowMappingKey(boolean first) { - this.first = first; + } + Token token = scanner.getToken(); + Event event = new SequenceEndEvent(token.getStartMark(), token.getEndMark()); + if (!scanner.checkToken(Token.ID.Comment)) { + state = states.pop(); + } else { + state = new ParseFlowEndComment(); + } + marks.pop(); + return event; + } + } + + private class ParseFlowEndComment implements Production { + + public Event produce() { + Event event = produceCommentEvent((CommentToken) scanner.getToken()); + if (!scanner.checkToken(Token.ID.Comment)) { + state = states.pop(); + } + return event; + } + } + + private class ParseFlowSequenceEntryMappingKey implements Production { + + public Event produce() { + Token token = scanner.getToken(); + if (!scanner.checkToken(Token.ID.Value, Token.ID.FlowEntry, Token.ID.FlowSequenceEnd)) { + states.push(new ParseFlowSequenceEntryMappingValue()); + return parseFlowNode(); + } else { + state = new ParseFlowSequenceEntryMappingValue(); + return processEmptyScalar(token.getEndMark()); + } + } + } + + private class ParseFlowSequenceEntryMappingValue implements Production { + + public Event produce() { + if (scanner.checkToken(Token.ID.Value)) { + Token token = scanner.getToken(); + if (!scanner.checkToken(Token.ID.FlowEntry, Token.ID.FlowSequenceEnd)) { + states.push(new ParseFlowSequenceEntryMappingEnd()); + return parseFlowNode(); + } else { + state = new ParseFlowSequenceEntryMappingEnd(); + return processEmptyScalar(token.getEndMark()); } - - public Event produce() { - if (!scanner.checkToken(Token.ID.FlowMappingEnd)) { - if (!first) { - if (scanner.checkToken(Token.ID.FlowEntry)) { - scanner.getToken(); - } else { - Token token = scanner.peekToken(); - throw new ParserException("while parsing a flow mapping", marks.pop(), - "expected ',' or '}', but got " + token.getTokenId(), - token.getStartMark()); - } - } - if (scanner.checkToken(Token.ID.Key)) { - Token token = scanner.getToken(); - if (!scanner.checkToken(Token.ID.Value, Token.ID.FlowEntry, - Token.ID.FlowMappingEnd)) { - states.push(new ParseFlowMappingValue()); - return parseFlowNode(); - } else { - state = new ParseFlowMappingValue(); - return processEmptyScalar(token.getEndMark()); - } - } else if (!scanner.checkToken(Token.ID.FlowMappingEnd)) { - states.push(new ParseFlowMappingEmptyValue()); - return parseFlowNode(); - } + } else { + state = new ParseFlowSequenceEntryMappingEnd(); + Token token = scanner.peekToken(); + return processEmptyScalar(token.getStartMark()); + } + } + } + + private class ParseFlowSequenceEntryMappingEnd implements Production { + + public Event produce() { + state = new ParseFlowSequenceEntry(false); + Token token = scanner.peekToken(); + return new MappingEndEvent(token.getStartMark(), token.getEndMark()); + } + } + + /** + * <pre> + * flow_mapping ::= FLOW-MAPPING-START + * (flow_mapping_entry FLOW-ENTRY)* + * flow_mapping_entry? + * FLOW-MAPPING-END + * flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + * </pre> + */ + private class ParseFlowMappingFirstKey implements Production { + + public Event produce() { + Token token = scanner.getToken(); + marks.push(token.getStartMark()); + return new ParseFlowMappingKey(true).produce(); + } + } + + private class ParseFlowMappingKey implements Production { + + private final boolean first; + + public ParseFlowMappingKey(boolean first) { + this.first = first; + } + + public Event produce() { + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseFlowMappingKey(first); + return produceCommentEvent((CommentToken) scanner.getToken()); + } + if (!scanner.checkToken(Token.ID.FlowMappingEnd)) { + if (!first) { + if (scanner.checkToken(Token.ID.FlowEntry)) { + scanner.getToken(); + if (scanner.checkToken(Token.ID.Comment)) { + state = new ParseFlowMappingKey(true); + return produceCommentEvent((CommentToken) scanner.getToken()); } - Token token = scanner.getToken(); - Event event = new MappingEndEvent(token.getStartMark(), token.getEndMark()); - state = states.pop(); - marks.pop(); - return event; + } else { + Token token = scanner.peekToken(); + throw new ParserException("while parsing a flow mapping", marks.pop(), + "expected ',' or '}', but got " + token.getTokenId(), token.getStartMark()); + } } - } - - private class ParseFlowMappingValue implements Production { - public Event produce() { - if (scanner.checkToken(Token.ID.Value)) { - Token token = scanner.getToken(); - if (!scanner.checkToken(Token.ID.FlowEntry, Token.ID.FlowMappingEnd)) { - states.push(new ParseFlowMappingKey(false)); - return parseFlowNode(); - } else { - state = new ParseFlowMappingKey(false); - return processEmptyScalar(token.getEndMark()); - } - } else { - state = new ParseFlowMappingKey(false); - Token token = scanner.peekToken(); - return processEmptyScalar(token.getStartMark()); - } + if (scanner.checkToken(Token.ID.Key)) { + Token token = scanner.getToken(); + if (!scanner.checkToken(Token.ID.Value, Token.ID.FlowEntry, Token.ID.FlowMappingEnd)) { + states.push(new ParseFlowMappingValue()); + return parseFlowNode(); + } else { + state = new ParseFlowMappingValue(); + return processEmptyScalar(token.getEndMark()); + } + } else if (!scanner.checkToken(Token.ID.FlowMappingEnd)) { + states.push(new ParseFlowMappingEmptyValue()); + return parseFlowNode(); } - } - - private class ParseFlowMappingEmptyValue implements Production { - public Event produce() { - state = new ParseFlowMappingKey(false); - return processEmptyScalar(scanner.peekToken().getStartMark()); + } + Token token = scanner.getToken(); + Event event = new MappingEndEvent(token.getStartMark(), token.getEndMark()); + marks.pop(); + if (!scanner.checkToken(Token.ID.Comment)) { + state = states.pop(); + } else { + state = new ParseFlowEndComment(); + } + return event; + } + } + + private class ParseFlowMappingValue implements Production { + + public Event produce() { + if (scanner.checkToken(Token.ID.Value)) { + Token token = scanner.getToken(); + if (!scanner.checkToken(Token.ID.FlowEntry, Token.ID.FlowMappingEnd)) { + states.push(new ParseFlowMappingKey(false)); + return parseFlowNode(); + } else { + state = new ParseFlowMappingKey(false); + return processEmptyScalar(token.getEndMark()); } - } - - /** - * <pre> - * block_mapping ::= BLOCK-MAPPING_START - * ((KEY block_node_or_indentless_sequence?)? - * (VALUE block_node_or_indentless_sequence?)?)* - * BLOCK-END - * </pre> - */ - private Event processEmptyScalar(Mark mark) { - return new ScalarEvent(null, null, new ImplicitTuple(true, false), "", mark, mark, (char) 0); - } + } else { + state = new ParseFlowMappingKey(false); + Token token = scanner.peekToken(); + return processEmptyScalar(token.getStartMark()); + } + } + } + + private class ParseFlowMappingEmptyValue implements Production { + + public Event produce() { + state = new ParseFlowMappingKey(false); + return processEmptyScalar(scanner.peekToken().getStartMark()); + } + } + + /** + * <pre> + * block_mapping ::= BLOCK-MAPPING_START + * ((KEY block_node_or_indentless_sequence?)? + * (VALUE block_node_or_indentless_sequence?)?)* + * BLOCK-END + * </pre> + */ + private Event processEmptyScalar(Mark mark) { + return new ScalarEvent(null, null, new ImplicitTuple(true, false), "", mark, mark, + DumperOptions.ScalarStyle.PLAIN); + } } diff --git a/src/main/java/org/yaml/snakeyaml/parser/Production.java b/src/main/java/org/yaml/snakeyaml/parser/Production.java index 5dd3949c..d886733c 100644 --- a/src/main/java/org/yaml/snakeyaml/parser/Production.java +++ b/src/main/java/org/yaml/snakeyaml/parser/Production.java @@ -1,28 +1,27 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.parser; import org.yaml.snakeyaml.events.Event; /** - * Helper for {@link ParserImpl}. A grammar rule to apply given the symbols on - * top of its stack and the next input token - * - * @see <a href="http://en.wikipedia.org/wiki/LL_parser"></a> + * Helper for {@link ParserImpl}. A grammar rule to apply given the symbols on top of its stack and + * the next input token + * + * @see <a href="http://en.wikipedia.org/wiki/LL_parser">LL parser</a> */ interface Production { - Event produce(); + + Event produce(); } diff --git a/src/main/java/org/yaml/snakeyaml/parser/VersionTagsTuple.java b/src/main/java/org/yaml/snakeyaml/parser/VersionTagsTuple.java index 44ed2fbb..6a2a1f64 100644 --- a/src/main/java/org/yaml/snakeyaml/parser/VersionTagsTuple.java +++ b/src/main/java/org/yaml/snakeyaml/parser/VersionTagsTuple.java @@ -1,46 +1,44 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.parser; import java.util.Map; - import org.yaml.snakeyaml.DumperOptions.Version; /** * Store the internal state for directives */ class VersionTagsTuple { - private Version version; - private Map<String, String> tags; - public VersionTagsTuple(Version version, Map<String, String> tags) { - this.version = version; - this.tags = tags; - } + private final Version version; + private final Map<String, String> tags; + + public VersionTagsTuple(Version version, Map<String, String> tags) { + this.version = version; + this.tags = tags; + } - public Version getVersion() { - return version; - } + public Version getVersion() { + return version; + } - public Map<String, String> getTags() { - return tags; - } + public Map<String, String> getTags() { + return tags; + } - @Override - public String toString() { - return String.format("VersionTagsTuple<%s, %s>", version, tags); - } + @Override + public String toString() { + return String.format("VersionTagsTuple<%s, %s>", version, tags); + } } diff --git a/src/main/java/org/yaml/snakeyaml/reader/ReaderException.java b/src/main/java/org/yaml/snakeyaml/reader/ReaderException.java index 7a8a06b2..d44141ec 100644 --- a/src/main/java/org/yaml/snakeyaml/reader/ReaderException.java +++ b/src/main/java/org/yaml/snakeyaml/reader/ReaderException.java @@ -1,51 +1,50 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.reader; import org.yaml.snakeyaml.error.YAMLException; public class ReaderException extends YAMLException { - private static final long serialVersionUID = 8710781187529689083L; - private final String name; - private final char character; - private final int position; - - public ReaderException(String name, int position, char character, String message) { - super(message); - this.name = name; - this.character = character; - this.position = position; - } - - public String getName() { - return name; - } - - public char getCharacter() { - return character; - } - - public int getPosition() { - return position; - } - - @Override - public String toString() { - return "unacceptable character '" + character + "' (0x" - + Integer.toHexString((int) character).toUpperCase() + ") " + getMessage() - + "\nin \"" + name + "\", position " + position; - } + + private static final long serialVersionUID = 8710781187529689083L; + private final String name; + private final int codePoint; + private final int position; + + public ReaderException(String name, int position, int codePoint, String message) { + super(message); + this.name = name; + this.codePoint = codePoint; + this.position = position; + } + + public String getName() { + return name; + } + + public int getCodePoint() { + return codePoint; + } + + public int getPosition() { + return position; + } + + @Override + public String toString() { + final String s = new String(Character.toChars(codePoint)); + return "unacceptable code point '" + s + "' (0x" + Integer.toHexString(codePoint).toUpperCase() + + ") " + getMessage() + "\nin \"" + name + "\", position " + position; + } } diff --git a/src/main/java/org/yaml/snakeyaml/reader/StreamReader.java b/src/main/java/org/yaml/snakeyaml/reader/StreamReader.java index 56ec0078..6799e76e 100644 --- a/src/main/java/org/yaml/snakeyaml/reader/StreamReader.java +++ b/src/main/java/org/yaml/snakeyaml/reader/StreamReader.java @@ -1,220 +1,230 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.reader; import java.io.IOException; import java.io.Reader; -import java.nio.charset.Charset; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import java.io.StringReader; +import java.util.Arrays; import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.scanner.Constant; /** - * Reader: checks if characters are in allowed range, adds '\0' to the end. + * Reader: checks if code points are in allowed range. Returns '\0' when end of data has been + * reached. */ public class StreamReader { - public final static Pattern NON_PRINTABLE = Pattern - .compile("[^\t\n\r\u0020-\u007E\u0085\u00A0-\uD7FF\uE000-\uFFFD]"); - private String name; - private final Reader stream; - private int pointer = 0; - private boolean eof = true; - private String buffer; - private int index = 0; - private int line = 0; - private int column = 0; - private char[] data; - - public StreamReader(String stream) { - this.name = "'string'"; - this.buffer = ""; // to set length to 0 - checkPrintable(stream); - this.buffer = stream + "\0"; - this.stream = null; - this.eof = true; - this.data = null; - } - - public StreamReader(Reader reader) { - this.name = "'reader'"; - this.buffer = ""; - this.stream = reader; - this.eof = false; - this.data = new char[1024]; - this.update(); - } - void checkPrintable(CharSequence data) { - Matcher em = NON_PRINTABLE.matcher(data); - if (em.find()) { - int position = this.index + this.buffer.length() - this.pointer + em.start(); - throw new ReaderException(name, position, em.group().charAt(0), - "special characters are not allowed"); + private String name; + private final Reader stream; + /** + * Read data (as a moving window for input stream) + */ + private int[] dataWindow; + + /** + * Real length of the data in dataWindow + */ + private int dataLength; + + /** + * The variable points to the current position in the data array + */ + private int pointer = 0; + private boolean eof; + private int index = 0; // in code points + private int line = 0; + private int column = 0; // in code points + private final char[] buffer; // temp buffer for one read operation (to avoid + // creating the array in stack) + + private static final int BUFFER_SIZE = 1025; + + public StreamReader(String stream) { + this(new StringReader(stream)); + this.name = "'string'"; + } + + public StreamReader(Reader reader) { + this.name = "'reader'"; + this.dataWindow = new int[0]; + this.dataLength = 0; + this.stream = reader; + this.eof = false; + this.buffer = new char[BUFFER_SIZE]; + } + + public static boolean isPrintable(final String data) { + final int length = data.length(); + for (int offset = 0; offset < length;) { + final int codePoint = data.codePointAt(offset); + + if (!isPrintable(codePoint)) { + return false; + } + + offset += Character.charCount(codePoint); + } + + return true; + } + + public static boolean isPrintable(final int c) { + return (c >= 0x20 && c <= 0x7E) || c == 0x9 || c == 0xA || c == 0xD || c == 0x85 + || (c >= 0xA0 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD) + || (c >= 0x10000 && c <= 0x10FFFF); + } + + public Mark getMark() { + return new Mark(name, this.index, this.line, this.column, this.dataWindow, this.pointer); + } + + public void forward() { + forward(1); + } + + /** + * read the next length characters and move the pointer. if the last character is high surrogate + * one more character will be read + * + * @param length amount of characters to move forward + */ + public void forward(int length) { + for (int i = 0; i < length && ensureEnoughData(); i++) { + int c = dataWindow[pointer++]; + this.index++; + if (Constant.LINEBR.has(c) + || (c == '\r' && (ensureEnoughData() && dataWindow[pointer] != '\n'))) { + this.line++; + this.column = 0; + } else if (c != 0xFEFF) { + this.column++; + } + } + } + + public int peek() { + return (ensureEnoughData()) ? dataWindow[pointer] : '\0'; + } + + /** + * Peek the next index-th code point + * + * @param index to peek + * @return the next index-th code point + */ + public int peek(int index) { + return (ensureEnoughData(index)) ? dataWindow[pointer + index] : '\0'; + } + + /** + * peek the next length code points + * + * @param length amount of the characters to peek + * @return the next length code points + */ + public String prefix(int length) { + if (length == 0) { + return ""; + } else if (ensureEnoughData(length)) { + return new String(this.dataWindow, pointer, length); + } else { + return new String(this.dataWindow, pointer, Math.min(length, dataLength - pointer)); + } + } + + /** + * prefix(length) immediately followed by forward(length) + * + * @param length amount of characters to get + * @return the next length code points + */ + public String prefixForward(int length) { + final String prefix = prefix(length); + this.pointer += length; + this.index += length; + // prefix never contains new line characters + this.column += length; + return prefix; + } + + private boolean ensureEnoughData() { + return ensureEnoughData(0); + } + + private boolean ensureEnoughData(int size) { + if (!eof && pointer + size >= dataLength) { + update(); + } + return (this.pointer + size) < dataLength; + } + + private void update() { + try { + int read = stream.read(buffer, 0, BUFFER_SIZE - 1); + if (read > 0) { + int cpIndex = (dataLength - pointer); + dataWindow = Arrays.copyOfRange(dataWindow, pointer, dataLength + read); + + if (Character.isHighSurrogate(buffer[read - 1])) { + if (stream.read(buffer, read, 1) == -1) { + eof = true; + } else { + read++; + } } - } - /** - * Checks <code>chars</chars> for the non-printable characters. - * - * @param chars - * the array where to search. - * @param begin - * the beginning index, inclusive. - * @param end - * the ending index, exclusive. - * @throws ReaderException - * if <code>chars</code> contains non-printable character(s). - */ - void checkPrintable(final char[] chars, final int begin, final int end) { - for (int i = begin; i < end; i++) { - final char c = chars[i]; - - if (isPrintable(c)) { - continue; - } - - int position = this.index + this.buffer.length() - this.pointer + i; - throw new ReaderException(name, position, c, "special characters are not allowed"); + int nonPrintable = ' '; + for (int i = 0; i < read; cpIndex++) { + int codePoint = Character.codePointAt(buffer, i); + dataWindow[cpIndex] = codePoint; + if (isPrintable(codePoint)) { + i += Character.charCount(codePoint); + } else { + nonPrintable = codePoint; + i = read; + } } - } - - public static boolean isPrintable(final char c) { - return (c >= '\u0020' && c <= '\u007E') || c == '\n' || c == '\r' || c == '\t' - || c == '\u0085' || (c >= '\u00A0' && c <= '\uD7FF') - || (c >= '\uE000' && c <= '\uFFFD'); - } - public Mark getMark() { - return new Mark(name, this.index, this.line, this.column, this.buffer, this.pointer); - } - - public void forward() { - forward(1); - } - - /** - * read the next length characters and move the pointer. - * - * @param length - */ - public void forward(int length) { - if (this.pointer + length + 1 >= this.buffer.length()) { - update(); - } - char ch = 0; - for (int i = 0; i < length; i++) { - ch = this.buffer.charAt(this.pointer); - this.pointer++; - this.index++; - if (Constant.LINEBR.has(ch) || (ch == '\r' && buffer.charAt(pointer) != '\n')) { - this.line++; - this.column = 0; - } else if (ch != '\uFEFF') { - this.column++; - } + dataLength = cpIndex; + pointer = 0; + if (nonPrintable != ' ') { + throw new ReaderException(name, cpIndex - 1, nonPrintable, + "special characters are not allowed"); } + } else { + eof = true; + } + } catch (IOException ioe) { + throw new YAMLException(ioe); } + } - public char peek() { - return this.buffer.charAt(this.pointer); - } - /** - * Peek the next index-th character - * - * @param index - * @return the next index-th character - */ - public char peek(int index) { - if (this.pointer + index + 1 > this.buffer.length()) { - update(); - } - return this.buffer.charAt(this.pointer + index); - } + public int getColumn() { + return column; + } - /** - * peek the next length characters - * - * @param length - * @return the next length characters - */ - public String prefix(int length) { - if (this.pointer + length >= this.buffer.length()) { - update(); - } - if (this.pointer + length > this.buffer.length()) { - return this.buffer.substring(this.pointer); - } - return this.buffer.substring(this.pointer, this.pointer + length); - } - - /** - * prefix(length) immediately followed by forward(length) - */ - public String prefixForward(int length) { - final String prefix = prefix(length); - this.pointer += length; - this.index += length; - // prefix never contains new line characters - this.column += length; - return prefix; - } - - private void update() { - if (!this.eof) { - this.buffer = buffer.substring(this.pointer); - this.pointer = 0; - try { - int converted = this.stream.read(data); - if (converted > 0) { - /* - * Let's create StringBuilder manually. Anyway str1 + str2 - * generates new StringBuilder(str1).append(str2).toSting() - * Giving correct capacity to the constructor prevents - * unnecessary operations in appends. - */ - checkPrintable(data, 0, converted); - this.buffer = new StringBuilder(buffer.length() + converted).append(buffer) - .append(data, 0, converted).toString(); - } else { - this.eof = true; - this.buffer += "\0"; - } - } catch (IOException ioe) { - throw new YAMLException(ioe); - } - } - } + /** + * @return current position as number (in characters) from the beginning of the stream + */ + public int getIndex() { + return index; + } - public int getColumn() { - return column; - } - - public Charset getEncoding() { - return Charset.forName(((UnicodeReader) this.stream).getEncoding()); - } - - public int getIndex() { - return index; - } - - public int getLine() { - return line; - } + public int getLine() { + return line; + } } diff --git a/src/main/java/org/yaml/snakeyaml/reader/UnicodeReader.java b/src/main/java/org/yaml/snakeyaml/reader/UnicodeReader.java index dd9dc39b..4c9c9039 100644 --- a/src/main/java/org/yaml/snakeyaml/reader/UnicodeReader.java +++ b/src/main/java/org/yaml/snakeyaml/reader/UnicodeReader.java @@ -1,40 +1,29 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.reader; /** - version: 1.1 / 2007-01-25 - - changed BOM recognition ordering (longer boms first) - - Original pseudocode : Thomas Weidenfeller - Implementation tweaked: Aki Nieminen - Implementation changed: Andrey Somov - * UTF-32 removed because it is not supported by YAML - * no default encoding - - http://www.unicode.org/unicode/faq/utf_bom.html - BOMs: - 00 00 FE FF = UTF-32, big-endian - FF FE 00 00 = UTF-32, little-endian - EF BB BF = UTF-8, - FE FF = UTF-16, big-endian - FF FE = UTF-16, little-endian - - Win2k Notepad: - Unicode format = UTF-16LE + * version: 1.1 / 2007-01-25 - changed BOM recognition ordering (longer boms first) + * + * Original pseudocode : Thomas Weidenfeller Implementation tweaked: Aki Nieminen Implementation + * changed: Andrey Somov UTF-32 removed because it is not supported by YAML no default encoding + * + * http://www.unicode.org/unicode/faq/utf_bom.html BOMs: 00 00 FE FF = UTF-32, big-endian FF FE 00 + * 00 = UTF-32, little-endian EF BB BF = UTF-8, FE FF = UTF-16, big-endian FF FE = UTF-16, + * little-endian + * + * Win2k Notepad: Unicode format = UTF-16LE ***/ import java.io.IOException; @@ -45,81 +34,87 @@ import java.io.Reader; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; /** - * Generic unicode textreader, which will use BOM mark to identify the encoding - * to be used. If BOM is not found then use a given default or system encoding. + * Generic unicode textreader, which will use BOM mark to identify the encoding to be used. If BOM + * is not found then use a given default or system encoding. */ public class UnicodeReader extends Reader { - private static final Charset UTF8 = Charset.forName("UTF-8"); - private static final Charset UTF16BE = Charset.forName("UTF-16BE"); - private static final Charset UTF16LE = Charset.forName("UTF-16LE"); - - PushbackInputStream internalIn; - InputStreamReader internalIn2 = null; - - private static final int BOM_SIZE = 3; - /** - * @param in - * InputStream to be read - */ - public UnicodeReader(InputStream in) { - internalIn = new PushbackInputStream(in, BOM_SIZE); + private static final Charset UTF8 = StandardCharsets.UTF_8; + private static final Charset UTF16BE = StandardCharsets.UTF_16BE; + private static final Charset UTF16LE = StandardCharsets.UTF_16LE; + + PushbackInputStream internalIn; + InputStreamReader internalIn2 = null; + + private static final int BOM_SIZE = 3; + + /** + * @param in InputStream to be read + */ + public UnicodeReader(InputStream in) { + internalIn = new PushbackInputStream(in, BOM_SIZE); + } + + /** + * Get stream encoding or NULL if stream is uninitialized. Call init() or read() method to + * initialize it. + * + * @return the name of the character encoding being used by this stream. + */ + public String getEncoding() { + return internalIn2.getEncoding(); + } + + /** + * Read-ahead four bytes and check for BOM marks. Extra bytes are unread back to the stream, only + * BOM bytes are skipped. + * + * @throws IOException if InputStream cannot be created + */ + protected void init() throws IOException { + if (internalIn2 != null) { + return; } - /** - * Get stream encoding or NULL if stream is uninitialized. Call init() or - * read() method to initialize it. - */ - public String getEncoding() { - return internalIn2.getEncoding(); + Charset encoding; + byte[] bom = new byte[BOM_SIZE]; + int n, unread; + n = internalIn.read(bom, 0, bom.length); + + if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) { + encoding = UTF8; + unread = n - 3; + } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) { + encoding = UTF16BE; + unread = n - 2; + } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) { + encoding = UTF16LE; + unread = n - 2; + } else { + // Unicode BOM mark not found, unread all bytes + encoding = UTF8; + unread = n; } - /** - * Read-ahead four bytes and check for BOM marks. Extra bytes are unread - * back to the stream, only BOM bytes are skipped. - */ - protected void init() throws IOException { - if (internalIn2 != null) - return; - - Charset encoding; - byte bom[] = new byte[BOM_SIZE]; - int n, unread; - n = internalIn.read(bom, 0, bom.length); - - if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) { - encoding = UTF8; - unread = n - 3; - } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) { - encoding = UTF16BE; - unread = n - 2; - } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) { - encoding = UTF16LE; - unread = n - 2; - } else { - // Unicode BOM mark not found, unread all bytes - encoding = UTF8; - unread = n; - } - - if (unread > 0) - internalIn.unread(bom, (n - unread), unread); - - // Use given encoding - CharsetDecoder decoder = encoding.newDecoder().onUnmappableCharacter( - CodingErrorAction.REPORT); - internalIn2 = new InputStreamReader(internalIn, decoder); + if (unread > 0) { + internalIn.unread(bom, (n - unread), unread); } - public void close() throws IOException { - init(); - internalIn2.close(); - } - - public int read(char[] cbuf, int off, int len) throws IOException { - init(); - return internalIn2.read(cbuf, off, len); - } -}
\ No newline at end of file + // Use given encoding + CharsetDecoder decoder = encoding.newDecoder().onUnmappableCharacter(CodingErrorAction.REPORT); + internalIn2 = new InputStreamReader(internalIn, decoder); + } + + public void close() throws IOException { + init(); + internalIn2.close(); + } + + public int read(char[] cbuf, int off, int len) throws IOException { + init(); + return internalIn2.read(cbuf, off, len); + } +} diff --git a/src/main/java/org/yaml/snakeyaml/representer/BaseRepresenter.java b/src/main/java/org/yaml/snakeyaml/representer/BaseRepresenter.java index c4419fb2..6257b4b6 100644 --- a/src/main/java/org/yaml/snakeyaml/representer/BaseRepresenter.java +++ b/src/main/java/org/yaml/snakeyaml/representer/BaseRepresenter.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.representer; @@ -21,7 +19,7 @@ import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions.FlowStyle; import org.yaml.snakeyaml.DumperOptions.ScalarStyle; import org.yaml.snakeyaml.introspector.PropertyUtils; @@ -37,166 +35,175 @@ import org.yaml.snakeyaml.nodes.Tag; * Represent basic YAML structures: scalar, sequence, mapping */ public abstract class BaseRepresenter { - protected final Map<Class<?>, Represent> representers = new HashMap<Class<?>, Represent>(); - /** - * in Java 'null' is not a type. So we have to keep the null representer - * separately otherwise it will coincide with the default representer which - * is stored with the key null. - */ - protected Represent nullRepresenter; - // the order is important (map can be also a sequence of key-values) - protected final Map<Class<?>, Represent> multiRepresenters = new LinkedHashMap<Class<?>, Represent>(); - protected Character defaultScalarStyle; - protected FlowStyle defaultFlowStyle = FlowStyle.AUTO; - protected final Map<Object, Node> representedObjects = new IdentityHashMap<Object, Node>() { - private static final long serialVersionUID = -5576159264232131854L; - - public Node put(Object key, Node value) { - return super.put(key, new AnchorNode(value)); - } - }; - protected Object objectToRepresent; - private PropertyUtils propertyUtils; - private boolean explicitPropertyUtils = false; - - public Node represent(Object data) { - Node node = representData(data); - representedObjects.clear(); - objectToRepresent = null; - return node; + protected final Map<Class<?>, Represent> representers = new HashMap<Class<?>, Represent>(); + /** + * in Java 'null' is not a type. So we have to keep the null representer separately otherwise it + * will coincide with the default representer which is stored with the key null. + */ + protected Represent nullRepresenter; + // the order is important (map can be also a sequence of key-values) + protected final Map<Class<?>, Represent> multiRepresenters = + new LinkedHashMap<Class<?>, Represent>(); + protected DumperOptions.ScalarStyle defaultScalarStyle = null; // not explicitly defined + protected FlowStyle defaultFlowStyle = FlowStyle.AUTO; + protected final Map<Object, Node> representedObjects = new IdentityHashMap<Object, Node>() { + private static final long serialVersionUID = -5576159264232131854L; + + public Node put(Object key, Node value) { + return super.put(key, new AnchorNode(value)); } - - protected final Node representData(Object data) { - objectToRepresent = data; - // check for identity - if (representedObjects.containsKey(objectToRepresent)) { - Node node = representedObjects.get(objectToRepresent); - return node; - } - // } - // check for null first - if (data == null) { - Node node = nullRepresenter.representData(null); - return node; - } - // check the same class - Node node; - Class<?> clazz = data.getClass(); - if (representers.containsKey(clazz)) { - Represent representer = representers.get(clazz); - node = representer.representData(data); - } else { - // check the parents - for (Class<?> repr : multiRepresenters.keySet()) { - if (repr.isInstance(data)) { - Represent representer = multiRepresenters.get(repr); - node = representer.representData(data); - return node; - } - } - - // check defaults - if (multiRepresenters.containsKey(null)) { - Represent representer = multiRepresenters.get(null); - node = representer.representData(data); - } else { - Represent representer = representers.get(null); - node = representer.representData(data); - } - } - return node; + }; + + protected Object objectToRepresent; + private PropertyUtils propertyUtils; + private boolean explicitPropertyUtils = false; + + public Node represent(Object data) { + Node node = representData(data); + representedObjects.clear(); + objectToRepresent = null; + return node; + } + + protected final Node representData(Object data) { + objectToRepresent = data; + // check for identity + if (representedObjects.containsKey(objectToRepresent)) { + Node node = representedObjects.get(objectToRepresent); + return node; } - - protected Node representScalar(Tag tag, String value, Character style) { - if (style == null) { - style = this.defaultScalarStyle; + // } + // check for null first + if (data == null) { + Node node = nullRepresenter.representData(null); + return node; + } + // check the same class + Node node; + Class<?> clazz = data.getClass(); + if (representers.containsKey(clazz)) { + Represent representer = representers.get(clazz); + node = representer.representData(data); + } else { + // check the parents + for (Class<?> repr : multiRepresenters.keySet()) { + if (repr != null && repr.isInstance(data)) { + Represent representer = multiRepresenters.get(repr); + node = representer.representData(data); + return node; } - Node node = new ScalarNode(tag, value, null, null, style); - return node; + } + + // check defaults + if (multiRepresenters.containsKey(null)) { + Represent representer = multiRepresenters.get(null); + node = representer.representData(data); + } else { + Represent representer = representers.get(null); + node = representer.representData(data); + } } + return node; + } - protected Node representScalar(Tag tag, String value) { - return representScalar(tag, value, null); + protected Node representScalar(Tag tag, String value, DumperOptions.ScalarStyle style) { + if (style == null) { + style = this.defaultScalarStyle; } - - protected Node representSequence(Tag tag, Iterable<?> sequence, Boolean flowStyle) { - int size = 10;// default for ArrayList - if (sequence instanceof List<?>) { - size = ((List<?>) sequence).size(); - } - List<Node> value = new ArrayList<Node>(size); - SequenceNode node = new SequenceNode(tag, value, flowStyle); - representedObjects.put(objectToRepresent, node); - boolean bestStyle = true; - for (Object item : sequence) { - Node nodeItem = representData(item); - if (!(nodeItem instanceof ScalarNode && ((ScalarNode) nodeItem).getStyle() == null)) { - bestStyle = false; - } - value.add(nodeItem); - } - if (flowStyle == null) { - if (defaultFlowStyle != FlowStyle.AUTO) { - node.setFlowStyle(defaultFlowStyle.getStyleBoolean()); - } else { - node.setFlowStyle(bestStyle); - } - } - return node; + Node node = new ScalarNode(tag, value, null, null, style); + return node; + } + + protected Node representScalar(Tag tag, String value) { + return representScalar(tag, value, null); + } + + protected Node representSequence(Tag tag, Iterable<?> sequence, + DumperOptions.FlowStyle flowStyle) { + int size = 10;// default for ArrayList + if (sequence instanceof List<?>) { + size = ((List<?>) sequence).size(); } - - protected Node representMapping(Tag tag, Map<?, ?> mapping, Boolean flowStyle) { - List<NodeTuple> value = new ArrayList<NodeTuple>(mapping.size()); - MappingNode node = new MappingNode(tag, value, flowStyle); - representedObjects.put(objectToRepresent, node); - boolean bestStyle = true; - for (Map.Entry<?, ?> entry : mapping.entrySet()) { - Node nodeKey = representData(entry.getKey()); - Node nodeValue = representData(entry.getValue()); - if (!(nodeKey instanceof ScalarNode && ((ScalarNode) nodeKey).getStyle() == null)) { - bestStyle = false; - } - if (!(nodeValue instanceof ScalarNode && ((ScalarNode) nodeValue).getStyle() == null)) { - bestStyle = false; - } - value.add(new NodeTuple(nodeKey, nodeValue)); - } - if (flowStyle == null) { - if (defaultFlowStyle != FlowStyle.AUTO) { - node.setFlowStyle(defaultFlowStyle.getStyleBoolean()); - } else { - node.setFlowStyle(bestStyle); - } - } - return node; + List<Node> value = new ArrayList<Node>(size); + SequenceNode node = new SequenceNode(tag, value, flowStyle); + representedObjects.put(objectToRepresent, node); + DumperOptions.FlowStyle bestStyle = FlowStyle.FLOW; + for (Object item : sequence) { + Node nodeItem = representData(item); + if (!(nodeItem instanceof ScalarNode && ((ScalarNode) nodeItem).isPlain())) { + bestStyle = FlowStyle.BLOCK; + } + value.add(nodeItem); } - - public void setDefaultScalarStyle(ScalarStyle defaultStyle) { - this.defaultScalarStyle = defaultStyle.getChar(); + if (flowStyle == FlowStyle.AUTO) { + if (defaultFlowStyle != FlowStyle.AUTO) { + node.setFlowStyle(defaultFlowStyle); + } else { + node.setFlowStyle(bestStyle); + } } - - public void setDefaultFlowStyle(FlowStyle defaultFlowStyle) { - this.defaultFlowStyle = defaultFlowStyle; + return node; + } + + protected Node representMapping(Tag tag, Map<?, ?> mapping, DumperOptions.FlowStyle flowStyle) { + List<NodeTuple> value = new ArrayList<NodeTuple>(mapping.size()); + MappingNode node = new MappingNode(tag, value, flowStyle); + representedObjects.put(objectToRepresent, node); + DumperOptions.FlowStyle bestStyle = FlowStyle.FLOW; + for (Map.Entry<?, ?> entry : mapping.entrySet()) { + Node nodeKey = representData(entry.getKey()); + Node nodeValue = representData(entry.getValue()); + if (!(nodeKey instanceof ScalarNode && ((ScalarNode) nodeKey).isPlain())) { + bestStyle = FlowStyle.BLOCK; + } + if (!(nodeValue instanceof ScalarNode && ((ScalarNode) nodeValue).isPlain())) { + bestStyle = FlowStyle.BLOCK; + } + value.add(new NodeTuple(nodeKey, nodeValue)); } - - public FlowStyle getDefaultFlowStyle() { - return this.defaultFlowStyle; + if (flowStyle == FlowStyle.AUTO) { + if (defaultFlowStyle != FlowStyle.AUTO) { + node.setFlowStyle(defaultFlowStyle); + } else { + node.setFlowStyle(bestStyle); + } } + return node; + } - public void setPropertyUtils(PropertyUtils propertyUtils) { - this.propertyUtils = propertyUtils; - this.explicitPropertyUtils = true; - } + public void setDefaultScalarStyle(ScalarStyle defaultStyle) { + this.defaultScalarStyle = defaultStyle; + } - public final PropertyUtils getPropertyUtils() { - if (propertyUtils == null) { - propertyUtils = new PropertyUtils(); - } - return propertyUtils; + public ScalarStyle getDefaultScalarStyle() { + if (defaultScalarStyle == null) { + return ScalarStyle.PLAIN; } + return defaultScalarStyle; + } + + public void setDefaultFlowStyle(FlowStyle defaultFlowStyle) { + this.defaultFlowStyle = defaultFlowStyle; + } - public final boolean isExplicitPropertyUtils() { - return explicitPropertyUtils; + public FlowStyle getDefaultFlowStyle() { + return this.defaultFlowStyle; + } + + public void setPropertyUtils(PropertyUtils propertyUtils) { + this.propertyUtils = propertyUtils; + this.explicitPropertyUtils = true; + } + + public final PropertyUtils getPropertyUtils() { + if (propertyUtils == null) { + propertyUtils = new PropertyUtils(); } + return propertyUtils; + } + + public final boolean isExplicitPropertyUtils() { + return explicitPropertyUtils; + } } diff --git a/src/main/java/org/yaml/snakeyaml/representer/Represent.java b/src/main/java/org/yaml/snakeyaml/representer/Represent.java index 55629ed4..0b0d6ef1 100644 --- a/src/main/java/org/yaml/snakeyaml/representer/Represent.java +++ b/src/main/java/org/yaml/snakeyaml/representer/Represent.java @@ -1,36 +1,32 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.representer; import org.yaml.snakeyaml.nodes.Node; /** - * Create a Node Graph out of the provided Native Data Structure (Java - * instance). - * - * @see <a href="http://yaml.org/spec/1.1/#id859109">Chapter 3. Processing YAML - * Information</a> + * Create a Node Graph out of the provided Native Data Structure (Java instance). + * + * @see <a href="http://yaml.org/spec/1.1/#id859109">Chapter 3. Processing YAML Information</a> */ public interface Represent { - /** - * Create a Node - * - * @param data - * the instance to represent - * @return Node to dump - */ - Node representData(Object data); + + /** + * Create a Node + * + * @param data the instance to represent + * @return Node to dump + */ + Node representData(Object data); } diff --git a/src/main/java/org/yaml/snakeyaml/representer/Representer.java b/src/main/java/org/yaml/snakeyaml/representer/Representer.java index e83a5ea2..5376b6e2 100644 --- a/src/main/java/org/yaml/snakeyaml/representer/Representer.java +++ b/src/main/java/org/yaml/snakeyaml/representer/Representer.java @@ -1,32 +1,32 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.representer; -import java.beans.IntrospectionException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; - +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions.FlowStyle; -import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.TypeDescription; import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.introspector.PropertyUtils; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeId; @@ -40,209 +40,225 @@ import org.yaml.snakeyaml.nodes.Tag; */ public class Representer extends SafeRepresenter { - public Representer() { - this.representers.put(null, new RepresentJavaBean()); + protected Map<Class<? extends Object>, TypeDescription> typeDefinitions = Collections.emptyMap(); + + public Representer() { + this.representers.put(null, new RepresentJavaBean()); + } + + public Representer(DumperOptions options) { + super(options); + this.representers.put(null, new RepresentJavaBean()); + } + + public TypeDescription addTypeDescription(TypeDescription td) { + if (Collections.EMPTY_MAP == typeDefinitions) { + typeDefinitions = new HashMap<Class<? extends Object>, TypeDescription>(); } + if (td.getTag() != null) { + addClassTag(td.getType(), td.getTag()); + } + td.setPropertyUtils(getPropertyUtils()); + return typeDefinitions.put(td.getType(), td); + } - protected class RepresentJavaBean implements Represent { - public Node representData(Object data) { - try { - return representJavaBean(getProperties(data.getClass()), data); - } catch (IntrospectionException e) { - throw new YAMLException(e); - } - } + @Override + public void setPropertyUtils(PropertyUtils propertyUtils) { + super.setPropertyUtils(propertyUtils); + Collection<TypeDescription> tds = typeDefinitions.values(); + for (TypeDescription typeDescription : tds) { + typeDescription.setPropertyUtils(propertyUtils); } + } - /** - * Tag logic:<br/> - * - explicit root tag is set in serializer <br/> - * - if there is a predefined class tag it is used<br/> - * - a global tag with class name is always used as tag. The JavaBean parent - * of the specified JavaBean may set another tag (tag:yaml.org,2002:map) - * when the property class is the same as runtime class - * - * @param properties - * JavaBean getters - * @param javaBean - * instance for Node - * @return Node to get serialized - */ - protected MappingNode representJavaBean(Set<Property> properties, Object javaBean) { - List<NodeTuple> value = new ArrayList<NodeTuple>(properties.size()); - Tag tag; - Tag customTag = classTags.get(javaBean.getClass()); - tag = customTag != null ? customTag : new Tag(javaBean.getClass()); - // flow style will be chosen by BaseRepresenter - MappingNode node = new MappingNode(tag, value, null); - representedObjects.put(javaBean, node); - boolean bestStyle = true; - for (Property property : properties) { - Object memberValue = property.get(javaBean); - Tag customPropertyTag = memberValue == null ? null : classTags.get(memberValue - .getClass()); - NodeTuple tuple = representJavaBeanProperty(javaBean, property, memberValue, - customPropertyTag); - if (tuple == null) { - continue; - } - if (((ScalarNode) tuple.getKeyNode()).getStyle() != null) { - bestStyle = false; - } - Node nodeValue = tuple.getValueNode(); - if (!(nodeValue instanceof ScalarNode && ((ScalarNode) nodeValue).getStyle() == null)) { - bestStyle = false; - } - value.add(tuple); - } - if (defaultFlowStyle != FlowStyle.AUTO) { - node.setFlowStyle(defaultFlowStyle.getStyleBoolean()); - } else { - node.setFlowStyle(bestStyle); - } - return node; + protected class RepresentJavaBean implements Represent { + + public Node representData(Object data) { + return representJavaBean(getProperties(data.getClass()), data); + } + } + + /** + * Tag logic: - explicit root tag is set in serializer - if there is a predefined class tag it is + * used - a global tag with class name is always used as tag. The JavaBean parent of the specified + * JavaBean may set another tag (tag:yaml.org,2002:map) when the property class is the same as + * runtime class + * + * @param properties JavaBean getters + * @param javaBean instance for Node + * @return Node to get serialized + */ + protected MappingNode representJavaBean(Set<Property> properties, Object javaBean) { + List<NodeTuple> value = new ArrayList<NodeTuple>(properties.size()); + Tag tag; + Tag customTag = classTags.get(javaBean.getClass()); + tag = customTag != null ? customTag : new Tag(javaBean.getClass()); + // flow style will be chosen by BaseRepresenter + MappingNode node = new MappingNode(tag, value, FlowStyle.AUTO); + representedObjects.put(javaBean, node); + DumperOptions.FlowStyle bestStyle = FlowStyle.FLOW; + for (Property property : properties) { + Object memberValue = property.get(javaBean); + Tag customPropertyTag = memberValue == null ? null : classTags.get(memberValue.getClass()); + NodeTuple tuple = + representJavaBeanProperty(javaBean, property, memberValue, customPropertyTag); + if (tuple == null) { + continue; + } + if (!((ScalarNode) tuple.getKeyNode()).isPlain()) { + bestStyle = FlowStyle.BLOCK; + } + Node nodeValue = tuple.getValueNode(); + if (!(nodeValue instanceof ScalarNode && ((ScalarNode) nodeValue).isPlain())) { + bestStyle = FlowStyle.BLOCK; + } + value.add(tuple); } + if (defaultFlowStyle != FlowStyle.AUTO) { + node.setFlowStyle(defaultFlowStyle); + } else { + node.setFlowStyle(bestStyle); + } + return node; + } + + /** + * Represent one JavaBean property. + * + * @param javaBean - the instance to be represented + * @param property - the property of the instance + * @param propertyValue - value to be represented + * @param customTag - user defined Tag + * @return NodeTuple to be used in a MappingNode. Return null to skip the property + */ + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, + Object propertyValue, Tag customTag) { + ScalarNode nodeKey = (ScalarNode) representData(property.getName()); + // the first occurrence of the node must keep the tag + boolean hasAlias = this.representedObjects.containsKey(propertyValue); - /** - * Represent one JavaBean property. - * - * @param javaBean - * - the instance to be represented - * @param property - * - the property of the instance - * @param propertyValue - * - value to be represented - * @param customTag - * - user defined Tag - * @return NodeTuple to be used in a MappingNode. Return null to skip the - * property - */ - protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, - Object propertyValue, Tag customTag) { - ScalarNode nodeKey = (ScalarNode) representData(property.getName()); - // the first occurrence of the node must keep the tag - boolean hasAlias = this.representedObjects.containsKey(propertyValue); - - Node nodeValue = representData(propertyValue); - - if (propertyValue != null && !hasAlias) { - NodeId nodeId = nodeValue.getNodeId(); - if (customTag == null) { - if (nodeId == NodeId.scalar) { - if (propertyValue instanceof Enum<?>) { - nodeValue.setTag(Tag.STR); - } - } else { - if (nodeId == NodeId.mapping) { - if (property.getType() == propertyValue.getClass()) { - if (!(propertyValue instanceof Map<?, ?>)) { - if (!nodeValue.getTag().equals(Tag.SET)) { - nodeValue.setTag(Tag.MAP); - } - } - } - } - checkGlobalTag(property, nodeValue, propertyValue); + Node nodeValue = representData(propertyValue); + + if (propertyValue != null && !hasAlias) { + NodeId nodeId = nodeValue.getNodeId(); + if (customTag == null) { + if (nodeId == NodeId.scalar) { + // generic Enum requires the full tag + if (property.getType() != java.lang.Enum.class) { + if (propertyValue instanceof Enum<?>) { + nodeValue.setTag(Tag.STR); + } + } + } else { + if (nodeId == NodeId.mapping) { + if (property.getType() == propertyValue.getClass()) { + if (!(propertyValue instanceof Map<?, ?>)) { + if (!nodeValue.getTag().equals(Tag.SET)) { + nodeValue.setTag(Tag.MAP); } + } } + } + checkGlobalTag(property, nodeValue, propertyValue); } + } + } - return new NodeTuple(nodeKey, nodeValue); + return new NodeTuple(nodeKey, nodeValue); + } + + /** + * Remove redundant global tag for a type safe (generic) collection if it is the same as defined + * by the JavaBean property + * + * @param property - JavaBean property + * @param node - representation of the property + * @param object - instance represented by the node + */ + @SuppressWarnings("unchecked") + protected void checkGlobalTag(Property property, Node node, Object object) { + // Skip primitive arrays. + if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) { + return; } - /** - * Remove redundant global tag for a type safe (generic) collection if it is - * the same as defined by the JavaBean property - * - * @param property - * - JavaBean property - * @param node - * - representation of the property - * @param object - * - instance represented by the node - */ - @SuppressWarnings("unchecked") - protected void checkGlobalTag(Property property, Node node, Object object) { - // Skip primitive arrays. - if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) { - return; + Class<?>[] arguments = property.getActualTypeArguments(); + if (arguments != null) { + if (node.getNodeId() == NodeId.sequence) { + // apply map tag where class is the same + Class<? extends Object> t = arguments[0]; + SequenceNode snode = (SequenceNode) node; + Iterable<Object> memberList = Collections.EMPTY_LIST; + if (object.getClass().isArray()) { + memberList = Arrays.asList((Object[]) object); + } else if (object instanceof Iterable<?>) { + // list + memberList = (Iterable<Object>) object; } - - Class<?>[] arguments = property.getActualTypeArguments(); - if (arguments != null) { - if (node.getNodeId() == NodeId.sequence) { - // apply map tag where class is the same - Class<? extends Object> t = arguments[0]; - SequenceNode snode = (SequenceNode) node; - Iterable<Object> memberList = Collections.EMPTY_LIST; - if (object.getClass().isArray()) { - memberList = Arrays.asList((Object[]) object); - } else if (object instanceof Iterable<?>) { - // list - memberList = (Iterable<Object>) object; + Iterator<Object> iter = memberList.iterator(); + if (iter.hasNext()) { + for (Node childNode : snode.getValue()) { + Object member = iter.next(); + if (member != null) { + if (t.equals(member.getClass())) { + if (childNode.getNodeId() == NodeId.mapping) { + childNode.setTag(Tag.MAP); } - Iterator<Object> iter = memberList.iterator(); - if (iter.hasNext()) { - for (Node childNode : snode.getValue()) { - Object member = iter.next(); - if (member != null) { - if (t.equals(member.getClass())) - if (childNode.getNodeId() == NodeId.mapping) { - childNode.setTag(Tag.MAP); - } - } - } - } - } else if (object instanceof Set) { - Class<?> t = arguments[0]; - MappingNode mnode = (MappingNode) node; - Iterator<NodeTuple> iter = mnode.getValue().iterator(); - Set<?> set = (Set<?>) object; - for (Object member : set) { - NodeTuple tuple = iter.next(); - Node keyNode = tuple.getKeyNode(); - if (t.equals(member.getClass())) { - if (keyNode.getNodeId() == NodeId.mapping) { - keyNode.setTag(Tag.MAP); - } - } - } - } else if (object instanceof Map) { - Class<?> keyType = arguments[0]; - Class<?> valueType = arguments[1]; - MappingNode mnode = (MappingNode) node; - for (NodeTuple tuple : mnode.getValue()) { - resetTag(keyType, tuple.getKeyNode()); - resetTag(valueType, tuple.getValueNode()); - } - } else { - // the type for collection entries cannot be - // detected + } } + } } - } - - private void resetTag(Class<? extends Object> type, Node node) { - Tag tag = node.getTag(); - if (tag.matches(type)) { - if (Enum.class.isAssignableFrom(type)) { - node.setTag(Tag.STR); - } else { - node.setTag(Tag.MAP); + } else if (object instanceof Set) { + Class<?> t = arguments[0]; + MappingNode mnode = (MappingNode) node; + Iterator<NodeTuple> iter = mnode.getValue().iterator(); + Set<?> set = (Set<?>) object; + for (Object member : set) { + NodeTuple tuple = iter.next(); + Node keyNode = tuple.getKeyNode(); + if (t.equals(member.getClass())) { + if (keyNode.getNodeId() == NodeId.mapping) { + keyNode.setTag(Tag.MAP); } + } + } + } else if (object instanceof Map) { // NodeId.mapping ends-up here + Class<?> keyType = arguments[0]; + Class<?> valueType = arguments[1]; + MappingNode mnode = (MappingNode) node; + for (NodeTuple tuple : mnode.getValue()) { + resetTag(keyType, tuple.getKeyNode()); + resetTag(valueType, tuple.getValueNode()); } + } else { + // the type for collection entries cannot be + // detected + } + } + } + + private void resetTag(Class<? extends Object> type, Node node) { + Tag tag = node.getTag(); + if (tag.matches(type)) { + if (Enum.class.isAssignableFrom(type)) { + node.setTag(Tag.STR); + } else { + node.setTag(Tag.MAP); + } } + } - /** - * Get JavaBean properties to be serialised. The order is respected. This - * method may be overridden to provide custom property selection or order. - * - * @param type - * - JavaBean to inspect the properties - * @return properties to serialise - */ - protected Set<Property> getProperties(Class<? extends Object> type) - throws IntrospectionException { - return getPropertyUtils().getProperties(type); + /** + * Get JavaBean properties to be serialised. The order is respected. This method may be overridden + * to provide custom property selection or order. + * + * @param type - JavaBean to inspect the properties + * @return properties to serialise + */ + protected Set<Property> getProperties(Class<? extends Object> type) { + if (typeDefinitions.containsKey(type)) { + return typeDefinitions.get(type).getProperties(); } + return getPropertyUtils().getProperties(type); + } } diff --git a/src/main/java/org/yaml/snakeyaml/representer/SafeRepresenter.java b/src/main/java/org/yaml/snakeyaml/representer/SafeRepresenter.java index 147e3af5..b46e07fe 100644 --- a/src/main/java/org/yaml/snakeyaml/representer/SafeRepresenter.java +++ b/src/main/java/org/yaml/snakeyaml/representer/SafeRepresenter.java @@ -1,22 +1,20 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.representer; -import java.io.UnsupportedEncodingException; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -30,7 +28,7 @@ import java.util.Set; import java.util.TimeZone; import java.util.UUID; import java.util.regex.Pattern; - +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; import org.yaml.snakeyaml.nodes.Node; @@ -42,385 +40,432 @@ import org.yaml.snakeyaml.reader.StreamReader; */ class SafeRepresenter extends BaseRepresenter { - protected Map<Class<? extends Object>, Tag> classTags; - protected TimeZone timeZone = null; - - public SafeRepresenter() { - this.nullRepresenter = new RepresentNull(); - this.representers.put(String.class, new RepresentString()); - this.representers.put(Boolean.class, new RepresentBoolean()); - this.representers.put(Character.class, new RepresentString()); - this.representers.put(UUID.class, new RepresentUuid()); - this.representers.put(byte[].class, new RepresentByteArray()); - - Represent primitiveArray = new RepresentPrimitiveArray(); - representers.put(short[].class, primitiveArray); - representers.put(int[].class, primitiveArray); - representers.put(long[].class, primitiveArray); - representers.put(float[].class, primitiveArray); - representers.put(double[].class, primitiveArray); - representers.put(char[].class, primitiveArray); - representers.put(boolean[].class, primitiveArray); - - this.multiRepresenters.put(Number.class, new RepresentNumber()); - this.multiRepresenters.put(List.class, new RepresentList()); - this.multiRepresenters.put(Map.class, new RepresentMap()); - this.multiRepresenters.put(Set.class, new RepresentSet()); - this.multiRepresenters.put(Iterator.class, new RepresentIterator()); - this.multiRepresenters.put(new Object[0].getClass(), new RepresentArray()); - this.multiRepresenters.put(Date.class, new RepresentDate()); - this.multiRepresenters.put(Enum.class, new RepresentEnum()); - this.multiRepresenters.put(Calendar.class, new RepresentDate()); - classTags = new HashMap<Class<? extends Object>, Tag>(); + protected Map<Class<? extends Object>, Tag> classTags; + protected TimeZone timeZone = null; + protected DumperOptions.NonPrintableStyle nonPrintableStyle; + + public SafeRepresenter() { + this(new DumperOptions()); + } + + public SafeRepresenter(DumperOptions options) { + this.nullRepresenter = new RepresentNull(); + this.representers.put(String.class, new RepresentString()); + this.representers.put(Boolean.class, new RepresentBoolean()); + this.representers.put(Character.class, new RepresentString()); + this.representers.put(UUID.class, new RepresentUuid()); + this.representers.put(byte[].class, new RepresentByteArray()); + + Represent primitiveArray = new RepresentPrimitiveArray(); + representers.put(short[].class, primitiveArray); + representers.put(int[].class, primitiveArray); + representers.put(long[].class, primitiveArray); + representers.put(float[].class, primitiveArray); + representers.put(double[].class, primitiveArray); + representers.put(char[].class, primitiveArray); + representers.put(boolean[].class, primitiveArray); + + this.multiRepresenters.put(Number.class, new RepresentNumber()); + this.multiRepresenters.put(List.class, new RepresentList()); + this.multiRepresenters.put(Map.class, new RepresentMap()); + this.multiRepresenters.put(Set.class, new RepresentSet()); + this.multiRepresenters.put(Iterator.class, new RepresentIterator()); + this.multiRepresenters.put(new Object[0].getClass(), new RepresentArray()); + this.multiRepresenters.put(Date.class, new RepresentDate()); + this.multiRepresenters.put(Enum.class, new RepresentEnum()); + this.multiRepresenters.put(Calendar.class, new RepresentDate()); + classTags = new HashMap<Class<? extends Object>, Tag>(); + this.nonPrintableStyle = options.getNonPrintableStyle(); + } + + protected Tag getTag(Class<?> clazz, Tag defaultTag) { + if (classTags.containsKey(clazz)) { + return classTags.get(clazz); + } else { + return defaultTag; + } + } + + /** + * Define a tag for the <code>Class</code> to serialize. + * + * @param clazz <code>Class</code> which tag is changed + * @param tag new tag to be used for every instance of the specified <code>Class</code> + * @return the previous tag associated with the <code>Class</code> + */ + public Tag addClassTag(Class<? extends Object> clazz, Tag tag) { + if (tag == null) { + throw new NullPointerException("Tag must be provided."); } + return classTags.put(clazz, tag); + } - protected Tag getTag(Class<?> clazz, Tag defaultTag) { - if (classTags.containsKey(clazz)) { - return classTags.get(clazz); - } else { - return defaultTag; + protected class RepresentNull implements Represent { + + public Node representData(Object data) { + return representScalar(Tag.NULL, "null"); + } + } + + private static final Pattern MULTILINE_PATTERN = Pattern.compile("\n|\u0085|\u2028|\u2029"); + + protected class RepresentString implements Represent { + + public Node representData(Object data) { + Tag tag = Tag.STR; + DumperOptions.ScalarStyle style = null;// not defined + String value = data.toString(); + if (nonPrintableStyle == DumperOptions.NonPrintableStyle.BINARY + && !StreamReader.isPrintable(value)) { + tag = Tag.BINARY; + char[] binary; + final byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + // sometimes above will just silently fail - it will return incomplete data + // it happens when String has invalid code points + // (for example half surrogate character without other half) + final String checkValue = new String(bytes, StandardCharsets.UTF_8); + if (!checkValue.equals(value)) { + throw new YAMLException("invalid string value has occurred"); } + binary = Base64Coder.encode(bytes); + value = String.valueOf(binary); + style = DumperOptions.ScalarStyle.LITERAL; + } + // if no other scalar style is explicitly set, use literal style for + // multiline scalars + if (defaultScalarStyle == DumperOptions.ScalarStyle.PLAIN + && MULTILINE_PATTERN.matcher(value).find()) { + style = DumperOptions.ScalarStyle.LITERAL; + } + return representScalar(tag, value, style); } - - /** - * Define a tag for the <code>Class</code> to serialize. - * - * @param clazz - * <code>Class</code> which tag is changed - * @param tag - * new tag to be used for every instance of the specified - * <code>Class</code> - * @return the previous tag associated with the <code>Class</code> - */ - public Tag addClassTag(Class<? extends Object> clazz, Tag tag) { - if (tag == null) { - throw new NullPointerException("Tag must be provided."); + } + + protected class RepresentBoolean implements Represent { + + public Node representData(Object data) { + String value; + if (Boolean.TRUE.equals(data)) { + value = "true"; + } else { + value = "false"; + } + return representScalar(Tag.BOOL, value); + } + } + + protected class RepresentNumber implements Represent { + + public Node representData(Object data) { + Tag tag; + String value; + if (data instanceof Byte || data instanceof Short || data instanceof Integer + || data instanceof Long || data instanceof BigInteger) { + tag = Tag.INT; + value = data.toString(); + } else { + Number number = (Number) data; + tag = Tag.FLOAT; + if (number.equals(Double.NaN)) { + value = ".NaN"; + } else if (number.equals(Double.POSITIVE_INFINITY)) { + value = ".inf"; + } else if (number.equals(Double.NEGATIVE_INFINITY)) { + value = "-.inf"; + } else { + value = number.toString(); } - return classTags.put(clazz, tag); + } + return representScalar(getTag(data.getClass(), tag), value); } + } - protected class RepresentNull implements Represent { - public Node representData(Object data) { - return representScalar(Tag.NULL, "null"); - } + protected class RepresentList implements Represent { + + @SuppressWarnings("unchecked") + public Node representData(Object data) { + return representSequence(getTag(data.getClass(), Tag.SEQ), (List<Object>) data, + DumperOptions.FlowStyle.AUTO); } + } - public static Pattern MULTILINE_PATTERN = Pattern.compile("\n|\u0085|\u2028|\u2029"); - - protected class RepresentString implements Represent { - public Node representData(Object data) { - Tag tag = Tag.STR; - Character style = null; - String value = data.toString(); - if (StreamReader.NON_PRINTABLE.matcher(value).find()) { - tag = Tag.BINARY; - char[] binary; - try { - binary = Base64Coder.encode(value.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new YAMLException(e); - } - value = String.valueOf(binary); - style = '|'; - } - // if no other scalar style is explicitly set, use literal style for - // multiline scalars - if (defaultScalarStyle == null && MULTILINE_PATTERN.matcher(value).find()) { - style = '|'; - } - return representScalar(tag, value, style); - } + protected class RepresentIterator implements Represent { + + @SuppressWarnings("unchecked") + public Node representData(Object data) { + Iterator<Object> iter = (Iterator<Object>) data; + return representSequence(getTag(data.getClass(), Tag.SEQ), new IteratorWrapper(iter), + DumperOptions.FlowStyle.AUTO); } + } - protected class RepresentBoolean implements Represent { - public Node representData(Object data) { - String value; - if (Boolean.TRUE.equals(data)) { - value = "true"; - } else { - value = "false"; - } - return representScalar(Tag.BOOL, value); - } + private static class IteratorWrapper implements Iterable<Object> { + + private final Iterator<Object> iter; + + public IteratorWrapper(Iterator<Object> iter) { + this.iter = iter; } - protected class RepresentNumber implements Represent { - public Node representData(Object data) { - Tag tag; - String value; - if (data instanceof Byte || data instanceof Short || data instanceof Integer - || data instanceof Long || data instanceof BigInteger) { - tag = Tag.INT; - value = data.toString(); - } else { - Number number = (Number) data; - tag = Tag.FLOAT; - if (number.equals(Double.NaN)) { - value = ".NaN"; - } else if (number.equals(Double.POSITIVE_INFINITY)) { - value = ".inf"; - } else if (number.equals(Double.NEGATIVE_INFINITY)) { - value = "-.inf"; - } else { - value = number.toString(); - } - } - return representScalar(getTag(data.getClass(), tag), value); - } + public Iterator<Object> iterator() { + return iter; } + } - protected class RepresentList implements Represent { - @SuppressWarnings("unchecked") - public Node representData(Object data) { - return representSequence(getTag(data.getClass(), Tag.SEQ), (List<Object>) data, null); - } + protected class RepresentArray implements Represent { + + public Node representData(Object data) { + Object[] array = (Object[]) data; + List<Object> list = Arrays.asList(array); + return representSequence(Tag.SEQ, list, DumperOptions.FlowStyle.AUTO); + } + } + + /** + * Represents primitive arrays, such as short[] and float[], by converting them into equivalent + * List<Short> and List<Float> using the appropriate autoboxing type. + */ + protected class RepresentPrimitiveArray implements Represent { + + public Node representData(Object data) { + Class<?> type = data.getClass().getComponentType(); + + if (byte.class == type) { + return representSequence(Tag.SEQ, asByteList(data), DumperOptions.FlowStyle.AUTO); + } else if (short.class == type) { + return representSequence(Tag.SEQ, asShortList(data), DumperOptions.FlowStyle.AUTO); + } else if (int.class == type) { + return representSequence(Tag.SEQ, asIntList(data), DumperOptions.FlowStyle.AUTO); + } else if (long.class == type) { + return representSequence(Tag.SEQ, asLongList(data), DumperOptions.FlowStyle.AUTO); + } else if (float.class == type) { + return representSequence(Tag.SEQ, asFloatList(data), DumperOptions.FlowStyle.AUTO); + } else if (double.class == type) { + return representSequence(Tag.SEQ, asDoubleList(data), DumperOptions.FlowStyle.AUTO); + } else if (char.class == type) { + return representSequence(Tag.SEQ, asCharList(data), DumperOptions.FlowStyle.AUTO); + } else if (boolean.class == type) { + return representSequence(Tag.SEQ, asBooleanList(data), DumperOptions.FlowStyle.AUTO); + } + + throw new YAMLException("Unexpected primitive '" + type.getCanonicalName() + "'"); } - protected class RepresentIterator implements Represent { - @SuppressWarnings("unchecked") - public Node representData(Object data) { - Iterator<Object> iter = (Iterator<Object>) data; - return representSequence(getTag(data.getClass(), Tag.SEQ), new IteratorWrapper(iter), - null); - } + private List<Byte> asByteList(Object in) { + byte[] array = (byte[]) in; + List<Byte> list = new ArrayList<Byte>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; } - private static class IteratorWrapper implements Iterable<Object> { - private Iterator<Object> iter; + private List<Short> asShortList(Object in) { + short[] array = (short[]) in; + List<Short> list = new ArrayList<Short>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; + } - public IteratorWrapper(Iterator<Object> iter) { - this.iter = iter; - } + private List<Integer> asIntList(Object in) { + int[] array = (int[]) in; + List<Integer> list = new ArrayList<Integer>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; + } - public Iterator<Object> iterator() { - return iter; - } + private List<Long> asLongList(Object in) { + long[] array = (long[]) in; + List<Long> list = new ArrayList<Long>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; } - protected class RepresentArray implements Represent { - public Node representData(Object data) { - Object[] array = (Object[]) data; - List<Object> list = Arrays.asList(array); - return representSequence(Tag.SEQ, list, null); - } + private List<Float> asFloatList(Object in) { + float[] array = (float[]) in; + List<Float> list = new ArrayList<Float>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; } - /** - * Represents primitive arrays, such as short[] and float[], by converting - * them into equivalent List<Short> and List<Float> using the appropriate - * autoboxing type. - */ - protected class RepresentPrimitiveArray implements Represent { - public Node representData(Object data) { - Class<?> type = data.getClass().getComponentType(); - - if (byte.class == type) { - return representSequence(Tag.SEQ, asByteList(data), null); - } else if (short.class == type) { - return representSequence(Tag.SEQ, asShortList(data), null); - } else if (int.class == type) { - return representSequence(Tag.SEQ, asIntList(data), null); - } else if (long.class == type) { - return representSequence(Tag.SEQ, asLongList(data), null); - } else if (float.class == type) { - return representSequence(Tag.SEQ, asFloatList(data), null); - } else if (double.class == type) { - return representSequence(Tag.SEQ, asDoubleList(data), null); - } else if (char.class == type) { - return representSequence(Tag.SEQ, asCharList(data), null); - } else if (boolean.class == type) { - return representSequence(Tag.SEQ, asBooleanList(data), null); - } - - throw new YAMLException("Unexpected primitive '" + type.getCanonicalName() + "'"); - } + private List<Double> asDoubleList(Object in) { + double[] array = (double[]) in; + List<Double> list = new ArrayList<Double>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; + } - private List<Byte> asByteList(Object in) { - byte[] array = (byte[]) in; - List<Byte> list = new ArrayList<Byte>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; - } + private List<Character> asCharList(Object in) { + char[] array = (char[]) in; + List<Character> list = new ArrayList<Character>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; + } - private List<Short> asShortList(Object in) { - short[] array = (short[]) in; - List<Short> list = new ArrayList<Short>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; - } + private List<Boolean> asBooleanList(Object in) { + boolean[] array = (boolean[]) in; + List<Boolean> list = new ArrayList<Boolean>(array.length); + for (int i = 0; i < array.length; ++i) { + list.add(array[i]); + } + return list; + } + } - private List<Integer> asIntList(Object in) { - int[] array = (int[]) in; - List<Integer> list = new ArrayList<Integer>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; - } + protected class RepresentMap implements Represent { - private List<Long> asLongList(Object in) { - long[] array = (long[]) in; - List<Long> list = new ArrayList<Long>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; + @SuppressWarnings("unchecked") + public Node representData(Object data) { + return representMapping(getTag(data.getClass(), Tag.MAP), (Map<Object, Object>) data, + DumperOptions.FlowStyle.AUTO); + } + } + + protected class RepresentSet implements Represent { + + @SuppressWarnings("unchecked") + public Node representData(Object data) { + Map<Object, Object> value = new LinkedHashMap<Object, Object>(); + Set<Object> set = (Set<Object>) data; + for (Object key : set) { + value.put(key, null); + } + return representMapping(getTag(data.getClass(), Tag.SET), value, + DumperOptions.FlowStyle.AUTO); + } + } + + protected class RepresentDate implements Represent { + + public Node representData(Object data) { + // because SimpleDateFormat ignores timezone we have to use Calendar + Calendar calendar; + if (data instanceof Calendar) { + calendar = (Calendar) data; + } else { + calendar = + Calendar.getInstance(getTimeZone() == null ? TimeZone.getTimeZone("UTC") : timeZone); + calendar.setTime((Date) data); + } + int years = calendar.get(Calendar.YEAR); + int months = calendar.get(Calendar.MONTH) + 1; // 0..12 + int days = calendar.get(Calendar.DAY_OF_MONTH); // 1..31 + int hour24 = calendar.get(Calendar.HOUR_OF_DAY); // 0..24 + int minutes = calendar.get(Calendar.MINUTE); // 0..59 + int seconds = calendar.get(Calendar.SECOND); // 0..59 + int millis = calendar.get(Calendar.MILLISECOND); + StringBuilder buffer = new StringBuilder(String.valueOf(years)); + while (buffer.length() < 4) { + // ancient years + buffer.insert(0, "0"); + } + buffer.append("-"); + if (months < 10) { + buffer.append("0"); + } + buffer.append(months); + buffer.append("-"); + if (days < 10) { + buffer.append("0"); + } + buffer.append(days); + buffer.append("T"); + if (hour24 < 10) { + buffer.append("0"); + } + buffer.append(hour24); + buffer.append(":"); + if (minutes < 10) { + buffer.append("0"); + } + buffer.append(minutes); + buffer.append(":"); + if (seconds < 10) { + buffer.append("0"); + } + buffer.append(seconds); + if (millis > 0) { + if (millis < 10) { + buffer.append(".00"); + } else if (millis < 100) { + buffer.append(".0"); + } else { + buffer.append("."); } - - private List<Float> asFloatList(Object in) { - float[] array = (float[]) in; - List<Float> list = new ArrayList<Float>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; + buffer.append(millis); + } + + // Get the offset from GMT taking DST into account + int gmtOffset = calendar.getTimeZone().getOffset(calendar.getTime().getTime()); + if (gmtOffset == 0) { + buffer.append('Z'); + } else { + if (gmtOffset < 0) { + buffer.append('-'); + gmtOffset *= -1; + } else { + buffer.append('+'); } + int minutesOffset = gmtOffset / (60 * 1000); + int hoursOffset = minutesOffset / 60; + int partOfHour = minutesOffset % 60; - private List<Double> asDoubleList(Object in) { - double[] array = (double[]) in; - List<Double> list = new ArrayList<Double>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; + if (hoursOffset < 10) { + buffer.append('0'); } - - private List<Character> asCharList(Object in) { - char[] array = (char[]) in; - List<Character> list = new ArrayList<Character>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; + buffer.append(hoursOffset); + buffer.append(':'); + if (partOfHour < 10) { + buffer.append('0'); } + buffer.append(partOfHour); + } - private List<Boolean> asBooleanList(Object in) { - boolean[] array = (boolean[]) in; - List<Boolean> list = new ArrayList<Boolean>(array.length); - for (int i = 0; i < array.length; ++i) - list.add(array[i]); - return list; - } + return representScalar(getTag(data.getClass(), Tag.TIMESTAMP), buffer.toString(), + DumperOptions.ScalarStyle.PLAIN); } + } - protected class RepresentMap implements Represent { - @SuppressWarnings("unchecked") - public Node representData(Object data) { - return representMapping(getTag(data.getClass(), Tag.MAP), (Map<Object, Object>) data, - null); - } - } + protected class RepresentEnum implements Represent { - protected class RepresentSet implements Represent { - @SuppressWarnings("unchecked") - public Node representData(Object data) { - Map<Object, Object> value = new LinkedHashMap<Object, Object>(); - Set<Object> set = (Set<Object>) data; - for (Object key : set) { - value.put(key, null); - } - return representMapping(getTag(data.getClass(), Tag.SET), value, null); - } + public Node representData(Object data) { + Tag tag = new Tag(data.getClass()); + return representScalar(getTag(data.getClass(), tag), ((Enum<?>) data).name()); } + } - protected class RepresentDate implements Represent { - public Node representData(Object data) { - // because SimpleDateFormat ignores timezone we have to use Calendar - Calendar calendar; - if (data instanceof Calendar) { - calendar = (Calendar) data; - } else { - calendar = Calendar.getInstance(getTimeZone() == null ? TimeZone.getTimeZone("UTC") - : timeZone); - calendar.setTime((Date) data); - } - int years = calendar.get(Calendar.YEAR); - int months = calendar.get(Calendar.MONTH) + 1; // 0..12 - int days = calendar.get(Calendar.DAY_OF_MONTH); // 1..31 - int hour24 = calendar.get(Calendar.HOUR_OF_DAY); // 0..24 - int minutes = calendar.get(Calendar.MINUTE); // 0..59 - int seconds = calendar.get(Calendar.SECOND); // 0..59 - int millis = calendar.get(Calendar.MILLISECOND); - StringBuilder buffer = new StringBuilder(String.valueOf(years)); - while (buffer.length() < 4) { - // ancient years - buffer.insert(0, "0"); - } - buffer.append("-"); - if (months < 10) { - buffer.append("0"); - } - buffer.append(String.valueOf(months)); - buffer.append("-"); - if (days < 10) { - buffer.append("0"); - } - buffer.append(String.valueOf(days)); - buffer.append("T"); - if (hour24 < 10) { - buffer.append("0"); - } - buffer.append(String.valueOf(hour24)); - buffer.append(":"); - if (minutes < 10) { - buffer.append("0"); - } - buffer.append(String.valueOf(minutes)); - buffer.append(":"); - if (seconds < 10) { - buffer.append("0"); - } - buffer.append(String.valueOf(seconds)); - if (millis > 0) { - if (millis < 10) { - buffer.append(".00"); - } else if (millis < 100) { - buffer.append(".0"); - } else { - buffer.append("."); - } - buffer.append(String.valueOf(millis)); - } - if (TimeZone.getTimeZone("UTC").equals(calendar.getTimeZone())) { - buffer.append("Z"); - } else { - // Get the Offset from GMT taking DST into account - int gmtOffset = calendar.getTimeZone().getOffset(calendar.get(Calendar.ERA), - calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), - calendar.get(Calendar.DAY_OF_MONTH), calendar.get(Calendar.DAY_OF_WEEK), - calendar.get(Calendar.MILLISECOND)); - int minutesOffset = gmtOffset / (60 * 1000); - int hoursOffset = minutesOffset / 60; - int partOfHour = minutesOffset % 60; - buffer.append((hoursOffset > 0 ? "+" : "") + hoursOffset + ":" - + (partOfHour < 10 ? "0" + partOfHour : partOfHour)); - } - return representScalar(getTag(data.getClass(), Tag.TIMESTAMP), buffer.toString(), null); - } - } + protected class RepresentByteArray implements Represent { - protected class RepresentEnum implements Represent { - public Node representData(Object data) { - Tag tag = new Tag(data.getClass()); - return representScalar(getTag(data.getClass(), tag), ((Enum<?>) data).name()); - } + public Node representData(Object data) { + char[] binary = Base64Coder.encode((byte[]) data); + return representScalar(Tag.BINARY, String.valueOf(binary), DumperOptions.ScalarStyle.LITERAL); } + } - protected class RepresentByteArray implements Represent { - public Node representData(Object data) { - char[] binary = Base64Coder.encode((byte[]) data); - return representScalar(Tag.BINARY, String.valueOf(binary), '|'); - } - } + public TimeZone getTimeZone() { + return timeZone; + } - public TimeZone getTimeZone() { - return timeZone; - } + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } - public void setTimeZone(TimeZone timeZone) { - this.timeZone = timeZone; - } + protected class RepresentUuid implements Represent { - protected class RepresentUuid implements Represent { - public Node representData(Object data) { - return representScalar(getTag(data.getClass(), new Tag(UUID.class)), data.toString()); - } + public Node representData(Object data) { + return representScalar(getTag(data.getClass(), new Tag(UUID.class)), data.toString()); } + } } diff --git a/src/main/java/org/yaml/snakeyaml/resolver/Resolver.java b/src/main/java/org/yaml/snakeyaml/resolver/Resolver.java index f8ec36fa..38960561 100644 --- a/src/main/java/org/yaml/snakeyaml/resolver/Resolver.java +++ b/src/main/java/org/yaml/snakeyaml/resolver/Resolver.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.resolver; @@ -20,7 +18,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; - import org.yaml.snakeyaml.nodes.NodeId; import org.yaml.snakeyaml.nodes.Tag; @@ -28,111 +25,123 @@ import org.yaml.snakeyaml.nodes.Tag; * Resolver tries to detect a type by content (when the tag is implicit) */ public class Resolver { - public static final Pattern BOOL = Pattern - .compile("^(?:yes|Yes|YES|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$"); - /** - * The regular expression is taken from the 1.2 specification but '_'s are - * added to keep backwards compatibility - */ - public static final Pattern FLOAT = Pattern - .compile("^([-+]?(\\.[0-9]+|[0-9_]+(\\.[0-9_]*)?)([eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"); - public static final Pattern INT = Pattern - .compile("^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$"); - public static final Pattern MERGE = Pattern.compile("^(?:<<)$"); - public static final Pattern NULL = Pattern.compile("^(?:~|null|Null|NULL| )$"); - public static final Pattern EMPTY = Pattern.compile("^$"); - public static final Pattern TIMESTAMP = Pattern - .compile("^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?(?:[Tt]|[ \t]+)[0-9][0-9]?:[0-9][0-9]:[0-9][0-9](?:\\.[0-9]*)?(?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$"); - public static final Pattern VALUE = Pattern.compile("^(?:=)$"); - public static final Pattern YAML = Pattern.compile("^(?:!|&|\\*)$"); + public static final Pattern BOOL = Pattern + .compile("^(?:yes|Yes|YES|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$"); - protected Map<Character, List<ResolverTuple>> yamlImplicitResolvers = new HashMap<Character, List<ResolverTuple>>(); + /** + * The regular expression is taken from the 1.2 specification but '_'s are added to keep backwards + * compatibility + */ + public static final Pattern FLOAT = + Pattern.compile("^(" + "[-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?" + // (base 10) + "|[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)" + // (base 10, scientific notation without .) + "|[-+]?\\.[0-9_]+(?:[eE][-+]?[0-9]+)?" + // (base 10, starting with .) + "|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*" + // (base 60) + "|[-+]?\\.(?:inf|Inf|INF)" + "|\\.(?:nan|NaN|NAN)" + ")$"); + public static final Pattern INT = Pattern.compile("^(?:" + "[-+]?0b_*[0-1][0-1_]*" + // (base 2) + "|[-+]?0_*[0-7][0-7_]*" + // (base 8) + "|[-+]?(?:0|[1-9][0-9_]*)" + // (base 10) + "|[-+]?0x_*[0-9a-fA-F][0-9a-fA-F_]*" + // (base 16) + "|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+" + // (base 60) + ")$"); + public static final Pattern MERGE = Pattern.compile("^(?:<<)$"); + public static final Pattern NULL = Pattern.compile("^(?:~|null|Null|NULL| )$"); + public static final Pattern EMPTY = Pattern.compile("^$"); + public static final Pattern TIMESTAMP = Pattern.compile( + "^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?(?:[Tt]|[ \t]+)[0-9][0-9]?:[0-9][0-9]:[0-9][0-9](?:\\.[0-9]*)?(?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$"); + public static final Pattern VALUE = Pattern.compile("^(?:=)$"); + public static final Pattern YAML = Pattern.compile("^(?:!|&|\\*)$"); - protected void addImplicitResolvers() { - addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO"); - /* - * INT must be before FLOAT because the regular expression for FLOAT - * matches INT (see issue 130) - * http://code.google.com/p/snakeyaml/issues/detail?id=130 - */ - addImplicitResolver(Tag.INT, INT, "-+0123456789"); - addImplicitResolver(Tag.FLOAT, FLOAT, "-+0123456789."); - addImplicitResolver(Tag.MERGE, MERGE, "<"); - addImplicitResolver(Tag.NULL, NULL, "~nN\0"); - addImplicitResolver(Tag.NULL, EMPTY, null); - addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789"); - // The following implicit resolver is only for documentation - // purposes. - // It cannot work - // because plain scalars cannot start with '!', '&', or '*'. - addImplicitResolver(Tag.YAML, YAML, "!&*"); - } + protected Map<Character, List<ResolverTuple>> yamlImplicitResolvers = + new HashMap<Character, List<ResolverTuple>>(); - public Resolver() { - addImplicitResolvers(); - } + protected void addImplicitResolvers() { + addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO", 10); + /* + * INT must be before FLOAT because the regular expression for FLOAT matches INT (see issue 130) + * http://code.google.com/p/snakeyaml/issues/detail?id=130 + */ + addImplicitResolver(Tag.INT, INT, "-+0123456789"); + addImplicitResolver(Tag.FLOAT, FLOAT, "-+0123456789."); + addImplicitResolver(Tag.MERGE, MERGE, "<", 10); + addImplicitResolver(Tag.NULL, NULL, "~nN\0", 10); + addImplicitResolver(Tag.NULL, EMPTY, null, 10); + addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789", 50); + // The following implicit resolver is only for documentation purposes. + // It cannot work because plain scalars cannot start with '!', '&', or '*'. + addImplicitResolver(Tag.YAML, YAML, "!&*", 10); + } - public void addImplicitResolver(Tag tag, Pattern regexp, String first) { - if (first == null) { - List<ResolverTuple> curr = yamlImplicitResolvers.get(null); - if (curr == null) { - curr = new ArrayList<ResolverTuple>(); - yamlImplicitResolvers.put(null, curr); - } - curr.add(new ResolverTuple(tag, regexp)); - } else { - char[] chrs = first.toCharArray(); - for (int i = 0, j = chrs.length; i < j; i++) { - Character theC = Character.valueOf(chrs[i]); - if (theC == 0) { - // special case: for null - theC = null; - } - List<ResolverTuple> curr = yamlImplicitResolvers.get(theC); - if (curr == null) { - curr = new ArrayList<ResolverTuple>(); - yamlImplicitResolvers.put(theC, curr); - } - curr.add(new ResolverTuple(tag, regexp)); - } + public Resolver() { + addImplicitResolvers(); + } + + public void addImplicitResolver(Tag tag, Pattern regexp, String first) { + addImplicitResolver(tag, regexp, first, 1024); + } + + public void addImplicitResolver(Tag tag, Pattern regexp, String first, int limit) { + if (first == null) { + List<ResolverTuple> curr = yamlImplicitResolvers.get(null); + if (curr == null) { + curr = new ArrayList<ResolverTuple>(); + yamlImplicitResolvers.put(null, curr); + } + curr.add(new ResolverTuple(tag, regexp, limit)); + } else { + char[] chrs = first.toCharArray(); + for (int i = 0, j = chrs.length; i < j; i++) { + Character theC = Character.valueOf(chrs[i]); + if (theC == 0) { + // special case: for null + theC = null; } + List<ResolverTuple> curr = yamlImplicitResolvers.get(theC); + if (curr == null) { + curr = new ArrayList<ResolverTuple>(); + yamlImplicitResolvers.put(theC, curr); + } + curr.add(new ResolverTuple(tag, regexp, limit)); + } } + } - public Tag resolve(NodeId kind, String value, boolean implicit) { - if (kind == NodeId.scalar && implicit) { - List<ResolverTuple> resolvers = null; - if (value.length() == 0) { - resolvers = yamlImplicitResolvers.get('\0'); - } else { - resolvers = yamlImplicitResolvers.get(value.charAt(0)); - } - if (resolvers != null) { - for (ResolverTuple v : resolvers) { - Tag tag = v.getTag(); - Pattern regexp = v.getRegexp(); - if (regexp.matcher(value).matches()) { - return tag; - } - } - } - if (yamlImplicitResolvers.containsKey(null)) { - for (ResolverTuple v : yamlImplicitResolvers.get(null)) { - Tag tag = v.getTag(); - Pattern regexp = v.getRegexp(); - if (regexp.matcher(value).matches()) { - return tag; - } - } - } + public Tag resolve(NodeId kind, String value, boolean implicit) { + if (kind == NodeId.scalar && implicit) { + final List<ResolverTuple> resolvers; + if (value.length() == 0) { + resolvers = yamlImplicitResolvers.get('\0'); + } else { + resolvers = yamlImplicitResolvers.get(value.charAt(0)); + } + if (resolvers != null) { + for (ResolverTuple v : resolvers) { + Tag tag = v.getTag(); + Pattern regexp = v.getRegexp(); + if (value.length() <= v.getLimit() && regexp.matcher(value).matches()) { + return tag; + } } - switch (kind) { - case scalar: - return Tag.STR; - case sequence: - return Tag.SEQ; - default: - return Tag.MAP; + } + if (yamlImplicitResolvers.containsKey(null)) { + // check null resolver + for (ResolverTuple v : yamlImplicitResolvers.get(null)) { + Tag tag = v.getTag(); + Pattern regexp = v.getRegexp(); + if (value.length() <= v.getLimit() && regexp.matcher(value).matches()) { + return tag; + } } + } + } + switch (kind) { + case scalar: + return Tag.STR; + case sequence: + return Tag.SEQ; + default: + return Tag.MAP; } -}
\ No newline at end of file + } +} diff --git a/src/main/java/org/yaml/snakeyaml/resolver/ResolverTuple.java b/src/main/java/org/yaml/snakeyaml/resolver/ResolverTuple.java index 3fbfac04..0f8df776 100644 --- a/src/main/java/org/yaml/snakeyaml/resolver/ResolverTuple.java +++ b/src/main/java/org/yaml/snakeyaml/resolver/ResolverTuple.java @@ -1,43 +1,47 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.resolver; import java.util.regex.Pattern; - import org.yaml.snakeyaml.nodes.Tag; final class ResolverTuple { - private final Tag tag; - private final Pattern regexp; - - public ResolverTuple(Tag tag, Pattern regexp) { - this.tag = tag; - this.regexp = regexp; - } - - public Tag getTag() { - return tag; - } - - public Pattern getRegexp() { - return regexp; - } - - @Override - public String toString() { - return "Tuple tag=" + tag + " regexp=" + regexp; - } -}
\ No newline at end of file + + private final Tag tag; + private final Pattern regexp; + private final int limit; + + public ResolverTuple(Tag tag, Pattern regexp, int limit) { + this.tag = tag; + this.regexp = regexp; + this.limit = limit; + } + + public Tag getTag() { + return tag; + } + + public Pattern getRegexp() { + return regexp; + } + + public int getLimit() { + return limit; + } + + @Override + public String toString() { + return "Tuple tag=" + tag + " regexp=" + regexp + " limit=" + limit; + } +} diff --git a/src/main/java/org/yaml/snakeyaml/scanner/Constant.java b/src/main/java/org/yaml/snakeyaml/scanner/Constant.java index 391bcaa7..83720afe 100644 --- a/src/main/java/org/yaml/snakeyaml/scanner/Constant.java +++ b/src/main/java/org/yaml/snakeyaml/scanner/Constant.java @@ -1,76 +1,76 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.scanner; import java.util.Arrays; public final class Constant { - private final static String ALPHA_S = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; - private final static String LINEBR_S = "\n\u0085\u2028\u2029"; - private final static String FULL_LINEBR_S = "\r" + LINEBR_S; - private final static String NULL_OR_LINEBR_S = "\0" + FULL_LINEBR_S; - private final static String NULL_BL_LINEBR_S = " " + NULL_OR_LINEBR_S; - private final static String NULL_BL_T_LINEBR_S = "\t" + NULL_BL_LINEBR_S; - private final static String NULL_BL_T_S = "\0 \t"; - private final static String URI_CHARS_S = ALPHA_S + "-;/?:@&=+$,_.!~*\'()[]%"; + private static final String ALPHA_S = + "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; - public final static Constant LINEBR = new Constant(LINEBR_S); - public final static Constant FULL_LINEBR = new Constant(FULL_LINEBR_S); - public final static Constant NULL_OR_LINEBR = new Constant(NULL_OR_LINEBR_S); - public final static Constant NULL_BL_LINEBR = new Constant(NULL_BL_LINEBR_S); - public final static Constant NULL_BL_T_LINEBR = new Constant(NULL_BL_T_LINEBR_S); - public final static Constant NULL_BL_T = new Constant(NULL_BL_T_S); - public final static Constant URI_CHARS = new Constant(URI_CHARS_S); + private static final String LINEBR_S = "\n\u0085\u2028\u2029"; + private static final String FULL_LINEBR_S = "\r" + LINEBR_S; + private static final String NULL_OR_LINEBR_S = "\0" + FULL_LINEBR_S; + private static final String NULL_BL_LINEBR_S = " " + NULL_OR_LINEBR_S; + private static final String NULL_BL_T_LINEBR_S = "\t" + NULL_BL_LINEBR_S; + private static final String NULL_BL_T_S = "\0 \t"; + private static final String URI_CHARS_S = ALPHA_S + "-;/?:@&=+$,_.!~*'()[]%"; - public final static Constant ALPHA = new Constant(ALPHA_S); + public static final Constant LINEBR = new Constant(LINEBR_S); + public static final Constant NULL_OR_LINEBR = new Constant(NULL_OR_LINEBR_S); + public static final Constant NULL_BL_LINEBR = new Constant(NULL_BL_LINEBR_S); + public static final Constant NULL_BL_T_LINEBR = new Constant(NULL_BL_T_LINEBR_S); + public static final Constant NULL_BL_T = new Constant(NULL_BL_T_S); + public static final Constant URI_CHARS = new Constant(URI_CHARS_S); - private String content; - boolean[] contains = new boolean[128]; - boolean noASCII = false; + public static final Constant ALPHA = new Constant(ALPHA_S); - private Constant(String content) { - Arrays.fill(contains, false); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < content.length(); i++) { - char ch = content.charAt(i); - if (ch < 128) - contains[ch] = true; - else - sb.append(ch); - } - if (sb.length() > 0) { - noASCII = true; - this.content = sb.toString(); - } - } + private String content; + boolean[] contains = new boolean[128]; + boolean noASCII = false; - public boolean has(char ch) { - return (ch < 128) ? contains[ch] : noASCII && content.indexOf(ch, 0) != -1; + private Constant(String content) { + Arrays.fill(contains, false); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < content.length(); i++) { + int c = content.codePointAt(i); + if (c < 128) { + contains[c] = true; + } else { + sb.appendCodePoint(c); + } } - - public boolean hasNo(char ch) { - return !has(ch); + if (sb.length() > 0) { + noASCII = true; + this.content = sb.toString(); } + } - public boolean has(char ch, String additional) { - return has(ch) || additional.indexOf(ch, 0) != -1; - } + public boolean has(int c) { + return (c < 128) ? contains[c] : noASCII && content.indexOf(c) != -1; + } - public boolean hasNo(char ch, String additional) { - return !has(ch, additional); - } + public boolean hasNo(int c) { + return !has(c); + } + + public boolean has(int c, String additional) { + return has(c) || additional.indexOf(c) != -1; + } + + public boolean hasNo(int c, String additional) { + return !has(c, additional); + } } diff --git a/src/main/java/org/yaml/snakeyaml/scanner/Scanner.java b/src/main/java/org/yaml/snakeyaml/scanner/Scanner.java index 6fc0d97c..ef9b8d65 100644 --- a/src/main/java/org/yaml/snakeyaml/scanner/Scanner.java +++ b/src/main/java/org/yaml/snakeyaml/scanner/Scanner.java @@ -1,65 +1,60 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.scanner; import org.yaml.snakeyaml.tokens.Token; /** - * This interface represents an input stream of {@link Token Tokens}. + * This interface represents an input stream of {@link Token}s. * <p> - * The parser and the scanner form together the 'Parse' step in the loading - * process (see chapter 3.1 of the <a href="http://yaml.org/spec/1.1/">YAML - * Specification</a>). + * The parser and the scanner form together the 'Parse' step in the loading process (see chapter 3.1 + * of the <a href="http://yaml.org/spec/1.1/">YAML Specification</a>). * </p> - * + * * @see org.yaml.snakeyaml.tokens.Token */ public interface Scanner { - /** - * Check if the next token is one of the given types. - * - * @param choices - * token IDs. - * @return <code>true</code> if the next token can be assigned to a variable - * of at least one of the given types. Returns <code>false</code> if - * no more tokens are available. - * @throws ScannerException - * Thrown in case of malformed input. - */ - boolean checkToken(Token.ID... choices); + /** + * Check if the next token is one of the given types. + * + * @param choices token IDs to match with + * @return <code>true</code> if the next token is one of the given types. Returns + * <code>false</code> if no more tokens are available. + * @throws ScannerException Thrown in case of malformed input. + */ + boolean checkToken(Token.ID... choices); - /** - * Return the next token, but do not delete it from the stream. - * - * @return The token that will be returned on the next call to - * {@link #getToken} - * @throws ScannerException - * Thrown in case of malformed input. - */ - Token peekToken(); + /** + * Return the next token, but do not delete it from the stream. + * + * @return The token that will be returned on the next call to {@link #getToken} + * @throws ScannerException Thrown in case of malformed input. + * @throws IndexOutOfBoundsException if no more token left + */ + Token peekToken(); - /** - * Returns the next token. - * <p> - * The token will be removed from the stream. - * </p> - * - * @throws ScannerException - * Thrown in case of malformed input. - */ - Token getToken(); + /** + * Returns the next token. + * <p> + * The token will be removed from the stream. (Every invocation of this method must happen after + * calling either {@link #checkToken} or {@link #peekToken()} + * </p> + * + * @return the coming token + * @throws ScannerException Thrown in case of malformed input. + * @throws IndexOutOfBoundsException if no more token left + */ + Token getToken(); } diff --git a/src/main/java/org/yaml/snakeyaml/scanner/ScannerException.java b/src/main/java/org/yaml/snakeyaml/scanner/ScannerException.java index b4ee9eef..1c3f5b51 100644 --- a/src/main/java/org/yaml/snakeyaml/scanner/ScannerException.java +++ b/src/main/java/org/yaml/snakeyaml/scanner/ScannerException.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.scanner; @@ -19,48 +17,35 @@ import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.MarkedYAMLException; /** - * Exception thrown by the {@link Scanner} implementations in case of malformed - * input. + * Exception thrown by the {@link Scanner} implementations in case of malformed input. */ public class ScannerException extends MarkedYAMLException { - private static final long serialVersionUID = 4782293188600445954L; + private static final long serialVersionUID = 4782293188600445954L; - /** - * Constructs an instance. - * - * @param context - * Part of the input document in which vicinity the problem - * occurred. - * @param contextMark - * Position of the <code>context</code> within the document. - * @param problem - * Part of the input document that caused the problem. - * @param problemMark - * Position of the <code>problem</code> within the document. - * @param note - * Message for the user with further information about the - * problem. - */ - public ScannerException(String context, Mark contextMark, String problem, Mark problemMark, - String note) { - super(context, contextMark, problem, problemMark, note); - } + /** + * Constructs an instance. + * + * @param context Part of the input document in which vicinity the problem occurred. + * @param contextMark Position of the <code>context</code> within the document. + * @param problem Part of the input document that caused the problem. + * @param problemMark Position of the <code>problem</code> within the document. + * @param note Message for the user with further information about the problem. + */ + public ScannerException(String context, Mark contextMark, String problem, Mark problemMark, + String note) { + super(context, contextMark, problem, problemMark, note); + } - /** - * Constructs an instance. - * - * @param context - * Part of the input document in which vicinity the problem - * occurred. - * @param contextMark - * Position of the <code>context</code> within the document. - * @param problem - * Part of the input document that caused the problem. - * @param problemMark - * Position of the <code>problem</code> within the document. - */ - public ScannerException(String context, Mark contextMark, String problem, Mark problemMark) { - this(context, contextMark, problem, problemMark, null); - } + /** + * Constructs an instance. + * + * @param context Part of the input document in which vicinity the problem occurred. + * @param contextMark Position of the <code>context</code> within the document. + * @param problem Part of the input document that caused the problem. + * @param problemMark Position of the <code>problem</code> within the document. + */ + public ScannerException(String context, Mark contextMark, String problem, Mark problemMark) { + this(context, contextMark, problem, problemMark, null); + } } diff --git a/src/main/java/org/yaml/snakeyaml/scanner/ScannerImpl.java b/src/main/java/org/yaml/snakeyaml/scanner/ScannerImpl.java index 4272aab7..ec8711f9 100644 --- a/src/main/java/org/yaml/snakeyaml/scanner/ScannerImpl.java +++ b/src/main/java/org/yaml/snakeyaml/scanner/ScannerImpl.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.scanner; @@ -24,7 +22,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; - +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.comments.CommentType; import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.reader.StreamReader; @@ -34,6 +34,7 @@ import org.yaml.snakeyaml.tokens.BlockEndToken; import org.yaml.snakeyaml.tokens.BlockEntryToken; import org.yaml.snakeyaml.tokens.BlockMappingStartToken; import org.yaml.snakeyaml.tokens.BlockSequenceStartToken; +import org.yaml.snakeyaml.tokens.CommentToken; import org.yaml.snakeyaml.tokens.DirectiveToken; import org.yaml.snakeyaml.tokens.DocumentEndToken; import org.yaml.snakeyaml.tokens.DocumentStartToken; @@ -58,6 +59,7 @@ import org.yaml.snakeyaml.util.UriEncoder; * Scanner produces tokens of the following types: * STREAM-START * STREAM-END + * COMMENT * DIRECTIVE(name, value) * DOCUMENT-START * DOCUMENT-END @@ -80,2209 +82,2311 @@ import org.yaml.snakeyaml.util.UriEncoder; * </pre> */ public final class ScannerImpl implements Scanner { - /** - * A regular expression matching characters which are not in the hexadecimal - * set (0-9, A-F, a-f). - */ - private final static Pattern NOT_HEXA = Pattern.compile("[^0-9A-Fa-f]"); - - /** - * A mapping from an escaped character in the input stream to the character - * that they should be replaced with. - * - * YAML defines several common and a few uncommon escape sequences. - * - * @see <a href="http://www.yaml.org/spec/current.html#id2517668">4.1.6. - * Escape Sequences</a> - */ - public final static Map<Character, String> ESCAPE_REPLACEMENTS = new HashMap<Character, String>(); - - /** - * A mapping from a character to a number of bytes to read-ahead for that - * escape sequence. These escape sequences are used to handle unicode - * escaping in the following formats, where H is a hexadecimal character: - * - * <pre> - * \xHH : escaped 8-bit Unicode character - * \uHHHH : escaped 16-bit Unicode character - * \UHHHHHHHH : escaped 32-bit Unicode character - * </pre> - * - * @see <a href="http://yaml.org/spec/1.1/current.html#id872840">5.6. Escape - * Sequences</a> - */ - public final static Map<Character, Integer> ESCAPE_CODES = new HashMap<Character, Integer>(); - - static { - // ASCII null - ESCAPE_REPLACEMENTS.put(Character.valueOf('0'), "\0"); - // ASCII bell - ESCAPE_REPLACEMENTS.put(Character.valueOf('a'), "\u0007"); - // ASCII backspace - ESCAPE_REPLACEMENTS.put(Character.valueOf('b'), "\u0008"); - // ASCII horizontal tab - ESCAPE_REPLACEMENTS.put(Character.valueOf('t'), "\u0009"); - // ASCII newline (line feed; \n maps to 0x0A) - ESCAPE_REPLACEMENTS.put(Character.valueOf('n'), "\n"); - // ASCII vertical tab - ESCAPE_REPLACEMENTS.put(Character.valueOf('v'), "\u000B"); - // ASCII form-feed - ESCAPE_REPLACEMENTS.put(Character.valueOf('f'), "\u000C"); - // carriage-return (\r maps to 0x0D) - ESCAPE_REPLACEMENTS.put(Character.valueOf('r'), "\r"); - // ASCII escape character (Esc) - ESCAPE_REPLACEMENTS.put(Character.valueOf('e'), "\u001B"); - // ASCII space - ESCAPE_REPLACEMENTS.put(Character.valueOf(' '), "\u0020"); - // ASCII double-quote - ESCAPE_REPLACEMENTS.put(Character.valueOf('"'), "\""); - // ASCII backslash - ESCAPE_REPLACEMENTS.put(Character.valueOf('\\'), "\\"); - // Unicode next line - ESCAPE_REPLACEMENTS.put(Character.valueOf('N'), "\u0085"); - // Unicode non-breaking-space - ESCAPE_REPLACEMENTS.put(Character.valueOf('_'), "\u00A0"); - // Unicode line-separator - ESCAPE_REPLACEMENTS.put(Character.valueOf('L'), "\u2028"); - // Unicode paragraph separator - ESCAPE_REPLACEMENTS.put(Character.valueOf('P'), "\u2029"); - - // 8-bit Unicode - ESCAPE_CODES.put(Character.valueOf('x'), 2); - // 16-bit Unicode - ESCAPE_CODES.put(Character.valueOf('u'), 4); - // 32-bit Unicode (Supplementary characters are supported) - ESCAPE_CODES.put(Character.valueOf('U'), 8); - } - private final StreamReader reader; - // Had we reached the end of the stream? - private boolean done = false; - - // The number of unclosed '{' and '['. `flow_level == 0` means block - // context. - private int flowLevel = 0; - - // List of processed tokens that are not yet emitted. - private List<Token> tokens; - - // Number of tokens that were emitted through the `get_token` method. - private int tokensTaken = 0; - - // The current indentation level. - private int indent = -1; - - // Past indentation levels. - private ArrayStack<Integer> indents; - - // Variables related to simple keys treatment. See PyYAML. - - /** - * <pre> - * A simple key is a key that is not denoted by the '?' indicator. - * Example of simple keys: - * --- - * block simple key: value - * ? not a simple key: - * : { flow simple key: value } - * We emit the KEY token before all keys, so when we find a potential - * simple key, we try to locate the corresponding ':' indicator. - * Simple keys should be limited to a single line and 1024 characters. - * - * Can a simple key start at the current position? A simple key may - * start: - * - at the beginning of the line, not counting indentation spaces - * (in block context), - * - after '{', '[', ',' (in the flow context), - * - after '?', ':', '-' (in the block context). - * In the block context, this flag also signifies if a block collection - * may start at the current position. - * </pre> - */ - private boolean allowSimpleKey = true; - /* - * Keep track of possible simple keys. This is a dictionary. The key is - * `flow_level`; there can be no more that one possible simple key for each - * level. The value is a SimpleKey record: (token_number, required, index, - * line, column, mark) A simple key may start with ALIAS, ANCHOR, TAG, - * SCALAR(flow), '[', or '{' tokens. - */ - private Map<Integer, SimpleKey> possibleSimpleKeys; - - public ScannerImpl(StreamReader reader) { - this.reader = reader; - this.tokens = new ArrayList<Token>(100); - this.indents = new ArrayStack<Integer>(10); - // The order in possibleSimpleKeys is kept for nextPossibleSimpleKey() - this.possibleSimpleKeys = new LinkedHashMap<Integer, SimpleKey>(); - fetchStreamStart();// Add the STREAM-START token. - } - - /** - * Check whether the next token is one of the given types. - */ - public boolean checkToken(Token.ID... choices) { - while (needMoreTokens()) { - fetchMoreTokens(); - } - if (!this.tokens.isEmpty()) { - if (choices.length == 0) { - return true; - } - // since profiler puts this method on top (it is used a lot), we - // should not use 'foreach' here because of the performance reasons - Token.ID first = this.tokens.get(0).getTokenId(); - for (int i = 0; i < choices.length; i++) { - if (first == choices[i]) { - return true; - } - } - } - return false; - } - - /** - * Return the next token, but do not delete it from the queue. - */ - public Token peekToken() { - while (needMoreTokens()) { - fetchMoreTokens(); - } - return this.tokens.get(0); - } - - /** - * Return the next token, removing it from the queue. - */ - public Token getToken() { - if (!this.tokens.isEmpty()) { - this.tokensTaken++; - return this.tokens.remove(0); - } - return null; - } - - // Private methods. - /** - * Returns true if more tokens should be scanned. - */ - private boolean needMoreTokens() { - // If we are done, we do not require more tokens. - if (this.done) { - return false; - } - // If we aren't done, but we have no tokens, we need to scan more. - if (this.tokens.isEmpty()) { - return true; - } - // The current token may be a potential simple key, so we - // need to look further. - stalePossibleSimpleKeys(); - return nextPossibleSimpleKey() == this.tokensTaken; - } - - /** - * Fetch one or more tokens from the StreamReader. - */ - private void fetchMoreTokens() { - // Eat whitespaces and comments until we reach the next token. - scanToNextToken(); - // Remove obsolete possible simple keys. - stalePossibleSimpleKeys(); - // Compare the current indentation and column. It may add some tokens - // and decrease the current indentation level. - unwindIndent(reader.getColumn()); - // Peek the next character, to decide what the next group of tokens - // will look like. - char ch = reader.peek(); - switch (ch) { - case '\0': - // Is it the end of stream? - fetchStreamEnd(); - return; - case '%': - // Is it a directive? - if (checkDirective()) { - fetchDirective(); - return; - } - break; - case '-': - // Is it the document start? - if (checkDocumentStart()) { - fetchDocumentStart(); - return; - // Is it the block entry indicator? - } else if (checkBlockEntry()) { - fetchBlockEntry(); - return; - } - break; - case '.': - // Is it the document end? - if (checkDocumentEnd()) { - fetchDocumentEnd(); - return; - } - break; - // TODO support for BOM within a stream. (not implemented in PyYAML) - case '[': - // Is it the flow sequence start indicator? - fetchFlowSequenceStart(); - return; - case '{': - // Is it the flow mapping start indicator? - fetchFlowMappingStart(); - return; - case ']': - // Is it the flow sequence end indicator? - fetchFlowSequenceEnd(); - return; - case '}': - // Is it the flow mapping end indicator? - fetchFlowMappingEnd(); - return; - case ',': - // Is it the flow entry indicator? - fetchFlowEntry(); - return; - // see block entry indicator above - case '?': - // Is it the key indicator? - if (checkKey()) { - fetchKey(); - return; - } - break; - case ':': - // Is it the value indicator? - if (checkValue()) { - fetchValue(); - return; - } - break; - case '*': - // Is it an alias? - fetchAlias(); - return; - case '&': - // Is it an anchor? - fetchAnchor(); - return; - case '!': - // Is it a tag? - fetchTag(); - return; - case '|': - // Is it a literal scalar? - if (this.flowLevel == 0) { - fetchLiteral(); - return; - } - break; - case '>': - // Is it a folded scalar? - if (this.flowLevel == 0) { - fetchFolded(); - return; - } - break; - case '\'': - // Is it a single quoted scalar? - fetchSingle(); - return; - case '"': - // Is it a double quoted scalar? - fetchDouble(); - return; - } - // It must be a plain scalar then. - if (checkPlain()) { - fetchPlain(); - return; - } - // No? It's an error. Let's produce a nice error message.We do this by - // converting escaped characters into their escape sequences. This is a - // backwards use of the ESCAPE_REPLACEMENTS map. - String chRepresentation = String.valueOf(ch); - for (Character s : ESCAPE_REPLACEMENTS.keySet()) { - String v = ESCAPE_REPLACEMENTS.get(s); - if (v.equals(chRepresentation)) { - chRepresentation = "\\" + s;// ' ' -> '\t' - break; - } - } - if (ch == '\t') - chRepresentation += "(TAB)"; - String text = String - .format("found character '%s' that cannot start any token. (Do not use %s for indentation)", - chRepresentation, chRepresentation); - throw new ScannerException("while scanning for the next token", null, text, - reader.getMark()); - } - - // Simple keys treatment. - - /** - * Return the number of the nearest possible simple key. Actually we don't - * need to loop through the whole dictionary. - */ - private int nextPossibleSimpleKey() { - /* - * the implementation is not as in PyYAML. Because - * this.possibleSimpleKeys is ordered we can simply take the first key - */ - if (!this.possibleSimpleKeys.isEmpty()) { - return this.possibleSimpleKeys.values().iterator().next().getTokenNumber(); - } - return -1; - } - - /** - * <pre> - * Remove entries that are no longer possible simple keys. According to - * the YAML specification, simple keys - * - should be limited to a single line, - * - should be no longer than 1024 characters. - * Disabling this procedure will allow simple keys of any length and - * height (may cause problems if indentation is broken though). - * </pre> - */ - private void stalePossibleSimpleKeys() { - if (!this.possibleSimpleKeys.isEmpty()) { - for (Iterator<SimpleKey> iterator = this.possibleSimpleKeys.values().iterator(); iterator - .hasNext();) { - SimpleKey key = iterator.next(); - if ((key.getLine() != reader.getLine()) - || (reader.getIndex() - key.getIndex() > 1024)) { - // If the key is not on the same line as the current - // position OR the difference in column between the token - // start and the current position is more than the maximum - // simple key length, then this cannot be a simple key. - if (key.isRequired()) { - // If the key was required, this implies an error - // condition. - throw new ScannerException("while scanning a simple key", key.getMark(), - "could not find expected ':'", reader.getMark()); - } - iterator.remove(); - } - } - } - } - - /** - * The next token may start a simple key. We check if it's possible and save - * its position. This function is called for ALIAS, ANCHOR, TAG, - * SCALAR(flow), '[', and '{'. - */ - private void savePossibleSimpleKey() { - // The next token may start a simple key. We check if it's possible - // and save its position. This function is called for - // ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. - - // Check if a simple key is required at the current position. - // A simple key is required if this position is the root flowLevel, AND - // the current indentation level is the same as the last indent-level. - boolean required = (this.flowLevel == 0) && (this.indent == this.reader.getColumn()); - - if (allowSimpleKey || !required) { - // A simple key is required only if it is the first token in the - // current line. Therefore it is always allowed. - } else { - throw new YAMLException( - "A simple key is required only if it is the first token in the current line"); - } - - // The next token might be a simple key. Let's save it's number and - // position. - if (this.allowSimpleKey) { - removePossibleSimpleKey(); - int tokenNumber = this.tokensTaken + this.tokens.size(); - SimpleKey key = new SimpleKey(tokenNumber, required, reader.getIndex(), - reader.getLine(), this.reader.getColumn(), this.reader.getMark()); - this.possibleSimpleKeys.put(this.flowLevel, key); + /** + * A regular expression matching characters which are not in the hexadecimal set (0-9, A-F, a-f). + */ + private static final Pattern NOT_HEXA = Pattern.compile("[^0-9A-Fa-f]"); + + /** + * A mapping from an escaped character in the input stream to the string representation that they + * should be replaced with. + * + * YAML defines several common and a few uncommon escape sequences. + * + * @see <a href="http://www.yaml.org/spec/current.html#id2517668">4.1.6. Escape Sequences</a> + */ + public static final Map<Character, String> ESCAPE_REPLACEMENTS = new HashMap<Character, String>(); + + /** + * A mapping from a character to a number of bytes to read-ahead for that escape sequence. These + * escape sequences are used to handle unicode escaping in the following formats, where H is a + * hexadecimal character: + * + * <pre> + * \xHH : escaped 8-bit Unicode character + * \uHHHH : escaped 16-bit Unicode character + * \UHHHHHHHH : escaped 32-bit Unicode character + * </pre> + * + * @see <a href="http://yaml.org/spec/1.1/current.html#id872840">5.6. Escape Sequences</a> + */ + public static final Map<Character, Integer> ESCAPE_CODES = new HashMap<Character, Integer>(); + + static { + // ASCII null + ESCAPE_REPLACEMENTS.put(Character.valueOf('0'), "\0"); + // ASCII bell + ESCAPE_REPLACEMENTS.put(Character.valueOf('a'), "\u0007"); + // ASCII backspace + ESCAPE_REPLACEMENTS.put(Character.valueOf('b'), "\u0008"); + // ASCII horizontal tab + ESCAPE_REPLACEMENTS.put(Character.valueOf('t'), "\u0009"); + // ASCII newline (line feed; \n maps to 0x0A) + ESCAPE_REPLACEMENTS.put(Character.valueOf('n'), "\n"); + // ASCII vertical tab + ESCAPE_REPLACEMENTS.put(Character.valueOf('v'), "\u000B"); + // ASCII form-feed + ESCAPE_REPLACEMENTS.put(Character.valueOf('f'), "\u000C"); + // carriage-return (\r maps to 0x0D) + ESCAPE_REPLACEMENTS.put(Character.valueOf('r'), "\r"); + // ASCII escape character (Esc) + ESCAPE_REPLACEMENTS.put(Character.valueOf('e'), "\u001B"); + // ASCII space + ESCAPE_REPLACEMENTS.put(Character.valueOf(' '), "\u0020"); + // ASCII double-quote + ESCAPE_REPLACEMENTS.put(Character.valueOf('"'), "\""); + // ASCII backslash + ESCAPE_REPLACEMENTS.put(Character.valueOf('\\'), "\\"); + // Unicode next line + ESCAPE_REPLACEMENTS.put(Character.valueOf('N'), "\u0085"); + // Unicode non-breaking-space + ESCAPE_REPLACEMENTS.put(Character.valueOf('_'), "\u00A0"); + // Unicode line-separator + ESCAPE_REPLACEMENTS.put(Character.valueOf('L'), "\u2028"); + // Unicode paragraph separator + ESCAPE_REPLACEMENTS.put(Character.valueOf('P'), "\u2029"); + + // 8-bit Unicode + ESCAPE_CODES.put(Character.valueOf('x'), 2); + // 16-bit Unicode + ESCAPE_CODES.put(Character.valueOf('u'), 4); + // 32-bit Unicode (Supplementary characters are supported) + ESCAPE_CODES.put(Character.valueOf('U'), 8); + } + + private final StreamReader reader; + // Had we reached the end of the stream? + private boolean done = false; + + // The number of unclosed '{' and '['. `flow_level == 0` means block context. + private int flowLevel = 0; + + // List of processed tokens that are not yet emitted. + private final List<Token> tokens; + + // The last added token + private Token lastToken; + + // Number of tokens that were emitted through the `getToken()` method. + private int tokensTaken = 0; + + // The current indentation level. + private int indent = -1; + + // Past indentation levels. + private final ArrayStack<Integer> indents; + + // A flag that indicates if comments should be parsed + private boolean parseComments; + + private final LoaderOptions loaderOptions; + + // Variables related to simple keys treatment. See PyYAML. + + /** + * <pre> + * A simple key is a key that is not denoted by the '?' indicator. + * Example of simple keys: + * --- + * block simple key: value + * ? not a simple key: + * : { flow simple key: value } + * We emit the KEY token before all keys, so when we find a potential + * simple key, we try to locate the corresponding ':' indicator. + * Simple keys should be limited to a single line and 1024 characters. + * + * Can a simple key start at the current position? A simple key may + * start: + * - at the beginning of the line, not counting indentation spaces + * (in block context), + * - after '{', '[', ',' (in the flow context), + * - after '?', ':', '-' (in the block context). + * In the block context, this flag also signifies if a block collection + * may start at the current position. + * </pre> + */ + private boolean allowSimpleKey = true; + + /* + * Keep track of possible simple keys. This is a dictionary. The key is `flow_level`; there can be + * no more than one possible simple key for each level. The value is a SimpleKey record: + * (token_number, required, index, line, column, mark) A simple key may start with ALIAS, ANCHOR, + * TAG, SCALAR(flow), '[', or '{' tokens. + */ + private final Map<Integer, SimpleKey> possibleSimpleKeys; + + public ScannerImpl(StreamReader reader) { + this(reader, new LoaderOptions()); + } + + public ScannerImpl(StreamReader reader, LoaderOptions options) { + this.parseComments = options.isProcessComments(); + this.reader = reader; + this.tokens = new ArrayList<Token>(100); + this.indents = new ArrayStack<Integer>(10); + // The order in possibleSimpleKeys is kept for nextPossibleSimpleKey() + this.possibleSimpleKeys = new LinkedHashMap<Integer, SimpleKey>(); + this.loaderOptions = options; + fetchStreamStart();// Add the STREAM-START token. + } + + /** + * Please use LoaderOptions instead Set the scanner to ignore comments or parse them as a + * <code>CommentToken</code>. + * + * @param parseComments <code>true</code> to parse; <code>false</code> to ignore + */ + @Deprecated + public ScannerImpl setParseComments(boolean parseComments) { + this.parseComments = parseComments; + return this; + } + + @Deprecated + public boolean isParseComments() { + return parseComments; + } + + /** + * Check whether the next token is one of the given types. + */ + public boolean checkToken(Token.ID... choices) { + while (needMoreTokens()) { + fetchMoreTokens(); + } + if (!this.tokens.isEmpty()) { + if (choices.length == 0) { + return true; + } + // since profiler puts this method on top (it is used a lot), we + // should not use 'foreach' here because of the performance reasons + Token.ID first = this.tokens.get(0).getTokenId(); + for (int i = 0; i < choices.length; i++) { + if (first == choices[i]) { + return true; + } + } + } + return false; + } + + /** + * Return the next token, but do not delete it from the queue. + */ + public Token peekToken() { + while (needMoreTokens()) { + fetchMoreTokens(); + } + return this.tokens.get(0); + } + + /** + * Return the next token, removing it from the queue. + */ + public Token getToken() { + this.tokensTaken++; + return this.tokens.remove(0); + } + + // Private methods. + + private void addToken(Token token) { + lastToken = token; + this.tokens.add(token); + } + + private void addToken(int index, Token token) { + if (index == this.tokens.size()) { + lastToken = token; + } + this.tokens.add(index, token); + } + + private void addAllTokens(List<Token> tokens) { + lastToken = tokens.get(tokens.size() - 1); + this.tokens.addAll(tokens); + } + + /** + * Returns true if more tokens should be scanned. + */ + private boolean needMoreTokens() { + // If we are done, we do not require more tokens. + if (this.done) { + return false; + } + // If we aren't done, but we have no tokens, we need to scan more. + if (this.tokens.isEmpty()) { + return true; + } + // The current token may be a potential simple key, so we + // need to look further. + stalePossibleSimpleKeys(); + return nextPossibleSimpleKey() == this.tokensTaken; + } + + /** + * Fetch one or more tokens from the StreamReader. + */ + private void fetchMoreTokens() { + if (reader.getIndex() > loaderOptions.getCodePointLimit()) { + throw new YAMLException("The incoming YAML document exceeds the limit: " + + loaderOptions.getCodePointLimit() + " code points."); + } + // Eat whitespaces and process comments until we reach the next token. + scanToNextToken(); + // Remove obsolete possible simple keys. + stalePossibleSimpleKeys(); + // Compare the current indentation and column. It may add some tokens + // and decrease the current indentation level. + unwindIndent(reader.getColumn()); + // Peek the next code point, to decide what the next group of tokens + // will look like. + int c = reader.peek(); + switch (c) { + case '\0': + // Is it the end of stream? + fetchStreamEnd(); + return; + case '%': + // Is it a directive? + if (checkDirective()) { + fetchDirective(); + return; + } + break; + case '-': + // Is it the document start? + if (checkDocumentStart()) { + fetchDocumentStart(); + return; + // Is it the block entry indicator? + } else if (checkBlockEntry()) { + fetchBlockEntry(); + return; + } + break; + case '.': + // Is it the document end? + if (checkDocumentEnd()) { + fetchDocumentEnd(); + return; + } + break; + // TODO support for BOM within a stream. (also not implemented in PyYAML) + case '[': + // Is it the flow sequence start indicator? + fetchFlowSequenceStart(); + return; + case '{': + // Is it the flow mapping start indicator? + fetchFlowMappingStart(); + return; + case ']': + // Is it the flow sequence end indicator? + fetchFlowSequenceEnd(); + return; + case '}': + // Is it the flow mapping end indicator? + fetchFlowMappingEnd(); + return; + case ',': + // Is it the flow entry indicator? + fetchFlowEntry(); + return; + // see block entry indicator above + case '?': + // Is it the key indicator? + if (checkKey()) { + fetchKey(); + return; + } + break; + case ':': + // Is it the value indicator? + if (checkValue()) { + fetchValue(); + return; + } + break; + case '*': + // Is it an alias? + fetchAlias(); + return; + case '&': + // Is it an anchor? + fetchAnchor(); + return; + case '!': + // Is it a tag? + fetchTag(); + return; + case '|': + // Is it a literal scalar? + if (this.flowLevel == 0) { + fetchLiteral(); + return; } - } - - /** - * Remove the saved possible key position at the current flow level. - */ - private void removePossibleSimpleKey() { - SimpleKey key = possibleSimpleKeys.remove(flowLevel); - if (key != null && key.isRequired()) { + break; + case '>': + // Is it a folded scalar? + if (this.flowLevel == 0) { + fetchFolded(); + return; + } + break; + case '\'': + // Is it a single quoted scalar? + fetchSingle(); + return; + case '"': + // Is it a double quoted scalar? + fetchDouble(); + return; + } + // It must be a plain scalar then. + if (checkPlain()) { + fetchPlain(); + return; + } + // No? It's an error. Let's produce a nice error message.We do this by + // converting escaped characters into their escape sequences. This is a + // backwards use of the ESCAPE_REPLACEMENTS map. + String chRepresentation = escapeChar(String.valueOf(Character.toChars(c))); + if (c == '\t') { + chRepresentation += "(TAB)"; + } + String text = String.format( + "found character '%s' that cannot start any token. (Do not use %s for indentation)", + chRepresentation, chRepresentation); + throw new ScannerException("while scanning for the next token", null, text, reader.getMark()); + } + + /** + * This is implemented in CharConstants in SnakeYAML Engine + */ + private String escapeChar(String chRepresentation) { + for (Character s : ESCAPE_REPLACEMENTS.keySet()) { + String v = ESCAPE_REPLACEMENTS.get(s); + if (v.equals(chRepresentation)) { + return "\\" + s;// ' ' -> '\t' + } + } + return chRepresentation; + } + + // Simple keys treatment. + + /** + * Return the number of the nearest possible simple key. Actually we don't need to loop through + * the whole dictionary. + */ + private int nextPossibleSimpleKey() { + /* + * the implementation is not as in PyYAML. Because this.possibleSimpleKeys is ordered we can + * simply take the first key + */ + if (!this.possibleSimpleKeys.isEmpty()) { + return this.possibleSimpleKeys.values().iterator().next().getTokenNumber(); + } + return -1; + } + + /** + * <pre> + * Remove entries that are no longer possible simple keys. According to + * the YAML specification, simple keys + * - should be limited to a single line, + * - should be no longer than 1024 characters. + * Disabling this procedure will allow simple keys of any length and + * height (may cause problems if indentation is broken though). + * </pre> + */ + private void stalePossibleSimpleKeys() { + if (!this.possibleSimpleKeys.isEmpty()) { + for (Iterator<SimpleKey> iterator = this.possibleSimpleKeys.values().iterator(); iterator + .hasNext();) { + SimpleKey key = iterator.next(); + if ((key.getLine() != reader.getLine()) || (reader.getIndex() - key.getIndex() > 1024)) { + // If the key is not on the same line as the current + // position OR the difference in column between the token + // start and the current position is more than the maximum + // simple key length, then this cannot be a simple key. + if (key.isRequired()) { + // If the key was required, this implies an error + // condition. throw new ScannerException("while scanning a simple key", key.getMark(), - "could not find expected ':'", reader.getMark()); - } - } - - // Indentation functions. - - /** - * * Handle implicitly ending multiple levels of block nodes by decreased - * indentation. This function becomes important on lines 4 and 7 of this - * example: - * - * <pre> - * 1) book one: - * 2) part one: - * 3) chapter one - * 4) part two: - * 5) chapter one - * 6) chapter two - * 7) book two: - * </pre> - * - * In flow context, tokens should respect indentation. Actually the - * condition should be `self.indent >= column` according to the spec. But - * this condition will prohibit intuitively correct constructions such as - * key : { } </pre> - */ - private void unwindIndent(int col) { - // In the flow context, indentation is ignored. We make the scanner less - // restrictive then specification requires. - if (this.flowLevel != 0) { - return; - } - - // In block context, we may need to issue the BLOCK-END tokens. - while (this.indent > col) { - Mark mark = reader.getMark(); - this.indent = this.indents.pop(); - this.tokens.add(new BlockEndToken(mark, mark)); - } - } - - /** - * Check if we need to increase indentation. - */ - private boolean addIndent(int column) { - if (this.indent < column) { - this.indents.push(this.indent); - this.indent = column; - return true; - } - return false; - } - - // Fetchers. - - /** - * We always add STREAM-START as the first token and STREAM-END as the last - * token. - */ - private void fetchStreamStart() { - // Read the token. + "could not find expected ':'", reader.getMark()); + } + iterator.remove(); + } + } + } + } + + /** + * The next token may start a simple key. We check if it's possible and save its position. This + * function is called for ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. + */ + private void savePossibleSimpleKey() { + // The next token may start a simple key. We check if it's possible + // and save its position. This function is called for + // ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. + + // Check if a simple key is required at the current position. + // A simple key is required if this position is the root flowLevel, AND + // the current indentation level is the same as the last indent-level. + boolean required = (this.flowLevel == 0) && (this.indent == this.reader.getColumn()); + + if (allowSimpleKey || !required) { + // A simple key is required only if it is the first token in the + // current line. Therefore it is always allowed. + } else { + throw new YAMLException( + "A simple key is required only if it is the first token in the current line"); + } + + // The next token might be a simple key. Let's save it's number and + // position. + if (this.allowSimpleKey) { + removePossibleSimpleKey(); + int tokenNumber = this.tokensTaken + this.tokens.size(); + SimpleKey key = new SimpleKey(tokenNumber, required, reader.getIndex(), reader.getLine(), + this.reader.getColumn(), this.reader.getMark()); + this.possibleSimpleKeys.put(this.flowLevel, key); + } + } + + /** + * Remove the saved possible key position at the current flow level. + */ + private void removePossibleSimpleKey() { + SimpleKey key = possibleSimpleKeys.remove(flowLevel); + if (key != null && key.isRequired()) { + throw new ScannerException("while scanning a simple key", key.getMark(), + "could not find expected ':'", reader.getMark()); + } + } + + // Indentation functions. + + /** + * * Handle implicitly ending multiple levels of block nodes by decreased indentation. This + * function becomes important on lines 4 and 7 of this example: + * + * <pre> + * 1) book one: + * 2) part one: + * 3) chapter one + * 4) part two: + * 5) chapter one + * 6) chapter two + * 7) book two: + * </pre> + * + * In flow context, tokens should respect indentation. Actually the condition should be + * `self.indent >= column` according to the spec. But this condition will prohibit intuitively + * correct constructions such as key : { } + * </pre> + */ + private void unwindIndent(int col) { + // In the flow context, indentation is ignored. We make the scanner less + // restrictive than specification requires. + if (this.flowLevel != 0) { + return; + } + + // In block context, we may need to issue the BLOCK-END tokens. + while (this.indent > col) { + Mark mark = reader.getMark(); + this.indent = this.indents.pop(); + addToken(new BlockEndToken(mark, mark)); + } + } + + /** + * Check if we need to increase indentation. + */ + private boolean addIndent(int column) { + if (this.indent < column) { + this.indents.push(this.indent); + this.indent = column; + return true; + } + return false; + } + + // Fetchers. + + /** + * We always add STREAM-START as the first token and STREAM-END as the last token. + */ + private void fetchStreamStart() { + // Read the token. + Mark mark = reader.getMark(); + + // Add STREAM-START. + Token token = new StreamStartToken(mark, mark); + addToken(token); + } + + private void fetchStreamEnd() { + // Set the current indentation to -1. + unwindIndent(-1); + + // Reset simple keys. + removePossibleSimpleKey(); + this.allowSimpleKey = false; + this.possibleSimpleKeys.clear(); + + // Read the token. + Mark mark = reader.getMark(); + + // Add STREAM-END. + Token token = new StreamEndToken(mark, mark); + addToken(token); + + // The stream is finished. + this.done = true; + } + + /** + * Fetch a YAML directive. Directives are presentation details that are interpreted as + * instructions to the processor. YAML defines two kinds of directives, YAML and TAG; all other + * types are reserved for future use. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id864824">3.2.3.4. Directives</a> + */ + private void fetchDirective() { + // Set the current indentation to -1. + unwindIndent(-1); + + // Reset simple keys. + removePossibleSimpleKey(); + this.allowSimpleKey = false; + + // Scan and add DIRECTIVE. + List<Token> tok = scanDirective(); + addAllTokens(tok); + } + + /** + * Fetch a document-start token ("---"). + */ + private void fetchDocumentStart() { + fetchDocumentIndicator(true); + } + + /** + * Fetch a document-end token ("..."). + */ + private void fetchDocumentEnd() { + fetchDocumentIndicator(false); + } + + /** + * Fetch a document indicator, either "---" for "document-start", or else "..." for "document-end. + * The type is chosen by the given boolean. + */ + private void fetchDocumentIndicator(boolean isDocumentStart) { + // Set the current indentation to -1. + unwindIndent(-1); + + // Reset simple keys. Note that there could not be a block collection + // after '---'. + removePossibleSimpleKey(); + this.allowSimpleKey = false; + + // Add DOCUMENT-START or DOCUMENT-END. + Mark startMark = reader.getMark(); + reader.forward(3); + Mark endMark = reader.getMark(); + Token token; + if (isDocumentStart) { + token = new DocumentStartToken(startMark, endMark); + } else { + token = new DocumentEndToken(startMark, endMark); + } + addToken(token); + } + + private void fetchFlowSequenceStart() { + fetchFlowCollectionStart(false); + } + + private void fetchFlowMappingStart() { + fetchFlowCollectionStart(true); + } + + /** + * Fetch a flow-style collection start, which is either a sequence or a mapping. The type is + * determined by the given boolean. + * + * A flow-style collection is in a format similar to JSON. Sequences are started by '[' and ended + * by ']'; mappings are started by '{' and ended by '}'. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + * + * @param isMappingStart + */ + private void fetchFlowCollectionStart(boolean isMappingStart) { + // '[' and '{' may start a simple key. + savePossibleSimpleKey(); + + // Increase the flow level. + this.flowLevel++; + + // Simple keys are allowed after '[' and '{'. + this.allowSimpleKey = true; + + // Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. + Mark startMark = reader.getMark(); + reader.forward(1); + Mark endMark = reader.getMark(); + Token token; + if (isMappingStart) { + token = new FlowMappingStartToken(startMark, endMark); + } else { + token = new FlowSequenceStartToken(startMark, endMark); + } + addToken(token); + } + + private void fetchFlowSequenceEnd() { + fetchFlowCollectionEnd(false); + } + + private void fetchFlowMappingEnd() { + fetchFlowCollectionEnd(true); + } + + /** + * Fetch a flow-style collection end, which is either a sequence or a mapping. The type is + * determined by the given boolean. + * + * A flow-style collection is in a format similar to JSON. Sequences are started by '[' and ended + * by ']'; mappings are started by '{' and ended by '}'. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + */ + private void fetchFlowCollectionEnd(boolean isMappingEnd) { + // Reset possible simple key on the current level. + removePossibleSimpleKey(); + + // Decrease the flow level. + this.flowLevel--; + + // No simple keys after ']' or '}'. + this.allowSimpleKey = false; + + // Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. + Mark startMark = reader.getMark(); + reader.forward(); + Mark endMark = reader.getMark(); + Token token; + if (isMappingEnd) { + token = new FlowMappingEndToken(startMark, endMark); + } else { + token = new FlowSequenceEndToken(startMark, endMark); + } + addToken(token); + } + + /** + * Fetch an entry in the flow style. Flow-style entries occur either immediately after the start + * of a collection, or else after a comma. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + */ + private void fetchFlowEntry() { + // Simple keys are allowed after ','. + this.allowSimpleKey = true; + + // Reset possible simple key on the current level. + removePossibleSimpleKey(); + + // Add FLOW-ENTRY. + Mark startMark = reader.getMark(); + reader.forward(); + Mark endMark = reader.getMark(); + Token token = new FlowEntryToken(startMark, endMark); + addToken(token); + } + + /** + * Fetch an entry in the block style. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + */ + private void fetchBlockEntry() { + // Block context needs additional checks. + if (this.flowLevel == 0) { + // Are we allowed to start a new entry? + if (!this.allowSimpleKey) { + throw new ScannerException(null, null, "sequence entries are not allowed here", + reader.getMark()); + } + + // We may need to add BLOCK-SEQUENCE-START. + if (addIndent(this.reader.getColumn())) { Mark mark = reader.getMark(); - - // Add STREAM-START. - Token token = new StreamStartToken(mark, mark); - this.tokens.add(token); - } - - private void fetchStreamEnd() { - // Set the current intendation to -1. - unwindIndent(-1); - - // Reset simple keys. - removePossibleSimpleKey(); - this.allowSimpleKey = false; - this.possibleSimpleKeys.clear(); - - // Read the token. + addToken(new BlockSequenceStartToken(mark, mark)); + } + } else { + // It's an error for the block entry to occur in the flow + // context,but we let the parser detect this. + } + // Simple keys are allowed after '-'. + this.allowSimpleKey = true; + + // Reset possible simple key on the current level. + removePossibleSimpleKey(); + + // Add BLOCK-ENTRY. + Mark startMark = reader.getMark(); + reader.forward(); + Mark endMark = reader.getMark(); + Token token = new BlockEntryToken(startMark, endMark); + addToken(token); + } + + /** + * Fetch a key in a block-style mapping. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + */ + private void fetchKey() { + // Block context needs additional checks. + if (this.flowLevel == 0) { + // Are we allowed to start a key (not necessary a simple)? + if (!this.allowSimpleKey) { + throw new ScannerException(null, null, "mapping keys are not allowed here", + reader.getMark()); + } + // We may need to add BLOCK-MAPPING-START. + if (addIndent(this.reader.getColumn())) { Mark mark = reader.getMark(); - - // Add STREAM-END. - Token token = new StreamEndToken(mark, mark); - this.tokens.add(token); - - // The stream is finished. - this.done = true; - } - - /** - * Fetch a YAML directive. Directives are presentation details that are - * interpreted as instructions to the processor. YAML defines two kinds of - * directives, YAML and TAG; all other types are reserved for future use. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id864824"></a> - */ - private void fetchDirective() { - // Set the current intendation to -1. - unwindIndent(-1); - - // Reset simple keys. - removePossibleSimpleKey(); - this.allowSimpleKey = false; - - // Scan and add DIRECTIVE. - Token tok = scanDirective(); - this.tokens.add(tok); - } - - /** - * Fetch a document-start token ("---"). - */ - private void fetchDocumentStart() { - fetchDocumentIndicator(true); - } - - /** - * Fetch a document-end token ("..."). - */ - private void fetchDocumentEnd() { - fetchDocumentIndicator(false); - } - - /** - * Fetch a document indicator, either "---" for "document-start", or else - * "..." for "document-end. The type is chosen by the given boolean. - */ - private void fetchDocumentIndicator(boolean isDocumentStart) { - // Set the current intendation to -1. - unwindIndent(-1); - - // Reset simple keys. Note that there could not be a block collection - // after '---'. - removePossibleSimpleKey(); - this.allowSimpleKey = false; - - // Add DOCUMENT-START or DOCUMENT-END. - Mark startMark = reader.getMark(); - reader.forward(3); - Mark endMark = reader.getMark(); - Token token; - if (isDocumentStart) { - token = new DocumentStartToken(startMark, endMark); - } else { - token = new DocumentEndToken(startMark, endMark); - } - this.tokens.add(token); - } - - private void fetchFlowSequenceStart() { - fetchFlowCollectionStart(false); - } - - private void fetchFlowMappingStart() { - fetchFlowCollectionStart(true); - } - + addToken(new BlockMappingStartToken(mark, mark)); + } + } + // Simple keys are allowed after '?' in the block context. + this.allowSimpleKey = this.flowLevel == 0; + + // Reset possible simple key on the current level. + removePossibleSimpleKey(); + + // Add KEY. + Mark startMark = reader.getMark(); + reader.forward(); + Mark endMark = reader.getMark(); + Token token = new KeyToken(startMark, endMark); + addToken(token); + } + + /** + * Fetch a value in a block-style mapping. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + */ + private void fetchValue() { + // Do we determine a simple key? + SimpleKey key = this.possibleSimpleKeys.remove(this.flowLevel); + if (key != null) { + // Add KEY. + addToken(key.getTokenNumber() - this.tokensTaken, new KeyToken(key.getMark(), key.getMark())); + + // If this key starts a new block mapping, we need to add + // BLOCK-MAPPING-START. + if (this.flowLevel == 0) { + if (addIndent(key.getColumn())) { + addToken(key.getTokenNumber() - this.tokensTaken, + new BlockMappingStartToken(key.getMark(), key.getMark())); + } + } + // There cannot be two simple keys one after another. + this.allowSimpleKey = false; + + } else { + // It must be a part of a complex key. + // Block context needs additional checks. Do we really need them? + // They will be caught by the parser anyway. + if (this.flowLevel == 0) { + + // We are allowed to start a complex value if and only if we can + // start a simple key. + if (!this.allowSimpleKey) { + throw new ScannerException(null, null, "mapping values are not allowed here", + reader.getMark()); + } + } + + // If this value starts a new block mapping, we need to add + // BLOCK-MAPPING-START. It will be detected as an error later by + // the parser. + if (flowLevel == 0) { + if (addIndent(reader.getColumn())) { + Mark mark = reader.getMark(); + addToken(new BlockMappingStartToken(mark, mark)); + } + } + + // Simple keys are allowed after ':' in the block context. + allowSimpleKey = flowLevel == 0; + + // Reset possible simple key on the current level. + removePossibleSimpleKey(); + } + // Add VALUE. + Mark startMark = reader.getMark(); + reader.forward(); + Mark endMark = reader.getMark(); + Token token = new ValueToken(startMark, endMark); + addToken(token); + } + + /** + * Fetch an alias, which is a reference to an anchor. Aliases take the format: + * + * <pre> + * *(anchor name) + * </pre> + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863390">3.2.2.2. Anchors and Aliases</a> + */ + private void fetchAlias() { + // ALIAS could be a simple key. + savePossibleSimpleKey(); + + // No simple keys after ALIAS. + this.allowSimpleKey = false; + + // Scan and add ALIAS. + Token tok = scanAnchor(false); + addToken(tok); + } + + /** + * Fetch an anchor. Anchors take the form: + * + * <pre> + * &(anchor name) + * </pre> + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863390">3.2.2.2. Anchors and Aliases</a> + */ + private void fetchAnchor() { + // ANCHOR could start a simple key. + savePossibleSimpleKey(); + + // No simple keys after ANCHOR. + this.allowSimpleKey = false; + + // Scan and add ANCHOR. + Token tok = scanAnchor(true); + addToken(tok); + } + + /** + * Fetch a tag. Tags take a complex form. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id861700">3.2.1.2. Tags</a> + */ + private void fetchTag() { + // TAG could start a simple key. + savePossibleSimpleKey(); + + // No simple keys after TAG. + this.allowSimpleKey = false; + + // Scan and add TAG. + Token tok = scanTag(); + addToken(tok); + } + + /** + * Fetch a literal scalar, denoted with a vertical-bar. This is the type best used for source code + * and other content, such as binary data, which must be included verbatim. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + */ + private void fetchLiteral() { + fetchBlockScalar('|'); + } + + /** + * Fetch a folded scalar, denoted with a greater-than sign. This is the type best used for long + * content, such as the text of a chapter or description. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + */ + private void fetchFolded() { + fetchBlockScalar('>'); + } + + /** + * Fetch a block scalar (literal or folded). + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + * + * @param style + */ + private void fetchBlockScalar(char style) { + // A simple key may follow a block scalar. + this.allowSimpleKey = true; + + // Reset possible simple key on the current level. + removePossibleSimpleKey(); + + // Scan and add SCALAR. + List<Token> tok = scanBlockScalar(style); + addAllTokens(tok); + } + + /** + * Fetch a single-quoted (') scalar. + */ + private void fetchSingle() { + fetchFlowScalar('\''); + } + + /** + * Fetch a double-quoted (") scalar. + */ + private void fetchDouble() { + fetchFlowScalar('"'); + } + + /** + * Fetch a flow scalar (single- or double-quoted). + * + * @see <a href="http://www.yaml.org/spec/1.1/#id863975">3.2.3.1. Node Styles</a> + * + * @param style + */ + private void fetchFlowScalar(char style) { + // A flow scalar could be a simple key. + savePossibleSimpleKey(); + + // No simple keys after flow scalars. + this.allowSimpleKey = false; + + // Scan and add SCALAR. + Token tok = scanFlowScalar(style); + addToken(tok); + } + + /** + * Fetch a plain scalar. + */ + private void fetchPlain() { + // A plain scalar could be a simple key. + savePossibleSimpleKey(); + + // No simple keys after plain scalars. But note that `scan_plain` will + // change this flag if the scan is finished at the beginning of the + // line. + this.allowSimpleKey = false; + + // Scan and add SCALAR. May change `allow_simple_key`. + Token tok = scanPlain(); + addToken(tok); + } + + // Checkers. + + /** + * Returns true if the next thing on the reader is a directive, given that the leading '%' has + * already been checked. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id864824">3.2.3.4. Directives</a> + */ + private boolean checkDirective() { + // DIRECTIVE: ^ '%' ... + // The '%' indicator is already checked. + return reader.getColumn() == 0; + } + + /** + * Returns true if the next thing on the reader is a document-start ("---"). A document-start is + * always followed immediately by a new line. + */ + private boolean checkDocumentStart() { + // DOCUMENT-START: ^ '---' (' '|'\n') + if (reader.getColumn() == 0) { + return "---".equals(reader.prefix(3)) && Constant.NULL_BL_T_LINEBR.has(reader.peek(3)); + } + return false; + } + + /** + * Returns true if the next thing on the reader is a document-end ("..."). A document-end is + * always followed immediately by a new line. + */ + private boolean checkDocumentEnd() { + // DOCUMENT-END: ^ '...' (' '|'\n') + if (reader.getColumn() == 0) { + return "...".equals(reader.prefix(3)) && Constant.NULL_BL_T_LINEBR.has(reader.peek(3)); + } + return false; + } + + /** + * Returns true if the next thing on the reader is a block token. + */ + private boolean checkBlockEntry() { + // BLOCK-ENTRY: '-' (' '|'\n') + return Constant.NULL_BL_T_LINEBR.has(reader.peek(1)); + } + + /** + * Returns true if the next thing on the reader is a key token. + */ + private boolean checkKey() { + // KEY(flow context): '?' + if (this.flowLevel != 0) { + return true; + } else { + // KEY(block context): '?' (' '|'\n') + return Constant.NULL_BL_T_LINEBR.has(reader.peek(1)); + } + } + + /** + * Returns true if the next thing on the reader is a value token. + */ + private boolean checkValue() { + // VALUE(flow context): ':' + if (flowLevel != 0) { + return true; + } else { + // VALUE(block context): ':' (' '|'\n') + return Constant.NULL_BL_T_LINEBR.has(reader.peek(1)); + } + } + + /** + * Returns true if the next thing on the reader is a plain token. + */ + private boolean checkPlain() { /** - * Fetch a flow-style collection start, which is either a sequence or a - * mapping. The type is determined by the given boolean. - * - * A flow-style collection is in a format similar to JSON. Sequences are - * started by '[' and ended by ']'; mappings are started by '{' and ended by - * '}'. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - * - * @param isMappingStart + * <pre> + * A plain scalar may start with any non-space character except: + * '-', '?', ':', ',', '[', ']', '{', '}', + * '#', '&', '*', '!', '|', '>', '\'', '\"', + * '%', '@', '`'. + * + * It may also start with + * '-', '?', ':' + * if it is followed by a non-space character. + * + * Note that we limit the last rule to the block context (except the + * '-' character) because we want the flow context to be space + * independent. + * </pre> */ - private void fetchFlowCollectionStart(boolean isMappingStart) { - // '[' and '{' may start a simple key. - savePossibleSimpleKey(); - - // Increase the flow level. - this.flowLevel++; - - // Simple keys are allowed after '[' and '{'. - this.allowSimpleKey = true; - - // Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. - Mark startMark = reader.getMark(); - reader.forward(1); - Mark endMark = reader.getMark(); - Token token; - if (isMappingStart) { - token = new FlowMappingStartToken(startMark, endMark); + int c = reader.peek(); + // If the next char is NOT one of the forbidden chars above or + // whitespace, then this is the start of a plain scalar. + return Constant.NULL_BL_T_LINEBR.hasNo(c, "-?:,[]{}#&*!|>'\"%@`") + || (Constant.NULL_BL_T_LINEBR.hasNo(reader.peek(1)) + && (c == '-' || (this.flowLevel == 0 && "?:".indexOf(c) != -1))); + } + + // Scanners. + + /** + * <pre> + * We ignore spaces, line breaks and comments. + * If we find a line break in the block context, we set the flag + * `allow_simple_key` on. + * The byte order mark is stripped if it's the first character in the + * stream. We do not yet support BOM inside the stream as the + * specification requires. Any such mark will be considered as a part + * of the document. + * TODO: We need to make tab handling rules more sane. A good rule is + * Tabs cannot precede tokens + * BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, + * KEY(block), VALUE(block), BLOCK-ENTRY + * So the checking code is + * if <TAB>: + * self.allow_simple_keys = False + * We also need to add the check for `allow_simple_keys == True` to + * `unwind_indent` before issuing BLOCK-END. + * Scanners for block, flow, and plain scalars need to be modified. + * </pre> + */ + private void scanToNextToken() { + // If there is a byte order mark (BOM) at the beginning of the stream, + // forward past it. + if (reader.getIndex() == 0 && reader.peek() == 0xFEFF) { + reader.forward(); + } + boolean found = false; + int inlineStartColumn = -1; + while (!found) { + Mark startMark = reader.getMark(); + int columnBeforeComment = reader.getColumn(); + boolean commentSeen = false; + int ff = 0; + // Peek ahead until we find the first non-space character, then + // move forward directly to that character. + while (reader.peek(ff) == ' ') { + ff++; + } + if (ff > 0) { + reader.forward(ff); + } + // If the character we have skipped forward to is a comment (#), + // then peek ahead until we find the next end of line. YAML + // comments are from a # to the next new-line. We then forward + // past the comment. + if (reader.peek() == '#') { + commentSeen = true; + CommentType type; + if (columnBeforeComment != 0 + && !(lastToken != null && lastToken.getTokenId() == Token.ID.BlockEntry)) { + type = CommentType.IN_LINE; + inlineStartColumn = reader.getColumn(); + } else if (inlineStartColumn == reader.getColumn()) { + type = CommentType.IN_LINE; } else { - token = new FlowSequenceStartToken(startMark, endMark); + inlineStartColumn = -1; + type = CommentType.BLOCK; + } + CommentToken token = scanComment(type); + if (parseComments) { + addToken(token); + } + } + // If we scanned a line break, then (depending on flow level), + // simple keys may be allowed. + String breaks = scanLineBreak(); + if (breaks.length() != 0) {// found a line-break + if (parseComments && !commentSeen) { + if (columnBeforeComment == 0) { + Mark endMark = reader.getMark(); + addToken(new CommentToken(CommentType.BLANK_LINE, breaks, startMark, endMark)); + } } - this.tokens.add(token); - } - - private void fetchFlowSequenceEnd() { - fetchFlowCollectionEnd(false); - } - - private void fetchFlowMappingEnd() { - fetchFlowCollectionEnd(true); - } - - /** - * Fetch a flow-style collection end, which is either a sequence or a - * mapping. The type is determined by the given boolean. - * - * A flow-style collection is in a format similar to JSON. Sequences are - * started by '[' and ended by ']'; mappings are started by '{' and ended by - * '}'. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - */ - private void fetchFlowCollectionEnd(boolean isMappingEnd) { - // Reset possible simple key on the current level. - removePossibleSimpleKey(); - - // Decrease the flow level. - this.flowLevel--; - - // No simple keys after ']' or '}'. - this.allowSimpleKey = false; - - // Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. - Mark startMark = reader.getMark(); + if (this.flowLevel == 0) { + // Simple keys are allowed at flow-level 0 after a line + // break + this.allowSimpleKey = true; + } + } else { + found = true; + } + } + } + + private CommentToken scanComment(CommentType type) { + // See the specification for details. + Mark startMark = reader.getMark(); + reader.forward(); + int length = 0; + while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(length))) { + length++; + } + String value = reader.prefixForward(length); + Mark endMark = reader.getMark(); + return new CommentToken(type, value, startMark, endMark); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private List<Token> scanDirective() { + // See the specification for details. + Mark startMark = reader.getMark(); + Mark endMark; + reader.forward(); + String name = scanDirectiveName(startMark); + List<?> value = null; + if ("YAML".equals(name)) { + value = scanYamlDirectiveValue(startMark); + endMark = reader.getMark(); + } else if ("TAG".equals(name)) { + value = scanTagDirectiveValue(startMark); + endMark = reader.getMark(); + } else { + endMark = reader.getMark(); + int ff = 0; + while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(ff))) { + ff++; + } + if (ff > 0) { + reader.forward(ff); + } + } + CommentToken commentToken = scanDirectiveIgnoredLine(startMark); + DirectiveToken token = new DirectiveToken(name, value, startMark, endMark); + return makeTokenList(token, commentToken); + } + + /** + * Scan a directive name. Directive names are a series of non-space characters. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id895217">7.1. Directives</a> + */ + private String scanDirectiveName(Mark startMark) { + // See the specification for details. + int length = 0; + // A Directive-name is a sequence of alphanumeric characters + // (a-z,A-Z,0-9). We scan until we find something that isn't. + // FIXME this disagrees with the specification. + int c = reader.peek(length); + while (Constant.ALPHA.has(c)) { + length++; + c = reader.peek(length); + } + // If the name would be empty, an error occurs. + if (length == 0) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected alphabetic or numeric character, but found " + s + "(" + c + ")", + reader.getMark()); + } + String value = reader.prefixForward(length); + c = reader.peek(); + if (Constant.NULL_BL_LINEBR.hasNo(c)) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected alphabetic or numeric character, but found " + s + "(" + c + ")", + reader.getMark()); + } + return value; + } + + private List<Integer> scanYamlDirectiveValue(Mark startMark) { + // See the specification for details. + while (reader.peek() == ' ') { + reader.forward(); + } + Integer major = scanYamlDirectiveNumber(startMark); + int c = reader.peek(); + if (c != '.') { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected a digit or '.', but found " + s + "(" + c + ")", reader.getMark()); + } + reader.forward(); + Integer minor = scanYamlDirectiveNumber(startMark); + c = reader.peek(); + if (Constant.NULL_BL_LINEBR.hasNo(c)) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected a digit or ' ', but found " + s + "(" + c + ")", reader.getMark()); + } + List<Integer> result = new ArrayList<Integer>(2); + result.add(major); + result.add(minor); + return result; + } + + /** + * Read a %YAML directive number: this is either the major or the minor part. Stop reading at a + * non-digit character (usually either '.' or '\n'). + * + * @see <a href="http://www.yaml.org/spec/1.1/#id895631">7.1.1. “YAML” Directive</a> + * @see <a href="http://www.yaml.org/spec/1.1/#ns-dec-digit"></a> + */ + private Integer scanYamlDirectiveNumber(Mark startMark) { + // See the specification for details. + int c = reader.peek(); + if (!Character.isDigit(c)) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected a digit, but found " + s + "(" + (c) + ")", reader.getMark()); + } + int length = 0; + while (Character.isDigit(reader.peek(length))) { + length++; + } + Integer value = Integer.parseInt(reader.prefixForward(length)); + return value; + } + + /** + * <p> + * Read a %TAG directive value: + * + * <pre> + * s-ignored-space+ c-tag-handle s-ignored-space+ ns-tag-prefix s-l-comments + * </pre> + * + * </p> + * + * @see <a href="http://www.yaml.org/spec/1.1/#id896044">7.1.2. “TAG” Directive</a> + */ + private List<String> scanTagDirectiveValue(Mark startMark) { + // See the specification for details. + while (reader.peek() == ' ') { + reader.forward(); + } + String handle = scanTagDirectiveHandle(startMark); + while (reader.peek() == ' ') { + reader.forward(); + } + String prefix = scanTagDirectivePrefix(startMark); + List<String> result = new ArrayList<String>(2); + result.add(handle); + result.add(prefix); + return result; + } + + /** + * Scan a %TAG directive's handle. This is YAML's c-tag-handle. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id896876">7.1.2.2. Tag Handles</a> + * @param startMark - beginning of the handle + * @return scanned handle + */ + private String scanTagDirectiveHandle(Mark startMark) { + // See the specification for details. + String value = scanTagHandle("directive", startMark); + int c = reader.peek(); + if (c != ' ') { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected ' ', but found " + s + "(" + c + ")", reader.getMark()); + } + return value; + } + + /** + * Scan a %TAG directive's prefix. This is YAML's ns-tag-prefix. + * + * @see <a href="http://www.yaml.org/spec/1.1/#ns-tag-prefix"></a> + */ + private String scanTagDirectivePrefix(Mark startMark) { + // See the specification for details. + String value = scanTagUri("directive", startMark); + int c = reader.peek(); + if (Constant.NULL_BL_LINEBR.hasNo(c)) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected ' ', but found " + s + "(" + c + ")", reader.getMark()); + } + return value; + } + + private CommentToken scanDirectiveIgnoredLine(Mark startMark) { + // See the specification for details. + while (reader.peek() == ' ') { + reader.forward(); + } + CommentToken commentToken = null; + if (reader.peek() == '#') { + CommentToken comment = scanComment(CommentType.IN_LINE); + if (parseComments) { + commentToken = comment; + } + } + int c = reader.peek(); + String lineBreak = scanLineBreak(); + if (lineBreak.length() == 0 && c != '\0') { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a directive", startMark, + "expected a comment or a line break, but found " + s + "(" + c + ")", reader.getMark()); + } + return commentToken; + } + + /** + * <pre> + * The YAML 1.1 specification does not restrict characters for anchors and + * aliases. This may lead to problems. + * see https://bitbucket.org/snakeyaml/snakeyaml/issues/485/alias-names-are-too-permissive-compared-to + * This implementation tries to follow https://github.com/yaml/yaml-spec/blob/master/rfc/RFC-0003.md + * </pre> + */ + private Token scanAnchor(boolean isAnchor) { + Mark startMark = reader.getMark(); + int indicator = reader.peek(); + String name = indicator == '*' ? "alias" : "anchor"; + reader.forward(); + int length = 0; + int c = reader.peek(length); + while (Constant.NULL_BL_T_LINEBR.hasNo(c, ":,[]{}/.*&")) { + length++; + c = reader.peek(length); + } + if (length == 0) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning an " + name, startMark, + "unexpected character found " + s + "(" + c + ")", reader.getMark()); + } + String value = reader.prefixForward(length); + c = reader.peek(); + if (Constant.NULL_BL_T_LINEBR.hasNo(c, "?:,]}%@`")) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning an " + name, startMark, + "unexpected character found " + s + "(" + c + ")", reader.getMark()); + } + Mark endMark = reader.getMark(); + Token tok; + if (isAnchor) { + tok = new AnchorToken(value, startMark, endMark); + } else { + tok = new AliasToken(value, startMark, endMark); + } + return tok; + } + + /** + * <p> + * Scan a Tag property. A Tag property may be specified in one of three ways: c-verbatim-tag, + * c-ns-shorthand-tag, or c-ns-non-specific-tag + * </p> + * + * <p> + * c-verbatim-tag takes the form !<ns-uri-char+> and must be delivered verbatim (as-is) to + * the application. In particular, verbatim tags are not subject to tag resolution. + * </p> + * + * <p> + * c-ns-shorthand-tag is a valid tag handle followed by a non-empty suffix. If the tag handle is a + * c-primary-tag-handle ('!') then the suffix must have all exclamation marks properly URI-escaped + * (%21); otherwise, the string will look like a named tag handle: !foo!bar would be interpreted + * as (handle="!foo!", suffix="bar"). + * </p> + * + * <p> + * c-ns-non-specific-tag is always a lone '!'; this is only useful for plain scalars, where its + * specification means that the scalar MUST be resolved to have type tag:yaml.org,2002:str. + * </p> + * + * TODO SnakeYaml incorrectly ignores c-ns-non-specific-tag right now. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id900262">8.2. Node Tags</a> + * + * TODO Note that this method does not enforce rules about local versus global tags! + */ + private Token scanTag() { + // See the specification for details. + Mark startMark = reader.getMark(); + // Determine the type of tag property based on the first character + // encountered + int c = reader.peek(1); + String handle = null; + String suffix = null; + // Verbatim tag! (c-verbatim-tag) + if (c == '<') { + // Skip the exclamation mark and >, then read the tag suffix (as + // a URI). + reader.forward(2); + suffix = scanTagUri("tag", startMark); + c = reader.peek(); + if (c != '>') { + // If there are any characters between the end of the tag-suffix + // URI and the closing >, then an error has occurred. + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a tag", startMark, + "expected '>', but found '" + s + "' (" + c + ")", reader.getMark()); + } + reader.forward(); + } else if (Constant.NULL_BL_T_LINEBR.has(c)) { + // A NUL, blank, tab, or line-break means that this was a + // c-ns-non-specific tag. + suffix = "!"; + reader.forward(); + } else { + // Any other character implies c-ns-shorthand-tag type. + + // Look ahead in the stream to determine whether this tag property + // is of the form !foo or !foo!bar. + int length = 1; + boolean useHandle = false; + while (Constant.NULL_BL_LINEBR.hasNo(c)) { + if (c == '!') { + useHandle = true; + break; + } + length++; + c = reader.peek(length); + } + // If we need to use a handle, scan it in; otherwise, the handle is + // presumed to be '!'. + if (useHandle) { + handle = scanTagHandle("tag", startMark); + } else { + handle = "!"; reader.forward(); - Mark endMark = reader.getMark(); - Token token; - if (isMappingEnd) { - token = new FlowMappingEndToken(startMark, endMark); + } + suffix = scanTagUri("tag", startMark); + } + c = reader.peek(); + // Check that the next character is allowed to follow a tag-property; + // if it is not, raise the error. + if (Constant.NULL_BL_LINEBR.hasNo(c)) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a tag", startMark, + "expected ' ', but found '" + s + "' (" + (c) + ")", reader.getMark()); + } + TagTuple value = new TagTuple(handle, suffix); + Mark endMark = reader.getMark(); + return new TagToken(value, startMark, endMark); + } + + private List<Token> scanBlockScalar(char style) { + // See the specification for details. + boolean folded; + // Depending on the given style, we determine whether the scalar is + // folded ('>') or literal ('|') + folded = style == '>'; + StringBuilder chunks = new StringBuilder(); + Mark startMark = reader.getMark(); + // Scan the header. + reader.forward(); + Chomping chompi = scanBlockScalarIndicators(startMark); + int increment = chompi.getIncrement(); + CommentToken commentToken = scanBlockScalarIgnoredLine(startMark); + + // Determine the indentation level and go to the first non-empty line. + int minIndent = this.indent + 1; + if (minIndent < 1) { + minIndent = 1; + } + String breaks; + int maxIndent; + int indent; + Mark endMark; + if (increment == -1) { + Object[] brme = scanBlockScalarIndentation(); + breaks = (String) brme[0]; + maxIndent = ((Integer) brme[1]).intValue(); + endMark = (Mark) brme[2]; + indent = Math.max(minIndent, maxIndent); + } else { + indent = minIndent + increment - 1; + Object[] brme = scanBlockScalarBreaks(indent); + breaks = (String) brme[0]; + endMark = (Mark) brme[1]; + } + + String lineBreak = ""; + + // Scan the inner part of the block scalar. + while (this.reader.getColumn() == indent && reader.peek() != '\0') { + chunks.append(breaks); + boolean leadingNonSpace = " \t".indexOf(reader.peek()) == -1; + int length = 0; + while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(length))) { + length++; + } + chunks.append(reader.prefixForward(length)); + lineBreak = scanLineBreak(); + Object[] brme = scanBlockScalarBreaks(indent); + breaks = (String) brme[0]; + endMark = (Mark) brme[1]; + if (this.reader.getColumn() == indent && reader.peek() != '\0') { + + // Unfortunately, folding rules are ambiguous. + // + // This is the folding according to the specification: + if (folded && "\n".equals(lineBreak) && leadingNonSpace + && " \t".indexOf(reader.peek()) == -1) { + if (breaks.length() == 0) { + chunks.append(" "); + } } else { - token = new FlowSequenceEndToken(startMark, endMark); + chunks.append(lineBreak); + } + // Clark Evans's interpretation (also in the spec examples) not + // imported from PyYAML + } else { + break; + } + } + // Chomp the tail. + if (chompi.chompTailIsNotFalse()) { + chunks.append(lineBreak); + } + if (chompi.chompTailIsTrue()) { + chunks.append(breaks); + } + // We are done. + ScalarToken scalarToken = new ScalarToken(chunks.toString(), false, startMark, endMark, + DumperOptions.ScalarStyle.createStyle(style)); + return makeTokenList(commentToken, scalarToken); + } + + /** + * Scan a block scalar indicator. The block scalar indicator includes two optional components, + * which may appear in either order. + * + * A block indentation indicator is a non-zero digit describing the indentation level of the block + * scalar to follow. This indentation is an additional number of spaces relative to the current + * indentation level. + * + * A block chomping indicator is a + or -, selecting the chomping mode away from the default + * (clip) to either -(strip) or +(keep). + * + * @see <a href="http://www.yaml.org/spec/1.1/#id868988">5.3. Indicator Characters</a> + * @see <a href="http://www.yaml.org/spec/1.1/#id927035">9.2.2. Block Indentation Indicator</a> + * @see <a href="http://www.yaml.org/spec/1.1/#id927557">9.2.3. Block Chomping Indicator</a> + */ + private Chomping scanBlockScalarIndicators(Mark startMark) { + // See the specification for details. + Boolean chomping = null; + int increment = -1; + int c = reader.peek(); + if (c == '-' || c == '+') { + if (c == '+') { + chomping = Boolean.TRUE; + } else { + chomping = Boolean.FALSE; + } + reader.forward(); + c = reader.peek(); + if (Character.isDigit(c)) { + final String s = String.valueOf(Character.toChars(c)); + increment = Integer.parseInt(s); + if (increment == 0) { + throw new ScannerException("while scanning a block scalar", startMark, + "expected indentation indicator in the range 1-9, but found 0", reader.getMark()); } - this.tokens.add(token); - } - - /** - * Fetch an entry in the flow style. Flow-style entries occur either - * immediately after the start of a collection, or else after a comma. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - */ - private void fetchFlowEntry() { - // Simple keys are allowed after ','. - this.allowSimpleKey = true; - - // Reset possible simple key on the current level. - removePossibleSimpleKey(); - - // Add FLOW-ENTRY. - Mark startMark = reader.getMark(); reader.forward(); - Mark endMark = reader.getMark(); - Token token = new FlowEntryToken(startMark, endMark); - this.tokens.add(token); - } - - /** - * Fetch an entry in the block style. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - */ - private void fetchBlockEntry() { - // Block context needs additional checks. - if (this.flowLevel == 0) { - // Are we allowed to start a new entry? - if (!this.allowSimpleKey) { - throw new ScannerException(null, null, "sequence entries are not allowed here", - reader.getMark()); - } - - // We may need to add BLOCK-SEQUENCE-START. - if (addIndent(this.reader.getColumn())) { - Mark mark = reader.getMark(); - this.tokens.add(new BlockSequenceStartToken(mark, mark)); - } + } + } else if (Character.isDigit(c)) { + final String s = String.valueOf(Character.toChars(c)); + increment = Integer.parseInt(s); + if (increment == 0) { + throw new ScannerException("while scanning a block scalar", startMark, + "expected indentation indicator in the range 1-9, but found 0", reader.getMark()); + } + reader.forward(); + c = reader.peek(); + if (c == '-' || c == '+') { + if (c == '+') { + chomping = Boolean.TRUE; } else { - // It's an error for the block entry to occur in the flow - // context,but we let the parser detect this. + chomping = Boolean.FALSE; } - // Simple keys are allowed after '-'. - this.allowSimpleKey = true; - - // Reset possible simple key on the current level. - removePossibleSimpleKey(); - - // Add BLOCK-ENTRY. - Mark startMark = reader.getMark(); reader.forward(); - Mark endMark = reader.getMark(); - Token token = new BlockEntryToken(startMark, endMark); - this.tokens.add(token); - } - - /** - * Fetch a key in a block-style mapping. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - */ - private void fetchKey() { - // Block context needs additional checks. - if (this.flowLevel == 0) { - // Are we allowed to start a key (not necessary a simple)? - if (!this.allowSimpleKey) { - throw new ScannerException(null, null, "mapping keys are not allowed here", - reader.getMark()); - } - // We may need to add BLOCK-MAPPING-START. - if (addIndent(this.reader.getColumn())) { - Mark mark = reader.getMark(); - this.tokens.add(new BlockMappingStartToken(mark, mark)); - } - } - // Simple keys are allowed after '?' in the block context. - this.allowSimpleKey = this.flowLevel == 0; - - // Reset possible simple key on the current level. - removePossibleSimpleKey(); - - // Add KEY. - Mark startMark = reader.getMark(); + } + } + c = reader.peek(); + if (Constant.NULL_BL_LINEBR.hasNo(c)) { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a block scalar", startMark, + "expected chomping or indentation indicators, but found " + s + "(" + c + ")", + reader.getMark()); + } + return new Chomping(chomping, increment); + } + + /** + * Scan to the end of the line after a block scalar has been scanned; the only things that are + * permitted at this time are comments and spaces. + */ + private CommentToken scanBlockScalarIgnoredLine(Mark startMark) { + // See the specification for details. + + // Forward past any number of trailing spaces + while (reader.peek() == ' ') { + reader.forward(); + } + + // If a comment occurs, scan to just before the end of line. + CommentToken commentToken = null; + if (reader.peek() == '#') { + commentToken = scanComment(CommentType.IN_LINE); + } + // If the next character is not a null or line break, an error has + // occurred. + int c = reader.peek(); + String lineBreak = scanLineBreak(); + if (lineBreak.length() == 0 && c != '\0') { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a block scalar", startMark, + "expected a comment or a line break, but found " + s + "(" + c + ")", reader.getMark()); + } + return commentToken; + } + + /** + * Scans for the indentation of a block scalar implicitly. This mechanism is used only if the + * block did not explicitly state an indentation to be used. + * + * @see <a href="http://www.yaml.org/spec/1.1/#id927035">9.2.2. Block Indentation Indicator</a> + */ + private Object[] scanBlockScalarIndentation() { + // See the specification for details. + StringBuilder chunks = new StringBuilder(); + int maxIndent = 0; + Mark endMark = reader.getMark(); + // Look ahead some number of lines until the first non-blank character + // occurs; the determined indentation will be the maximum number of + // leading spaces on any of these lines. + while (Constant.LINEBR.has(reader.peek(), " \r")) { + if (reader.peek() != ' ') { + // If the character isn't a space, it must be some kind of + // line-break; scan the line break and track it. + chunks.append(scanLineBreak()); + endMark = reader.getMark(); + } else { + // If the character is a space, move forward to the next + // character; if we surpass our previous maximum for indent + // level, update that too. reader.forward(); - Mark endMark = reader.getMark(); - Token token = new KeyToken(startMark, endMark); - this.tokens.add(token); - } - - /** - * Fetch a value in a block-style mapping. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - */ - private void fetchValue() { - // Do we determine a simple key? - SimpleKey key = this.possibleSimpleKeys.remove(this.flowLevel); - if (key != null) { - // Add KEY. - this.tokens.add(key.getTokenNumber() - this.tokensTaken, new KeyToken(key.getMark(), - key.getMark())); - - // If this key starts a new block mapping, we need to add - // BLOCK-MAPPING-START. - if (this.flowLevel == 0) { - if (addIndent(key.getColumn())) { - this.tokens.add(key.getTokenNumber() - this.tokensTaken, - new BlockMappingStartToken(key.getMark(), key.getMark())); - } - } - // There cannot be two simple keys one after another. - this.allowSimpleKey = false; - - } else { - // It must be a part of a complex key. - // Block context needs additional checks. Do we really need them? - // They will be caught by the parser anyway. - if (this.flowLevel == 0) { - - // We are allowed to start a complex value if and only if we can - // start a simple key. - if (!this.allowSimpleKey) { - throw new ScannerException(null, null, "mapping values are not allowed here", - reader.getMark()); - } - } - - // If this value starts a new block mapping, we need to add - // BLOCK-MAPPING-START. It will be detected as an error later by - // the parser. - if (flowLevel == 0) { - if (addIndent(reader.getColumn())) { - Mark mark = reader.getMark(); - this.tokens.add(new BlockMappingStartToken(mark, mark)); - } - } - - // Simple keys are allowed after ':' in the block context. - allowSimpleKey = flowLevel == 0; - - // Reset possible simple key on the current level. - removePossibleSimpleKey(); - } - // Add VALUE. - Mark startMark = reader.getMark(); - reader.forward(); - Mark endMark = reader.getMark(); - Token token = new ValueToken(startMark, endMark); - this.tokens.add(token); - } - - /** - * Fetch an alias, which is a reference to an anchor. Aliases take the - * format: - * - * <pre> - * *(anchor name) - * </pre> - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863390"></a> - */ - private void fetchAlias() { - // ALIAS could be a simple key. - savePossibleSimpleKey(); - - // No simple keys after ALIAS. - this.allowSimpleKey = false; - - // Scan and add ALIAS. - Token tok = scanAnchor(false); - this.tokens.add(tok); - } - - /** - * Fetch an anchor. Anchors take the form: - * - * <pre> - * &(anchor name) - * </pre> - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863390"></a> - */ - private void fetchAnchor() { - // ANCHOR could start a simple key. - savePossibleSimpleKey(); - - // No simple keys after ANCHOR. - this.allowSimpleKey = false; - - // Scan and add ANCHOR. - Token tok = scanAnchor(true); - this.tokens.add(tok); - } - - /** - * Fetch a tag. Tags take a complex form. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id861700"></a> - */ - private void fetchTag() { - // TAG could start a simple key. - savePossibleSimpleKey(); - - // No simple keys after TAG. - this.allowSimpleKey = false; - - // Scan and add TAG. - Token tok = scanTag(); - this.tokens.add(tok); - } - - /** - * Fetch a literal scalar, denoted with a vertical-bar. This is the type - * best used for source code and other content, such as binary data, which - * must be included verbatim. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - */ - private void fetchLiteral() { - fetchBlockScalar('|'); - } - - /** - * Fetch a folded scalar, denoted with a greater-than sign. This is the type - * best used for long content, such as the text of a chapter or description. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - */ - private void fetchFolded() { - fetchBlockScalar('>'); - } - - /** - * Fetch a block scalar (literal or folded). - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - * - * @param style - */ - private void fetchBlockScalar(char style) { - // A simple key may follow a block scalar. - this.allowSimpleKey = true; - - // Reset possible simple key on the current level. - removePossibleSimpleKey(); - - // Scan and add SCALAR. - Token tok = scanBlockScalar(style); - this.tokens.add(tok); - } - - /** - * Fetch a single-quoted (') scalar. - */ - private void fetchSingle() { - fetchFlowScalar('\''); - } - - /** - * Fetch a double-quoted (") scalar. - */ - private void fetchDouble() { - fetchFlowScalar('"'); - } - - /** - * Fetch a flow scalar (single- or double-quoted). - * - * @see <a href="http://www.yaml.org/spec/1.1/#id863975"></a> - * - * @param style - */ - private void fetchFlowScalar(char style) { - // A flow scalar could be a simple key. - savePossibleSimpleKey(); - - // No simple keys after flow scalars. - this.allowSimpleKey = false; - - // Scan and add SCALAR. - Token tok = scanFlowScalar(style); - this.tokens.add(tok); - } - - /** - * Fetch a plain scalar. - */ - private void fetchPlain() { - // A plain scalar could be a simple key. - savePossibleSimpleKey(); - - // No simple keys after plain scalars. But note that `scan_plain` will - // change this flag if the scan is finished at the beginning of the - // line. - this.allowSimpleKey = false; - - // Scan and add SCALAR. May change `allow_simple_key`. - Token tok = scanPlain(); - this.tokens.add(tok); - } - - // Checkers. - /** - * Returns true if the next thing on the reader is a directive, given that - * the leading '%' has already been checked. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id864824"></a> - */ - private boolean checkDirective() { - // DIRECTIVE: ^ '%' ... - // The '%' indicator is already checked. - return reader.getColumn() == 0; - } - - /** - * Returns true if the next thing on the reader is a document-start ("---"). - * A document-start is always followed immediately by a new line. - */ - private boolean checkDocumentStart() { - // DOCUMENT-START: ^ '---' (' '|'\n') - if (reader.getColumn() == 0) { - if ("---".equals(reader.prefix(3)) && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { - return true; - } - } - return false; - } - - /** - * Returns true if the next thing on the reader is a document-end ("..."). A - * document-end is always followed immediately by a new line. - */ - private boolean checkDocumentEnd() { - // DOCUMENT-END: ^ '...' (' '|'\n') - if (reader.getColumn() == 0) { - if ("...".equals(reader.prefix(3)) && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { - return true; - } - } - return false; - } - - /** - * Returns true if the next thing on the reader is a block token. - */ - private boolean checkBlockEntry() { - // BLOCK-ENTRY: '-' (' '|'\n') - return Constant.NULL_BL_T_LINEBR.has(reader.peek(1)); - } - - /** - * Returns true if the next thing on the reader is a key token. - */ - private boolean checkKey() { - // KEY(flow context): '?' - if (this.flowLevel != 0) { - return true; - } else { - // KEY(block context): '?' (' '|'\n') - return Constant.NULL_BL_T_LINEBR.has(reader.peek(1)); - } - } - - /** - * Returns true if the next thing on the reader is a value token. - */ - private boolean checkValue() { - // VALUE(flow context): ':' - if (flowLevel != 0) { - return true; - } else { - // VALUE(block context): ':' (' '|'\n') - return Constant.NULL_BL_T_LINEBR.has(reader.peek(1)); - } - } - - /** - * Returns true if the next thing on the reader is a plain token. - */ - private boolean checkPlain() { - /** - * <pre> - * A plain scalar may start with any non-space character except: - * '-', '?', ':', ',', '[', ']', '{', '}', - * '#', '&', '*', '!', '|', '>', '\'', '\"', - * '%', '@', '`'. - * - * It may also start with - * '-', '?', ':' - * if it is followed by a non-space character. - * - * Note that we limit the last rule to the block context (except the - * '-' character) because we want the flow context to be space - * independent. - * </pre> - */ - char ch = reader.peek(); - // If the next char is NOT one of the forbidden chars above or - // whitespace, then this is the start of a plain scalar. - return Constant.NULL_BL_T_LINEBR.hasNo(ch, "-?:,[]{}#&*!|>\'\"%@`") - || (Constant.NULL_BL_T_LINEBR.hasNo(reader.peek(1)) && (ch == '-' || (this.flowLevel == 0 && "?:" - .indexOf(ch) != -1))); - } - - // Scanners. - - /** - * <pre> - * We ignore spaces, line breaks and comments. - * If we find a line break in the block context, we set the flag - * `allow_simple_key` on. - * The byte order mark is stripped if it's the first character in the - * stream. We do not yet support BOM inside the stream as the - * specification requires. Any such mark will be considered as a part - * of the document. - * TODO: We need to make tab handling rules more sane. A good rule is - * Tabs cannot precede tokens - * BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, - * KEY(block), VALUE(block), BLOCK-ENTRY - * So the checking code is - * if <TAB>: - * self.allow_simple_keys = False - * We also need to add the check for `allow_simple_keys == True` to - * `unwind_indent` before issuing BLOCK-END. - * Scanners for block, flow, and plain scalars need to be modified. - * </pre> - */ - private void scanToNextToken() { - // If there is a byte order mark (BOM) at the beginning of the stream, - // forward past it. - if (reader.getIndex() == 0 && reader.peek() == '\uFEFF') { - reader.forward(); - } - boolean found = false; - while (!found) { - int ff = 0; - // Peek ahead until we find the first non-space character, then - // move forward directly to that character. - while (reader.peek(ff) == ' ') { - ff++; - } - if (ff > 0) { - reader.forward(ff); - } - // If the character we have skipped forward to is a comment (#), - // then peek ahead until we find the next end of line. YAML - // comments are from a # to the next new-line. We then forward - // past the comment. - if (reader.peek() == '#') { - ff = 0; - while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(ff))) { - ff++; - } - if (ff > 0) { - reader.forward(ff); - } - } - // If we scanned a line break, then (depending on flow level), - // simple keys may be allowed. - if (scanLineBreak().length() != 0) {// found a line-break - if (this.flowLevel == 0) { - // Simple keys are allowed at flow-level 0 after a line - // break - this.allowSimpleKey = true; - } - } else { - found = true; - } - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Token scanDirective() { - // See the specification for details. - Mark startMark = reader.getMark(); - Mark endMark; + if (this.reader.getColumn() > maxIndent) { + maxIndent = reader.getColumn(); + } + } + } + // Pass several results back together. + return new Object[] {chunks.toString(), maxIndent, endMark}; + } + + private Object[] scanBlockScalarBreaks(int indent) { + // See the specification for details. + StringBuilder chunks = new StringBuilder(); + Mark endMark = reader.getMark(); + int col = this.reader.getColumn(); + // Scan for up to the expected indentation-level of spaces, then move + // forward past that amount. + while (col < indent && reader.peek() == ' ') { + reader.forward(); + col++; + } + + // Consume one or more line breaks followed by any amount of spaces, + // until we find something that isn't a line-break. + String lineBreak = null; + while ((lineBreak = scanLineBreak()).length() != 0) { + chunks.append(lineBreak); + endMark = reader.getMark(); + // Scan past up to (indent) spaces on the next line, then forward + // past them. + col = this.reader.getColumn(); + while (col < indent && reader.peek() == ' ') { reader.forward(); - String name = scanDirectiveName(startMark); - List<?> value = null; - if ("YAML".equals(name)) { - value = scanYamlDirectiveValue(startMark); - endMark = reader.getMark(); - } else if ("TAG".equals(name)) { - value = scanTagDirectiveValue(startMark); - endMark = reader.getMark(); - } else { - endMark = reader.getMark(); - int ff = 0; - while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(ff))) { - ff++; - } - if (ff > 0) { - reader.forward(ff); - } - } - scanDirectiveIgnoredLine(startMark); - return new DirectiveToken(name, value, startMark, endMark); - } - - /** - * Scan a directive name. Directive names are a series of non-space - * characters. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id895217"></a> - */ - private String scanDirectiveName(Mark startMark) { - // See the specification for details. - int length = 0; - // A Directive-name is a sequence of alphanumeric characters - // (a-z,A-Z,0-9). We scan until we find something that isn't. - // FIXME this disagrees with the specification. - char ch = reader.peek(length); - while (Constant.ALPHA.has(ch)) { - length++; - ch = reader.peek(length); - } - // If the name would be empty, an error occurs. - if (length == 0) { - throw new ScannerException("while scanning a directive", startMark, - "expected alphabetic or numeric character, but found " + ch + "(" + ((int) ch) - + ")", reader.getMark()); - } - String value = reader.prefixForward(length); - ch = reader.peek(); - if (Constant.NULL_BL_LINEBR.hasNo(ch)) { - throw new ScannerException("while scanning a directive", startMark, - "expected alphabetic or numeric character, but found " + ch + "(" + ((int) ch) - + ")", reader.getMark()); - } - return value; - } - - private List<Integer> scanYamlDirectiveValue(Mark startMark) { - // See the specification for details. - while (reader.peek() == ' ') { - reader.forward(); - } - Integer major = scanYamlDirectiveNumber(startMark); - if (reader.peek() != '.') { - throw new ScannerException("while scanning a directive", startMark, - "expected a digit or '.', but found " + reader.peek() + "(" - + ((int) reader.peek()) + ")", reader.getMark()); - } + col++; + } + } + // Return both the assembled intervening string and the end-mark. + return new Object[] {chunks.toString(), endMark}; + } + + /** + * Scan a flow-style scalar. Flow scalars are presented in one of two forms; first, a flow scalar + * may be a double-quoted string; second, a flow scalar may be a single-quoted string. + * + * @see <a href="https://yaml.org/spec/1.1/#id904158">9.1. Flow Scalar Styles</a> style/syntax + * + * <pre> + * See the specification for details. + * Note that we loose indentation rules for quoted scalars. Quoted + * scalars don't need to adhere indentation because " and ' clearly + * mark the beginning and the end of them. Therefore we are less + * restrictive then the specification requires. We only need to check + * that document separators are not included in scalars. + * </pre> + */ + private Token scanFlowScalar(char style) { + boolean _double; + // The style will be either single- or double-quoted; we determine this + // by the first character in the entry (supplied) + _double = style == '"'; + StringBuilder chunks = new StringBuilder(); + Mark startMark = reader.getMark(); + int quote = reader.peek(); + reader.forward(); + chunks.append(scanFlowScalarNonSpaces(_double, startMark)); + while (reader.peek() != quote) { + chunks.append(scanFlowScalarSpaces(startMark)); + chunks.append(scanFlowScalarNonSpaces(_double, startMark)); + } + reader.forward(); + Mark endMark = reader.getMark(); + return new ScalarToken(chunks.toString(), false, startMark, endMark, + DumperOptions.ScalarStyle.createStyle(style)); + } + + /** + * Scan some number of flow-scalar non-space characters. + */ + private String scanFlowScalarNonSpaces(boolean doubleQuoted, Mark startMark) { + // See the specification for details. + StringBuilder chunks = new StringBuilder(); + while (true) { + // Scan through any number of characters which are not: NUL, blank, + // tabs, line breaks, single-quotes, double-quotes, or backslashes. + int length = 0; + while (Constant.NULL_BL_T_LINEBR.hasNo(reader.peek(length), "'\"\\")) { + length++; + } + if (length != 0) { + chunks.append(reader.prefixForward(length)); + } + // Depending on our quoting-type, the characters ', " and \ have + // differing meanings. + int c = reader.peek(); + if (!doubleQuoted && c == '\'' && reader.peek(1) == '\'') { + chunks.append("'"); + reader.forward(2); + } else if ((doubleQuoted && c == '\'') || (!doubleQuoted && "\"\\".indexOf(c) != -1)) { + chunks.appendCodePoint(c); reader.forward(); - Integer minor = scanYamlDirectiveNumber(startMark); - if (Constant.NULL_BL_LINEBR.hasNo(reader.peek())) { - throw new ScannerException("while scanning a directive", startMark, - "expected a digit or ' ', but found " + reader.peek() + "(" - + ((int) reader.peek()) + ")", reader.getMark()); - } - List<Integer> result = new ArrayList<Integer>(2); - result.add(major); - result.add(minor); - return result; - } - - /** - * Read a %YAML directive number: this is either the major or the minor - * part. Stop reading at a non-digit character (usually either '.' or '\n'). - * - * @see <a href="http://www.yaml.org/spec/1.1/#id895631"></a> - * @see <a href="http://www.yaml.org/spec/1.1/#ns-dec-digit"></a> - */ - private Integer scanYamlDirectiveNumber(Mark startMark) { - // See the specification for details. - char ch = reader.peek(); - if (!Character.isDigit(ch)) { - throw new ScannerException("while scanning a directive", startMark, - "expected a digit, but found " + ch + "(" + ((int) ch) + ")", reader.getMark()); - } - int length = 0; - while (Character.isDigit(reader.peek(length))) { - length++; - } - Integer value = Integer.parseInt(reader.prefixForward(length)); - return value; - } - - /** - * <p> - * Read a %TAG directive value: - * - * <pre> - * s-ignored-space+ c-tag-handle s-ignored-space+ ns-tag-prefix s-l-comments - * </pre> - * - * </p> - * - * @see <a href="http://www.yaml.org/spec/1.1/#id896044"></a> - */ - private List<String> scanTagDirectiveValue(Mark startMark) { - // See the specification for details. - while (reader.peek() == ' ') { - reader.forward(); - } - String handle = scanTagDirectiveHandle(startMark); - while (reader.peek() == ' ') { - reader.forward(); - } - String prefix = scanTagDirectivePrefix(startMark); - List<String> result = new ArrayList<String>(2); - result.add(handle); - result.add(prefix); - return result; - } - - /** - * Scan a %TAG directive's handle. This is YAML's c-tag-handle. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id896876"></a> - * @param startMark - * @return - */ - private String scanTagDirectiveHandle(Mark startMark) { - // See the specification for details. - String value = scanTagHandle("directive", startMark); - char ch = reader.peek(); - if (ch != ' ') { - throw new ScannerException("while scanning a directive", startMark, - "expected ' ', but found " + reader.peek() + "(" + ch + ")", reader.getMark()); - } - return value; - } - - /** - * Scan a %TAG directive's prefix. This is YAML's ns-tag-prefix. - * - * @see <a href="http://www.yaml.org/spec/1.1/#ns-tag-prefix"></a> - */ - private String scanTagDirectivePrefix(Mark startMark) { - // See the specification for details. - String value = scanTagUri("directive", startMark); - if (Constant.NULL_BL_LINEBR.hasNo(reader.peek())) { - throw new ScannerException("while scanning a directive", startMark, - "expected ' ', but found " + reader.peek() + "(" + ((int) reader.peek()) + ")", - reader.getMark()); - } - return value; - } - - private String scanDirectiveIgnoredLine(Mark startMark) { - // See the specification for details. - int ff = 0; - while (reader.peek(ff) == ' ') { - ff++; - } - if (ff > 0) { - reader.forward(ff); - } - if (reader.peek() == '#') { - ff = 0; - while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(ff))) { - ff++; - } - reader.forward(ff); - } - char ch = reader.peek(); - String lineBreak = scanLineBreak(); - if (lineBreak.length() == 0 && ch != '\0') { - throw new ScannerException("while scanning a directive", startMark, - "expected a comment or a line break, but found " + ch + "(" + ((int) ch) + ")", - reader.getMark()); - } - return lineBreak; - } - - /** - * <pre> - * The specification does not restrict characters for anchors and - * aliases. This may lead to problems, for instance, the document: - * [ *alias, value ] - * can be interpreted in two ways, as - * [ "value" ] - * and - * [ *alias , "value" ] - * Therefore we restrict aliases to numbers and ASCII letters. - * </pre> - */ - private Token scanAnchor(boolean isAnchor) { - Mark startMark = reader.getMark(); - char indicator = reader.peek(); - String name = indicator == '*' ? "alias" : "anchor"; + } else if (doubleQuoted && c == '\\') { reader.forward(); - int length = 0; - char ch = reader.peek(length); - while (Constant.ALPHA.has(ch)) { - length++; - ch = reader.peek(length); - } - if (length == 0) { - throw new ScannerException("while scanning an " + name, startMark, - "expected alphabetic or numeric character, but found " + ch, - reader.getMark()); - } - String value = reader.prefixForward(length); - ch = reader.peek(); - if (Constant.NULL_BL_T_LINEBR.hasNo(ch, "?:,]}%@`")) { - throw new ScannerException("while scanning an " + name, startMark, - "expected alphabetic or numeric character, but found " + ch + "(" - + ((int) reader.peek()) + ")", reader.getMark()); - } - Mark endMark = reader.getMark(); - Token tok; - if (isAnchor) { - tok = new AnchorToken(value, startMark, endMark); - } else { - tok = new AliasToken(value, startMark, endMark); - } - return tok; - } - - /** - * <p> - * Scan a Tag property. A Tag property may be specified in one of three - * ways: c-verbatim-tag, c-ns-shorthand-tag, or c-ns-non-specific-tag - * </p> - * - * <p> - * c-verbatim-tag takes the form !<ns-uri-char+> and must be delivered - * verbatim (as-is) to the application. In particular, verbatim tags are not - * subject to tag resolution. - * </p> - * - * <p> - * c-ns-shorthand-tag is a valid tag handle followed by a non-empty suffix. - * If the tag handle is a c-primary-tag-handle ('!') then the suffix must - * have all exclamation marks properly URI-escaped (%21); otherwise, the - * string will look like a named tag handle: !foo!bar would be interpreted - * as (handle="!foo!", suffix="bar"). - * </p> - * - * <p> - * c-ns-non-specific-tag is always a lone '!'; this is only useful for plain - * scalars, where its specification means that the scalar MUST be resolved - * to have type tag:yaml.org,2002:str. - * </p> - * - * TODO SnakeYaml incorrectly ignores c-ns-non-specific-tag right now. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id900262"></a> - * - * TODO Note that this method does not enforce rules about local versus - * global tags! - */ - private Token scanTag() { - // See the specification for details. - Mark startMark = reader.getMark(); - // Determine the type of tag property based on the first character - // encountered - char ch = reader.peek(1); - String handle = null; - String suffix = null; - // Verbatim tag! (c-verbatim-tag) - if (ch == '<') { - // Skip the exclamation mark and >, then read the tag suffix (as - // a URI). - reader.forward(2); - suffix = scanTagUri("tag", startMark); - if (reader.peek() != '>') { - // If there are any characters between the end of the tag-suffix - // URI and the closing >, then an error has occurred. - throw new ScannerException("while scanning a tag", startMark, - "expected '>', but found '" + reader.peek() + "' (" + ((int) reader.peek()) - + ")", reader.getMark()); - } - reader.forward(); - } else if (Constant.NULL_BL_T_LINEBR.has(ch)) { - // A NUL, blank, tab, or line-break means that this was a - // c-ns-non-specific tag. - suffix = "!"; - reader.forward(); - } else { - // Any other character implies c-ns-shorthand-tag type. - - // Look ahead in the stream to determine whether this tag property - // is of the form !foo or !foo!bar. - int length = 1; - boolean useHandle = false; - while (Constant.NULL_BL_LINEBR.hasNo(ch)) { - if (ch == '!') { - useHandle = true; - break; - } - length++; - ch = reader.peek(length); - } - handle = "!"; - // If we need to use a handle, scan it in; otherwise, the handle is - // presumed to be '!'. - if (useHandle) { - handle = scanTagHandle("tag", startMark); - } else { - handle = "!"; - reader.forward(); - } - suffix = scanTagUri("tag", startMark); - } - ch = reader.peek(); - // Check that the next character is allowed to follow a tag-property; - // if it is not, raise the error. - if (Constant.NULL_BL_LINEBR.hasNo(ch)) { - throw new ScannerException("while scanning a tag", startMark, - "expected ' ', but found '" + ch + "' (" + ((int) ch) + ")", reader.getMark()); - } - TagTuple value = new TagTuple(handle, suffix); - Mark endMark = reader.getMark(); - return new TagToken(value, startMark, endMark); - } - - private Token scanBlockScalar(char style) { - // See the specification for details. - boolean folded; - // Depending on the given style, we determine whether the scalar is - // folded ('>') or literal ('|') - if (style == '>') { - folded = true; + c = reader.peek(); + if (!Character.isSupplementaryCodePoint(c) + && ESCAPE_REPLACEMENTS.containsKey(Character.valueOf((char) c))) { + // The character is one of the single-replacement + // types; these are replaced with a literal character + // from the mapping. + chunks.append(ESCAPE_REPLACEMENTS.get(Character.valueOf((char) c))); + reader.forward(); + } else if (!Character.isSupplementaryCodePoint(c) + && ESCAPE_CODES.containsKey(Character.valueOf((char) c))) { + // The character is a multi-digit escape sequence, with + // length defined by the value in the ESCAPE_CODES map. + length = ESCAPE_CODES.get(Character.valueOf((char) c)).intValue(); + reader.forward(); + String hex = reader.prefix(length); + if (NOT_HEXA.matcher(hex).find()) { + throw new ScannerException("while scanning a double-quoted scalar", startMark, + "expected escape sequence of " + length + " hexadecimal numbers, but found: " + hex, + reader.getMark()); + } + int decimal = Integer.parseInt(hex, 16); + String unicode = new String(Character.toChars(decimal)); + chunks.append(unicode); + reader.forward(length); + } else if (scanLineBreak().length() != 0) { + chunks.append(scanFlowScalarBreaks(startMark)); } else { - folded = false; + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a double-quoted scalar", startMark, + "found unknown escape character " + s + "(" + c + ")", reader.getMark()); } - StringBuilder chunks = new StringBuilder(); - Mark startMark = reader.getMark(); - // Scan the header. + } else { + return chunks.toString(); + } + } + } + + private String scanFlowScalarSpaces(Mark startMark) { + // See the specification for details. + StringBuilder chunks = new StringBuilder(); + int length = 0; + // Scan through any number of whitespace (space, tab) characters, + // consuming them. + while (" \t".indexOf(reader.peek(length)) != -1) { + length++; + } + String whitespaces = reader.prefixForward(length); + int c = reader.peek(); + if (c == '\0') { + // A flow scalar cannot end with an end-of-stream + throw new ScannerException("while scanning a quoted scalar", startMark, + "found unexpected end of stream", reader.getMark()); + } + // If we encounter a line break, scan it into our assembled string... + String lineBreak = scanLineBreak(); + if (lineBreak.length() != 0) { + String breaks = scanFlowScalarBreaks(startMark); + if (!"\n".equals(lineBreak)) { + chunks.append(lineBreak); + } else if (breaks.length() == 0) { + chunks.append(" "); + } + chunks.append(breaks); + } else { + chunks.append(whitespaces); + } + return chunks.toString(); + } + + private String scanFlowScalarBreaks(Mark startMark) { + // See the specification for details. + StringBuilder chunks = new StringBuilder(); + while (true) { + // Instead of checking indentation, we check for document + // separators. + String prefix = reader.prefix(3); + if (("---".equals(prefix) || "...".equals(prefix)) + && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { + throw new ScannerException("while scanning a quoted scalar", startMark, + "found unexpected document separator", reader.getMark()); + } + // Scan past any number of spaces and tabs, ignoring them + while (" \t".indexOf(reader.peek()) != -1) { reader.forward(); - Chomping chompi = scanBlockScalarIndicators(startMark); - int increment = chompi.getIncrement(); - scanBlockScalarIgnoredLine(startMark); - - // Determine the indentation level and go to the first non-empty line. - int minIndent = this.indent + 1; - if (minIndent < 1) { - minIndent = 1; - } - String breaks = null; - int maxIndent = 0; - int indent = 0; - Mark endMark; - if (increment == -1) { - Object[] brme = scanBlockScalarIndentation(); - breaks = (String) brme[0]; - maxIndent = ((Integer) brme[1]).intValue(); - endMark = (Mark) brme[2]; - indent = Math.max(minIndent, maxIndent); + } + // If we stopped at a line break, add that; otherwise, return the + // assembled set of scalar breaks. + String lineBreak = scanLineBreak(); + if (lineBreak.length() != 0) { + chunks.append(lineBreak); + } else { + return chunks.toString(); + } + } + } + + /** + * Scan a plain scalar. + * + * <pre> + * See the specification for details. + * We add an additional restriction for the flow context: + * plain scalars in the flow context cannot contain ',', ':' and '?'. + * We also keep track of the `allow_simple_key` flag here. + * Indentation rules are loosed for the flow context. + * </pre> + */ + private Token scanPlain() { + StringBuilder chunks = new StringBuilder(); + Mark startMark = reader.getMark(); + Mark endMark = startMark; + int indent = this.indent + 1; + String spaces = ""; + while (true) { + int c; + int length = 0; + // A comment indicates the end of the scalar. + if (reader.peek() == '#') { + break; + } + while (true) { + c = reader.peek(length); + if (Constant.NULL_BL_T_LINEBR.has(c) + || (c == ':' && Constant.NULL_BL_T_LINEBR.has(reader.peek(length + 1), + flowLevel != 0 ? ",[]{}" : "")) + || (this.flowLevel != 0 && ",?[]{}".indexOf(c) != -1)) { + break; + } + length++; + } + if (length == 0) { + break; + } + this.allowSimpleKey = false; + chunks.append(spaces); + chunks.append(reader.prefixForward(length)); + endMark = reader.getMark(); + spaces = scanPlainSpaces(); + // System.out.printf("spaces[%s]\n", spaces); + if (spaces.length() == 0 || reader.peek() == '#' + || (this.flowLevel == 0 && this.reader.getColumn() < indent)) { + break; + } + } + return new ScalarToken(chunks.toString(), startMark, endMark, true); + } + + // Helper for scanPlainSpaces method when comments are enabled. + // The ensures that blank lines and comments following a multi-line plain token are not swallowed + // up + private boolean atEndOfPlain() { + // peak ahead to find end of whitespaces and the column at which it occurs + int wsLength = 0; + int wsColumn = this.reader.getColumn(); + { + int c; + while ((c = reader.peek(wsLength)) != '\0' && Constant.NULL_BL_T_LINEBR.has(c)) { + wsLength++; + if (!Constant.LINEBR.has(c) && (c != '\r' || reader.peek(wsLength + 1) != '\n') + && c != 0xFEFF) { + wsColumn++; } else { - indent = minIndent + increment - 1; - Object[] brme = scanBlockScalarBreaks(indent); - breaks = (String) brme[0]; - endMark = (Mark) brme[1]; - } - - String lineBreak = ""; - - // Scan the inner part of the block scalar. - while (this.reader.getColumn() == indent && reader.peek() != '\0') { - chunks.append(breaks); - boolean leadingNonSpace = " \t".indexOf(reader.peek()) == -1; - int length = 0; - while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(length))) { - length++; - } - chunks.append(reader.prefixForward(length)); - lineBreak = scanLineBreak(); - Object[] brme = scanBlockScalarBreaks(indent); - breaks = (String) brme[0]; - endMark = (Mark) brme[1]; - if (this.reader.getColumn() == indent && reader.peek() != '\0') { - - // Unfortunately, folding rules are ambiguous. - // - // This is the folding according to the specification: - if (folded && "\n".equals(lineBreak) && leadingNonSpace - && " \t".indexOf(reader.peek()) == -1) { - if (breaks.length() == 0) { - chunks.append(" "); - } - } else { - chunks.append(lineBreak); - } - // Clark Evans's interpretation (also in the spec examples) not - // imported from PyYAML - } else { - break; - } - } - // Chomp the tail. - if (chompi.chompTailIsNotFalse()) { - chunks.append(lineBreak); + wsColumn = 0; } - if (chompi.chompTailIsTrue()) { - chunks.append(breaks); - } - // We are done. - return new ScalarToken(chunks.toString(), false, startMark, endMark, style); + } } - /** - * Scan a block scalar indicator. The block scalar indicator includes two - * optional components, which may appear in either order. - * - * A block indentation indicator is a non-zero digit describing the - * indentation level of the block scalar to follow. This indentation is an - * additional number of spaces relative to the current indentation level. - * - * A block chomping indicator is a + or -, selecting the chomping mode away - * from the default (clip) to either -(strip) or +(keep). - * - * @see <a href="http://www.yaml.org/spec/1.1/#id868988"></a> - * @see <a href="http://www.yaml.org/spec/1.1/#id927035"></a> - * @see <a href="http://www.yaml.org/spec/1.1/#id927557"></a> - */ - private Chomping scanBlockScalarIndicators(Mark startMark) { - // See the specification for details. - Boolean chomping = null; - int increment = -1; - char ch = reader.peek(); - if (ch == '-' || ch == '+') { - if (ch == '+') { - chomping = Boolean.TRUE; - } else { - chomping = Boolean.FALSE; - } - reader.forward(); - ch = reader.peek(); - if (Character.isDigit(ch)) { - increment = Integer.parseInt(String.valueOf(ch)); - if (increment == 0) { - throw new ScannerException("while scanning a block scalar", startMark, - "expected indentation indicator in the range 1-9, but found 0", - reader.getMark()); - } - reader.forward(); - } - } else if (Character.isDigit(ch)) { - increment = Integer.parseInt(String.valueOf(ch)); - if (increment == 0) { - throw new ScannerException("while scanning a block scalar", startMark, - "expected indentation indicator in the range 1-9, but found 0", - reader.getMark()); - } - reader.forward(); - ch = reader.peek(); - if (ch == '-' || ch == '+') { - if (ch == '+') { - chomping = Boolean.TRUE; - } else { - chomping = Boolean.FALSE; - } - reader.forward(); - } - } - ch = reader.peek(); - if (Constant.NULL_BL_LINEBR.hasNo(ch)) { - throw new ScannerException("while scanning a block scalar", startMark, - "expected chomping or indentation indicators, but found " + ch, - reader.getMark()); - } - return new Chomping(chomping, increment); + // if we see, a comment or end of string or change decrease in indent, we are done + // Do not chomp end of lines and blanks, they will be handled by the main loop. + if (reader.peek(wsLength) == '#' || reader.peek(wsLength + 1) == '\0' + || this.flowLevel == 0 && wsColumn < this.indent) { + return true; } - /** - * Scan to the end of the line after a block scalar has been scanned; the - * only things that are permitted at this time are comments and spaces. - */ - private String scanBlockScalarIgnoredLine(Mark startMark) { - // See the specification for details. - int ff = 0; - // Forward past any number of trailing spaces - while (reader.peek(ff) == ' ') { - ff++; - } - if (ff > 0) { - reader.forward(ff); - } - // If a comment occurs, scan to just before the end of line. - if (reader.peek() == '#') { - ff = 0; - while (Constant.NULL_OR_LINEBR.hasNo(reader.peek(ff))) { - ff++; - } - if (ff > 0) { - reader.forward(ff); - } - } - // If the next character is not a null or line break, an error has - // occurred. - char ch = reader.peek(); - String lineBreak = scanLineBreak(); - if (lineBreak.length() == 0 && ch != '\0') { - throw new ScannerException("while scanning a block scalar", startMark, - "expected a comment or a line break, but found " + ch, reader.getMark()); + // if we see, after the space, a key-value followed by a ':', we are done + // Do not chomp end of lines and blanks, they will be handled by the main loop. + if (this.flowLevel == 0) { + int c; + for (int extra = 1; (c = reader.peek(wsLength + extra)) != 0 + && !Constant.NULL_BL_T_LINEBR.has(c); extra++) { + if (c == ':' && Constant.NULL_BL_T_LINEBR.has(reader.peek(wsLength + extra + 1))) { + return true; } - return lineBreak; + } } - /** - * Scans for the indentation of a block scalar implicitly. This mechanism is - * used only if the block did not explicitly state an indentation to be - * used. - * - * @see <a href="http://www.yaml.org/spec/1.1/#id927035"></a> - */ - private Object[] scanBlockScalarIndentation() { - // See the specification for details. - StringBuilder chunks = new StringBuilder(); - int maxIndent = 0; - Mark endMark = reader.getMark(); - // Look ahead some number of lines until the first non-blank character - // occurs; the determined indentation will be the maximum number of - // leading spaces on any of these lines. - while (Constant.LINEBR.has(reader.peek(), " \r")) { - if (reader.peek() != ' ') { - // If the character isn't a space, it must be some kind of - // line-break; scan the line break and track it. - chunks.append(scanLineBreak()); - endMark = reader.getMark(); - } else { - // If the character is a space, move forward to the next - // character; if we surpass our previous maximum for indent - // level, update that too. - reader.forward(); - if (this.reader.getColumn() > maxIndent) { - maxIndent = reader.getColumn(); - } - } - } - // Pass several results back together. - return new Object[] { chunks.toString(), maxIndent, endMark }; - } - - private Object[] scanBlockScalarBreaks(int indent) { - // See the specification for details. - StringBuilder chunks = new StringBuilder(); - Mark endMark = reader.getMark(); - int ff = 0; - int col = this.reader.getColumn(); - // Scan for up to the expected indentation-level of spaces, then move - // forward past that amount. - while (col < indent && reader.peek(ff) == ' ') { - ff++; - col++; - } - if (ff > 0) { - reader.forward(ff); - } - // Consume one or more line breaks followed by any amount of spaces, - // until we find something that isn't a line-break. - String lineBreak = null; - while ((lineBreak = scanLineBreak()).length() != 0) { - chunks.append(lineBreak); - endMark = reader.getMark(); - // Scan past up to (indent) spaces on the next line, then forward - // past them. - ff = 0; - col = this.reader.getColumn(); - while (col < indent && reader.peek(ff) == ' ') { - ff++; - col++; - } - if (ff > 0) { - reader.forward(ff); - } - } - // Return both the assembled intervening string and the end-mark. - return new Object[] { chunks.toString(), endMark }; - } + // None of the above so safe to chomp the spaces. + return false; + } - /** - * Scan a flow-style scalar. Flow scalars are presented in one of two forms; - * first, a flow scalar may be a double-quoted string; second, a flow scalar - * may be a single-quoted string. - * - * @see <a href="http://www.yaml.org/spec/1.1/#flow"></a> style/syntax - * - * <pre> - * See the specification for details. - * Note that we loose indentation rules for quoted scalars. Quoted - * scalars don't need to adhere indentation because " and ' clearly - * mark the beginning and the end of them. Therefore we are less - * restrictive then the specification requires. We only need to check - * that document separators are not included in scalars. - * </pre> - */ - private Token scanFlowScalar(char style) { - boolean _double; - // The style will be either single- or double-quoted; we determine this - // by the first character in the entry (supplied) - if (style == '"') { - _double = true; - } else { - _double = false; - } - StringBuilder chunks = new StringBuilder(); - Mark startMark = reader.getMark(); - char quote = reader.peek(); - reader.forward(); - chunks.append(scanFlowScalarNonSpaces(_double, startMark)); - while (reader.peek() != quote) { - chunks.append(scanFlowScalarSpaces(startMark)); - chunks.append(scanFlowScalarNonSpaces(_double, startMark)); - } - reader.forward(); - Mark endMark = reader.getMark(); - return new ScalarToken(chunks.toString(), false, startMark, endMark, style); + /** + * See the specification for details. SnakeYAML and libyaml allow tabs inside plain scalar + */ + private String scanPlainSpaces() { + int length = 0; + while (reader.peek(length) == ' ' || reader.peek(length) == '\t') { + length++; } - - /** - * Scan some number of flow-scalar non-space characters. - */ - private String scanFlowScalarNonSpaces(boolean doubleQuoted, Mark startMark) { - // See the specification for details. - StringBuilder chunks = new StringBuilder(); - while (true) { - // Scan through any number of characters which are not: NUL, blank, - // tabs, line breaks, single-quotes, double-quotes, or backslashes. - int length = 0; - while (Constant.NULL_BL_T_LINEBR.hasNo(reader.peek(length), "\'\"\\")) { - length++; - } - if (length != 0) { - chunks.append(reader.prefixForward(length)); - } - // Depending on our quoting-type, the characters ', " and \ have - // differing meanings. - char ch = reader.peek(); - if (!doubleQuoted && ch == '\'' && reader.peek(1) == '\'') { - chunks.append("'"); - reader.forward(2); - } else if ((doubleQuoted && ch == '\'') || (!doubleQuoted && "\"\\".indexOf(ch) != -1)) { - chunks.append(ch); - reader.forward(); - } else if (doubleQuoted && ch == '\\') { - reader.forward(); - ch = reader.peek(); - if (ESCAPE_REPLACEMENTS.containsKey(Character.valueOf(ch))) { - // The character is one of the single-replacement - // types; these are replaced with a literal character - // from the mapping. - chunks.append(ESCAPE_REPLACEMENTS.get(Character.valueOf(ch))); - reader.forward(); - } else if (ESCAPE_CODES.containsKey(Character.valueOf(ch))) { - // The character is a multi-digit escape sequence, with - // length defined by the value in the ESCAPE_CODES map. - length = ESCAPE_CODES.get(Character.valueOf(ch)).intValue(); - reader.forward(); - String hex = reader.prefix(length); - if (NOT_HEXA.matcher(hex).find()) { - throw new ScannerException("while scanning a double-quoted scalar", - startMark, "expected escape sequence of " + length - + " hexadecimal numbers, but found: " + hex, - reader.getMark()); - } - int decimal = Integer.parseInt(hex, 16); - String unicode = new String(Character.toChars(decimal)); - chunks.append(unicode); - reader.forward(length); - } else if (scanLineBreak().length() != 0) { - chunks.append(scanFlowScalarBreaks(startMark)); - } else { - throw new ScannerException("while scanning a double-quoted scalar", startMark, - "found unknown escape character " + ch + "(" + ((int) ch) + ")", - reader.getMark()); - } - } else { - return chunks.toString(); - } - } - } - - private String scanFlowScalarSpaces(Mark startMark) { - // See the specification for details. - StringBuilder chunks = new StringBuilder(); - int length = 0; - // Scan through any number of whitespace (space, tab) characters, - // consuming them. - while (" \t".indexOf(reader.peek(length)) != -1) { - length++; - } - String whitespaces = reader.prefixForward(length); - char ch = reader.peek(); - if (ch == '\0') { - // A flow scalar cannot end with an end-of-stream - throw new ScannerException("while scanning a quoted scalar", startMark, - "found unexpected end of stream", reader.getMark()); - } - // If we encounter a line break, scan it into our assembled string... - String lineBreak = scanLineBreak(); - if (lineBreak.length() != 0) { - String breaks = scanFlowScalarBreaks(startMark); - if (!"\n".equals(lineBreak)) { - chunks.append(lineBreak); - } else if (breaks.length() == 0) { - chunks.append(" "); - } - chunks.append(breaks); + String whitespaces = reader.prefixForward(length); + String lineBreak = scanLineBreak(); + if (lineBreak.length() != 0) { + this.allowSimpleKey = true; + String prefix = reader.prefix(3); + if ("---".equals(prefix) + || "...".equals(prefix) && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { + return ""; + } + if (parseComments && atEndOfPlain()) { + return ""; + } + StringBuilder breaks = new StringBuilder(); + while (true) { + if (reader.peek() == ' ') { + reader.forward(); } else { - chunks.append(whitespaces); - } - return chunks.toString(); - } - - private String scanFlowScalarBreaks(Mark startMark) { - // See the specification for details. - StringBuilder chunks = new StringBuilder(); - while (true) { - // Instead of checking indentation, we check for document - // separators. - String prefix = reader.prefix(3); - if (("---".equals(prefix) || "...".equals(prefix)) - && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { - throw new ScannerException("while scanning a quoted scalar", startMark, - "found unexpected document separator", reader.getMark()); - } - // Scan past any number of spaces and tabs, ignoring them - while (" \t".indexOf(reader.peek()) != -1) { - reader.forward(); + String lb = scanLineBreak(); + if (lb.length() != 0) { + breaks.append(lb); + prefix = reader.prefix(3); + if ("---".equals(prefix) + || "...".equals(prefix) && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { + return ""; } - // If we stopped at a line break, add that; otherwise, return the - // assembled set of scalar breaks. - String lineBreak = scanLineBreak(); - if (lineBreak.length() != 0) { - chunks.append(lineBreak); - } else { - return chunks.toString(); - } - } + } else { + break; + } + } + } + if (!"\n".equals(lineBreak)) { + return lineBreak + breaks; + } else if (breaks.length() == 0) { + return " "; + } + return breaks.toString(); + } + return whitespaces; + } + + /** + * <p> + * Scan a Tag handle. A Tag handle takes one of three forms: + * + * <pre> + * "!" (c-primary-tag-handle) + * "!!" (ns-secondary-tag-handle) + * "!(name)!" (c-named-tag-handle) + * </pre> + * + * Where (name) must be formatted as an ns-word-char. + * </p> + * + * @see <a href="http://www.yaml.org/spec/1.1/#c-tag-handle"></a> + * @see <a href="http://www.yaml.org/spec/1.1/#ns-word-char"></a> + * + * <pre> + * See the specification for details. + * For some strange reasons, the specification does not allow '_' in + * tag handles. I have allowed it anyway. + * </pre> + */ + private String scanTagHandle(String name, Mark startMark) { + int c = reader.peek(); + if (c != '!') { + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a " + name, startMark, + "expected '!', but found " + s + "(" + (c) + ")", reader.getMark()); + } + // Look for the next '!' in the stream, stopping if we hit a + // non-word-character. If the first character is a space, then the + // tag-handle is a c-primary-tag-handle ('!'). + int length = 1; + c = reader.peek(length); + if (c != ' ') { + // Scan through 0+ alphabetic characters. + // FIXME According to the specification, these should be + // ns-word-char only, which prohibits '_'. This might be a + // candidate for a configuration option. + while (Constant.ALPHA.has(c)) { + length++; + c = reader.peek(length); + } + // Found the next non-word-char. If this is not a space and not an + // '!', then this is an error, as the tag-handle was specified as: + // !(name) or similar; the trailing '!' is missing. + if (c != '!') { + reader.forward(length); + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a " + name, startMark, + "expected '!', but found " + s + "(" + (c) + ")", reader.getMark()); + } + length++; + } + String value = reader.prefixForward(length); + return value; + } + + /** + * <p> + * Scan a Tag URI. This scanning is valid for both local and global tag directives, because both + * appear to be valid URIs as far as scanning is concerned. The difference may be distinguished + * later, in parsing. This method will scan for ns-uri-char*, which covers both cases. + * </p> + * + * <p> + * This method performs no verification that the scanned URI conforms to any particular kind of + * URI specification. + * </p> + * + * @see <a href="http://www.yaml.org/spec/1.1/#ns-uri-char"></a> + */ + private String scanTagUri(String name, Mark startMark) { + // See the specification for details. + // Note: we do not check if URI is well-formed. + StringBuilder chunks = new StringBuilder(); + // Scan through accepted URI characters, which includes the standard + // URI characters, plus the start-escape character ('%'). When we get + // to a start-escape, scan the escaped sequence, then return. + int length = 0; + int c = reader.peek(length); + while (Constant.URI_CHARS.has(c)) { + if (c == '%') { + chunks.append(reader.prefixForward(length)); + length = 0; + chunks.append(scanUriEscapes(name, startMark)); + } else { + length++; + } + c = reader.peek(length); + } + // Consume the last "chunk", which would not otherwise be consumed by + // the loop above. + if (length != 0) { + chunks.append(reader.prefixForward(length)); + } + if (chunks.length() == 0) { + // If no URI was found, an error has occurred. + final String s = String.valueOf(Character.toChars(c)); + throw new ScannerException("while scanning a " + name, startMark, + "expected URI, but found " + s + "(" + (c) + ")", reader.getMark()); + } + return chunks.toString(); + } + + /** + * <p> + * Scan a sequence of %-escaped URI escape codes and convert them into a String representing the + * unescaped values. + * </p> + * + * FIXME This method fails for more than 256 bytes' worth of URI-encoded characters in a row. Is + * this possible? Is this a use-case? + * + * @see <a href="http://www.ietf.org/rfc/rfc2396.txt">section 2.4, Escaped Encoding</a> + */ + private String scanUriEscapes(String name, Mark startMark) { + // First, look ahead to see how many URI-escaped characters we should + // expect, so we can use the correct buffer size. + int length = 1; + while (reader.peek(length * 3) == '%') { + length++; + } + // See the specification for details. + // URIs containing 16 and 32 bit Unicode characters are + // encoded in UTF-8, and then each octet is written as a + // separate character. + Mark beginningMark = reader.getMark(); + ByteBuffer buff = ByteBuffer.allocate(length); + while (reader.peek() == '%') { + reader.forward(); + try { + byte code = (byte) Integer.parseInt(reader.prefix(2), 16); + buff.put(code); + } catch (NumberFormatException nfe) { + int c1 = reader.peek(); + final String s1 = String.valueOf(Character.toChars(c1)); + int c2 = reader.peek(1); + final String s2 = String.valueOf(Character.toChars(c2)); + throw new ScannerException("while scanning a " + name, startMark, + "expected URI escape sequence of 2 hexadecimal numbers, but found " + s1 + "(" + c1 + + ") and " + s2 + "(" + c2 + ")", + reader.getMark()); + } + reader.forward(2); + } + buff.flip(); + try { + return UriEncoder.decode(buff); + } catch (CharacterCodingException e) { + throw new ScannerException("while scanning a " + name, startMark, + "expected URI in UTF-8: " + e.getMessage(), beginningMark); + } + } + + /** + * Scan a line break, transforming: + * + * <pre> + * '\r\n' : '\n' + * '\r' : '\n' + * '\n' : '\n' + * '\x85' : '\n' + * default : '' + * </pre> + */ + private String scanLineBreak() { + int c = reader.peek(); + if (c == '\r' || c == '\n' || c == '\u0085') { + if (c == '\r' && '\n' == reader.peek(1)) { + reader.forward(2); + } else { + reader.forward(); + } + return "\n"; + } else if (c == '\u2028' || c == '\u2029') { + reader.forward(); + return String.valueOf(Character.toChars(c)); } + return ""; + } - /** - * Scan a plain scalar. - * - * <pre> - * See the specification for details. - * We add an additional restriction for the flow context: - * plain scalars in the flow context cannot contain ',', ':' and '?'. - * We also keep track of the `allow_simple_key` flag here. - * Indentation rules are loosed for the flow context. - * </pre> - */ - private Token scanPlain() { - StringBuilder chunks = new StringBuilder(); - Mark startMark = reader.getMark(); - Mark endMark = startMark; - int indent = this.indent + 1; - String spaces = ""; - while (true) { - char ch; - int length = 0; - // A comment indicates the end of the scalar. - if (reader.peek() == '#') { - break; - } - while (true) { - ch = reader.peek(length); - if (Constant.NULL_BL_T_LINEBR.has(ch) - || (this.flowLevel == 0 && ch == ':' && Constant.NULL_BL_T_LINEBR - .has(reader.peek(length + 1))) - || (this.flowLevel != 0 && ",:?[]{}".indexOf(ch) != -1)) { - break; - } - length++; - } - // It's not clear what we should do with ':' in the flow context. - if (this.flowLevel != 0 && ch == ':' - && Constant.NULL_BL_T_LINEBR.hasNo(reader.peek(length + 1), ",[]{}")) { - reader.forward(length); - throw new ScannerException("while scanning a plain scalar", startMark, - "found unexpected ':'", reader.getMark(), - "Please check http://pyyaml.org/wiki/YAMLColonInFlowContext for details."); - } - if (length == 0) { - break; - } - this.allowSimpleKey = false; - chunks.append(spaces); - chunks.append(reader.prefixForward(length)); - endMark = reader.getMark(); - spaces = scanPlainSpaces(); - // System.out.printf("spaces[%s]\n", spaces); - if (spaces.length() == 0 || reader.peek() == '#' - || (this.flowLevel == 0 && this.reader.getColumn() < indent)) { - break; - } - } - return new ScalarToken(chunks.toString(), startMark, endMark, true); + private List<Token> makeTokenList(Token... tokens) { + List<Token> tokenList = new ArrayList<>(); + for (int ix = 0; ix < tokens.length; ix++) { + if (tokens[ix] == null) { + continue; + } + if (!parseComments && (tokens[ix] instanceof CommentToken)) { + continue; + } + tokenList.add(tokens[ix]); } + return tokenList; + } - /** - * See the specification for details. SnakeYAML and libyaml allow tabs - * inside plain scalar - */ - private String scanPlainSpaces() { - int length = 0; - while (reader.peek(length) == ' ' || reader.peek(length) == '\t') { - length++; - } - String whitespaces = reader.prefixForward(length); - String lineBreak = scanLineBreak(); - if (lineBreak.length() != 0) { - this.allowSimpleKey = true; - String prefix = reader.prefix(3); - if ("---".equals(prefix) || "...".equals(prefix) - && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { - return ""; - } - StringBuilder breaks = new StringBuilder(); - while (true) { - if (reader.peek() == ' ') { - reader.forward(); - } else { - String lb = scanLineBreak(); - if (lb.length() != 0) { - breaks.append(lb); - prefix = reader.prefix(3); - if ("---".equals(prefix) || "...".equals(prefix) - && Constant.NULL_BL_T_LINEBR.has(reader.peek(3))) { - return ""; - } - } else { - break; - } - } - } - if (!"\n".equals(lineBreak)) { - return lineBreak + breaks; - } else if (breaks.length() == 0) { - return " "; - } - return breaks.toString(); - } - return whitespaces; - } + /** + * Chomping the tail may have 3 values - yes, no, not defined. + */ + private static class Chomping { - /** - * <p> - * Scan a Tag handle. A Tag handle takes one of three forms: - * - * <pre> - * "!" (c-primary-tag-handle) - * "!!" (ns-secondary-tag-handle) - * "!(name)!" (c-named-tag-handle) - * </pre> - * - * Where (name) must be formatted as an ns-word-char. - * </p> - * - * @see <a href="http://www.yaml.org/spec/1.1/#c-tag-handle"></a> - * @see <a href="http://www.yaml.org/spec/1.1/#ns-word-char"></a> - * - * <pre> - * See the specification for details. - * For some strange reasons, the specification does not allow '_' in - * tag handles. I have allowed it anyway. - * </pre> - */ - private String scanTagHandle(String name, Mark startMark) { - char ch = reader.peek(); - if (ch != '!') { - throw new ScannerException("while scanning a " + name, startMark, - "expected '!', but found " + ch + "(" + ((int) ch) + ")", reader.getMark()); - } - // Look for the next '!' in the stream, stopping if we hit a - // non-word-character. If the first character is a space, then the - // tag-handle is a c-primary-tag-handle ('!'). - int length = 1; - ch = reader.peek(length); - if (ch != ' ') { - // Scan through 0+ alphabetic characters. - // FIXME According to the specification, these should be - // ns-word-char only, which prohibits '_'. This might be a - // candidate for a configuration option. - while (Constant.ALPHA.has(ch)) { - length++; - ch = reader.peek(length); - } - // Found the next non-word-char. If this is not a space and not an - // '!', then this is an error, as the tag-handle was specified as: - // !(name) or similar; the trailing '!' is missing. - if (ch != '!') { - reader.forward(length); - throw new ScannerException("while scanning a " + name, startMark, - "expected '!', but found " + ch + "(" + ((int) ch) + ")", reader.getMark()); - } - length++; - } - String value = reader.prefixForward(length); - return value; - } + private final Boolean value; + private final int increment; - /** - * <p> - * Scan a Tag URI. This scanning is valid for both local and global tag - * directives, because both appear to be valid URIs as far as scanning is - * concerned. The difference may be distinguished later, in parsing. This - * method will scan for ns-uri-char*, which covers both cases. - * </p> - * - * <p> - * This method performs no verification that the scanned URI conforms to any - * particular kind of URI specification. - * </p> - * - * @see <a href="http://www.yaml.org/spec/1.1/#ns-uri-char"></a> - */ - private String scanTagUri(String name, Mark startMark) { - // See the specification for details. - // Note: we do not check if URI is well-formed. - StringBuilder chunks = new StringBuilder(); - // Scan through accepted URI characters, which includes the standard - // URI characters, plus the start-escape character ('%'). When we get - // to a start-escape, scan the escaped sequence, then return. - int length = 0; - char ch = reader.peek(length); - while (Constant.URI_CHARS.has(ch)) { - if (ch == '%') { - chunks.append(reader.prefixForward(length)); - length = 0; - chunks.append(scanUriEscapes(name, startMark)); - } else { - length++; - } - ch = reader.peek(length); - } - // Consume the last "chunk", which would not otherwise be consumed by - // the loop above. - if (length != 0) { - chunks.append(reader.prefixForward(length)); - length = 0; - } - if (chunks.length() == 0) { - // If no URI was found, an error has occurred. - throw new ScannerException("while scanning a " + name, startMark, - "expected URI, but found " + ch + "(" + ((int) ch) + ")", reader.getMark()); - } - return chunks.toString(); + public Chomping(Boolean value, int increment) { + this.value = value; + this.increment = increment; } - /** - * <p> - * Scan a sequence of %-escaped URI escape codes and convert them into a - * String representing the unescaped values. - * </p> - * - * FIXME This method fails for more than 256 bytes' worth of URI-encoded - * characters in a row. Is this possible? Is this a use-case? - * - * @see <a href="http://www.ietf.org/rfc/rfc2396.txt"></a>, section 2.4, Escaped Encoding. - */ - private String scanUriEscapes(String name, Mark startMark) { - // First, look ahead to see how many URI-escaped characters we should - // expect, so we can use the correct buffer size. - int length = 1; - while (reader.peek(length * 3) == '%') { - length++; - } - // See the specification for details. - // URIs containing 16 and 32 bit Unicode characters are - // encoded in UTF-8, and then each octet is written as a - // separate character. - Mark beginningMark = reader.getMark(); - ByteBuffer buff = ByteBuffer.allocate(length); - while (reader.peek() == '%') { - reader.forward(); - try { - byte code = (byte) Integer.parseInt(reader.prefix(2), 16); - buff.put(code); - } catch (NumberFormatException nfe) { - throw new ScannerException("while scanning a " + name, startMark, - "expected URI escape sequence of 2 hexadecimal numbers, but found " - + reader.peek() + "(" + ((int) reader.peek()) + ") and " - + reader.peek(1) + "(" + ((int) reader.peek(1)) + ")", - reader.getMark()); - } - reader.forward(2); - } - buff.flip(); - try { - return UriEncoder.decode(buff); - } catch (CharacterCodingException e) { - throw new ScannerException("while scanning a " + name, startMark, - "expected URI in UTF-8: " + e.getMessage(), beginningMark); - } + public boolean chompTailIsNotFalse() { + return value == null || value; } - /** - * Scan a line break, transforming: - * - * <pre> - * '\r\n' : '\n' - * '\r' : '\n' - * '\n' : '\n' - * '\x85' : '\n' - * default : '' - * </pre> - */ - private String scanLineBreak() { - // Transforms: - // '\r\n' : '\n' - // '\r' : '\n' - // '\n' : '\n' - // '\x85' : '\n' - // default : '' - char ch = reader.peek(); - if (ch == '\r' || ch == '\n' || ch == '\u0085') { - if (ch == '\r' && '\n' == reader.peek(1)) { - reader.forward(2); - } else { - reader.forward(); - } - return "\n"; - } else if (ch == '\u2028' || ch == '\u2029') { - reader.forward(); - return String.valueOf(ch); - } - return ""; + public boolean chompTailIsTrue() { + return value != null && value; } - /** - * Chomping the tail may have 3 values - yes, no, not defined. - */ - private static class Chomping { - private final Boolean value; - private final int increment; - - public Chomping(Boolean value, int increment) { - this.value = value; - this.increment = increment; - } - - public boolean chompTailIsNotFalse() { - return value == null || value; - } - - public boolean chompTailIsTrue() { - return value != null && value; - } - - public int getIncrement() { - return increment; - } + public int getIncrement() { + return increment; } + } } diff --git a/src/main/java/org/yaml/snakeyaml/scanner/SimpleKey.java b/src/main/java/org/yaml/snakeyaml/scanner/SimpleKey.java index 3fe710cc..931c4e03 100644 --- a/src/main/java/org/yaml/snakeyaml/scanner/SimpleKey.java +++ b/src/main/java/org/yaml/snakeyaml/scanner/SimpleKey.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.scanner; @@ -22,53 +20,54 @@ import org.yaml.snakeyaml.error.Mark; * <p> * Helper class for {@link ScannerImpl}. * </p> - * + * * @see ScannerImpl */ final class SimpleKey { - private int tokenNumber; - private boolean required; - private int index; - private int line; - private int column; - private Mark mark; - public SimpleKey(int tokenNumber, boolean required, int index, int line, int column, Mark mark) { - this.tokenNumber = tokenNumber; - this.required = required; - this.index = index; - this.line = line; - this.column = column; - this.mark = mark; - } + private final int tokenNumber; + private final boolean required; + private final int index; + private final int line; + private final int column; + private final Mark mark; + + public SimpleKey(int tokenNumber, boolean required, int index, int line, int column, Mark mark) { + this.tokenNumber = tokenNumber; + this.required = required; + this.index = index; + this.line = line; + this.column = column; + this.mark = mark; + } - public int getTokenNumber() { - return this.tokenNumber; - } + public int getTokenNumber() { + return this.tokenNumber; + } - public int getColumn() { - return this.column; - } + public int getColumn() { + return this.column; + } - public Mark getMark() { - return mark; - } + public Mark getMark() { + return mark; + } - public int getIndex() { - return index; - } + public int getIndex() { + return index; + } - public int getLine() { - return line; - } + public int getLine() { + return line; + } - public boolean isRequired() { - return required; - } + public boolean isRequired() { + return required; + } - @Override - public String toString() { - return "SimpleKey - tokenNumber=" + tokenNumber + " required=" + required + " index=" - + index + " line=" + line + " column=" + column; - } + @Override + public String toString() { + return "SimpleKey - tokenNumber=" + tokenNumber + " required=" + required + " index=" + index + + " line=" + line + " column=" + column; + } } diff --git a/src/main/java/org/yaml/snakeyaml/serializer/AnchorGenerator.java b/src/main/java/org/yaml/snakeyaml/serializer/AnchorGenerator.java index 2308eb59..6674901e 100644 --- a/src/main/java/org/yaml/snakeyaml/serializer/AnchorGenerator.java +++ b/src/main/java/org/yaml/snakeyaml/serializer/AnchorGenerator.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.serializer; @@ -19,5 +17,5 @@ import org.yaml.snakeyaml.nodes.Node; public interface AnchorGenerator { - String nextAnchor(Node node); + String nextAnchor(Node node); } diff --git a/src/main/java/org/yaml/snakeyaml/serializer/NumberAnchorGenerator.java b/src/main/java/org/yaml/snakeyaml/serializer/NumberAnchorGenerator.java index 2f316f88..b33ad9a1 100644 --- a/src/main/java/org/yaml/snakeyaml/serializer/NumberAnchorGenerator.java +++ b/src/main/java/org/yaml/snakeyaml/serializer/NumberAnchorGenerator.java @@ -1,39 +1,36 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.serializer; -import org.yaml.snakeyaml.nodes.Node; - import java.text.NumberFormat; +import org.yaml.snakeyaml.nodes.Node; public class NumberAnchorGenerator implements AnchorGenerator { - private int lastAnchorId = 0; + private int lastAnchorId = 0; - public NumberAnchorGenerator(int lastAnchorId) { - this.lastAnchorId = lastAnchorId; - } + public NumberAnchorGenerator(int lastAnchorId) { + this.lastAnchorId = lastAnchorId; + } - public String nextAnchor(Node node) { - this.lastAnchorId++; - NumberFormat format = NumberFormat.getNumberInstance(); - format.setMinimumIntegerDigits(3); - format.setMaximumFractionDigits(0);// issue 172 - format.setGroupingUsed(false); - String anchorId = format.format(this.lastAnchorId); - return "id" + anchorId; - } + public String nextAnchor(Node node) { + this.lastAnchorId++; + NumberFormat format = NumberFormat.getNumberInstance(); + format.setMinimumIntegerDigits(3); + format.setMaximumFractionDigits(0);// issue 172 + format.setGroupingUsed(false); + String anchorId = format.format(this.lastAnchorId); + return "id" + anchorId; + } } diff --git a/src/main/java/org/yaml/snakeyaml/serializer/Serializer.java b/src/main/java/org/yaml/snakeyaml/serializer/Serializer.java index 2decf3f3..7d394308 100644 --- a/src/main/java/org/yaml/snakeyaml/serializer/Serializer.java +++ b/src/main/java/org/yaml/snakeyaml/serializer/Serializer.java @@ -1,32 +1,30 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.serializer; import java.io.IOException; -import java.text.NumberFormat; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; - import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions.Version; +import org.yaml.snakeyaml.comments.CommentLine; import org.yaml.snakeyaml.emitter.Emitable; import org.yaml.snakeyaml.events.AliasEvent; +import org.yaml.snakeyaml.events.CommentEvent; import org.yaml.snakeyaml.events.DocumentEndEvent; import org.yaml.snakeyaml.events.DocumentStartEvent; import org.yaml.snakeyaml.events.ImplicitTuple; @@ -38,7 +36,6 @@ import org.yaml.snakeyaml.events.SequenceStartEvent; import org.yaml.snakeyaml.events.StreamEndEvent; import org.yaml.snakeyaml.events.StreamStartEvent; import org.yaml.snakeyaml.nodes.AnchorNode; -import org.yaml.snakeyaml.nodes.CollectionNode; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeId; @@ -49,153 +46,181 @@ import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.resolver.Resolver; public final class Serializer { - private final Emitable emitter; - private final Resolver resolver; - private boolean explicitStart; - private boolean explicitEnd; - private Version useVersion; - private Map<String, String> useTags; - private Set<Node> serializedNodes; - private Map<Node, String> anchors; - private AnchorGenerator anchorGenerator; - private Boolean closed; - private Tag explicitRoot; - public Serializer(Emitable emitter, Resolver resolver, DumperOptions opts, Tag rootTag) { - this.emitter = emitter; - this.resolver = resolver; - this.explicitStart = opts.isExplicitStart(); - this.explicitEnd = opts.isExplicitEnd(); - if (opts.getVersion() != null) { - this.useVersion = opts.getVersion(); - } - this.useTags = opts.getTags(); - this.serializedNodes = new HashSet<Node>(); - this.anchors = new HashMap<Node, String>(); - this.anchorGenerator = opts.getAnchorGenerator(); - this.closed = null; - this.explicitRoot = rootTag; + private final Emitable emitter; + private final Resolver resolver; + private final boolean explicitStart; + private final boolean explicitEnd; + private Version useVersion; + private final Map<String, String> useTags; + private final Set<Node> serializedNodes; + private final Map<Node, String> anchors; + private final AnchorGenerator anchorGenerator; + private Boolean closed; + private final Tag explicitRoot; + + public Serializer(Emitable emitter, Resolver resolver, DumperOptions opts, Tag rootTag) { + this.emitter = emitter; + this.resolver = resolver; + this.explicitStart = opts.isExplicitStart(); + this.explicitEnd = opts.isExplicitEnd(); + if (opts.getVersion() != null) { + this.useVersion = opts.getVersion(); } + this.useTags = opts.getTags(); + this.serializedNodes = new HashSet<Node>(); + this.anchors = new HashMap<Node, String>(); + this.anchorGenerator = opts.getAnchorGenerator(); + this.closed = null; + this.explicitRoot = rootTag; + } - public void open() throws IOException { - if (closed == null) { - this.emitter.emit(new StreamStartEvent(null, null)); - this.closed = Boolean.FALSE; - } else if (Boolean.TRUE.equals(closed)) { - throw new SerializerException("serializer is closed"); - } else { - throw new SerializerException("serializer is already opened"); - } + public void open() throws IOException { + if (closed == null) { + this.emitter.emit(new StreamStartEvent(null, null)); + this.closed = Boolean.FALSE; + } else if (Boolean.TRUE.equals(closed)) { + throw new SerializerException("serializer is closed"); + } else { + throw new SerializerException("serializer is already opened"); } + } - public void close() throws IOException { - if (closed == null) { - throw new SerializerException("serializer is not opened"); - } else if (!Boolean.TRUE.equals(closed)) { - this.emitter.emit(new StreamEndEvent(null, null)); - this.closed = Boolean.TRUE; - } + public void close() throws IOException { + if (closed == null) { + throw new SerializerException("serializer is not opened"); + } else if (!Boolean.TRUE.equals(closed)) { + this.emitter.emit(new StreamEndEvent(null, null)); + this.closed = Boolean.TRUE; + // release unused resources + this.serializedNodes.clear(); + this.anchors.clear(); } + } - public void serialize(Node node) throws IOException { - if (closed == null) { - throw new SerializerException("serializer is not opened"); - } else if (closed) { - throw new SerializerException("serializer is closed"); - } - this.emitter.emit(new DocumentStartEvent(null, null, this.explicitStart, this.useVersion, - useTags)); - anchorNode(node); - if (explicitRoot != null) { - node.setTag(explicitRoot); - } - serializeNode(node, null); - this.emitter.emit(new DocumentEndEvent(null, null, this.explicitEnd)); - this.serializedNodes.clear(); - this.anchors.clear(); + public void serialize(Node node) throws IOException { + if (closed == null) { + throw new SerializerException("serializer is not opened"); + } else if (closed) { + throw new SerializerException("serializer is closed"); + } + this.emitter + .emit(new DocumentStartEvent(null, null, this.explicitStart, this.useVersion, useTags)); + anchorNode(node); + if (explicitRoot != null) { + node.setTag(explicitRoot); } + serializeNode(node, null); + this.emitter.emit(new DocumentEndEvent(null, null, this.explicitEnd)); + this.serializedNodes.clear(); + this.anchors.clear(); + } - private void anchorNode(Node node) { - if (node.getNodeId() == NodeId.anchor) { - node = ((AnchorNode) node).getRealNode(); - } - if (this.anchors.containsKey(node)) { - String anchor = this.anchors.get(node); - if (null == anchor) { - anchor = this.anchorGenerator.nextAnchor(node); - this.anchors.put(node, anchor); - } - } else { - this.anchors.put(node, null); - switch (node.getNodeId()) { - case sequence: - SequenceNode seqNode = (SequenceNode) node; - List<Node> list = seqNode.getValue(); - for (Node item : list) { - anchorNode(item); - } - break; - case mapping: - MappingNode mnode = (MappingNode) node; - List<NodeTuple> map = mnode.getValue(); - for (NodeTuple object : map) { - Node key = object.getKeyNode(); - Node value = object.getValueNode(); - anchorNode(key); - anchorNode(value); - } - break; - } - } + private void anchorNode(Node node) { + if (node.getNodeId() == NodeId.anchor) { + node = ((AnchorNode) node).getRealNode(); } + if (this.anchors.containsKey(node)) { + String anchor = this.anchors.get(node); + if (null == anchor) { + anchor = this.anchorGenerator.nextAnchor(node); + this.anchors.put(node, anchor); + } + } else { + this.anchors.put(node, + node.getAnchor() != null ? this.anchorGenerator.nextAnchor(node) : null); + switch (node.getNodeId()) { + case sequence: + SequenceNode seqNode = (SequenceNode) node; + List<Node> list = seqNode.getValue(); + for (Node item : list) { + anchorNode(item); + } + break; + case mapping: + MappingNode mnode = (MappingNode) node; + List<NodeTuple> map = mnode.getValue(); + for (NodeTuple object : map) { + Node key = object.getKeyNode(); + Node value = object.getValueNode(); + anchorNode(key); + anchorNode(value); + } + break; + } + } + } - private void serializeNode(Node node, Node parent) throws IOException { - if (node.getNodeId() == NodeId.anchor) { - node = ((AnchorNode) node).getRealNode(); - } - String tAlias = this.anchors.get(node); - if (this.serializedNodes.contains(node)) { - this.emitter.emit(new AliasEvent(tAlias, null, null)); - } else { - this.serializedNodes.add(node); - switch (node.getNodeId()) { - case scalar: - ScalarNode scalarNode = (ScalarNode) node; - Tag detectedTag = this.resolver.resolve(NodeId.scalar, scalarNode.getValue(), true); - Tag defaultTag = this.resolver.resolve(NodeId.scalar, scalarNode.getValue(), false); - ImplicitTuple tuple = new ImplicitTuple(node.getTag().equals(detectedTag), node - .getTag().equals(defaultTag)); - ScalarEvent event = new ScalarEvent(tAlias, node.getTag().getValue(), tuple, - scalarNode.getValue(), null, null, scalarNode.getStyle()); - this.emitter.emit(event); - break; - case sequence: - SequenceNode seqNode = (SequenceNode) node; - boolean implicitS = node.getTag().equals(this.resolver.resolve(NodeId.sequence, - null, true)); - this.emitter.emit(new SequenceStartEvent(tAlias, node.getTag().getValue(), - implicitS, null, null, seqNode.getFlowStyle())); - List<Node> list = seqNode.getValue(); - for (Node item : list) { - serializeNode(item, node); - } - this.emitter.emit(new SequenceEndEvent(null, null)); - break; - default:// instance of MappingNode - Tag implicitTag = this.resolver.resolve(NodeId.mapping, null, true); - boolean implicitM = node.getTag().equals(implicitTag); - this.emitter.emit(new MappingStartEvent(tAlias, node.getTag().getValue(), - implicitM, null, null, ((CollectionNode) node).getFlowStyle())); - MappingNode mnode = (MappingNode) node; - List<NodeTuple> map = mnode.getValue(); - for (NodeTuple row : map) { - Node key = row.getKeyNode(); - Node value = row.getValueNode(); - serializeNode(key, mnode); - serializeNode(value, mnode); - } - this.emitter.emit(new MappingEndEvent(null, null)); + // parent Node is not used but might be used in the future + private void serializeNode(Node node, Node parent) throws IOException { + if (node.getNodeId() == NodeId.anchor) { + node = ((AnchorNode) node).getRealNode(); + } + String tAlias = this.anchors.get(node); + if (this.serializedNodes.contains(node)) { + this.emitter.emit(new AliasEvent(tAlias, null, null)); + } else { + this.serializedNodes.add(node); + switch (node.getNodeId()) { + case scalar: + ScalarNode scalarNode = (ScalarNode) node; + serializeComments(node.getBlockComments()); + Tag detectedTag = this.resolver.resolve(NodeId.scalar, scalarNode.getValue(), true); + Tag defaultTag = this.resolver.resolve(NodeId.scalar, scalarNode.getValue(), false); + ImplicitTuple tuple = new ImplicitTuple(node.getTag().equals(detectedTag), + node.getTag().equals(defaultTag)); + ScalarEvent event = new ScalarEvent(tAlias, node.getTag().getValue(), tuple, + scalarNode.getValue(), null, null, scalarNode.getScalarStyle()); + this.emitter.emit(event); + serializeComments(node.getInLineComments()); + serializeComments(node.getEndComments()); + break; + case sequence: + SequenceNode seqNode = (SequenceNode) node; + serializeComments(node.getBlockComments()); + boolean implicitS = + node.getTag().equals(this.resolver.resolve(NodeId.sequence, null, true)); + this.emitter.emit(new SequenceStartEvent(tAlias, node.getTag().getValue(), implicitS, + null, null, seqNode.getFlowStyle())); + List<Node> list = seqNode.getValue(); + for (Node item : list) { + serializeNode(item, node); + } + this.emitter.emit(new SequenceEndEvent(null, null)); + serializeComments(node.getInLineComments()); + serializeComments(node.getEndComments()); + break; + default:// instance of MappingNode + serializeComments(node.getBlockComments()); + Tag implicitTag = this.resolver.resolve(NodeId.mapping, null, true); + boolean implicitM = node.getTag().equals(implicitTag); + MappingNode mnode = (MappingNode) node; + List<NodeTuple> map = mnode.getValue(); + if (mnode.getTag() != Tag.COMMENT) { + this.emitter.emit(new MappingStartEvent(tAlias, mnode.getTag().getValue(), implicitM, + null, null, mnode.getFlowStyle())); + for (NodeTuple row : map) { + Node key = row.getKeyNode(); + Node value = row.getValueNode(); + serializeNode(key, mnode); + serializeNode(value, mnode); } - } + this.emitter.emit(new MappingEndEvent(null, null)); + serializeComments(node.getInLineComments()); + serializeComments(node.getEndComments()); + } + } + } + } + + private void serializeComments(List<CommentLine> comments) throws IOException { + if (comments == null) { + return; + } + for (CommentLine line : comments) { + CommentEvent commentEvent = new CommentEvent(line.getCommentType(), line.getValue(), + line.getStartMark(), line.getEndMark()); + this.emitter.emit(commentEvent); } + } } diff --git a/src/main/java/org/yaml/snakeyaml/serializer/SerializerException.java b/src/main/java/org/yaml/snakeyaml/serializer/SerializerException.java index 0cb6e887..21e9f38f 100644 --- a/src/main/java/org/yaml/snakeyaml/serializer/SerializerException.java +++ b/src/main/java/org/yaml/snakeyaml/serializer/SerializerException.java @@ -1,26 +1,25 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.serializer; import org.yaml.snakeyaml.error.YAMLException; public class SerializerException extends YAMLException { - private static final long serialVersionUID = 2632638197498912433L; - public SerializerException(String message) { - super(message); - } + private static final long serialVersionUID = 2632638197498912433L; + + public SerializerException(String message) { + super(message); + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/AliasToken.java b/src/main/java/org/yaml/snakeyaml/tokens/AliasToken.java index df2ee2e9..be8a5144 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/AliasToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/AliasToken.java @@ -1,41 +1,35 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; import org.yaml.snakeyaml.error.Mark; public final class AliasToken extends Token { - private final String value; - public AliasToken(String value, Mark startMark, Mark endMark) { - super(startMark, endMark); - this.value = value; - } + private final String value; - public String getValue() { - return this.value; - } + public AliasToken(String value, Mark startMark, Mark endMark) { + super(startMark, endMark); + this.value = value; + } - @Override - protected String getArguments() { - return "value=" + value; - } + public String getValue() { + return this.value; + } - @Override - public Token.ID getTokenId() { - return ID.Alias; - } + @Override + public Token.ID getTokenId() { + return ID.Alias; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/AnchorToken.java b/src/main/java/org/yaml/snakeyaml/tokens/AnchorToken.java index 3629eeae..6d10cf71 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/AnchorToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/AnchorToken.java @@ -1,41 +1,35 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; import org.yaml.snakeyaml.error.Mark; public final class AnchorToken extends Token { - private final String value; - public AnchorToken(String value, Mark startMark, Mark endMark) { - super(startMark, endMark); - this.value = value; - } + private final String value; - public String getValue() { - return this.value; - } + public AnchorToken(String value, Mark startMark, Mark endMark) { + super(startMark, endMark); + this.value = value; + } - @Override - protected String getArguments() { - return "value=" + value; - } + public String getValue() { + return this.value; + } - @Override - public Token.ID getTokenId() { - return ID.Anchor; - } + @Override + public Token.ID getTokenId() { + return ID.Anchor; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/BlockEndToken.java b/src/main/java/org/yaml/snakeyaml/tokens/BlockEndToken.java index 3315bc40..0f994154 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/BlockEndToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/BlockEndToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class BlockEndToken extends Token { - public BlockEndToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public BlockEndToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.BlockEnd; - } + @Override + public Token.ID getTokenId() { + return ID.BlockEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/BlockEntryToken.java b/src/main/java/org/yaml/snakeyaml/tokens/BlockEntryToken.java index 574445f0..3657ae70 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/BlockEntryToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/BlockEntryToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class BlockEntryToken extends Token { - public BlockEntryToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public BlockEntryToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.BlockEntry; - } + @Override + public Token.ID getTokenId() { + return ID.BlockEntry; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/BlockMappingStartToken.java b/src/main/java/org/yaml/snakeyaml/tokens/BlockMappingStartToken.java index 95a61642..4a35c917 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/BlockMappingStartToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/BlockMappingStartToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class BlockMappingStartToken extends Token { - public BlockMappingStartToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public BlockMappingStartToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.BlockMappingStart; - } + @Override + public Token.ID getTokenId() { + return ID.BlockMappingStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/BlockSequenceStartToken.java b/src/main/java/org/yaml/snakeyaml/tokens/BlockSequenceStartToken.java index d70194c7..3ea7a20d 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/BlockSequenceStartToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/BlockSequenceStartToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class BlockSequenceStartToken extends Token { - public BlockSequenceStartToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public BlockSequenceStartToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.BlockSequenceStart; - } + @Override + public Token.ID getTokenId() { + return ID.BlockSequenceStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/CommentToken.java b/src/main/java/org/yaml/snakeyaml/tokens/CommentToken.java index 12c067ef..36fe079e 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/CommentToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/CommentToken.java @@ -1,29 +1,45 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; +import java.util.Objects; +import org.yaml.snakeyaml.comments.CommentType; import org.yaml.snakeyaml.error.Mark; -public class CommentToken extends Token { - public CommentToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } +public final class CommentToken extends Token { - @Override - public ID getTokenId() { - return ID.Comment; - } + private final CommentType type; + private final String value; + + public CommentToken(CommentType type, String value, Mark startMark, Mark endMark) { + super(startMark, endMark); + Objects.requireNonNull(type); + this.type = type; + Objects.requireNonNull(value); + this.value = value; + } + + public CommentType getCommentType() { + return this.type; + } + + public String getValue() { + return this.value; + } + + @Override + public Token.ID getTokenId() { + return ID.Comment; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/DirectiveToken.java b/src/main/java/org/yaml/snakeyaml/tokens/DirectiveToken.java index af1743f8..d6d53035 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/DirectiveToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/DirectiveToken.java @@ -1,58 +1,46 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; import java.util.List; - import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.YAMLException; public final class DirectiveToken<T> extends Token { - private final String name; - private final List<T> value; - public DirectiveToken(String name, List<T> value, Mark startMark, Mark endMark) { - super(startMark, endMark); - this.name = name; - if (value != null && value.size() != 2) { - throw new YAMLException("Two strings must be provided instead of " - + String.valueOf(value.size())); - } - this.value = value; - } + private final String name; + private final List<T> value; - public String getName() { - return this.name; + public DirectiveToken(String name, List<T> value, Mark startMark, Mark endMark) { + super(startMark, endMark); + this.name = name; + if (value != null && value.size() != 2) { + throw new YAMLException("Two strings must be provided instead of " + value.size()); } + this.value = value; + } - public List<T> getValue() { - return this.value; - } + public String getName() { + return this.name; + } - @Override - protected String getArguments() { - if (value != null) { - return "name=" + name + ", value=[" + value.get(0) + ", " + value.get(1) + "]"; - } else { - return "name=" + name; - } - } + public List<T> getValue() { + return this.value; + } - @Override - public Token.ID getTokenId() { - return ID.Directive; - } + @Override + public Token.ID getTokenId() { + return ID.Directive; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/DocumentEndToken.java b/src/main/java/org/yaml/snakeyaml/tokens/DocumentEndToken.java index ee17dab1..4c2319c3 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/DocumentEndToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/DocumentEndToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class DocumentEndToken extends Token { - public DocumentEndToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public DocumentEndToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.DocumentEnd; - } + @Override + public Token.ID getTokenId() { + return ID.DocumentEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/DocumentStartToken.java b/src/main/java/org/yaml/snakeyaml/tokens/DocumentStartToken.java index 0b72deb3..1ce5445d 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/DocumentStartToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/DocumentStartToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class DocumentStartToken extends Token { - public DocumentStartToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public DocumentStartToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.DocumentStart; - } + @Override + public Token.ID getTokenId() { + return ID.DocumentStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/FlowEntryToken.java b/src/main/java/org/yaml/snakeyaml/tokens/FlowEntryToken.java index b1afb0f5..03e5c9ff 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/FlowEntryToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/FlowEntryToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class FlowEntryToken extends Token { - public FlowEntryToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public FlowEntryToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.FlowEntry; - } + @Override + public Token.ID getTokenId() { + return ID.FlowEntry; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingEndToken.java b/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingEndToken.java index 1659a9f0..8ec5dd4c 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingEndToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingEndToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class FlowMappingEndToken extends Token { - public FlowMappingEndToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public FlowMappingEndToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.FlowMappingEnd; - } + @Override + public Token.ID getTokenId() { + return ID.FlowMappingEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingStartToken.java b/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingStartToken.java index 5a984c72..8ee9814b 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingStartToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/FlowMappingStartToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class FlowMappingStartToken extends Token { - public FlowMappingStartToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public FlowMappingStartToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.FlowMappingStart; - } + @Override + public Token.ID getTokenId() { + return ID.FlowMappingStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceEndToken.java b/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceEndToken.java index 39b03c43..cc991d42 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceEndToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceEndToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class FlowSequenceEndToken extends Token { - public FlowSequenceEndToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public FlowSequenceEndToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.FlowSequenceEnd; - } + @Override + public Token.ID getTokenId() { + return ID.FlowSequenceEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceStartToken.java b/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceStartToken.java index da89785e..1e2de015 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceStartToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/FlowSequenceStartToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class FlowSequenceStartToken extends Token { - public FlowSequenceStartToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public FlowSequenceStartToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.FlowSequenceStart; - } + @Override + public Token.ID getTokenId() { + return ID.FlowSequenceStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/KeyToken.java b/src/main/java/org/yaml/snakeyaml/tokens/KeyToken.java index 0f880438..2b36521c 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/KeyToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/KeyToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class KeyToken extends Token { - public KeyToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public KeyToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.Key; - } + @Override + public Token.ID getTokenId() { + return ID.Key; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/ScalarToken.java b/src/main/java/org/yaml/snakeyaml/tokens/ScalarToken.java index 828189e8..5a6ccd51 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/ScalarToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/ScalarToken.java @@ -1,57 +1,56 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.error.Mark; public final class ScalarToken extends Token { - private final String value; - private final boolean plain; - private final char style; - public ScalarToken(String value, Mark startMark, Mark endMark, boolean plain) { - this(value, plain, startMark, endMark, (char) 0); + private final String value; + private final boolean plain; + private final DumperOptions.ScalarStyle style; + + public ScalarToken(String value, Mark startMark, Mark endMark, boolean plain) { + this(value, plain, startMark, endMark, DumperOptions.ScalarStyle.PLAIN); + } + + public ScalarToken(String value, boolean plain, Mark startMark, Mark endMark, + DumperOptions.ScalarStyle style) { + super(startMark, endMark); + this.value = value; + this.plain = plain; + if (style == null) { + throw new NullPointerException("Style must be provided."); } + this.style = style; + } - public ScalarToken(String value, boolean plain, Mark startMark, Mark endMark, char style) { - super(startMark, endMark); - this.value = value; - this.plain = plain; - this.style = style; - } - - public boolean getPlain() { - return this.plain; - } - - public String getValue() { - return this.value; - } + public boolean getPlain() { + return this.plain; + } - public char getStyle() { - return this.style; - } + public String getValue() { + return this.value; + } - @Override - protected String getArguments() { - return "value=" + value + ", plain=" + plain + ", style=" + style; - } + public DumperOptions.ScalarStyle getStyle() { + return this.style; + } - @Override - public Token.ID getTokenId() { - return ID.Scalar; - } + @Override + public Token.ID getTokenId() { + return ID.Scalar; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/StreamEndToken.java b/src/main/java/org/yaml/snakeyaml/tokens/StreamEndToken.java index ece87b95..4998ee32 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/StreamEndToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/StreamEndToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class StreamEndToken extends Token { - public StreamEndToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public StreamEndToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.StreamEnd; - } + @Override + public Token.ID getTokenId() { + return ID.StreamEnd; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/StreamStartToken.java b/src/main/java/org/yaml/snakeyaml/tokens/StreamStartToken.java index 4b5419a6..f085f45c 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/StreamStartToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/StreamStartToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class StreamStartToken extends Token { - public StreamStartToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public StreamStartToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.StreamStart; - } + @Override + public Token.ID getTokenId() { + return ID.StreamStart; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/TagToken.java b/src/main/java/org/yaml/snakeyaml/tokens/TagToken.java index 505a360f..affa00a1 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/TagToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/TagToken.java @@ -1,41 +1,35 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; import org.yaml.snakeyaml.error.Mark; public final class TagToken extends Token { - private final TagTuple value; - public TagToken(TagTuple value, Mark startMark, Mark endMark) { - super(startMark, endMark); - this.value = value; - } + private final TagTuple value; - public TagTuple getValue() { - return this.value; - } + public TagToken(TagTuple value, Mark startMark, Mark endMark) { + super(startMark, endMark); + this.value = value; + } - @Override - protected String getArguments() { - return "value=[" + value.getHandle() + ", " + value.getSuffix() + "]"; - } + public TagTuple getValue() { + return this.value; + } - @Override - public Token.ID getTokenId() { - return ID.Tag; - } + @Override + public Token.ID getTokenId() { + return ID.Tag; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/TagTuple.java b/src/main/java/org/yaml/snakeyaml/tokens/TagTuple.java index b4ea0646..dd0ef388 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/TagTuple.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/TagTuple.java @@ -1,37 +1,36 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; public final class TagTuple { - private final String handle; - private final String suffix; - public TagTuple(String handle, String suffix) { - if (suffix == null) { - throw new NullPointerException("Suffix must be provided."); - } - this.handle = handle; - this.suffix = suffix; - } + private final String handle; + private final String suffix; - public String getHandle() { - return handle; + public TagTuple(String handle, String suffix) { + if (suffix == null) { + throw new NullPointerException("Suffix must be provided."); } + this.handle = handle; + this.suffix = suffix; + } - public String getSuffix() { - return suffix; - } + public String getHandle() { + return handle; + } + + public String getSuffix() { + return suffix; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/Token.java b/src/main/java/org/yaml/snakeyaml/tokens/Token.java index 8b583f5d..63a49271 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/Token.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/Token.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,64 +17,53 @@ import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.error.YAMLException; public abstract class Token { - public enum ID { - Alias, Anchor, BlockEnd, BlockEntry, BlockMappingStart, BlockSequenceStart, Directive, DocumentEnd, DocumentStart, FlowEntry, FlowMappingEnd, FlowMappingStart, FlowSequenceEnd, FlowSequenceStart, Key, Scalar, StreamEnd, StreamStart, Tag, Value, Whitespace, Comment, Error - } - private final Mark startMark; - private final Mark endMark; + public enum ID { + Alias("<alias>"), Anchor("<anchor>"), BlockEnd("<block end>"), BlockEntry( + "-"), BlockMappingStart("<block mapping start>"), BlockSequenceStart( + "<block sequence start>"), Directive("<directive>"), DocumentEnd( + "<document end>"), DocumentStart("<document start>"), FlowEntry( + ","), FlowMappingEnd("}"), FlowMappingStart("{"), FlowSequenceEnd( + "]"), FlowSequenceStart("["), Key("?"), Scalar("<scalar>"), StreamEnd( + "<stream end>"), StreamStart("<stream start>"), Tag("<tag>"), Value( + ":"), Whitespace("<whitespace>"), Comment("#"), Error("<error>"); - public Token(Mark startMark, Mark endMark) { - if (startMark == null || endMark == null) { - throw new YAMLException("Token requires marks."); - } - this.startMark = startMark; - this.endMark = endMark; - } + private final String description; - public String toString() { - return "<" + this.getClass().getName() + "(" + getArguments() + ")>"; + ID(String s) { + description = s; } - public Mark getStartMark() { - return startMark; + @Override + public String toString() { + return description; } + } - public Mark getEndMark() { - return endMark; - } + private final Mark startMark; + private final Mark endMark; - /** - * @see "__repr__ for Token in PyYAML" - */ - protected String getArguments() { - return ""; + public Token(Mark startMark, Mark endMark) { + if (startMark == null || endMark == null) { + throw new YAMLException("Token requires marks."); } + this.startMark = startMark; + this.endMark = endMark; + } - /** - * For error reporting. - * - * @see "class variable 'id' in PyYAML" - */ - public abstract Token.ID getTokenId(); + public Mark getStartMark() { + return startMark; + } - /* - * for tests only - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof Token) { - return toString().equals(obj.toString()); - } else { - return false; - } - } + public Mark getEndMark() { + return endMark; + } - /* - * for tests only - */ - @Override - public int hashCode() { - return toString().hashCode(); - } + /** + * For error reporting. + * + * @see "class variable 'id' in PyYAML" + * @return ID of this token + */ + public abstract Token.ID getTokenId(); } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/ValueToken.java b/src/main/java/org/yaml/snakeyaml/tokens/ValueToken.java index 58fe0576..504f1c6f 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/ValueToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/ValueToken.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; @@ -19,12 +17,12 @@ import org.yaml.snakeyaml.error.Mark; public final class ValueToken extends Token { - public ValueToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } + public ValueToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } - @Override - public Token.ID getTokenId() { - return ID.Value; - } + @Override + public Token.ID getTokenId() { + return ID.Value; + } } diff --git a/src/main/java/org/yaml/snakeyaml/tokens/WhitespaceToken.java b/src/main/java/org/yaml/snakeyaml/tokens/WhitespaceToken.java index 65af212d..f61de83e 100644 --- a/src/main/java/org/yaml/snakeyaml/tokens/WhitespaceToken.java +++ b/src/main/java/org/yaml/snakeyaml/tokens/WhitespaceToken.java @@ -1,29 +1,32 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.tokens; import org.yaml.snakeyaml.error.Mark; +/** + * @deprecated it will be removed because it is not used + */ +@Deprecated public class WhitespaceToken extends Token { - public WhitespaceToken(Mark startMark, Mark endMark) { - super(startMark, endMark); - } - @Override - public ID getTokenId() { - return ID.Whitespace; - } + public WhitespaceToken(Mark startMark, Mark endMark) { + super(startMark, endMark); + } + + @Override + public ID getTokenId() { + return ID.Whitespace; + } } diff --git a/src/main/java/org/yaml/snakeyaml/util/ArrayStack.java b/src/main/java/org/yaml/snakeyaml/util/ArrayStack.java index 4bab182a..d0782d15 100644 --- a/src/main/java/org/yaml/snakeyaml/util/ArrayStack.java +++ b/src/main/java/org/yaml/snakeyaml/util/ArrayStack.java @@ -1,42 +1,41 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.util; import java.util.ArrayList; public class ArrayStack<T> { - private ArrayList<T> stack; - public ArrayStack(int initSize) { - stack = new ArrayList<T>(initSize); - } + private final ArrayList<T> stack; - public void push(T obj) { - stack.add(obj); - } + public ArrayStack(int initSize) { + stack = new ArrayList<T>(initSize); + } - public T pop() { - return stack.remove(stack.size() - 1); - } + public void push(T obj) { + stack.add(obj); + } - public boolean isEmpty() { - return stack.isEmpty(); - } + public T pop() { + return stack.remove(stack.size() - 1); + } - public void clear() { - stack.clear(); - } + public boolean isEmpty() { + return stack.isEmpty(); + } + + public void clear() { + stack.clear(); + } } diff --git a/src/main/java/org/yaml/snakeyaml/util/ArrayUtils.java b/src/main/java/org/yaml/snakeyaml/util/ArrayUtils.java new file mode 100644 index 00000000..9d0aa10d --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/util/ArrayUtils.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.util; + +import java.util.AbstractList; +import java.util.Collections; +import java.util.List; + +public class ArrayUtils { + + private ArrayUtils() {} + + /** + * Returns an unmodifiable {@code List} backed by the given array. The method doesn't copy the + * array, so the changes to the array will affect the {@code List} as well. + * + * @param <E> class of the elements in the array + * @param elements - array to convert + * @return {@code List} backed by the given array + */ + public static <E> List<E> toUnmodifiableList(E[] elements) { + return elements.length == 0 ? Collections.<E>emptyList() + : new UnmodifiableArrayList<E>(elements); + } + + /** + * Returns an unmodifiable {@code List} containing the second array appended to the first one. The + * method doesn't copy the arrays, so the changes to the arrays will affect the {@code List} as + * well. + * + * @param <E> class of the elements in the array + * @param array1 - the array to extend + * @param array2 - the array to add to the first + * @return {@code List} backed by the given arrays + */ + public static <E> List<E> toUnmodifiableCompositeList(E[] array1, E[] array2) { + List<E> result; + if (array1.length == 0) { + result = toUnmodifiableList(array2); + } else if (array2.length == 0) { + result = toUnmodifiableList(array1); + } else { + result = new CompositeUnmodifiableArrayList<E>(array1, array2); + } + return result; + } + + private static class UnmodifiableArrayList<E> extends AbstractList<E> { + + private final E[] array; + + UnmodifiableArrayList(E[] array) { + this.array = array; + } + + @Override + public E get(int index) { + if (index >= array.length) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size()); + } + return array[index]; + } + + @Override + public int size() { + return array.length; + } + } + + private static class CompositeUnmodifiableArrayList<E> extends AbstractList<E> { + + private final E[] array1; + private final E[] array2; + + CompositeUnmodifiableArrayList(E[] array1, E[] array2) { + this.array1 = array1; + this.array2 = array2; + } + + @Override + public E get(int index) { + E element; + if (index < array1.length) { + element = array1[index]; + } else if (index - array1.length < array2.length) { + element = array2[index - array1.length]; + } else { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size()); + } + return element; + } + + @Override + public int size() { + return array1.length + array2.length; + } + } +} diff --git a/src/main/java/org/yaml/snakeyaml/util/EnumUtils.java b/src/main/java/org/yaml/snakeyaml/util/EnumUtils.java new file mode 100644 index 00000000..da2eb3ed --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/util/EnumUtils.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.util; + +public class EnumUtils { + + /** + * Looks for an enumeration constant that matches the string without being case sensitive + * + * @param enumType - the Class object of the enum type from which to return a constant + * @param name - the name of the constant to return + * @param <T> - the enum type whose constant is to be returned + * @return the enum constant of the specified enum type with the specified name, insensitive to + * case + * @throws IllegalArgumentException – if the specified enum type has no constant with the + * specified name, insensitive case + */ + public static <T extends Enum<T>> T findEnumInsensitiveCase(Class<T> enumType, String name) { + for (T constant : enumType.getEnumConstants()) { + if (constant.name().compareToIgnoreCase(name) == 0) { + return constant; + } + } + throw new IllegalArgumentException( + "No enum constant " + enumType.getCanonicalName() + "." + name); + } +} diff --git a/src/main/java/org/yaml/snakeyaml/util/PlatformFeatureDetector.java b/src/main/java/org/yaml/snakeyaml/util/PlatformFeatureDetector.java new file mode 100644 index 00000000..8d8f0757 --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/util/PlatformFeatureDetector.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.yaml.snakeyaml.util; + +public class PlatformFeatureDetector { + + private Boolean isRunningOnAndroid = null; + + public boolean isRunningOnAndroid() { + if (isRunningOnAndroid == null) { + String name = System.getProperty("java.runtime.name"); + isRunningOnAndroid = (name != null && name.startsWith("Android Runtime")); + } + return isRunningOnAndroid; + } +} diff --git a/src/main/java/org/yaml/snakeyaml/util/UriEncoder.java b/src/main/java/org/yaml/snakeyaml/util/UriEncoder.java index e23904ff..02c3e434 100644 --- a/src/main/java/org/yaml/snakeyaml/util/UriEncoder.java +++ b/src/main/java/org/yaml/snakeyaml/util/UriEncoder.java @@ -1,17 +1,15 @@ /** - * Copyright (c) 2008, http://www.snakeyaml.org + * Copyright (c) 2008, SnakeYAML * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. */ package org.yaml.snakeyaml.util; @@ -20,43 +18,50 @@ import java.net.URLDecoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; -import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; - +import java.nio.charset.StandardCharsets; import org.yaml.snakeyaml.error.YAMLException; import org.yaml.snakeyaml.external.com.google.gdata.util.common.base.Escaper; import org.yaml.snakeyaml.external.com.google.gdata.util.common.base.PercentEscaper; public abstract class UriEncoder { - private static final CharsetDecoder UTF8Decoder = Charset.forName("UTF-8").newDecoder() - .onMalformedInput(CodingErrorAction.REPORT); - // Include the [] chars to the SAFEPATHCHARS_URLENCODER to avoid - // its escape as required by spec. See - // http://yaml.org/spec/1.1/#escaping%20in%20URI/ - private static final String SAFE_CHARS = PercentEscaper.SAFEPATHCHARS_URLENCODER + "[]/"; - private static final Escaper escaper = new PercentEscaper(SAFE_CHARS, false); - /** - * Escape special characters with '%' - */ - public static String encode(String uri) { - return escaper.escape(uri); - } + private static final CharsetDecoder UTF8Decoder = + StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT); + // Include the [] chars to the SAFEPATHCHARS_URLENCODER to avoid + // its escape as required by spec. See + // http://yaml.org/spec/1.1/#escaping%20in%20URI/ + private static final String SAFE_CHARS = PercentEscaper.SAFEPATHCHARS_URLENCODER + "[]/"; + private static final Escaper escaper = new PercentEscaper(SAFE_CHARS, false); - /** - * Decode '%'-escaped characters. Decoding fails in case of invalid UTF-8 - */ - public static String decode(ByteBuffer buff) throws CharacterCodingException { - CharBuffer chars = UTF8Decoder.decode(buff); - return chars.toString(); - } + /** + * Escape special characters with '%' + * + * @param uri URI to be escaped + * @return encoded URI + */ + public static String encode(String uri) { + return escaper.escape(uri); + } + + /** + * Decode '%'-escaped characters. Decoding fails in case of invalid UTF-8 + * + * @param buff data to decode + * @return decoded data + * @throws CharacterCodingException if cannot be decoded + */ + public static String decode(ByteBuffer buff) throws CharacterCodingException { + CharBuffer chars = UTF8Decoder.decode(buff); + return chars.toString(); + } - public static String decode(String buff) { - try { - return URLDecoder.decode(buff, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new YAMLException(e); - } + public static String decode(String buff) { + try { + return URLDecoder.decode(buff, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new YAMLException(e); } + } } |