001/* 002 * Configurate 003 * Copyright (C) zml and Configurate contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.spongepowered.configurate.xml; 018 019import net.kyori.option.Option; 020import net.kyori.option.OptionSchema; 021import org.checkerframework.checker.nullness.qual.NonNull; 022import org.checkerframework.checker.nullness.qual.Nullable; 023import org.spongepowered.configurate.AttributedConfigurationNode; 024import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary; 025import org.spongepowered.configurate.ConfigurateException; 026import org.spongepowered.configurate.ConfigurationNode; 027import org.spongepowered.configurate.ConfigurationOptions; 028import org.spongepowered.configurate.loader.AbstractConfigurationLoader; 029import org.spongepowered.configurate.loader.CommentHandler; 030import org.spongepowered.configurate.loader.CommentHandlers; 031import org.spongepowered.configurate.loader.ParsingException; 032import org.spongepowered.configurate.util.UnmodifiableCollections; 033import org.w3c.dom.Document; 034import org.w3c.dom.Element; 035import org.w3c.dom.NamedNodeMap; 036import org.w3c.dom.Node; 037import org.w3c.dom.NodeList; 038import org.xml.sax.InputSource; 039import org.xml.sax.SAXException; 040import org.xml.sax.SAXParseException; 041 042import java.io.BufferedReader; 043import java.io.FileNotFoundException; 044import java.io.IOException; 045import java.io.Writer; 046import java.nio.file.NoSuchFileException; 047import java.util.ArrayList; 048import java.util.Collection; 049import java.util.Collections; 050import java.util.LinkedHashMap; 051import java.util.Map; 052import java.util.Objects; 053import java.util.Set; 054import java.util.stream.Collectors; 055 056import javax.xml.XMLConstants; 057import javax.xml.parsers.DocumentBuilder; 058import javax.xml.parsers.DocumentBuilderFactory; 059import javax.xml.parsers.ParserConfigurationException; 060import javax.xml.transform.OutputKeys; 061import javax.xml.transform.Transformer; 062import javax.xml.transform.TransformerConfigurationException; 063import javax.xml.transform.TransformerException; 064import javax.xml.transform.TransformerFactory; 065import javax.xml.transform.dom.DOMSource; 066import javax.xml.transform.stream.StreamResult; 067import javax.xml.validation.Schema; 068 069/** 070 * A loader for XML (Extensible Markup Language), using the native javax library 071 * for parsing and generation. 072 * 073 * @since 4.0.0 074 */ 075public final class XmlConfigurationLoader extends AbstractConfigurationLoader<AttributedConfigurationNode> { 076 077 private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet(Double.class, Long.class, 078 Integer.class, Boolean.class, String.class, Number.class); 079 080 /** 081 * The prefix of lines within the header. 082 */ 083 private static final String HEADER_PREFIX = "~"; 084 085 private static final String ATTRIBUTE_TYPE = "configurate-type"; 086 087 /** 088 * The user data used to store comments on nodes. 089 */ 090 private static final String USER_DATA_COMMENT = "configurate-comment"; 091 092 /** 093 * The property used to mark how many spaces should be used to indent. 094 */ 095 private static final String INDENT_PROPERTY = "{http://xml.apache.org/xslt}indent-amount"; 096 097 private static final String FEATURE_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; 098 099 private static final String FEATURE_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; 100 101 private static final String FEATURE_LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; 102 103 /** 104 * Creates a new {@link XmlConfigurationLoader} builder. 105 * 106 * @return a new builder 107 * @since 4.0.0 108 */ 109 public static @NonNull Builder builder() { 110 return new Builder(); 111 } 112 113 /** 114 * Builds a {@link XmlConfigurationLoader}. 115 * 116 * <p>This builder supports the following options:</p> 117 * <ul> 118 * <li>{@link #DEFAULT_TAG_NAME}</li> 119 * <li>{@link #INDENT}</li> 120 * <li>{@link #WRITE_EXPLICIT_TYPE}</li> 121 * <li>{@link #RESOLVE_EXTERNAL_CONTENT}</li> 122 * <li>{@link #INCLUDE_XML_DECLARATION}</li> 123 * </ul> 124 * 125 * @since 4.0.0 126 */ 127 public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, XmlConfigurationLoader> { 128 129 private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.childSchema(AbstractConfigurationLoader.Builder.SCHEMA); 130 /** 131 * A schema of options available to configure the XML loader. 132 * 133 * @since 4.2.0 134 */ 135 public static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView(); 136 /** 137 * The default tag name the created loader should use. 138 * 139 * @see #defaultTagName(String) 140 * @since 4.2.0 141 */ 142 public static final Option<String> DEFAULT_TAG_NAME = UNSAFE_SCHEMA.stringOption("xml:default_tag_name", "element"); 143 144 /** 145 * The size of indent (in spaces) the created loader should use. 146 * 147 * @see #indent(int) 148 * @since 4.2.0 149 */ 150 public static final Option<Integer> INDENT = UNSAFE_SCHEMA.intOption("xml:indent", 2); 151 /** 152 * Whether explicit node types should be written out (to disambiguate 153 * between maps and lists). 154 * 155 * @see #writesExplicitType(boolean) 156 * @since 4.2.0 157 */ 158 public static final Option<Boolean> WRITE_EXPLICIT_TYPE = UNSAFE_SCHEMA.booleanOption("xml:writes_explicit_type", true); 159 /** 160 * Whether externally included content should be resolved in documents 161 * read by the created loader. 162 * 163 * @see #resolvesExternalContent(boolean) 164 * @since 4.2.0 165 */ 166 public static final Option<Boolean> RESOLVE_EXTERNAL_CONTENT = UNSAFE_SCHEMA.booleanOption("xml:resolve_external_content", false); 167 /** 168 * Whether an XML declaration should be included as the first line of 169 * emitted documents from the created loader. 170 * 171 * @see #includesXmlDeclaration(boolean) 172 * @since 4.2.0 173 */ 174 public static final Option<Boolean> INCLUDE_XML_DECLARATION = UNSAFE_SCHEMA.booleanOption("xml:include_xml_declaration", true); 175 176 private @Nullable Schema schema; 177 178 @Override 179 protected OptionSchema optionSchema() { 180 return SCHEMA; 181 } 182 183 /** 184 * Sets the level of indentation the resultant loader should use. 185 * 186 * @param indent the indent level 187 * @return this builder (for chaining) 188 * @since 4.0.0 189 */ 190 public @NonNull Builder indent(final int indent) { 191 this.optionStateBuilder().value(INDENT, indent); 192 return this; 193 } 194 195 /** 196 * Gets the level of indentation to be used by the resultant loader. 197 * 198 * @return the indent level 199 * @since 4.0.0 200 */ 201 public int indent() { 202 return this.optionState().value(INDENT); 203 } 204 205 /** 206 * Sets the {@link Schema} the resultant loader should use. 207 * 208 * @param schema the schema 209 * @return this builder (for chaining) 210 * @since 4.0.0 211 */ 212 public Builder schema(final @Nullable Schema schema) { 213 this.schema = schema; 214 return this; 215 } 216 217 /** 218 * Gets the {@link Schema} to be used by the resultant loader. 219 * 220 * @return the schema 221 * @since 4.0.0 222 */ 223 public @Nullable Schema schema() { 224 return this.schema; 225 } 226 227 /** 228 * Sets the default tag name the resultant loader should use. 229 * 230 * @param defaultTagName the default tag name 231 * @return this builder (for chaining) 232 * @since 4.0.0 233 */ 234 public Builder defaultTagName(final String defaultTagName) { 235 this.optionStateBuilder().value(DEFAULT_TAG_NAME, defaultTagName); 236 return this; 237 } 238 239 /** 240 * Gets the default tag name to be used by the resultant loader. 241 * 242 * @return the default tag name 243 * @since 4.0.0 244 */ 245 public @NonNull String defaultTagName() { 246 return this.optionState().value(DEFAULT_TAG_NAME); 247 } 248 249 /** 250 * Sets if the resultant loader should write the explicit type of each 251 * node when saving nodes. 252 * 253 * <p>This is necessary in some cases, as XML has no explicit definition 254 * of an array or list. The loader is able to infer the type in some 255 * cases, but this is inaccurate in some cases, for example lists with 256 * only one element.</p> 257 * 258 * @param writeExplicitType if the loader should write explicit types 259 * @return this builder (for chaining) 260 * @since 4.0.0 261 */ 262 public Builder writesExplicitType(final boolean writeExplicitType) { 263 this.optionStateBuilder().value(WRITE_EXPLICIT_TYPE, writeExplicitType); 264 return this; 265 } 266 267 /** 268 * Gets if explicit type attributes should be written by the loader. 269 * 270 * <p>See the method doc at {@link #writesExplicitType(boolean)} for 271 * a more detailed explanation.</p> 272 * 273 * @return the default tag name 274 * @since 4.0.0 275 */ 276 public boolean writesExplicitType() { 277 return this.optionState().value(WRITE_EXPLICIT_TYPE); 278 } 279 280 /** 281 * Sets if the resultant loader should include the XML declaration 282 * header when saving. 283 * 284 * @param includeXmlDeclaration if the XML declaration should be 285 * included 286 * @return this builder (for chaining) 287 * @since 4.0.0 288 */ 289 public Builder includesXmlDeclaration(final boolean includeXmlDeclaration) { 290 this.optionStateBuilder().value(INCLUDE_XML_DECLARATION, includeXmlDeclaration); 291 return this; 292 } 293 294 /** 295 * Gets if the resultant loader should include the XML declaration 296 * header when saving. 297 * 298 * @return if the XML declaration should be included 299 * @since 4.0.0 300 */ 301 public boolean includesXmlDeclaration() { 302 return this.optionState().value(INCLUDE_XML_DECLARATION); 303 } 304 305 /** 306 * Sets whether external content should be resolved when loading data. 307 * 308 * <p>Resolving this content could result in network requests being 309 * made, and will allow configuration files to access arbitrary URLs 310 * This setting should only be enabled with caution. 311 * 312 * <p>Additionally, through use of features such as entity expansion and 313 * XInclude, documents can be crafted that will grow exponentially 314 * when parsed, requiring an amount of memory to store that may be 315 * greater than what is available for the JVM. 316 * 317 * <p>By default, this is false. 318 * 319 * @param resolvesExternalContent whether to resolve external entities 320 * @return this builder 321 * @since 4.0.0 322 */ 323 public Builder resolvesExternalContent(final boolean resolvesExternalContent) { 324 this.optionStateBuilder().value(RESOLVE_EXTERNAL_CONTENT, resolvesExternalContent); 325 return this; 326 } 327 328 /** 329 * Get whether external content should be resolved. 330 * 331 * @return value, defaulting to false 332 * @since 4.0.0 333 */ 334 public boolean resolvesExternalContent() { 335 return this.optionState().value(RESOLVE_EXTERNAL_CONTENT); 336 } 337 338 @Override 339 public XmlConfigurationLoader build() { 340 this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES)); 341 return new XmlConfigurationLoader(this); 342 } 343 } 344 345 private final @Nullable Schema schema; 346 private final String defaultTagName; 347 private final int indent; 348 private final boolean writeExplicitType; 349 private final boolean includeXmlDeclaration; 350 private final boolean resolvesExternalContent; 351 352 private XmlConfigurationLoader(final Builder builder) { 353 super(builder, new CommentHandler[] {CommentHandlers.XML_STYLE}); 354 this.schema = builder.schema(); 355 this.defaultTagName = builder.defaultTagName(); 356 this.indent = builder.indent(); 357 this.writeExplicitType = builder.writesExplicitType(); 358 this.includeXmlDeclaration = builder.includesXmlDeclaration(); 359 this.resolvesExternalContent = builder.resolvesExternalContent(); 360 } 361 362 private DocumentBuilder newDocumentBuilder() throws ConfigurateException { 363 final DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); 364 if (this.schema != null) { 365 builderFactory.setSchema(this.schema); 366 } 367 if (!this.resolvesExternalContent) { 368 // Settings based on https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html 369 try { 370 builderFactory.setFeature(FEATURE_EXTERNAL_GENERAL_ENTITIES, false); 371 builderFactory.setFeature(FEATURE_EXTERNAL_PARAMETER_ENTITIES, false); 372 builderFactory.setFeature(FEATURE_LOAD_EXTERNAL_DTD, false); 373 } catch (final ParserConfigurationException e) { 374 throw new ConfigurateException(e); 375 } 376 builderFactory.setXIncludeAware(false); 377 builderFactory.setExpandEntityReferences(false); 378 } 379 380 try { 381 return builderFactory.newDocumentBuilder(); 382 } catch (final ParserConfigurationException e) { 383 throw new ConfigurateException(e); 384 } 385 } 386 387 private Transformer newTransformer() throws ConfigurateException { 388 final TransformerFactory transformerFactory = TransformerFactory.newInstance(); 389 if (!this.resolvesExternalContent) { 390 transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 391 transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); 392 } 393 try { 394 final Transformer transformer = transformerFactory.newTransformer(); 395 396 // we write the header ourselves. 397 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); 398 399 if (this.indent > 0) { 400 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 401 transformer.setOutputProperty(INDENT_PROPERTY, Integer.toString(this.indent)); 402 } 403 return transformer; 404 } catch (final TransformerConfigurationException e) { 405 throw new ConfigurateException(e); 406 } 407 } 408 409 @Override 410 public @NonNull AttributedConfigurationNode load(@NonNull ConfigurationOptions options) throws ParsingException { 411 if (this.source == null) { 412 throw new ParsingException(-1, -1, "", "No source present to read from!", null); 413 } 414 try (BufferedReader reader = this.source.call()) { 415 final DocumentBuilder documentBuilder = this.newDocumentBuilder(); 416 417 final Document document; 418 try { 419 document = documentBuilder.parse(new InputSource(reader)); 420 } catch (final SAXParseException ex) { 421 throw new ParsingException(ex.getLineNumber(), ex.getColumnNumber(), "", ex.getMessage(), ex.getCause()); 422 } catch (final SAXException e) { 423 throw new ParsingException(-1, -1, null, null, e); 424 } 425 426 final NodeList children = document.getChildNodes(); 427 for (int i = 0; i < children.getLength(); ++i) { 428 final Node child = children.item(i); 429 if (child.getNodeType() == Node.COMMENT_NODE) { 430 options = options.header(this.unwrapHeader(child.getTextContent().trim())); 431 } else if (child.getNodeType() == Node.ELEMENT_NODE) { 432 final AttributedConfigurationNode node = this.createNode(options); 433 this.readElement(child, node); 434 return node; 435 } 436 } 437 // empty document, fall through 438 } catch (final FileNotFoundException | NoSuchFileException e) { 439 // Squash -- there's nothing to read 440 } catch (final ParsingException ex) { 441 throw ex; 442 } catch (final Exception e) { 443 throw new ParsingException(-1, -1, "", null, e); 444 } 445 return this.createNode(options); 446 } 447 448 /** 449 * Given a single comment node's comment, clear any prefix lines. 450 * 451 * @param headerContent the content of a header 452 * @return a formatted header, with lines separated by {@link #CONFIGURATE_LINE_SEPARATOR} 453 */ 454 private String unwrapHeader(final String headerContent) { 455 if (headerContent.isEmpty()) { 456 return headerContent; 457 } 458 // TODO: 4.0 may have changed behaviour here when moving away from Guava 459 return CONFIGURATE_LINE_PATTERN.splitAsStream(headerContent) 460 .map(line -> { 461 final String trimmedLine = line.trim(); 462 if (trimmedLine.startsWith(HEADER_PREFIX)) { 463 line = line.substring(line.indexOf(HEADER_PREFIX) + 1); 464 } 465 466 if (line.length() > 0 && line.charAt(0) == ' ') { 467 line = line.substring(1); 468 } 469 return line; 470 }).filter(line -> !line.isEmpty()) 471 .collect(Collectors.joining(CONFIGURATE_LINE_SEPARATOR)); 472 } 473 474 @Override 475 protected void loadInternal(final AttributedConfigurationNode node, final BufferedReader reader) { 476 throw new UnsupportedOperationException("XMLConfigurationLoader provides custom loading logic to handle headers"); 477 } 478 479 private enum NodeType { 480 MAP, LIST 481 } 482 483 private void readElement(final Node from, final AttributedConfigurationNode to) { 484 @Nullable NodeType type = null; 485 486 // copy the name of the tag 487 to.tagName(from.getNodeName()); 488 489 final String potentialComment = (String) from.getUserData(USER_DATA_COMMENT); 490 if (potentialComment != null) { 491 to.comment(potentialComment); 492 } 493 494 // copy attributes 495 if (from.hasAttributes()) { 496 final NamedNodeMap attributes = from.getAttributes(); 497 for (int i = 0; i < attributes.getLength(); i++) { 498 final Node attribute = attributes.item(i); 499 final String key = attribute.getNodeName(); 500 final String value = attribute.getNodeValue(); 501 502 // read the type of the node 503 if (key.equals(ATTRIBUTE_TYPE)) { 504 if (value.equals("map")) { 505 type = NodeType.MAP; 506 } else if (value.equals("list")) { 507 type = NodeType.LIST; 508 } 509 510 // don't add internal configurate attributes to the node 511 continue; 512 } 513 514 to.addAttribute(key, value); 515 } 516 } 517 518 // read out the child nodes into a multimap 519 final Map<String, Collection<Node>> children = new LinkedHashMap<>(); 520 if (from.hasChildNodes()) { 521 final StringBuilder comment = new StringBuilder(); 522 final NodeList childNodes = from.getChildNodes(); 523 for (int i = 0; i < childNodes.getLength(); i++) { 524 final Node child = childNodes.item(i); 525 if (child.getNodeType() == Node.ELEMENT_NODE) { 526 children.computeIfAbsent(child.getNodeName(), $ -> new ArrayList<>()).add(child); 527 if (comment.length() > 0) { 528 child.setUserData(USER_DATA_COMMENT, comment.toString(), null); 529 comment.setLength(0); 530 } 531 } else if (child.getNodeType() == Node.COMMENT_NODE) { 532 if (comment.length() > 0) { 533 comment.append('\n'); 534 } 535 536 comment.append(child.getTextContent().trim()); 537 } 538 } 539 } 540 541 // if there are no child nodes present, assume it's a scalar value 542 if (children.isEmpty()) { 543 to.raw(parseValue(from.getTextContent())); 544 return; 545 } 546 547 // if type is null, we need to infer what type the element is 548 if (type == null) { 549 // if there are no duplicate keys, we can infer that it is a map 550 // otherwise, assume it's a list 551 type = NodeType.MAP; 552 for (final Collection<Node> child : children.values()) { 553 if (child.size() > 1) { 554 type = NodeType.LIST; 555 break; 556 } 557 } 558 } 559 560 if (type == NodeType.MAP) { 561 to.raw(Collections.emptyMap()); 562 } else { 563 to.raw(Collections.emptyList()); 564 } 565 566 // read out the elements 567 for (final Map.Entry<String, Collection<Node>> entry : children.entrySet()) { 568 AttributedConfigurationNode child; 569 if (type == NodeType.MAP) { 570 child = to.node(entry.getKey()); 571 this.readElement(entry.getValue().iterator().next(), child); 572 } else { 573 for (final Node element : entry.getValue()) { 574 child = to.appendListNode(); 575 this.readElement(element, child); 576 } 577 } 578 } 579 } 580 581 @Override 582 protected void writeHeaderInternal(final Writer writer) throws IOException { 583 if (this.includeXmlDeclaration) { 584 writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 585 writer.write(SYSTEM_LINE_SEPARATOR); 586 } 587 } 588 589 @Override 590 protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException { 591 final DocumentBuilder documentBuilder = this.newDocumentBuilder(); 592 final Document document = documentBuilder.newDocument(); 593 594 final @Nullable Node comment = this.createCommentNode(document, node); 595 if (comment != null) { 596 document.appendChild(comment); 597 } 598 599 document.appendChild(this.writeNode(document, node, null)); 600 601 final Transformer transformer = this.newTransformer(); 602 final DOMSource source = new DOMSource(document); 603 try { 604 transformer.transform(source, new StreamResult(writer)); 605 } catch (final TransformerException e) { 606 throw new ConfigurateException(node, e); 607 } 608 } 609 610 private void appendCommentIfNecessary(final Element parent, final ConfigurationNode node) { 611 final @Nullable Node possibleComment = this.createCommentNode(parent.getOwnerDocument(), node); 612 if (possibleComment != null) { 613 parent.appendChild(possibleComment); 614 } 615 } 616 617 private @Nullable Node createCommentNode(final Document doc, final ConfigurationNode node) { 618 if (node instanceof CommentedConfigurationNodeIntermediary<?>) { 619 final @Nullable String comment = ((CommentedConfigurationNodeIntermediary<?>) node).comment(); 620 if (comment != null) { 621 return doc.createComment(" " + comment.trim() + " "); 622 } 623 } 624 return null; 625 } 626 627 private Element writeNode(final Document document, final ConfigurationNode node, final @Nullable String forcedTag) { 628 String tag = this.defaultTagName; 629 Map<String, String> attributes = Collections.emptyMap(); 630 631 if (node instanceof AttributedConfigurationNode) { 632 final AttributedConfigurationNode attributedNode = (AttributedConfigurationNode) node; 633 tag = attributedNode.tagName(); 634 attributes = attributedNode.attributes(); 635 } 636 637 final Element element = document.createElement(forcedTag == null ? tag : forcedTag); 638 for (final Map.Entry<String, String> attribute : attributes.entrySet()) { 639 element.setAttribute(attribute.getKey(), attribute.getValue()); 640 } 641 642 if (node.isMap()) { 643 for (final Map.Entry<Object, ? extends ConfigurationNode> child : node.childrenMap().entrySet()) { 644 this.appendCommentIfNecessary(element, child.getValue()); 645 element.appendChild(this.writeNode(document, child.getValue(), child.getKey().toString())); 646 } 647 } else if (node.isList()) { 648 if (this.writeExplicitType) { 649 element.setAttribute(ATTRIBUTE_TYPE, "list"); 650 } 651 for (final ConfigurationNode child : node.childrenList()) { 652 this.appendCommentIfNecessary(element, child); 653 element.appendChild(this.writeNode(document, child, null)); 654 } 655 } else { 656 element.appendChild(document.createTextNode(Objects.toString(node.rawScalar()))); 657 } 658 659 return element; 660 } 661 662 @Override 663 public AttributedConfigurationNode createNode(ConfigurationOptions options) { 664 options = options.nativeTypes(NATIVE_TYPES); 665 return AttributedConfigurationNode.root("root", options); 666 } 667 668 private static Object parseValue(final String value) { 669 if (value.equals("true") || value.equals("false")) { 670 return Boolean.parseBoolean(value); 671 } 672 673 try { 674 final double doubleValue = Double.parseDouble(value); 675 if (isInteger(doubleValue)) { 676 final long longValue = Long.parseLong(value); // prevent losing precision 677 final int intValue = (int) longValue; 678 if (longValue == intValue) { 679 return intValue; 680 } else { 681 return longValue; 682 } 683 } 684 return doubleValue; 685 } catch (final NumberFormatException e) { 686 return value; 687 } 688 } 689 690 private static boolean isInteger(final double value) { 691 return !Double.isNaN(value) && Double.isFinite(value) && value == Math.rint(value); 692 } 693 694}