├── .gitattributes ├── .travis.yml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── fasterxml │ │ │ └── jackson │ │ │ └── dataformat │ │ │ └── javaprop │ │ │ ├── util │ │ │ ├── package-info.java │ │ │ ├── JPropNodeBuilder.java │ │ │ ├── Markers.java │ │ │ ├── JPropNode.java │ │ │ └── JPropPathSplitter.java │ │ │ ├── io │ │ │ ├── package-info.java │ │ │ ├── JPropEscapes.java │ │ │ ├── JPropWriteContext.java │ │ │ ├── Latin1Reader.java │ │ │ └── JPropReadContext.java │ │ │ ├── PackageVersion.java.in │ │ │ ├── package-info.java │ │ │ ├── impl │ │ │ ├── PropertiesBackedGenerator.java │ │ │ └── WriterBackedGenerator.java │ │ │ ├── JavaPropsMapper.java │ │ │ ├── JavaPropsFactory.java │ │ │ ├── JavaPropsParser.java │ │ │ ├── JavaPropsSchema.java │ │ │ └── JavaPropsGenerator.java │ └── resources │ │ └── META-INF │ │ ├── LICENSE │ │ ├── services │ │ └── com.fasterxml.jackson.core.JsonFactory │ │ └── NOTICE └── test │ └── java │ └── com │ └── fasterxml │ └── jackson │ └── dataformat │ └── javaprop │ ├── TestVersions.java │ ├── util │ └── JPropPathSplitterTest.java │ ├── MapParsingTest.java │ ├── CustomSeparatorsTest.java │ ├── SchemaConstructionTest.java │ ├── GenerationEscapingTest.java │ ├── PropertiesSupportTest.java │ ├── DefaultConfigsTest.java │ ├── ArrayGenerationTest.java │ ├── SimpleParsingTest.java │ ├── SimpleGenerationTest.java │ ├── SimpleStreamingTest.java │ ├── ArrayParsingTest.java │ └── ModuleTestBase.java ├── README.md ├── .gitignore ├── release-notes ├── CREDITS └── VERSION └── pom.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not merge `pom.xml` from older version, as it will typically conflict 2 | 3 | pom.xml merge=ours 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk7 5 | - oraclejdk8 6 | 7 | # whitelist 8 | branches: 9 | only: 10 | - master 11 | - "2.8" 12 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/util/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | Helper classes for dealing with property key to structured path conversion. 3 | */ 4 | 5 | package com.fasterxml.jackson.dataformat.javaprop.util; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE**: This module has become part of 2 | [Jackson Text Dataformats](../../../jackson-dataformats-text) 3 | as of Jackson 2.9 4 | 5 | This repo still exists to allow release of patch versions of older versions; it will be 6 | hidden (made private) in near future. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # use glob syntax. 2 | syntax: glob 3 | *.class 4 | *~ 5 | *.bak 6 | *.off 7 | *.old 8 | .DS_Store 9 | 10 | # building 11 | target 12 | 13 | # Eclipse 14 | .classpath 15 | .project 16 | .settings 17 | 18 | # IDEA 19 | .idea 20 | *.iml 21 | *.ipr 22 | *.iws 23 | /target 24 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/io/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | Internal helper classes used for handling of input and out contexts and 3 | efficient reading/writing of Java Properties encoded content. 4 | */ 5 | 6 | package com.fasterxml.jackson.dataformat.javaprop.io; 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/LICENSE: -------------------------------------------------------------------------------- 1 | This copy of Jackson JSON processor databind module is licensed under the 2 | Apache (Software) License, version 2.0 ("the License"). 3 | See the License for details about distribution rights, and the 4 | specific rights regarding derivate works. 5 | 6 | You may obtain a copy of the License at: 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | -------------------------------------------------------------------------------- /release-notes/CREDITS: -------------------------------------------------------------------------------- 1 | Project: jackson-dataformat-properties 2 | 3 | Here are people who have contributed to development of this project: 4 | (version numbers in parenthesis indicate release in which the problem was fixed) 5 | 6 | Tatu Saloranta, tatu.saloranta@iki.fi: author 7 | 8 | Jordi Sola (someth2say@github) 9 | 10 | * Reported #6: Null-check for JPropNode byName map 11 | (2.8.8) 12 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/PackageVersion.java.in: -------------------------------------------------------------------------------- 1 | package @package@; 2 | 3 | import com.fasterxml.jackson.core.Version; 4 | import com.fasterxml.jackson.core.Versioned; 5 | import com.fasterxml.jackson.core.util.VersionUtil; 6 | 7 | /** 8 | * Automatically generated from PackageVersion.java.in during 9 | * packageVersion-generate execution of maven-replacer-plugin in 10 | * pom.xml. 11 | */ 12 | public final class PackageVersion implements Versioned { 13 | public final static Version VERSION = VersionUtil.parseVersion( 14 | "@projectversion@", "@projectgroupid@", "@projectartifactid@"); 15 | 16 | @Override 17 | public Version version() { 18 | return VERSION; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.fasterxml.jackson.core.JsonFactory: -------------------------------------------------------------------------------- 1 | # Copyright 2012 FasterXML.com 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | com.fasterxml.jackson.dataformat.javaprop.JavaPropsFactory 16 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/NOTICE: -------------------------------------------------------------------------------- 1 | # Jackson JSON processor 2 | 3 | Jackson is a high-performance, Free/Open Source JSON processing library. 4 | It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has 5 | been in development since 2007. 6 | It is currently developed by a community of developers, as well as supported 7 | commercially by FasterXML.com. 8 | 9 | ## Licensing 10 | 11 | Jackson core and extension components may be licensed under different licenses. 12 | To find the details that apply to this artifact see the accompanying LICENSE file. 13 | For more information, including possible other licensing options, contact 14 | FasterXML.com (http://fasterxml.com). 15 | 16 | ## Credits 17 | 18 | A list of contributors may be found from CREDITS file, which is included 19 | in some artifacts (usually source distributions); but is always available 20 | from the source code management (SCM) system project uses. 21 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/util/JPropNodeBuilder.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.util; 2 | 3 | import java.util.Map; 4 | import java.util.Properties; 5 | 6 | import com.fasterxml.jackson.dataformat.javaprop.*; 7 | 8 | public class JPropNodeBuilder 9 | { 10 | public static JPropNode build(JavaPropsSchema schema, 11 | Properties props) 12 | { 13 | JPropNode root = new JPropNode(); 14 | JPropPathSplitter splitter = schema.pathSplitter(); 15 | for (Map.Entry entry : props.entrySet()) { 16 | // these should be Strings; but due to possible "compromised" properties, 17 | // let's play safe, coerce if and as necessary 18 | String key = String.valueOf(entry.getKey()); 19 | String value = String.valueOf(entry.getValue()); 20 | 21 | splitter.splitAndAdd(root, key, value); 22 | } 23 | return root; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /release-notes/VERSION: -------------------------------------------------------------------------------- 1 | Project: jackson-dataformat-properties 2 | 3 | ------------------------------------------------------------------------ 4 | === Releases === 5 | ------------------------------------------------------------------------ 6 | 7 | 2.9.0 (not yet released) 8 | 9 | #1: Add convenience method(s) for reading System properties 10 | #3: Write into `Properties` instance (factory, mapper) using 11 | `JavaPropsMapper.writeValue()` with `Properties` and 12 | `JavaPropsMapper.writeValueAsProperties()` 13 | #4: Allow binding from `Properties` instance 14 | 15 | 2.8.8 (not yet released) 16 | 17 | #6: Null-check for JPropNode byName map 18 | (reported by Jordi S) 19 | 20 | 2.8.7 (21-Feb-2017) 21 | 2.8.6 (12-Jan-2017) 22 | 2.8.5 (14-Nov-2016) 23 | 2.8.4 (14-Oct-2016) 24 | 2.8.3 (17-Sep-2016) 25 | 2.8.2 (30-Aug-2016) 26 | 2.8.1 (20-Jul-2016) 27 | 28 | No changes since 2.8.0. 29 | 30 | 2.8.0 (04-Jul-2016) 31 | 32 | No changes since 2.7. 33 | 34 | 2.7.x: 35 | 36 | First experimental/prototype versions. 37 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/TestVersions.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.*; 4 | 5 | import com.fasterxml.jackson.core.*; 6 | 7 | public class TestVersions extends ModuleTestBase 8 | { 9 | public void testMapperVersions() throws IOException 10 | { 11 | JavaPropsFactory f = new JavaPropsFactory(); 12 | assertVersion(f); 13 | JavaPropsParser p = (JavaPropsParser) f.createParser("abc=foo"); 14 | assertVersion(p); 15 | JsonGenerator gen = f.createGenerator(new ByteArrayOutputStream()); 16 | assertVersion(gen); 17 | p.close(); 18 | gen.close(); 19 | } 20 | 21 | /* 22 | /********************************************************** 23 | /* Helper methods 24 | /********************************************************** 25 | */ 26 | 27 | private void assertVersion(Versioned vers) 28 | { 29 | assertNotNull(vers); 30 | assertEquals(PackageVersion.VERSION, vers.version()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/util/JPropPathSplitterTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.util; 2 | 3 | import com.fasterxml.jackson.dataformat.javaprop.JavaPropsSchema; 4 | import com.fasterxml.jackson.dataformat.javaprop.ModuleTestBase; 5 | 6 | public class JPropPathSplitterTest extends ModuleTestBase 7 | { 8 | public void testSplitters() 9 | { 10 | JPropPathSplitter sp = JPropPathSplitter.create(JavaPropsSchema.emptySchema()); 11 | // by default we should be getting general-purpose one 12 | assertEquals(JPropPathSplitter.FullSplitter.class, sp.getClass()); 13 | 14 | // but if disabling index: 15 | sp = JPropPathSplitter.create(JavaPropsSchema.emptySchema() 16 | .withoutIndexMarker()); 17 | assertEquals(JPropPathSplitter.CharPathOnlySplitter.class, sp.getClass()); 18 | 19 | // or, if disabling index and path 20 | sp = JPropPathSplitter.create(JavaPropsSchema.emptySchema() 21 | .withoutPathSeparator() 22 | .withoutIndexMarker()); 23 | assertEquals(JPropPathSplitter.NonSplitting.class, sp.getClass()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/util/Markers.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.util; 2 | 3 | /** 4 | * Simple value class for encapsulating a pair of start and end markers; 5 | * initially needed for index markers (like "[" and "]"). 6 | */ 7 | public class Markers 8 | { 9 | protected final String _start, _end; 10 | 11 | protected Markers(String start, String end) { 12 | if (start == null || start.isEmpty()) { 13 | throw new IllegalArgumentException("Missing 'start' value"); 14 | } 15 | if (end == null || end.isEmpty()) { 16 | throw new IllegalArgumentException("Missing 'end' value"); 17 | } 18 | _start = start; 19 | _end = end; 20 | } 21 | 22 | /** 23 | * Factory method for creating simple marker pair with given 24 | * start and end markers. Note that both are needed; neither may 25 | * be empty or null 26 | */ 27 | public static Markers create(String start, String end) { 28 | return new Markers(start, end); 29 | } 30 | 31 | public String getStart() { 32 | return _start; 33 | } 34 | 35 | public String getEnd() { 36 | return _end; 37 | } 38 | 39 | // public StringBuilder appendIn(String) 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/MapParsingTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.util.Map; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | public class MapParsingTest extends ModuleTestBase 8 | { 9 | static class MapWrapper { 10 | public Map map; 11 | } 12 | 13 | private final ObjectMapper MAPPER = mapperForProps(); 14 | 15 | /* 16 | /********************************************************************** 17 | /* Test methods 18 | /********************************************************************** 19 | */ 20 | 21 | public void testMapWithBranch() throws Exception 22 | { 23 | // basically "extra" branch should become as first element, and 24 | // after that ordering by numeric value 25 | final String INPUT = "map=first\n" 26 | +"map.b=second\n" 27 | +"map.xyz=third\n" 28 | ; 29 | MapWrapper w = MAPPER.readValue(INPUT, MapWrapper.class); 30 | assertNotNull(w.map); 31 | assertEquals(3, w.map.size()); 32 | assertEquals("first", w.map.get("")); 33 | assertEquals("second", w.map.get("b")); 34 | assertEquals("third", w.map.get("xyz")); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/CustomSeparatorsTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | public class CustomSeparatorsTest extends ModuleTestBase 6 | { 7 | private final ObjectMapper MAPPER = mapperForProps(); 8 | 9 | public void testCustomPathSeparator() throws Exception 10 | { 11 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 12 | .withPathSeparator("->") 13 | ; 14 | String props = MAPPER.writer(schema) 15 | .writeValueAsString( 16 | new Rectangle(new Point(1, -2), new Point(5, 10))); 17 | assertEquals("topLeft->x=1\n" 18 | +"topLeft->y=-2\n" 19 | +"bottomRight->x=5\n" 20 | +"bottomRight->y=10\n" 21 | ,props); 22 | 23 | // and should come back as well, even with some white space sprinkled 24 | final String INPUT = "topLeft->x = 1\n" 25 | +"topLeft->y= -2\n" 26 | +"bottomRight->x =5\n" 27 | +"bottomRight->y = 10\n"; 28 | 29 | Rectangle result = MAPPER.readerFor(Rectangle.class) 30 | .with(schema) 31 | .readValue(INPUT); 32 | assertEquals(10, result.bottomRight.y); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/SchemaConstructionTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | public class SchemaConstructionTest extends ModuleTestBase 4 | { 5 | // Tests to verify stickiness of settings 6 | public void testMutantFactories() 7 | { 8 | JavaPropsSchema empty = JavaPropsSchema.emptySchema(); 9 | JavaPropsSchema schema2; 10 | 11 | assertFalse(empty.writeIndexUsingMarkers()); 12 | 13 | schema2 = empty.withFirstArrayOffset(1); 14 | assertEquals(1, schema2.firstArrayOffset()); 15 | assertEquals(1, schema2.withFirstArrayOffset(1).firstArrayOffset()); 16 | 17 | schema2 = empty.withPathSeparator("//"); 18 | assertEquals("//", schema2.pathSeparator()); 19 | assertEquals("//", schema2.withPathSeparator("//").pathSeparator()); 20 | assertEquals("", schema2.withoutPathSeparator().pathSeparator()); 21 | assertEquals("", schema2.withPathSeparator(null).pathSeparator()); 22 | 23 | schema2 = empty.withLineIndentation(" "); 24 | assertEquals(" ", schema2.lineIndentation()); 25 | assertEquals(" ", schema2.withLineIndentation(" ").lineIndentation()); 26 | 27 | schema2 = empty.withHeader(""); 28 | assertEquals("", schema2.header()); 29 | assertEquals("", schema2.withHeader("").header()); 30 | 31 | schema2 = empty.withLineEnding("\r"); 32 | assertEquals("\r", schema2.lineEnding()); 33 | assertEquals("\r", schema2.withLineEnding("\r").lineEnding()); 34 | 35 | assertEquals("JavaProps", schema2.getSchemaType()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | Basic API types to use with this module: 3 | 27 | */ 28 | 29 | package com.fasterxml.jackson.dataformat.javaprop; 30 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/GenerationEscapingTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.util.*; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | public class GenerationEscapingTest extends ModuleTestBase 8 | { 9 | private final ObjectMapper MAPPER = mapperForProps(); 10 | 11 | public void testKeyEscaping() throws Exception 12 | { 13 | Map input = new HashMap<>(); 14 | input.put("ns:key", "value"); 15 | assertEquals("ns\\:key=value\n", MAPPER.writeValueAsString(input)); 16 | 17 | input = new HashMap<>(); 18 | input.put("combo key", "value"); 19 | assertEquals("combo\\ key=value\n", MAPPER.writeValueAsString(input)); 20 | 21 | input = new HashMap<>(); 22 | input.put("combo\tkey", "value"); 23 | assertEquals("combo\\tkey=value\n", MAPPER.writeValueAsString(input)); 24 | } 25 | 26 | public void testValueEscaping() throws Exception 27 | { 28 | /* 29 | Properties props = new Properties(); 30 | props.put("ns:key", "value:foo=faa"); 31 | java.io.StringWriter sw = new java.io.StringWriter(); 32 | props.store(sw, null); 33 | System.err.println("-> "+sw); 34 | */ 35 | 36 | Map input = new HashMap<>(); 37 | input.put("key", "tab:\tTAB!"); 38 | assertEquals("key=tab:\\tTAB!\n", MAPPER.writeValueAsString(input)); 39 | 40 | input = new HashMap<>(); 41 | input.put("key", "NUL=\0..."); 42 | assertEquals("key=NUL=\\u0000...\n", MAPPER.writeValueAsString(input)); 43 | 44 | input = new HashMap<>(); 45 | input.put("key", "multi\nline"); 46 | assertEquals("key=multi\\nline\n", MAPPER.writeValueAsString(input)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/PropertiesSupportTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * Tests for extended functionality to work with JDK `Properties` Object 7 | */ 8 | public class PropertiesSupportTest extends ModuleTestBase 9 | { 10 | private final JavaPropsMapper MAPPER = mapperForProps(); 11 | 12 | public void testSimpleEmployee() throws Exception 13 | { 14 | Properties props = new Properties(); 15 | props.put("a.b", "14"); 16 | props.put("x", "foo"); 17 | Map result = MAPPER.readPropertiesAs(props, Map.class); 18 | assertNotNull(result); 19 | assertEquals(2, result.size()); 20 | assertEquals("foo", result.get("x")); 21 | Object ob = result.get("a"); 22 | assertNotNull(ob); 23 | assertTrue(ob instanceof Map); 24 | Map m2 = (Map) ob; 25 | assertEquals(1, m2.size()); 26 | assertEquals("14", m2.get("b")); 27 | } 28 | 29 | public void testWithCustomSchema() throws Exception 30 | { 31 | Properties props = new Properties(); 32 | props.put("a/b", "14"); 33 | props.put("x.y/z", "foo"); 34 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 35 | .withPathSeparator("/"); 36 | Map result = MAPPER.readPropertiesAs(props, schema, 37 | MAPPER.constructType(Map.class)); 38 | assertNotNull(result); 39 | assertEquals(2, result.size()); 40 | Object ob = result.get("a"); 41 | assertNotNull(ob); 42 | assertTrue(ob instanceof Map); 43 | Map m2 = (Map) ob; 44 | assertEquals(1, m2.size()); 45 | assertEquals("14", m2.get("b")); 46 | 47 | ob = result.get("x.y"); 48 | assertNotNull(ob); 49 | assertTrue(ob instanceof Map); 50 | m2 = (Map) ob; 51 | assertEquals(1, m2.size()); 52 | assertEquals("foo", m2.get("z")); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/DefaultConfigsTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.StringWriter; 4 | 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.core.Version; 8 | import com.fasterxml.jackson.core.Versioned; 9 | 10 | public class DefaultConfigsTest extends ModuleTestBase 11 | { 12 | private final String ARTIFACT_ID = "jackson-dataformat-properties"; 13 | 14 | public void testMapperBaseConfig() 15 | { 16 | JavaPropsMapper mapper = new JavaPropsMapper(); 17 | _verifyVersion(mapper); 18 | JavaPropsMapper copy = mapper.copy(); 19 | assertNotSame(mapper, copy); 20 | } 21 | 22 | public void testFactoryBaseConfig() 23 | { 24 | JavaPropsFactory f = new JavaPropsFactory(); 25 | _verifyVersion(f); 26 | JavaPropsFactory copy = f.copy(); 27 | assertNotSame(f, copy); 28 | assertEquals(JavaPropsFactory.FORMAT_NAME_JAVA_PROPERTIES, f.getFormatName()); 29 | assertFalse(f.requiresCustomCodec()); 30 | assertFalse(f.requiresPropertyOrdering()); 31 | assertFalse(f.canHandleBinaryNatively()); 32 | assertFalse(f.canUseCharArrays()); 33 | } 34 | 35 | public void testGeneratorConfig() throws Exception 36 | { 37 | JavaPropsFactory f = new JavaPropsFactory(); 38 | JsonGenerator gen = f.createGenerator(new StringWriter()); 39 | _verifyVersion(gen); 40 | assertTrue(gen.canOmitFields()); 41 | assertFalse(gen.canWriteBinaryNatively()); 42 | assertTrue(gen.canWriteFormattedNumbers()); 43 | assertFalse(gen.canWriteObjectId()); 44 | assertFalse(gen.canWriteTypeId()); 45 | assertTrue(gen.canUseSchema(JavaPropsSchema.emptySchema())); 46 | 47 | gen.setSchema(JavaPropsSchema.emptySchema()); 48 | assertSame(JavaPropsSchema.emptySchema(), gen.getSchema()); 49 | gen.close(); 50 | } 51 | 52 | public void testParserConfig() throws Exception 53 | { 54 | JavaPropsFactory f = new JavaPropsFactory(); 55 | JsonParser p = f.createParser("#foo".getBytes("UTF-8")); 56 | _verifyVersion(p); 57 | assertFalse(p.canReadObjectId()); 58 | assertFalse(p.canReadTypeId()); 59 | assertFalse(p.hasTextCharacters()); 60 | assertTrue(p.canUseSchema(JavaPropsSchema.emptySchema())); 61 | assertFalse(p.requiresCustomCodec()); 62 | 63 | p.setSchema(JavaPropsSchema.emptySchema()); 64 | assertSame(JavaPropsSchema.emptySchema(), p.getSchema()); 65 | p.close(); 66 | } 67 | 68 | private void _verifyVersion(Versioned v) { 69 | Version v2 = v.version(); 70 | assertEquals(ARTIFACT_ID, v2.getArtifactId()); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/ArrayGenerationTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.util.Properties; 4 | 5 | import com.fasterxml.jackson.dataformat.javaprop.util.Markers; 6 | 7 | public class ArrayGenerationTest extends ModuleTestBase 8 | { 9 | private final JavaPropsMapper MAPPER = mapperForProps(); 10 | 11 | public void testPointListSimple() throws Exception 12 | { 13 | Points input = new Points 14 | (new Point(1, 2), new Point(3, 4), new Point(5, 6)); 15 | String output = MAPPER.writeValueAsString(input); 16 | assertEquals("p.1.x=1\n" 17 | +"p.1.y=2\n" 18 | +"p.2.x=3\n" 19 | +"p.2.y=4\n" 20 | +"p.3.x=5\n" 21 | +"p.3.y=6\n" 22 | ,output); 23 | Properties props = MAPPER.writeValueAsProperties(input); 24 | assertEquals(6, props.size()); 25 | assertEquals("6", props.get("p.3.y")); 26 | assertEquals("1", props.get("p.1.x")); 27 | } 28 | 29 | public void testPointListWithIndex() throws Exception 30 | { 31 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 32 | .withWriteIndexUsingMarkers(true) 33 | .withFirstArrayOffset(3); 34 | Points input = new Points 35 | (new Point(1, 2), new Point(3, 4), new Point(5, 6)); 36 | String output = MAPPER.writer(schema) 37 | .writeValueAsString(input); 38 | assertEquals("p[3].x=1\n" 39 | +"p[3].y=2\n" 40 | +"p[4].x=3\n" 41 | +"p[4].y=4\n" 42 | +"p[5].x=5\n" 43 | +"p[5].y=6\n" 44 | ,output); 45 | Properties props = MAPPER.writeValueAsProperties(input, schema); 46 | assertEquals(6, props.size()); 47 | assertEquals("2", props.get("p[3].y")); 48 | assertEquals("3", props.get("p[4].x")); 49 | } 50 | 51 | public void testPointListWithCustomMarkers() throws Exception 52 | { 53 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 54 | .withWriteIndexUsingMarkers(true) 55 | .withIndexMarker(Markers.create("<<", ">>")) 56 | ; 57 | Points input = new Points(new Point(1, 2), new Point(3, 4)); 58 | String output = MAPPER.writer(schema) 59 | .writeValueAsString(input); 60 | assertEquals("p<<1>>.x=1\n" 61 | +"p<<1>>.y=2\n" 62 | +"p<<2>>.x=3\n" 63 | +"p<<2>>.y=4\n" 64 | ,output); 65 | Properties props = MAPPER.writeValueAsProperties(input, schema); 66 | assertEquals(4, props.size()); 67 | assertEquals("1", props.get("p<<1>>.x")); 68 | assertEquals("4", props.get("p<<2>>.y")); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fasterxml.jackson 6 | jackson-bom 7 | 2.9.0.pr2-SNAPSHOT 8 | 9 | com.fasterxml.jackson.dataformat 10 | jackson-dataformat-properties 11 | 2.9.0.pr2-SNAPSHOT 12 | Jackson-dataformat-Properties 13 | bundle 14 | Support for reading and writing content of "Java Properties" style 15 | configuration files as if there was implied nesting structure (by default using dots as separators). 16 | 17 | 18 | http://github.com/FasterXML/jackson-dataformat-properties 19 | 20 | 21 | scm:git:git@github.com:FasterXML/jackson-dataformat-properties.git 22 | scm:git:git@github.com:FasterXML/jackson-dataformat-properties.git 23 | http://github.com/FasterXML/jackson-dataformat-properties 24 | HEAD 25 | 26 | 27 | 28 | com/fasterxml/jackson/dataformat/javaprop 29 | ${project.groupId}.javaprop 30 | 31 | 32 | 33 | 34 | 35 | com.fasterxml.jackson.core 36 | jackson-core 37 | 38 | 39 | 40 | com.fasterxml.jackson.core 41 | jackson-databind 42 | provided 43 | 44 | 45 | 46 | junit 47 | junit 48 | test 49 | 50 | 51 | com.fasterxml.jackson.core 52 | jackson-annotations 53 | test 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | com.google.code.maven-replacer-plugin 62 | replacer 63 | 64 | 65 | process-packageVersion 66 | generate-sources 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-surefire-plugin 73 | ${version.plugin.surefire} 74 | 75 | 76 | com/fasterxml/jackson/**/failing/*.java 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/SimpleParsingTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.util.Map; 4 | 5 | import org.junit.Assert; 6 | 7 | import com.fasterxml.jackson.core.Base64Variants; 8 | import com.fasterxml.jackson.core.JsonParser; 9 | import com.fasterxml.jackson.core.JsonToken; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | 12 | public class SimpleParsingTest extends ModuleTestBase 13 | { 14 | private final ObjectMapper MAPPER = mapperForProps(); 15 | 16 | private final ObjectMapper JSON_MAPPER = new ObjectMapper(); 17 | 18 | public void testSimpleNonNested() throws Exception { 19 | _testSimpleNonNested(false); 20 | _testSimpleNonNested(true); 21 | } 22 | 23 | public void testSimpleNested() throws Exception { 24 | _testSimpleNested(false); 25 | _testSimpleNested(true); 26 | } 27 | 28 | public void testSimpleRectangle() throws Exception { 29 | _testSimpleRectangle(false); 30 | _testSimpleRectangle(true); 31 | } 32 | 33 | public void testNonSplittingParsing() throws Exception { 34 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 35 | .withoutPathSeparator(); 36 | Map result = MAPPER.readerFor(Map.class) 37 | .with(schema) 38 | .readValue("a.b.c = 3"); 39 | assertEquals("{\"a.b.c\":\"3\"}", JSON_MAPPER.writeValueAsString(result)); 40 | } 41 | 42 | private void _testSimpleNonNested(boolean useBytes) throws Exception 43 | { 44 | final String INPUT = "firstName=Bob\n" 45 | +"lastName=Palmer\n" 46 | +"gender=MALE\n" 47 | +"verified=true\n" 48 | +"userImage=AQIDBA==\n"; 49 | FiveMinuteUser result = _mapFrom(MAPPER, INPUT, FiveMinuteUser.class, useBytes); 50 | assertEquals(Gender.MALE, result.getGender()); 51 | assertEquals(4, result.getUserImage().length); 52 | 53 | // and then with streaming parser 54 | final String INPUT2 = "firstName=Bob\n" 55 | +"verified=true\n"; 56 | JsonParser p = MAPPER.getFactory().createParser(INPUT2); 57 | assertToken(JsonToken.START_OBJECT, p.nextToken()); 58 | assertToken(JsonToken.FIELD_NAME, p.nextToken()); 59 | assertToken(JsonToken.VALUE_STRING, p.nextToken()); 60 | byte[] b = p.getBinaryValue(Base64Variants.MIME); 61 | assertEquals(3, b.length); 62 | // and verify it'll remain stable 63 | byte[] b2 = p.getBinaryValue(Base64Variants.MIME); 64 | Assert.assertArrayEquals(b, b2); 65 | p.close(); 66 | } 67 | 68 | private void _testSimpleNested(boolean useBytes) throws Exception 69 | { 70 | final String INPUT = "comparison.source.database=test\n" 71 | +"comparison.target.database=test2\n" 72 | ; 73 | Map result = _mapFrom(MAPPER, INPUT, Map.class, useBytes); 74 | assertEquals(1, result.size()); 75 | } 76 | 77 | private void _testSimpleRectangle(boolean useBytes) throws Exception 78 | { 79 | final String INPUT = "topLeft.x=1\n" 80 | +"topLeft.y=-2\n" 81 | +"bottomRight.x=5\n" 82 | +"bottomRight.y=10\n"; 83 | Rectangle result = _mapFrom(MAPPER, INPUT, Rectangle.class, useBytes); 84 | assertEquals(5, result.bottomRight.x); 85 | assertEquals(10, result.bottomRight.y); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/SimpleGenerationTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.util.Properties; 4 | 5 | public class SimpleGenerationTest extends ModuleTestBase 6 | { 7 | private final JavaPropsMapper MAPPER = mapperForProps(); 8 | 9 | public void testSimpleEmployee() throws Exception 10 | { 11 | FiveMinuteUser input = new FiveMinuteUser("Bob", "Palmer", true, Gender.MALE, 12 | new byte[] { 1, 2, 3, 4 }); 13 | String output = MAPPER.writeValueAsString(input); 14 | assertEquals("firstName=Bob\n" 15 | +"lastName=Palmer\n" 16 | +"gender=MALE\n" 17 | +"verified=true\n" 18 | +"userImage=AQIDBA==\n" 19 | ,output); 20 | Properties props = MAPPER.writeValueAsProperties(input); 21 | assertEquals(5, props.size()); 22 | assertEquals("true", props.get("verified")); 23 | assertEquals("MALE", props.get("gender")); 24 | } 25 | 26 | public void testSimpleRectangle() throws Exception 27 | { 28 | Rectangle input = new Rectangle(new Point(1, -2), new Point(5, 10)); 29 | String output = MAPPER.writeValueAsString(input); 30 | assertEquals("topLeft.x=1\n" 31 | +"topLeft.y=-2\n" 32 | +"bottomRight.x=5\n" 33 | +"bottomRight.y=10\n" 34 | ,output); 35 | Properties props = MAPPER.writeValueAsProperties(input); 36 | assertEquals(4, props.size()); 37 | assertEquals("5", props.get("bottomRight.x")); 38 | } 39 | 40 | public void testRectangleWithCustomKeyValueSeparator() throws Exception 41 | { 42 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 43 | .withKeyValueSeparator(": "); 44 | Rectangle input = new Rectangle(new Point(1, -2), new Point(5, 10)); 45 | String output = MAPPER.writer(schema).writeValueAsString(input); 46 | assertEquals("topLeft.x: 1\n" 47 | +"topLeft.y: -2\n" 48 | +"bottomRight.x: 5\n" 49 | +"bottomRight.y: 10\n" 50 | ,output); 51 | Properties props = MAPPER.writeValueAsProperties(input, schema); 52 | assertEquals(4, props.size()); 53 | assertEquals("5", props.get("bottomRight.x")); 54 | } 55 | 56 | public void testRectangleWithHeader() throws Exception 57 | { 58 | final String HEADER = "# SUPER IMPORTANT!\n"; 59 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 60 | .withHeader(HEADER); 61 | Rectangle input = new Rectangle(new Point(1, -2), new Point(5, 10)); 62 | String output = MAPPER.writer(schema) 63 | .writeValueAsString(input); 64 | assertEquals(HEADER 65 | +"topLeft.x=1\n" 66 | +"topLeft.y=-2\n" 67 | +"bottomRight.x=5\n" 68 | +"bottomRight.y=10\n" 69 | ,output); 70 | } 71 | 72 | public void testRectangleWithIndent() throws Exception 73 | { 74 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 75 | .withLineIndentation(" "); 76 | Rectangle input = new Rectangle(new Point(1, -2), new Point(5, 10)); 77 | String output = MAPPER.writer(schema) 78 | .writeValueAsString(input); 79 | assertEquals(" topLeft.x=1\n" 80 | +" topLeft.y=-2\n" 81 | +" bottomRight.x=5\n" 82 | +" bottomRight.y=10\n" 83 | ,output); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/impl/PropertiesBackedGenerator.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.impl; 2 | 3 | import java.io.*; 4 | import java.util.Properties; 5 | 6 | import com.fasterxml.jackson.core.*; 7 | import com.fasterxml.jackson.core.io.IOContext; 8 | import com.fasterxml.jackson.dataformat.javaprop.JavaPropsGenerator; 9 | 10 | public class PropertiesBackedGenerator extends JavaPropsGenerator 11 | { 12 | /* 13 | /********************************************************** 14 | /* Configuration 15 | /********************************************************** 16 | */ 17 | 18 | /** 19 | * Underlying {@link Properties} that we will update with logical 20 | * properties written out. 21 | */ 22 | final protected Properties _props; 23 | 24 | /* 25 | /********************************************************** 26 | /* Life-cycle 27 | /********************************************************** 28 | */ 29 | 30 | public PropertiesBackedGenerator(IOContext ctxt, Properties props, 31 | int stdFeatures, ObjectCodec codec) 32 | { 33 | super(ctxt, stdFeatures, codec); 34 | _props = props; 35 | } 36 | 37 | /* 38 | /********************************************************** 39 | /* Overridden methods, configuration 40 | /********************************************************** 41 | */ 42 | 43 | @Override 44 | public Object getOutputTarget() { 45 | return _props; 46 | } 47 | 48 | /* 49 | /********************************************************** 50 | /* Overridden methods: low-level I/O 51 | /********************************************************** 52 | */ 53 | 54 | @Override 55 | public void close() throws IOException { } 56 | 57 | @Override 58 | public void flush() throws IOException { } 59 | 60 | /* 61 | /********************************************************** 62 | /* Implementations for methods from base class 63 | /********************************************************** 64 | */ 65 | 66 | @Override 67 | protected void _releaseBuffers() { } 68 | 69 | /* 70 | /********************************************************** 71 | /* Internal methods; escaping writes 72 | /********************************************************** 73 | */ 74 | 75 | @Override 76 | protected void _writeEscapedEntry(char[] text, int offset, int len) throws IOException { 77 | _writeEscapedEntry(new String(text, offset, len)); 78 | } 79 | 80 | @Override 81 | protected void _writeEscapedEntry(String value) throws IOException 82 | { 83 | _props.put(_basePath.toString(), value); 84 | } 85 | 86 | @Override 87 | protected void _writeUnescapedEntry(String value) throws IOException 88 | { 89 | _props.put(_basePath.toString(), value); 90 | } 91 | 92 | /* 93 | /********************************************************** 94 | /* Internal methods; raw writes 95 | /********************************************************** 96 | */ 97 | 98 | /* 02-Jun-2016, tatu: no way to support raw writes, so two things we 99 | * could do instead: throw exception, or just quietly ignore. Typically 100 | * I favor throwing exception, but here it's probably better to simply 101 | * ignore. 102 | */ 103 | 104 | @Override 105 | protected void _writeRaw(char c) throws IOException 106 | { 107 | } 108 | 109 | @Override 110 | protected void _writeRaw(String text) throws IOException 111 | { 112 | } 113 | 114 | @Override 115 | protected void _writeRaw(StringBuilder text) throws IOException 116 | { 117 | } 118 | 119 | @Override 120 | protected void _writeRaw(char[] text, int offset, int len) throws IOException 121 | { 122 | } 123 | 124 | protected void _writeRawLong(String text) throws IOException 125 | { 126 | } 127 | 128 | protected void _writeRawLong(StringBuilder text) throws IOException 129 | { 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/io/JPropEscapes.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.io; 2 | 3 | import java.util.Arrays; 4 | 5 | /** 6 | * Container class for definitions of characters to escape. 7 | */ 8 | public class JPropEscapes 9 | { 10 | private final static char[] HEX = "0123456789ABCDEF".toCharArray(); 11 | 12 | private final static int UNICODE_ESCAPE = -1; 13 | 14 | private final static int[] sValueEscapes; 15 | static { 16 | final int[] table = new int[256]; 17 | // For values, fewer escapes needed, but most control chars need them 18 | for (int i = 0; i < 32; ++i) { 19 | table[i] = UNICODE_ESCAPE; 20 | // also high-bit ones 21 | table[128+i] = UNICODE_ESCAPE; 22 | } 23 | // also, that one weird character needs escaping: 24 | table[0x7F] = UNICODE_ESCAPE; 25 | 26 | // except for "well-known" ones 27 | table['\t'] = 't'; 28 | table['\r'] = 'r'; 29 | table['\n'] = 'n'; 30 | 31 | // Beyond that, just backslash 32 | table['\\'] = '\\'; 33 | sValueEscapes = table; 34 | } 35 | 36 | private final static int[] sKeyEscapes; 37 | static { 38 | // with keys, start with value escapes, and add the rest 39 | final int[] table = Arrays.copyOf(sValueEscapes, 256); 40 | 41 | // comment line starters (could get by with just start char but whatever) 42 | table['#'] = '#'; 43 | table['!'] = '!'; 44 | // and then equals (and equivalents) that mark end of key 45 | table['='] = '='; 46 | table[':'] = ':'; 47 | // plus space chars are escapes too 48 | table[' '] = ' '; 49 | 50 | sKeyEscapes = table; 51 | } 52 | 53 | public static void appendKey(StringBuilder sb, String key) { 54 | final int end = key.length(); 55 | if (end == 0) { 56 | return; 57 | } 58 | final int[] esc = sKeyEscapes; 59 | // first quick loop for common case of no escapes 60 | int i = 0; 61 | 62 | while (true) { 63 | char c = key.charAt(i); 64 | if ((c > 0xFF) || esc[c] != 0) { 65 | break; 66 | } 67 | sb.append(c); 68 | if (++i == end) { 69 | return; 70 | } 71 | } 72 | _appendWithEscapes(sb, key, esc, i); 73 | } 74 | 75 | public static StringBuilder appendValue(String value) { 76 | final int end = value.length(); 77 | if (end == 0) { 78 | return null; // nothing to write, but "write as is" 79 | } 80 | final int[] esc = sValueEscapes; 81 | int i = 0; 82 | 83 | // little bit different here; we scan to ensure nothing to escape 84 | while (true) { 85 | char c = value.charAt(i); 86 | if ((c > 0xFF) || esc[c] != 0) { 87 | break; 88 | } 89 | if (++i == end) { 90 | return null; // write as-is 91 | } 92 | } 93 | // if not real work 94 | StringBuilder sb = new StringBuilder(end + 5 + (end>>3)); 95 | for (int j = 0; j < i; ++j) { 96 | sb.append(value.charAt(j)); 97 | } 98 | _appendWithEscapes(sb, value, esc, i); 99 | return sb; 100 | } 101 | 102 | private static void _appendWithEscapes(StringBuilder sb, String key, 103 | int[] esc, int i) 104 | { 105 | final int end = key.length(); 106 | do { 107 | char c = key.charAt(i); 108 | int type = (c > 0xFF) ? UNICODE_ESCAPE : esc[c]; 109 | if (type == 0) { 110 | sb.append(c); 111 | continue; 112 | } 113 | if (type == UNICODE_ESCAPE) { 114 | sb.append('\\'); 115 | sb.append('u'); 116 | sb.append(HEX[c >>> 12]); 117 | sb.append(HEX[(c >> 8) & 0xF]); 118 | sb.append(HEX[(c >> 4) & 0xF]); 119 | sb.append(HEX[c & 0xF]); 120 | } else { 121 | sb.append('\\'); 122 | sb.append((char) type); 123 | } 124 | } while (++i < end); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/util/JPropNode.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.util; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * Value in an ordered tree presentation built from an arbitrarily ordered 7 | * set of flat input values. Since either index- OR name-based access is to 8 | * be supported (similar to, say, Javascript objects) -- but only one, not both -- 9 | * storage is bit of a hybrid. In addition, branches may also have values. 10 | * So, code does bit coercion as necessary, trying to maintain something 11 | * consistent and usable at all times, without failure. 12 | */ 13 | public class JPropNode 14 | { 15 | /** 16 | * Value for the path, for leaf nodes; usually null for branches. 17 | * If both children and value exists, typically need to construct 18 | * bogus value with empty String key. 19 | */ 20 | protected String _value; 21 | 22 | /** 23 | * Child entries with integral number index, if any. 24 | */ 25 | protected TreeMap _byIndex; 26 | 27 | /** 28 | * Child entries accessed with String property name, if any. 29 | */ 30 | protected Map _byName; 31 | 32 | protected boolean _hasContents = false; 33 | 34 | public JPropNode setValue(String v) { 35 | // should we care about overwrite? 36 | _value = v; 37 | return this; 38 | } 39 | 40 | public JPropNode addByIndex(int index) { 41 | // if we already have named entries, coerce into name 42 | if (_byName != null) { 43 | return addByName(String.valueOf(index)); 44 | } 45 | _hasContents = true; 46 | if (_byIndex == null) { 47 | _byIndex = new TreeMap<>(); 48 | } 49 | Integer key = Integer.valueOf(index); 50 | JPropNode n = _byIndex.get(key); 51 | if (n == null) { 52 | n = new JPropNode(); 53 | _byIndex.put(key, n); 54 | } 55 | return n; 56 | } 57 | 58 | public JPropNode addByName(String name) { 59 | // if former index entries, first coerce them 60 | _hasContents = true; 61 | if (_byIndex != null) { 62 | if (_byName == null) { 63 | _byName = new LinkedHashMap<>(); 64 | } 65 | for (Map.Entry entry : _byIndex.entrySet()) { 66 | _byName.put(entry.getKey().toString(), entry.getValue()); 67 | } 68 | _byIndex = null; 69 | } 70 | if (_byName == null) { 71 | _byName = new LinkedHashMap<>(); 72 | } else { 73 | JPropNode old = _byName.get(name); 74 | if (old != null) { 75 | return old; 76 | } 77 | } 78 | JPropNode result = new JPropNode(); 79 | _byName.put(name, result); 80 | return result; 81 | } 82 | 83 | public boolean isLeaf() { 84 | return !_hasContents && (_value != null); 85 | } 86 | 87 | public boolean isArray() { 88 | return _byIndex != null; 89 | } 90 | 91 | public String getValue() { 92 | return _value; 93 | } 94 | 95 | public Iterator arrayContents() { 96 | // should never be called if `_byIndex` is null, hence no checks 97 | return _byIndex.values().iterator(); 98 | } 99 | 100 | /** 101 | * Child entries accessed with String property name, if any. 102 | */ 103 | public Iterator> objectContents() { 104 | if (_byName == null) { // only value, most likely 105 | return Collections.emptyIterator(); 106 | } 107 | return _byName.entrySet().iterator(); 108 | } 109 | 110 | /** 111 | * Helper method, mostly for debugging/testing, to convert structure contained 112 | * into simple List/Map/String equivalent. 113 | */ 114 | public Object asRaw() { 115 | if (isArray()) { 116 | List result = new ArrayList<>(); 117 | if (_value != null) { 118 | result.add(_value); 119 | } 120 | for (JPropNode v : _byIndex.values()) { 121 | result.add(v.asRaw()); 122 | } 123 | return result; 124 | } 125 | if (_byName != null) { 126 | Map result = new LinkedHashMap<>(); 127 | if (_value != null) { 128 | result.put("", _value); 129 | } 130 | for (Map.Entry entry : _byName.entrySet()) { 131 | result.put(entry.getKey(), entry.getValue().asRaw()); 132 | } 133 | return result; 134 | } 135 | return _value; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/SimpleStreamingTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.Reader; 4 | import java.io.StringWriter; 5 | import java.io.Writer; 6 | import java.math.BigDecimal; 7 | import java.math.BigInteger; 8 | import java.util.Map; 9 | 10 | import com.fasterxml.jackson.core.JsonGenerator; 11 | import com.fasterxml.jackson.core.JsonParser; 12 | import com.fasterxml.jackson.core.JsonProcessingException; 13 | import com.fasterxml.jackson.core.JsonStreamContext; 14 | import com.fasterxml.jackson.core.JsonToken; 15 | import com.fasterxml.jackson.core.io.SerializedString; 16 | import com.fasterxml.jackson.databind.ObjectMapper; 17 | import com.fasterxml.jackson.dataformat.javaprop.io.JPropWriteContext; 18 | 19 | public class SimpleStreamingTest extends ModuleTestBase 20 | { 21 | private final ObjectMapper MAPPER = mapperForProps(); 22 | 23 | private final JavaPropsFactory F = new JavaPropsFactory(); 24 | 25 | public void testParsing() throws Exception 26 | { 27 | JsonParser p = F.createParser("foo = bar"); 28 | Object src = p.getInputSource(); 29 | assertTrue(src instanceof Reader); 30 | assertToken(JsonToken.START_OBJECT, p.nextToken()); 31 | assertNull(p.getEmbeddedObject()); 32 | assertNotNull(p.getCurrentLocation()); // N/A 33 | assertNotNull(p.getTokenLocation()); // N/A 34 | assertToken(JsonToken.FIELD_NAME, p.nextToken()); 35 | assertEquals("foo", p.getText()); 36 | assertToken(JsonToken.VALUE_STRING, p.nextToken()); 37 | StringWriter sw = new StringWriter(); 38 | assertEquals(3, p.getText(sw)); 39 | assertEquals("bar", sw.toString()); 40 | p.close(); 41 | assertTrue(p.isClosed()); 42 | 43 | // one more thing, verify handling of non-binary 44 | p = F.createParser("foo = bar"); 45 | assertToken(JsonToken.START_OBJECT, p.nextToken()); 46 | assertToken(JsonToken.FIELD_NAME, p.nextToken()); 47 | try { 48 | p.getBinaryValue(); 49 | fail("Should not pass"); 50 | } catch (JsonProcessingException e) { 51 | verifyException(e, "can not access as binary"); 52 | } 53 | try { 54 | p.getDoubleValue(); 55 | fail("Should not pass"); 56 | } catch (JsonProcessingException e) { 57 | verifyException(e, "can not use numeric"); 58 | } 59 | p.close(); 60 | } 61 | 62 | public void testStreamingGeneration() throws Exception 63 | { 64 | StringWriter strw = new StringWriter(); 65 | JsonGenerator gen = F.createGenerator(strw); 66 | 67 | Object target = gen.getOutputTarget(); 68 | assertTrue(target instanceof Writer); 69 | 70 | gen.writeStartObject(); 71 | gen.writeBooleanField("flagTrue", true); 72 | gen.writeBooleanField("flagFalse", false); 73 | gen.writeNullField("null"); 74 | gen.writeNumberField("long", 10L); 75 | gen.writeNumberField("int", 10); 76 | gen.writeNumberField("double", 0.25); 77 | gen.writeNumberField("float", 0.5f); 78 | gen.writeNumberField("decimal", BigDecimal.valueOf(0.125)); 79 | gen.writeFieldName(new SerializedString("bigInt")); 80 | gen.writeNumber(BigInteger.valueOf(123)); 81 | gen.writeFieldName("numString"); 82 | gen.writeNumber("123.0"); 83 | gen.writeFieldName("charString"); 84 | gen.writeString(new char[] { 'a', 'b', 'c' }, 1, 2); 85 | 86 | gen.writeFieldName("arr"); 87 | gen.writeStartArray(); 88 | 89 | JsonStreamContext ctxt = gen.getOutputContext(); 90 | String path = ctxt.toString(); 91 | assertTrue(ctxt instanceof JPropWriteContext); 92 | // Note: this context gives full path, unlike many others 93 | assertEquals("/arr/0", path); 94 | 95 | gen.writeEndArray(); 96 | 97 | gen.writeEndObject(); 98 | assertFalse(gen.isClosed()); 99 | gen.flush(); 100 | gen.close(); 101 | 102 | String props = strw.toString(); 103 | 104 | // Plus read back for fun 105 | Map stuff = MAPPER.readValue(props, Map.class); 106 | assertEquals(11, stuff.size()); 107 | assertEquals("10", stuff.get("long")); 108 | } 109 | 110 | public void testStreamingGenerationRaw() throws Exception 111 | { 112 | StringWriter strw = new StringWriter(); 113 | JsonGenerator gen = F.createGenerator(strw); 114 | 115 | String COMMENT = "# comment!\n"; 116 | gen.writeRaw(COMMENT); 117 | gen.writeRaw(new SerializedString(COMMENT)); 118 | gen.writeRaw(COMMENT, 0, COMMENT.length()); 119 | gen.writeRaw('#'); 120 | gen.writeRaw('\n'); 121 | 122 | gen.writeStartObject(); 123 | gen.writeBooleanField("enabled", true); 124 | gen.writeEndObject(); 125 | 126 | gen.close(); 127 | 128 | assertEquals(COMMENT + COMMENT + COMMENT 129 | + "#\nenabled=true\n", strw.toString()); 130 | 131 | // Plus read back for fun 132 | Map stuff = MAPPER.readValue(strw.toString(), Map.class); 133 | assertEquals(1, stuff.size()); 134 | assertEquals("true", stuff.get("enabled")); 135 | } 136 | 137 | public void testStreamingLongRaw() throws Exception 138 | { 139 | StringWriter strw = new StringWriter(); 140 | JsonGenerator gen = F.createGenerator(strw); 141 | 142 | StringBuilder sb = new StringBuilder(); 143 | sb.append("# "); 144 | for (int i = 0; i < 12000; ++i) { 145 | sb.append('a'); 146 | } 147 | gen.writeRaw(sb.toString()); 148 | gen.close(); 149 | 150 | assertEquals(sb.toString(), strw.toString()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/io/JPropWriteContext.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.io; 2 | 3 | import com.fasterxml.jackson.core.*; 4 | //import com.fasterxml.jackson.core.json.JsonWriteContext; 5 | 6 | public class JPropWriteContext 7 | extends JsonStreamContext 8 | { 9 | /** 10 | * Parent context for this context; null for root context. 11 | */ 12 | protected final JPropWriteContext _parent; 13 | 14 | /* 15 | /********************************************************** 16 | /* Simple instance reuse slots; speed up things 17 | /* a bit (10-15%) for docs with lots of small 18 | /* arrays/objects 19 | /********************************************************** 20 | */ 21 | 22 | protected JPropWriteContext _child = null; 23 | 24 | /* 25 | /********************************************************** 26 | /* Location/state information (minus source reference) 27 | /********************************************************** 28 | */ 29 | 30 | /** 31 | * Value that is being serialized and caused this context to be created; 32 | * typically a POJO or container type. 33 | */ 34 | protected Object _currentValue; 35 | 36 | /** 37 | * Marker used to indicate that we just received a name, and 38 | * now expect a value 39 | */ 40 | protected boolean _gotName; 41 | 42 | /** 43 | * Name of the field of which value is to be parsed; only 44 | * used for OBJECT contexts 45 | */ 46 | protected String _currentName; 47 | 48 | protected int _basePathLength; 49 | 50 | /* 51 | /********************************************************** 52 | /* Life-cycle 53 | /********************************************************** 54 | */ 55 | 56 | protected JPropWriteContext(int type, JPropWriteContext parent, 57 | int basePathLength) 58 | { 59 | super(); 60 | _type = type; 61 | _parent = parent; 62 | _basePathLength = basePathLength; 63 | _index = -1; 64 | } 65 | 66 | private void reset(int type, int basePathLength) { 67 | _type = type; 68 | _basePathLength = basePathLength; 69 | _index = -1; 70 | } 71 | 72 | // // // Factory methods 73 | 74 | public static JPropWriteContext createRootContext() { 75 | return new JPropWriteContext(TYPE_ROOT, null, 0); 76 | } 77 | 78 | public static JPropWriteContext createRootContext(int basePathLength) { 79 | return new JPropWriteContext(TYPE_ROOT, null, basePathLength); 80 | } 81 | 82 | public JPropWriteContext createChildArrayContext(int basePathLength) { 83 | JPropWriteContext ctxt = _child; 84 | if (ctxt == null) { 85 | _child = ctxt = new JPropWriteContext(TYPE_ARRAY, this, basePathLength); 86 | return ctxt; 87 | } 88 | ctxt.reset(TYPE_ARRAY, basePathLength); 89 | return ctxt; 90 | } 91 | 92 | public JPropWriteContext createChildObjectContext(int basePathLength) { 93 | JPropWriteContext ctxt = _child; 94 | if (ctxt == null) { 95 | _child = ctxt = new JPropWriteContext(TYPE_OBJECT, this, basePathLength); 96 | return ctxt; 97 | } 98 | ctxt.reset(TYPE_OBJECT, basePathLength); 99 | return ctxt; 100 | } 101 | 102 | /* 103 | /********************************************************** 104 | /* State changes 105 | /********************************************************** 106 | */ 107 | 108 | public boolean writeFieldName(String name) throws JsonProcessingException { 109 | if (_gotName) { 110 | return false; 111 | } 112 | _gotName = true; 113 | _currentName = name; 114 | return true; 115 | } 116 | 117 | public boolean writeValue() { 118 | // Most likely, object: 119 | if (_type == TYPE_OBJECT) { 120 | if (!_gotName) { 121 | return false; 122 | } 123 | _gotName = false; 124 | } 125 | // Array fine, and must allow root context for Object values too so... 126 | ++_index; 127 | return true; 128 | } 129 | 130 | public void truncatePath(StringBuilder sb) { 131 | int len = sb.length(); 132 | if (len != _basePathLength) { 133 | if (len < _basePathLength) { // sanity check 134 | throw new IllegalStateException(String.format 135 | ("Internal error: base path length %d, buffered %d, trying to truncate", 136 | _basePathLength, len)); 137 | } 138 | sb.setLength(_basePathLength); 139 | } 140 | } 141 | 142 | /* 143 | /********************************************************** 144 | /* Simple accessors, mutators 145 | /********************************************************** 146 | */ 147 | 148 | @Override 149 | public final JPropWriteContext getParent() { return _parent; } 150 | 151 | @Override 152 | public String getCurrentName() { 153 | return _currentName; 154 | } 155 | 156 | @Override 157 | public Object getCurrentValue() { 158 | return _currentValue; 159 | } 160 | 161 | @Override 162 | public void setCurrentValue(Object v) { 163 | _currentValue = v; 164 | } 165 | 166 | public StringBuilder appendDesc(StringBuilder sb) { 167 | if (_parent != null) { 168 | sb = _parent.appendDesc(sb); 169 | sb.append('/'); 170 | } 171 | switch (_type) { 172 | case TYPE_OBJECT: 173 | if (_currentName != null) { 174 | sb.append(_currentName); 175 | } 176 | break; 177 | case TYPE_ARRAY: 178 | sb.append(getCurrentIndex()); 179 | break; 180 | case TYPE_ROOT: 181 | } 182 | return sb; 183 | } 184 | 185 | // // // Overridden standard methods 186 | 187 | /** 188 | * Overridden to provide developer JsonPointer representation 189 | * of the context. 190 | */ 191 | @Override 192 | public final String toString() { 193 | return appendDesc(new StringBuilder(64)).toString(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/io/Latin1Reader.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.io; 2 | 3 | import java.io.*; 4 | 5 | import com.fasterxml.jackson.core.io.IOContext; 6 | 7 | /** 8 | * Optimized Reader that reads ISO-8859-1 encoded content from an input stream. 9 | * The reason for custom implementation is that this allows recycling of 10 | * underlying read buffer, which is important for small content. 11 | */ 12 | public final class Latin1Reader extends Reader 13 | { 14 | /** 15 | * IO context to use for returning input buffer, iff 16 | * buffer is to be recycled when input ends. 17 | */ 18 | private final IOContext _ioContext; 19 | 20 | private InputStream _inputSource; 21 | 22 | private byte[] _inputBuffer; 23 | 24 | /** 25 | * Pointer to the next available byte (if any), iff less than 26 | * mByteBufferEnd 27 | */ 28 | private int _inputPtr; 29 | 30 | /** 31 | * Pointed to the end marker, that is, position one after the last 32 | * valid available byte. 33 | */ 34 | private int _inputEnd; 35 | 36 | /** 37 | * Total read character count; used for error reporting purposes 38 | */ 39 | private int _charCount = 0; 40 | 41 | /* 42 | /********************************************************************** 43 | /* Life-cycle 44 | /********************************************************************** 45 | */ 46 | 47 | /* 48 | public Latin1Reader(IOContext ctxt, InputStream in, 49 | byte[] buf, int ptr, int len) 50 | { 51 | super((in == null) ? buf : in); 52 | _ioContext = ctxt; 53 | _inputSource = in; 54 | _inputBuffer = buf; 55 | _inputPtr = ptr; 56 | _inputEnd = ptr+len; 57 | } 58 | */ 59 | 60 | public Latin1Reader(byte[] buf, int ptr, int len) 61 | { 62 | super(new Object()); 63 | _ioContext = null; 64 | _inputSource = null; 65 | _inputBuffer = buf; 66 | _inputPtr = ptr; 67 | _inputEnd = ptr+len; 68 | } 69 | 70 | public Latin1Reader(IOContext ctxt, InputStream in) 71 | { 72 | super(in); 73 | _ioContext = ctxt; 74 | _inputSource = in; 75 | _inputBuffer = ctxt.allocReadIOBuffer(); 76 | _inputPtr = 0; 77 | _inputEnd = 0; 78 | } 79 | 80 | /* 81 | /********************************************************************** 82 | /* Extended API 83 | /********************************************************************** 84 | */ 85 | 86 | public int getReadCharsCount() { 87 | return _charCount; 88 | } 89 | 90 | /* 91 | /********************************************************************** 92 | /* Reader API 93 | /********************************************************************** 94 | */ 95 | 96 | @Override 97 | public void close() throws IOException 98 | { 99 | _inputSource = null; 100 | freeBuffers(); 101 | } 102 | 103 | private char[] _tmpBuffer = null; 104 | 105 | // Although this method is implemented by the base class, AND it should 106 | // never be called by parser code, let's still implement it bit more 107 | // efficiently just in case 108 | @Override 109 | public int read() throws IOException 110 | { 111 | if (_tmpBuffer == null) { 112 | _tmpBuffer = new char[1]; 113 | } 114 | if (read(_tmpBuffer, 0, 1) < 1) { 115 | return -1; 116 | } 117 | return _tmpBuffer[0]; 118 | } 119 | 120 | @Override 121 | public int read(char[] cbuf) throws IOException { 122 | return read(cbuf, 0, cbuf.length); 123 | } 124 | 125 | @Override 126 | public int read(char[] cbuf, int start, int len) throws IOException 127 | { 128 | if (_inputBuffer == null) { 129 | return -1; 130 | } 131 | if (len < 1) { 132 | return len; 133 | } 134 | 135 | // To prevent unnecessary blocking (esp. with network streams), 136 | // we'll only require decoding of a single char 137 | int left = (_inputEnd - _inputPtr); 138 | if (left < 1) { 139 | if (!loadMore()) { // (legal) EOF? 140 | return -1; 141 | } 142 | left = (_inputEnd - _inputPtr); 143 | } 144 | if (left > len) { 145 | left = len; 146 | } 147 | 148 | final byte[] inBuf = _inputBuffer; 149 | int inPtr = _inputPtr; 150 | int outPtr = start; 151 | final int inEnd = inPtr + left; 152 | 153 | do { 154 | cbuf[outPtr++] = (char) inBuf[inPtr++]; 155 | } while (inPtr < inEnd); 156 | _inputPtr = inPtr; 157 | return left; 158 | } 159 | 160 | /* 161 | /********************************************************************** 162 | /* Internal methods 163 | /********************************************************************** 164 | */ 165 | 166 | /** 167 | * @param available Number of "unused" bytes in the input buffer 168 | * 169 | * @return True, if enough bytes were read to allow decoding of at least 170 | * one full character; false if EOF was encountered instead. 171 | */ 172 | private boolean loadMore() throws IOException 173 | { 174 | _charCount += _inputEnd; 175 | _inputPtr = 0; 176 | _inputEnd = 0; 177 | if (_inputSource == null) { 178 | freeBuffers(); 179 | return false; 180 | } 181 | int count = _inputSource.read(_inputBuffer, 0, _inputBuffer.length); 182 | if (count < 1) { 183 | freeBuffers(); 184 | if (count < 0) { // -1 185 | return false; 186 | } 187 | // 0 count is no good; let's err out 188 | throw new IOException("Strange I/O stream, returned 0 bytes on read"); 189 | } 190 | _inputEnd = count; 191 | return true; 192 | } 193 | 194 | /** 195 | * This method should be called along with (or instead of) normal 196 | * close. After calling this method, no further reads should be tried. 197 | * Method will try to recycle read buffers (if any). 198 | */ 199 | private final void freeBuffers() 200 | { 201 | if (_ioContext != null) { 202 | byte[] buf = _inputBuffer; 203 | if (buf != null) { 204 | _inputBuffer = null; 205 | _ioContext.releaseReadIOBuffer(buf); 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/ArrayParsingTest.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.File; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import com.fasterxml.jackson.annotation.JsonCreator; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonValue; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import com.fasterxml.jackson.databind.ObjectReader; 12 | 13 | public class ArrayParsingTest extends ModuleTestBase 14 | { 15 | static class ZKConfig { 16 | public int tickTime; 17 | public File dataDir; 18 | public int clientPort; 19 | public int initLimit, syncLimit; 20 | @JsonProperty("server") 21 | public List servers; 22 | } 23 | 24 | static class ZKServer { 25 | public final int srcPort, dstPort; 26 | public final String host; 27 | 28 | @JsonCreator 29 | public ZKServer(String combo) { 30 | // should validate better; should work for now 31 | String[] parts = combo.split(":"); 32 | try { 33 | host = parts[0]; 34 | srcPort = Integer.parseInt(parts[1]); 35 | dstPort = Integer.parseInt(parts[2]); 36 | } catch (Exception e) { 37 | throw new IllegalArgumentException("Invalid input String for ZKServer-valued property: \"" 38 | +combo+"\""); 39 | } 40 | } 41 | 42 | @JsonValue 43 | public String asString() { 44 | return String.format("%s:%d:%d", host, srcPort, dstPort); 45 | } 46 | } 47 | 48 | static class StringArrayWrapper { 49 | public String[] str; 50 | } 51 | 52 | private final ObjectMapper MAPPER = mapperForProps(); 53 | 54 | /* 55 | /********************************************************************** 56 | /* Test methods 57 | /********************************************************************** 58 | */ 59 | 60 | public void testArrayWithBranch() throws Exception 61 | { 62 | // basically "extra" branch should become as first element, and 63 | // after that ordering by numeric value 64 | final String INPUT = "str=first\n" 65 | +"str.11=third\n" 66 | +"str.2=second\n" 67 | ; 68 | StringArrayWrapper w = MAPPER.readValue(INPUT, StringArrayWrapper.class); 69 | assertNotNull(w.str); 70 | assertEquals(3, w.str.length); 71 | assertEquals("first", w.str[0]); 72 | assertEquals("second", w.str[1]); 73 | assertEquals("third", w.str[2]); 74 | 75 | // Also should work if bound to a Map 76 | Map map = MAPPER.readerFor(Map.class) 77 | .readValue(INPUT); 78 | assertEquals(1, map.size()); 79 | Object ob = map.get("str"); 80 | assertNotNull(ob); 81 | assertTrue(ob instanceof List); 82 | 83 | // but let's see how things work with auto-detection disabled: 84 | JavaPropsSchema schema = JavaPropsSchema.emptySchema() 85 | .withParseSimpleIndexes(false); 86 | map = MAPPER.readerFor(Map.class) 87 | .with(schema) 88 | .readValue(INPUT); 89 | assertEquals(1, map.size()); 90 | ob = map.get("str"); 91 | assertNotNull(ob); 92 | assertTrue(ob instanceof Map); 93 | } 94 | 95 | public void testPointList() throws Exception 96 | { 97 | _testPointList(false, false); 98 | _testPointList(true, false); 99 | _testPointList(false, true); 100 | _testPointList(true, true); 101 | } 102 | 103 | private void _testPointList(boolean useBytes, boolean allowIndex) throws Exception 104 | { 105 | final String INPUT = "p.1.x=1\n" 106 | +"p.1.y=2\n" 107 | +"p.2.x=3\n" 108 | +"p.2.y=4\n" 109 | +"p.3.x=5\n" 110 | +"p.3.y=6\n" 111 | ; 112 | JavaPropsSchema schema = JavaPropsSchema.emptySchema(); 113 | if (allowIndex) { // default schema marker is fine 114 | ; 115 | } else { 116 | schema = schema.withoutIndexMarker(); 117 | } 118 | ObjectReader r = MAPPER.reader(schema); 119 | Points result = _mapFrom(r, INPUT, Points.class, useBytes); 120 | assertNotNull(result); 121 | assertNotNull(result.p); 122 | //System.err.println("As JSON: "+new ObjectMapper().writeValueAsString(result)); 123 | assertEquals(3, result.p.size()); 124 | assertEquals(1, result.p.get(0).x); 125 | assertEquals(2, result.p.get(0).y); 126 | assertEquals(3, result.p.get(1).x); 127 | assertEquals(4, result.p.get(1).y); 128 | assertEquals(5, result.p.get(2).x); 129 | assertEquals(6, result.p.get(2).y); 130 | } 131 | 132 | public void testPointListWithIndex() throws Exception 133 | { 134 | _testPointListWithIndex(false); 135 | _testPointListWithIndex(true); 136 | } 137 | 138 | private void _testPointListWithIndex(boolean useBytes) throws Exception 139 | { 140 | final String INPUT = "p[1].x=1\n" 141 | +"p[1].y=2\n" 142 | +"p[2].y=4\n" 143 | +"p[2].x=3\n" 144 | ; 145 | 146 | Points result = _mapFrom(MAPPER, INPUT, Points.class, useBytes); 147 | assertNotNull(result); 148 | assertNotNull(result.p); 149 | assertEquals(2, result.p.size()); 150 | assertEquals(1, result.p.get(0).x); 151 | assertEquals(2, result.p.get(0).y); 152 | assertEquals(3, result.p.get(1).x); 153 | assertEquals(4, result.p.get(1).y); 154 | } 155 | 156 | public void testZKPojo() throws Exception 157 | { 158 | _testZKPojo(false, false); 159 | _testZKPojo(true, false); 160 | _testZKPojo(false, true); 161 | _testZKPojo(true, true); 162 | } 163 | 164 | public void _testZKPojo(boolean useBytes, boolean allowIndex) throws Exception 165 | { 166 | final String INPUT 167 | ="tickTime=2000\n" 168 | +"dataDir=/var/zookeeper\n" 169 | +"clientPort=2181\n" 170 | +"initLimit=5\n" 171 | +"syncLimit=2\n" 172 | +"server.1=zoo1:2888:2889\n" 173 | +"server.2=zoo2:3888:3889\n" 174 | +"server.3=zoo3:4888:4889\n" 175 | ; 176 | 177 | JavaPropsSchema schema = JavaPropsSchema.emptySchema(); 178 | if (allowIndex) { // default schema marker is fine 179 | ; 180 | } else { 181 | schema = schema.withoutIndexMarker(); 182 | } 183 | ObjectReader r = MAPPER.reader(schema); 184 | 185 | ZKConfig config = _mapFrom(r, INPUT, ZKConfig.class, useBytes); 186 | assertNotNull(config.servers); 187 | assertEquals(3, config.servers.size()); 188 | assertEquals(4889, config.servers.get(2).dstPort); 189 | assertEquals(2, config.syncLimit); 190 | assertEquals(2181, config.clientPort); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/test/java/com/fasterxml/jackson/dataformat/javaprop/ModuleTestBase.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.StringReader; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import com.fasterxml.jackson.core.*; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.fasterxml.jackson.databind.ObjectReader; 13 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 14 | 15 | public abstract class ModuleTestBase extends junit.framework.TestCase 16 | { 17 | @JsonPropertyOrder({ "topLeft", "bottomRight" }) 18 | static class Rectangle { 19 | public Point topLeft; 20 | public Point bottomRight; 21 | 22 | protected Rectangle() { } 23 | public Rectangle(Point p1, Point p2) { 24 | topLeft = p1; 25 | bottomRight = p2; 26 | } 27 | } 28 | 29 | @JsonPropertyOrder({ "x", "y" }) 30 | static class Point { 31 | public int x, y; 32 | 33 | protected Point() { } 34 | public Point(int x0, int y0) { 35 | x = x0; 36 | y = y0; 37 | } 38 | } 39 | 40 | static class Points { 41 | public List p; 42 | 43 | protected Points() { } 44 | public Points(Point... p0) { 45 | p = Arrays.asList(p0); 46 | } 47 | } 48 | 49 | public enum Gender { MALE, FEMALE }; 50 | 51 | /** 52 | * Slightly modified sample class from Jackson tutorial ("JacksonInFiveMinutes") 53 | */ 54 | @JsonPropertyOrder({"firstName", "lastName", "gender" ,"verified", "userImage"}) 55 | protected static class FiveMinuteUser { 56 | 57 | private Gender _gender; 58 | 59 | public String firstName, lastName; 60 | 61 | private boolean _isVerified; 62 | private byte[] _userImage; 63 | 64 | public FiveMinuteUser() { } 65 | 66 | public FiveMinuteUser(String first, String last, boolean verified, Gender g, byte[] data) 67 | { 68 | firstName = first; 69 | lastName = last; 70 | _isVerified = verified; 71 | _gender = g; 72 | _userImage = data; 73 | } 74 | 75 | public boolean isVerified() { return _isVerified; } 76 | public Gender getGender() { return _gender; } 77 | public byte[] getUserImage() { return _userImage; } 78 | 79 | public void setVerified(boolean b) { _isVerified = b; } 80 | public void setGender(Gender g) { _gender = g; } 81 | public void setUserImage(byte[] b) { _userImage = b; } 82 | 83 | @Override 84 | public boolean equals(Object o) 85 | { 86 | if (o == this) return true; 87 | if (o == null || o.getClass() != getClass()) return false; 88 | FiveMinuteUser other = (FiveMinuteUser) o; 89 | if (_isVerified != other._isVerified) return false; 90 | if (_gender != other._gender) return false; 91 | if (!firstName.equals(other.firstName)) return false; 92 | if (!lastName.equals(other.lastName)) return false; 93 | byte[] otherImage = other._userImage; 94 | if (otherImage.length != _userImage.length) return false; 95 | for (int i = 0, len = _userImage.length; i < len; ++i) { 96 | if (_userImage[i] != otherImage[i]) { 97 | return false; 98 | } 99 | } 100 | return true; 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | // not really good but whatever: 106 | return firstName.hashCode(); 107 | } 108 | } 109 | 110 | @JsonPropertyOrder({"id", "desc"}) 111 | protected static class IdDesc { 112 | public String id, desc; 113 | 114 | protected IdDesc() { } 115 | public IdDesc(String id, String desc) { 116 | this.id = id; 117 | this.desc = desc; 118 | } 119 | } 120 | 121 | protected ModuleTestBase() { } 122 | 123 | /* 124 | /********************************************************************** 125 | /* Helper methods, setup 126 | /********************************************************************** 127 | */ 128 | 129 | protected JavaPropsMapper mapperForProps() 130 | { 131 | return new JavaPropsMapper(); 132 | } 133 | 134 | /* 135 | /********************************************************** 136 | /* Helper methods; read helpers 137 | /********************************************************** 138 | */ 139 | 140 | protected final T _mapFrom(ObjectMapper mapper, String input, Class type, 141 | boolean useBytes) 142 | throws IOException 143 | { 144 | if (useBytes) { 145 | InputStream in = new ByteArrayInputStream(input.getBytes("ISO-8859-1")); 146 | return mapper.readValue(in, type); 147 | } 148 | return mapper.readValue(new StringReader(input), type); 149 | } 150 | 151 | protected final T _mapFrom(ObjectReader reader, String input, Class type, 152 | boolean useBytes) 153 | throws IOException 154 | { 155 | if (useBytes) { 156 | InputStream in = new ByteArrayInputStream(input.getBytes("ISO-8859-1")); 157 | return reader.forType(type).readValue(in); 158 | } 159 | return reader.forType(type).readValue(new StringReader(input)); 160 | } 161 | 162 | /* 163 | /********************************************************** 164 | /* Helper methods; low-level 165 | /********************************************************** 166 | */ 167 | 168 | public String quote(String str) { 169 | return '"'+str+'"'; 170 | } 171 | 172 | protected String aposToQuotes(String json) { 173 | return json.replace("'", "\""); 174 | } 175 | 176 | protected void assertToken(JsonToken expToken, JsonToken actToken) { 177 | if (actToken != expToken) { 178 | fail("Expected token "+expToken+", current token "+actToken); 179 | } 180 | } 181 | 182 | protected void assertToken(JsonToken expToken, JsonParser jp) { 183 | assertToken(expToken, jp.getCurrentToken()); 184 | } 185 | 186 | protected void assertType(Object ob, Class expType) 187 | { 188 | if (ob == null) { 189 | fail("Expected an object of type "+expType.getName()+", got null"); 190 | } 191 | Class cls = ob.getClass(); 192 | if (!expType.isAssignableFrom(cls)) { 193 | fail("Expected type "+expType.getName()+", got "+cls.getName()); 194 | } 195 | } 196 | 197 | /** 198 | * Method that gets textual contents of the current token using 199 | * available methods, and ensures results are consistent, before 200 | * returning them 201 | */ 202 | protected String getAndVerifyText(JsonParser jp) throws IOException 203 | { 204 | // Ok, let's verify other accessors 205 | int actLen = jp.getTextLength(); 206 | char[] ch = jp.getTextCharacters(); 207 | String str2 = new String(ch, jp.getTextOffset(), actLen); 208 | String str = jp.getText(); 209 | 210 | if (str.length() != actLen) { 211 | fail("Internal problem (jp.token == "+jp.getCurrentToken()+"): jp.getText().length() ['"+str+"'] == "+str.length()+"; jp.getTextLength() == "+actLen); 212 | } 213 | assertEquals("String access via getText(), getTextXxx() must be the same", str, str2); 214 | 215 | return str; 216 | } 217 | 218 | protected void verifyFieldName(JsonParser jp, String expName) 219 | throws IOException 220 | { 221 | assertEquals(expName, jp.getText()); 222 | assertEquals(expName, jp.getCurrentName()); 223 | } 224 | 225 | protected void verifyIntValue(JsonParser jp, long expValue) 226 | throws IOException 227 | { 228 | // First, via textual 229 | assertEquals(String.valueOf(expValue), jp.getText()); 230 | } 231 | 232 | protected void verifyException(Throwable e, String... matches) 233 | { 234 | String msg = e.getMessage(); 235 | String lmsg = (msg == null) ? "" : msg.toLowerCase(); 236 | for (String match : matches) { 237 | String lmatch = match.toLowerCase(); 238 | if (lmsg.indexOf(lmatch) >= 0) { 239 | return; 240 | } 241 | } 242 | fail("Expected an exception with one of substrings ("+Arrays.asList(matches)+"): got one with message \""+msg+"\""); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/io/JPropReadContext.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.io; 2 | 3 | import java.util.*; 4 | 5 | import com.fasterxml.jackson.core.*; 6 | import com.fasterxml.jackson.dataformat.javaprop.util.JPropNode; 7 | 8 | /** 9 | * Helper class used to keep track of traversal over contents of 10 | * content tree expressed as {@link JPropNode}s. 11 | */ 12 | public abstract class JPropReadContext 13 | extends JsonStreamContext 14 | { 15 | /** 16 | * Parent cursor of this cursor, if any; null for root 17 | * cursors. 18 | */ 19 | protected final JPropReadContext _parent; 20 | 21 | /** 22 | * Current field name 23 | */ 24 | protected String _currentName; 25 | 26 | protected String _currentText; 27 | 28 | /** 29 | * Java-level Object that corresponds to this level of input hierarchy, 30 | * if any; used by databinding functionality, opaque for parser. 31 | * 32 | * @since 2.5 33 | */ 34 | protected java.lang.Object _currentValue; 35 | 36 | /** 37 | * We need to keep track of value nodes to construct further contexts. 38 | */ 39 | protected JPropNode _nextNode; 40 | 41 | /** 42 | * Optional "this" value for cases where path branches have 43 | * direct values; these are exposed before child values 44 | * with bogus 'name' of empty String. 45 | */ 46 | protected String _branchText; 47 | 48 | protected int _state; 49 | 50 | public JPropReadContext(int contextType, JPropReadContext p, JPropNode node) 51 | { 52 | super(); 53 | _type = contextType; 54 | _index = -1; 55 | _parent = p; 56 | _branchText = node.getValue(); 57 | } 58 | 59 | public static JPropReadContext create(JPropNode root) { 60 | if (root.isArray()) { // can this ever occur? 61 | return new ArrayContext(null, root); 62 | } 63 | return new ObjectContext(null, root); 64 | } 65 | 66 | /* 67 | /********************************************************** 68 | /* JsonStreamContext impl 69 | /********************************************************** 70 | */ 71 | 72 | // note: co-variant return type 73 | @Override 74 | public final JPropReadContext getParent() { return _parent; } 75 | 76 | @Override 77 | public final String getCurrentName() { 78 | return _currentName; 79 | } 80 | 81 | public void overrideCurrentName(String name) { 82 | _currentName = name; 83 | } 84 | 85 | @Override 86 | public java.lang.Object getCurrentValue() { 87 | return _currentValue; 88 | } 89 | 90 | @Override 91 | public void setCurrentValue(java.lang.Object v) { 92 | _currentValue = v; 93 | } 94 | 95 | /* 96 | /********************************************************** 97 | /* Extended API 98 | /********************************************************** 99 | */ 100 | 101 | public abstract JsonToken nextToken(); 102 | 103 | /** 104 | * Method called to figure out child or parent context when change 105 | * is needed, as indicated by this context returning `null`. 106 | */ 107 | public JPropReadContext nextContext() 108 | { 109 | JPropNode n = _nextNode; 110 | if (n == null) { 111 | return _parent; 112 | } 113 | _nextNode = null; 114 | if (n.isArray()) { 115 | return new ArrayContext(this, n); 116 | } 117 | return new ObjectContext(this, n); 118 | } 119 | 120 | public String getCurrentText() { 121 | return _currentText; 122 | } 123 | 124 | /* 125 | /********************************************************** 126 | /* Concrete implementations 127 | /********************************************************** 128 | */ 129 | 130 | /** 131 | * Cursor used for traversing non-empty JSON Array nodes 132 | */ 133 | protected final static class ArrayContext 134 | extends JPropReadContext 135 | { 136 | final static int STATE_START = 0; // before START_ARRAY 137 | final static int STATE_BRANCH_VALUE = 1; 138 | final static int STATE_CONTENT_VALUE = 2; 139 | final static int STATE_END = 3; // after END_ARRAY 140 | 141 | protected Iterator _contents; 142 | 143 | public ArrayContext(JPropReadContext p, JPropNode arrayNode) { 144 | super(JsonStreamContext.TYPE_ARRAY, p, arrayNode); 145 | _contents = arrayNode.arrayContents(); 146 | _state = STATE_START; 147 | } 148 | 149 | @Override 150 | public JsonToken nextToken() 151 | { 152 | switch (_state) { 153 | case STATE_START: // START_ARRAY 154 | _state = (_branchText == null) ? STATE_CONTENT_VALUE : STATE_BRANCH_VALUE; 155 | return JsonToken.START_ARRAY; 156 | case STATE_BRANCH_VALUE: 157 | _state = STATE_CONTENT_VALUE; 158 | _currentText = _branchText; 159 | return JsonToken.VALUE_STRING; 160 | case STATE_CONTENT_VALUE: 161 | if (!_contents.hasNext()) { 162 | _state = STATE_END; 163 | return JsonToken.END_ARRAY; 164 | } 165 | JPropNode n = _contents.next(); 166 | if (n.isLeaf()) { 167 | _currentText = n.getValue(); 168 | return JsonToken.VALUE_STRING; 169 | } 170 | _nextNode = n; 171 | // Structured; need to indicate indirectly 172 | return null; 173 | 174 | case STATE_END: 175 | default: 176 | } 177 | return null; 178 | } 179 | } 180 | 181 | /** 182 | * Cursor used for traversing non-empty JSON Object nodes 183 | */ 184 | protected final static class ObjectContext 185 | extends JPropReadContext 186 | { 187 | final static int STATE_START = 0; // before START_OBJECT 188 | final static int STATE_BRANCH_KEY = 1; // if (and only if) we have intermediate node ("branch") value 189 | final static int STATE_BRANCH_VALUE = 2; 190 | final static int STATE_CONTENT_KEY = 3; 191 | final static int STATE_CONTENT_VALUE = 4; 192 | final static int STATE_END = 5; // after END_OBJECT 193 | 194 | /** 195 | * Iterator over child values. 196 | */ 197 | protected Iterator> _contents; 198 | 199 | public ObjectContext(JPropReadContext p, JPropNode objectNode) 200 | { 201 | super(JsonStreamContext.TYPE_OBJECT, p, objectNode); 202 | _contents = objectNode.objectContents(); 203 | _state = STATE_START; 204 | } 205 | 206 | @Override 207 | public JsonToken nextToken() 208 | { 209 | switch (_state) { 210 | case STATE_START: // START_OBJECT 211 | _state = (_branchText == null) ? STATE_CONTENT_KEY : STATE_BRANCH_KEY; 212 | return JsonToken.START_OBJECT; 213 | case STATE_BRANCH_KEY: 214 | _currentName = ""; 215 | _state = STATE_BRANCH_VALUE; 216 | return JsonToken.FIELD_NAME; 217 | case STATE_BRANCH_VALUE: 218 | _currentText = _branchText; 219 | _state = STATE_CONTENT_KEY; 220 | return JsonToken.VALUE_STRING; 221 | case STATE_CONTENT_KEY: 222 | if (!_contents.hasNext()) { 223 | _state = STATE_END; 224 | _nextNode = null; 225 | return JsonToken.END_OBJECT; 226 | } 227 | Map.Entry entry = _contents.next(); 228 | _currentName = entry.getKey(); 229 | _nextNode = entry.getValue(); 230 | _state = STATE_CONTENT_VALUE; 231 | return JsonToken.FIELD_NAME; 232 | case STATE_CONTENT_VALUE: 233 | _state = STATE_CONTENT_KEY; 234 | // Simple textual leaf? 235 | if (_nextNode.isLeaf()) { 236 | _currentText = _nextNode.getValue(); 237 | _nextNode = null; 238 | return JsonToken.VALUE_STRING; 239 | } 240 | // Structured; need to indicate indirectly 241 | return null; 242 | 243 | case STATE_END: 244 | default: 245 | } 246 | return null; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/JavaPropsMapper.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.IOException; 4 | import java.util.Properties; 5 | 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.core.Version; 8 | import com.fasterxml.jackson.databind.JavaType; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | 11 | public class JavaPropsMapper extends ObjectMapper 12 | { 13 | private static final long serialVersionUID = 1L; 14 | 15 | /* 16 | /********************************************************** 17 | /* Life-cycle 18 | /********************************************************** 19 | */ 20 | 21 | public JavaPropsMapper() { 22 | this(new JavaPropsFactory()); 23 | } 24 | 25 | public JavaPropsMapper(JavaPropsFactory f) { 26 | super(f); 27 | } 28 | 29 | protected JavaPropsMapper(JavaPropsMapper src) { 30 | super(src); 31 | } 32 | 33 | @Override 34 | public JavaPropsMapper copy() 35 | { 36 | _checkInvalidCopy(JavaPropsMapper.class); 37 | return new JavaPropsMapper(this); 38 | } 39 | 40 | @Override 41 | public Version version() { 42 | return PackageVersion.VERSION; 43 | } 44 | 45 | @Override 46 | public JavaPropsFactory getFactory() { 47 | return (JavaPropsFactory) _jsonFactory; 48 | } 49 | 50 | /* 51 | /********************************************************** 52 | /* Extended read methods 53 | /********************************************************** 54 | */ 55 | 56 | /** 57 | * Convenience method which uses given `Properties` as the source 58 | * as if they had been read from an external source, processes 59 | * them (splits paths etc), and then binds as given result 60 | * value. 61 | *

62 | * Note that this is NOT identical to calling {@link #convertValue(Object, Class)}; 63 | * rather, it would be similar to writing `Properties` out into a File, 64 | * then calling `readValue()` on this mapper to bind contents. 65 | * 66 | * @since 2.9 67 | */ 68 | @SuppressWarnings("resource") 69 | public T readPropertiesAs(Properties props, JavaPropsSchema schema, 70 | Class valueType) throws IOException { 71 | JsonParser p = getFactory().createParser(props); 72 | p.setSchema(schema); 73 | return (T) readValue(p, valueType); 74 | } 75 | 76 | /** 77 | * Convenience method which uses given `Properties` as the source 78 | * as if they had been read from an external source, processes 79 | * them (splits paths etc), and then binds as given result 80 | * value. 81 | *

82 | * Note that this is NOT identical to calling {@link #convertValue(Object, Class)}; 83 | * rather, it would be similar to writing `Properties` out into a File, 84 | * then calling `readValue()` on this mapper to bind contents. 85 | * 86 | * @since 2.9 87 | */ 88 | @SuppressWarnings({ "resource", "unchecked" }) 89 | public T readPropertiesAs(Properties props, JavaPropsSchema schema, 90 | JavaType valueType) throws IOException { 91 | JsonParser p = getFactory().createParser(props); 92 | p.setSchema(schema); 93 | return (T) readValue(p, valueType); 94 | } 95 | 96 | /** 97 | * Convenience method, functionally equivalent to: 98 | *

 99 |      *   readPropertiesAs(props, JavaPropsSchema.emptySchema(), valueType);
100 |      *
101 | * 102 | * @since 2.9 103 | */ 104 | public T readPropertiesAs(Properties props, Class valueType) throws IOException { 105 | return readPropertiesAs(props, JavaPropsSchema.emptySchema(), valueType); 106 | } 107 | 108 | /** 109 | * Convenience method, functionally equivalent to: 110 | *
111 |      *   readPropertiesAs(props, JavaPropsSchema.emptySchema(), valueType);
112 |      *
113 | * 114 | * @since 2.9 115 | */ 116 | public T readPropertiesAs(Properties props, JavaType valueType) throws IOException { 117 | return readPropertiesAs(props, JavaPropsSchema.emptySchema(), valueType); 118 | } 119 | 120 | /** 121 | * Convenience method, functionally equivalent to: 122 | *
123 |      *   readPropertiesAs(System.getProperties(), schema, valueType);
124 |      *
125 | * 126 | * @since 2.9 127 | */ 128 | public T readSystemPropertiesAs(JavaPropsSchema schema, 129 | Class valueType) throws IOException { 130 | return readPropertiesAs(System.getProperties(), schema, valueType); 131 | } 132 | 133 | /** 134 | * Convenience method, functionally equivalent to: 135 | *
136 |      *   readPropertiesAs(System.getProperties(), schema, valueType);
137 |      *
138 | * 139 | * @since 2.9 140 | */ 141 | public T readSystemPropertiesAs(JavaPropsSchema schema, 142 | JavaType valueType) throws IOException { 143 | return readPropertiesAs(System.getProperties(), schema, valueType); 144 | } 145 | 146 | /** 147 | * Convenience method, functionally equivalent to: 148 | *
149 |      *   readPropertiesAs(convertMapToProperties(System.getenv()), schema, valueType);
150 |      *
151 | * 152 | * @since 2.9 153 | */ 154 | public T readEnvVariablesAs(JavaPropsSchema schema, 155 | Class valueType) throws IOException { 156 | return readPropertiesAs(_env(), schema, valueType); 157 | } 158 | 159 | /** 160 | * Convenience method, functionally equivalent to: 161 | *
162 |      *   readPropertiesAs(convertMapToProperties(System.getenv()), schema, valueType);
163 |      *
164 | * 165 | * @since 2.9 166 | */ 167 | public T readEnvVariablesAs(JavaPropsSchema schema, 168 | JavaType valueType) throws IOException { 169 | return readPropertiesAs(_env(), schema, valueType); 170 | } 171 | 172 | protected Properties _env() { 173 | Properties props = new Properties(); 174 | props.putAll(System.getenv()); 175 | return props; 176 | } 177 | 178 | /* 179 | /********************************************************** 180 | /* Extended write methods 181 | /********************************************************** 182 | */ 183 | 184 | /** 185 | * Convenience method that "writes" given `value` as properties 186 | * in given {@link Properties} object. 187 | * 188 | * @since 2.9 189 | */ 190 | public void writeValue(Properties targetProps, Object value) throws IOException 191 | { 192 | if (targetProps == null) { 193 | throw new IllegalArgumentException("Can not pass null Properties as target"); 194 | } 195 | JavaPropsGenerator g = ((JavaPropsFactory) getFactory()) 196 | .createGenerator(targetProps); 197 | writeValue(g, value); 198 | g.close(); 199 | } 200 | 201 | /** 202 | * Convenience method that "writes" given `value` as properties 203 | * in given {@link Properties} object. 204 | * 205 | * @since 2.9 206 | */ 207 | public void writeValue(Properties targetProps, Object value, JavaPropsSchema schema) 208 | throws IOException 209 | { 210 | if (targetProps == null) { 211 | throw new IllegalArgumentException("Can not pass null Properties as target"); 212 | } 213 | JavaPropsGenerator g = ((JavaPropsFactory) getFactory()) 214 | .createGenerator(targetProps); 215 | if (schema != null) { 216 | g.setSchema(schema); 217 | } 218 | writeValue(g, value); 219 | g.close(); 220 | } 221 | 222 | /** 223 | * Convenience method that serializes given value but so that results are 224 | * stored in a newly constructed {@link Properties}. Functionally equivalent 225 | * to serializing in a File and reading contents into {@link Properties}. 226 | * 227 | * @since 2.9 228 | */ 229 | public Properties writeValueAsProperties(Object value) 230 | throws IOException 231 | { 232 | Properties props = new Properties(); 233 | writeValue(props, value); 234 | return props; 235 | } 236 | 237 | /** 238 | * Convenience method that serializes given value but so that results are 239 | * stored in given {@link Properties} instance. 240 | * 241 | * @since 2.9 242 | */ 243 | public Properties writeValueAsProperties(Object value, JavaPropsSchema schema) 244 | throws IOException 245 | { 246 | Properties props = new Properties(); 247 | writeValue(props, value, schema); 248 | return props; 249 | } 250 | 251 | /* 252 | /********************************************************** 253 | /* Schema support methods? 254 | /********************************************************** 255 | */ 256 | 257 | // do we have any actually? 258 | } 259 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/impl/WriterBackedGenerator.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.impl; 2 | 3 | import java.io.*; 4 | 5 | import com.fasterxml.jackson.core.*; 6 | import com.fasterxml.jackson.core.io.IOContext; 7 | import com.fasterxml.jackson.dataformat.javaprop.JavaPropsGenerator; 8 | import com.fasterxml.jackson.dataformat.javaprop.io.JPropEscapes; 9 | 10 | public class WriterBackedGenerator extends JavaPropsGenerator 11 | { 12 | /* 13 | /********************************************************** 14 | /* Configuration 15 | /********************************************************** 16 | */ 17 | 18 | /** 19 | * Underlying {@link Writer} used for output. 20 | */ 21 | final protected Writer _out; 22 | 23 | /* 24 | /********************************************************** 25 | /* Output buffering 26 | /********************************************************** 27 | */ 28 | 29 | /** 30 | * Intermediate buffer in which contents are buffered before 31 | * being written using {@link #_out}. 32 | */ 33 | protected char[] _outputBuffer; 34 | 35 | /** 36 | * Pointer to the next available location in {@link #_outputBuffer} 37 | */ 38 | protected int _outputTail = 0; 39 | 40 | /** 41 | * Offset to index after the last valid index in {@link #_outputBuffer}. 42 | * Typically same as length of the buffer. 43 | */ 44 | protected final int _outputEnd; 45 | 46 | /* 47 | /********************************************************** 48 | /* Life-cycle 49 | /********************************************************** 50 | */ 51 | 52 | public WriterBackedGenerator(IOContext ctxt, Writer out, 53 | int stdFeatures, ObjectCodec codec) 54 | { 55 | super(ctxt, stdFeatures, codec); 56 | _out = out; 57 | _outputBuffer = ctxt.allocConcatBuffer(); 58 | _outputEnd = _outputBuffer.length; 59 | } 60 | 61 | /* 62 | /********************************************************** 63 | /* Overridden methods, configuration 64 | /********************************************************** 65 | */ 66 | 67 | @Override 68 | public Object getOutputTarget() { 69 | return _out; 70 | } 71 | 72 | /* 73 | /********************************************************** 74 | /* Overridden methods: low-level I/O 75 | /********************************************************** 76 | */ 77 | 78 | @Override 79 | public void close() throws IOException 80 | { 81 | super.close(); 82 | _flushBuffer(); 83 | _outputTail = 0; // just to ensure we don't think there's anything buffered 84 | 85 | if (_out != null) { 86 | if (_ioContext.isResourceManaged() || isEnabled(Feature.AUTO_CLOSE_TARGET)) { 87 | _out.close(); 88 | } else if (isEnabled(Feature.FLUSH_PASSED_TO_STREAM)) { 89 | // If we can't close it, we should at least flush 90 | _out.flush(); 91 | } 92 | } 93 | // Internal buffer(s) generator has can now be released as well 94 | _releaseBuffers(); 95 | } 96 | 97 | @Override 98 | public void flush() throws IOException 99 | { 100 | _flushBuffer(); 101 | if (_out != null) { 102 | if (isEnabled(Feature.FLUSH_PASSED_TO_STREAM)) { 103 | _out.flush(); 104 | } 105 | } 106 | } 107 | 108 | /* 109 | /********************************************************** 110 | /* Implementations for methods from base class 111 | /********************************************************** 112 | */ 113 | 114 | @Override 115 | protected void _releaseBuffers() 116 | { 117 | char[] buf = _outputBuffer; 118 | if (buf != null) { 119 | _outputBuffer = null; 120 | _ioContext.releaseConcatBuffer(buf); 121 | } 122 | } 123 | 124 | protected void _flushBuffer() throws IOException 125 | { 126 | if (_outputTail > 0) { 127 | _out.write(_outputBuffer, 0, _outputTail); 128 | _outputTail = 0; 129 | } 130 | } 131 | 132 | /* 133 | /********************************************************** 134 | /* Internal methods; escaping writes 135 | /********************************************************** 136 | */ 137 | 138 | @Override 139 | protected void _writeEscapedEntry(String value) throws IOException 140 | { 141 | // note: key has been already escaped so: 142 | _writeRaw(_basePath); 143 | _writeRaw(_schema.keyValueSeparator()); 144 | 145 | _writeEscaped(value); 146 | _writeLinefeed(); 147 | } 148 | 149 | @Override 150 | protected void _writeEscapedEntry(char[] text, int offset, int len) throws IOException 151 | { 152 | // note: key has been already escaped so: 153 | _writeRaw(_basePath); 154 | _writeRaw(_schema.keyValueSeparator()); 155 | 156 | _writeEscaped(text, offset, len); 157 | _writeLinefeed(); 158 | } 159 | 160 | @Override 161 | protected void _writeUnescapedEntry(String value) throws IOException 162 | { 163 | // note: key has been already escaped so: 164 | _writeRaw(_basePath); 165 | _writeRaw(_schema.keyValueSeparator()); 166 | 167 | _writeRaw(value); 168 | _writeLinefeed(); 169 | } 170 | 171 | protected void _writeEscaped(String value) throws IOException 172 | { 173 | StringBuilder sb = JPropEscapes.appendValue(value); 174 | if (sb == null) { 175 | _writeRaw(value); 176 | } else { 177 | _writeRaw(sb); 178 | } 179 | } 180 | 181 | protected void _writeEscaped(char[] text, int offset, int len) throws IOException 182 | { 183 | _writeEscaped(new String(text, offset, len)); 184 | } 185 | 186 | protected void _writeLinefeed() throws IOException 187 | { 188 | _writeRaw(_schema.lineEnding()); 189 | } 190 | 191 | /* 192 | /********************************************************** 193 | /* Internal methods; raw writes 194 | /********************************************************** 195 | */ 196 | 197 | @Override 198 | protected void _writeRaw(char c) throws IOException 199 | { 200 | if (_outputTail >= _outputEnd) { 201 | _flushBuffer(); 202 | } 203 | _outputBuffer[_outputTail++] = c; 204 | } 205 | 206 | @Override 207 | protected void _writeRaw(String text) throws IOException 208 | { 209 | // Nothing to check, can just output as is 210 | int len = text.length(); 211 | int room = _outputEnd - _outputTail; 212 | 213 | if (room == 0) { 214 | _flushBuffer(); 215 | room = _outputEnd - _outputTail; 216 | } 217 | // But would it nicely fit in? If yes, it's easy 218 | if (room >= len) { 219 | text.getChars(0, len, _outputBuffer, _outputTail); 220 | _outputTail += len; 221 | } else { 222 | _writeRawLong(text); 223 | } 224 | } 225 | 226 | @Override 227 | protected void _writeRaw(StringBuilder text) throws IOException 228 | { 229 | // Nothing to check, can just output as is 230 | int len = text.length(); 231 | int room = _outputEnd - _outputTail; 232 | 233 | if (room == 0) { 234 | _flushBuffer(); 235 | room = _outputEnd - _outputTail; 236 | } 237 | // But would it nicely fit in? If yes, it's easy 238 | if (room >= len) { 239 | text.getChars(0, len, _outputBuffer, _outputTail); 240 | _outputTail += len; 241 | } else { 242 | _writeRawLong(text); 243 | } 244 | } 245 | 246 | @Override 247 | protected void _writeRaw(char[] text, int offset, int len) throws IOException 248 | { 249 | // Only worth buffering if it's a short write? 250 | if (len < SHORT_WRITE) { 251 | int room = _outputEnd - _outputTail; 252 | if (len > room) { 253 | _flushBuffer(); 254 | } 255 | System.arraycopy(text, offset, _outputBuffer, _outputTail, len); 256 | _outputTail += len; 257 | return; 258 | } 259 | // Otherwise, better just pass through: 260 | _flushBuffer(); 261 | _out.write(text, offset, len); 262 | } 263 | 264 | protected void _writeRawLong(String text) throws IOException 265 | { 266 | int room = _outputEnd - _outputTail; 267 | text.getChars(0, room, _outputBuffer, _outputTail); 268 | _outputTail += room; 269 | _flushBuffer(); 270 | int offset = room; 271 | int len = text.length() - room; 272 | 273 | while (len > _outputEnd) { 274 | int amount = _outputEnd; 275 | text.getChars(offset, offset+amount, _outputBuffer, 0); 276 | _outputTail = amount; 277 | _flushBuffer(); 278 | offset += amount; 279 | len -= amount; 280 | } 281 | // And last piece (at most length of buffer) 282 | text.getChars(offset, offset+len, _outputBuffer, 0); 283 | _outputTail = len; 284 | } 285 | 286 | protected void _writeRawLong(StringBuilder text) throws IOException 287 | { 288 | int room = _outputEnd - _outputTail; 289 | text.getChars(0, room, _outputBuffer, _outputTail); 290 | _outputTail += room; 291 | _flushBuffer(); 292 | int offset = room; 293 | int len = text.length() - room; 294 | 295 | while (len > _outputEnd) { 296 | int amount = _outputEnd; 297 | text.getChars(offset, offset+amount, _outputBuffer, 0); 298 | _outputTail = amount; 299 | _flushBuffer(); 300 | offset += amount; 301 | len -= amount; 302 | } 303 | // And last piece (at most length of buffer) 304 | text.getChars(offset, offset+len, _outputBuffer, 0); 305 | _outputTail = len; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/JavaPropsFactory.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.*; 4 | import java.net.URL; 5 | import java.util.*; 6 | 7 | import com.fasterxml.jackson.core.*; 8 | import com.fasterxml.jackson.core.format.InputAccessor; 9 | import com.fasterxml.jackson.core.format.MatchStrength; 10 | import com.fasterxml.jackson.core.io.IOContext; 11 | import com.fasterxml.jackson.dataformat.javaprop.impl.PropertiesBackedGenerator; 12 | import com.fasterxml.jackson.dataformat.javaprop.impl.WriterBackedGenerator; 13 | import com.fasterxml.jackson.dataformat.javaprop.io.Latin1Reader; 14 | 15 | @SuppressWarnings("resource") 16 | public class JavaPropsFactory extends JsonFactory 17 | { 18 | private static final long serialVersionUID = 1L; 19 | 20 | public final static String FORMAT_NAME_JAVA_PROPERTIES = "java_properties"; 21 | 22 | protected final static String CHARSET_ID_LATIN1 = "ISO-8859-1"; 23 | 24 | /* 25 | /********************************************************** 26 | /* Factory construction, configuration 27 | /********************************************************** 28 | */ 29 | 30 | public JavaPropsFactory() { } 31 | 32 | public JavaPropsFactory(ObjectCodec codec) { 33 | super(codec); 34 | } 35 | 36 | protected JavaPropsFactory(JavaPropsFactory src, ObjectCodec oc) 37 | { 38 | super(src, oc); 39 | } 40 | 41 | @Override 42 | public JavaPropsFactory copy() 43 | { 44 | _checkInvalidCopy(JavaPropsFactory.class); 45 | return new JavaPropsFactory(this, null); 46 | } 47 | 48 | /* 49 | /********************************************************** 50 | /* Versioned 51 | /********************************************************** 52 | */ 53 | 54 | @Override 55 | public Version version() { 56 | return PackageVersion.VERSION; 57 | } 58 | 59 | /* 60 | /********************************************************** 61 | /* Format detection functionality 62 | /********************************************************** 63 | */ 64 | 65 | @Override 66 | public String getFormatName() { 67 | return FORMAT_NAME_JAVA_PROPERTIES; 68 | } 69 | 70 | /** 71 | * Sub-classes need to override this method 72 | */ 73 | @Override 74 | public MatchStrength hasFormat(InputAccessor acc) throws IOException 75 | { 76 | // TODO, if possible... probably isn't? 77 | return MatchStrength.INCONCLUSIVE; 78 | } 79 | 80 | /* 81 | /********************************************************** 82 | /* Capability introspection 83 | /********************************************************** 84 | */ 85 | 86 | // Not positional 87 | @Override 88 | public boolean requiresPropertyOrdering() { 89 | return false; 90 | } 91 | 92 | // Can not handle raw binary data 93 | @Override 94 | public boolean canHandleBinaryNatively() { 95 | return false; 96 | } 97 | 98 | // Not using char[] internally 99 | @Override 100 | public boolean canUseCharArrays() { return false; } 101 | 102 | // No format-specific configuration, yet: 103 | /* 104 | @Override 105 | public Class getFormatReadFeatureType() { 106 | return null; 107 | } 108 | 109 | @Override 110 | public Class getFormatWriteFeatureType() { 111 | return null; 112 | } 113 | */ 114 | 115 | @Override 116 | public boolean canUseSchema(FormatSchema schema) { 117 | return schema instanceof JavaPropsSchema; 118 | } 119 | 120 | /* 121 | /********************************************************** 122 | /* Extended parser/generator factory methods 123 | /********************************************************** 124 | */ 125 | 126 | /** 127 | * Convenience method to allow feeding a pre-parsed {@link Properties} 128 | * instance as input. 129 | * 130 | * @since 2.9 131 | */ 132 | public JavaPropsParser createParser(Properties props) { 133 | return new JavaPropsParser(_createContext(props, true), 134 | props, _parserFeatures, _objectCodec, props); 135 | } 136 | 137 | /** 138 | * Convenience method to allow using a pre-constructed {@link Properties} 139 | * instance as output target, so that serialized property values 140 | * are added. 141 | * 142 | * @since 2.9 143 | */ 144 | public JavaPropsGenerator createGenerator(Properties props) { 145 | return new PropertiesBackedGenerator(_createContext(props, true), 146 | props, _generatorFeatures, _objectCodec); 147 | } 148 | 149 | /* 150 | /********************************************************** 151 | /* Overridden parser factory methods 152 | /********************************************************** 153 | */ 154 | 155 | @Override 156 | public JsonParser createParser(File f) throws IOException { 157 | return _createParser(new FileInputStream(f), _createContext(f, true)); 158 | } 159 | 160 | @Override 161 | public JsonParser createParser(URL url) throws IOException { 162 | return _createParser(_optimizedStreamFromURL(url), _createContext(url, true)); 163 | } 164 | 165 | @Override 166 | public JsonParser createParser(InputStream in) throws IOException { 167 | return _createParser(in, _createContext(in, false)); 168 | } 169 | 170 | @Override 171 | public JsonParser createParser(byte[] data) throws IOException { 172 | return _createParser(data, 0, data.length, _createContext(data, true)); 173 | } 174 | 175 | @Override 176 | public JsonParser createParser(byte[] data, int offset, int len) throws IOException { 177 | return _createParser(data, offset, len, _createContext(data, true)); 178 | } 179 | 180 | /* 181 | /********************************************************** 182 | /* Overridden generator factory methods 183 | /********************************************************** 184 | */ 185 | 186 | @Override 187 | public JsonGenerator createGenerator(OutputStream out, JsonEncoding enc) throws IOException { 188 | IOContext ctxt = _createContext(out, false); 189 | ctxt.setEncoding(enc); 190 | out = _decorate(out, ctxt); 191 | return _createJavaPropsGenerator(ctxt, _generatorFeatures, _objectCodec, out); 192 | } 193 | 194 | /** 195 | * Method for constructing {@link JsonGenerator} for generating 196 | * CBOR-encoded output. 197 | *

198 | * Since CBOR format always uses UTF-8 internally, no encoding need 199 | * to be passed to this method. 200 | */ 201 | @Override 202 | public JsonGenerator createGenerator(OutputStream out) throws IOException { 203 | IOContext ctxt = _createContext(out, false); 204 | out = _decorate(out, ctxt); 205 | return _createJavaPropsGenerator(ctxt, _generatorFeatures, _objectCodec, out); 206 | } 207 | 208 | /* 209 | /****************************************************** 210 | /* Overridden internal factory methods, parser 211 | /****************************************************** 212 | */ 213 | 214 | /* // fine as-is: 215 | @Override 216 | protected IOContext _createContext(Object srcRef, boolean resourceManaged) { 217 | return super._createContext(srcRef, resourceManaged); 218 | } 219 | */ 220 | 221 | @Override 222 | protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException 223 | { 224 | Properties props = _loadProperties(in, ctxt); 225 | return new JavaPropsParser(ctxt, in, _parserFeatures, _objectCodec, props); 226 | } 227 | 228 | @Override 229 | protected JsonParser _createParser(Reader r, IOContext ctxt) throws IOException { 230 | Properties props = _loadProperties(r, ctxt); 231 | return new JavaPropsParser(ctxt, r, _parserFeatures, _objectCodec, props); 232 | } 233 | 234 | @Override 235 | protected JsonParser _createParser(char[] data, int offset, int len, IOContext ctxt, 236 | boolean recyclable) throws IOException 237 | { 238 | return _createParser(new CharArrayReader(data, offset, len), ctxt); 239 | } 240 | 241 | @Override 242 | protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException 243 | { 244 | return _createParser(new Latin1Reader(data, offset, len), ctxt); 245 | } 246 | 247 | /* 248 | /****************************************************** 249 | /* Overridden internal factory methods, generator 250 | /****************************************************** 251 | */ 252 | 253 | @Override 254 | protected JsonGenerator _createGenerator(Writer out, IOContext ctxt) throws IOException 255 | { 256 | return new WriterBackedGenerator(ctxt, out, _generatorFeatures, _objectCodec); 257 | } 258 | 259 | @Override 260 | protected JsonGenerator _createUTF8Generator(OutputStream out, IOContext ctxt) throws IOException { 261 | return _createJavaPropsGenerator(ctxt, _generatorFeatures, _objectCodec, out); 262 | } 263 | 264 | @Override 265 | protected Writer _createWriter(OutputStream out, JsonEncoding enc, IOContext ctxt) throws IOException { 266 | // 27-Jan-2016, tatu: Properties javadoc is quite clear on Latin-1 (ISO-8859-1) being 267 | // the default, so let's actually override 268 | return new OutputStreamWriter(out, CHARSET_ID_LATIN1); 269 | } 270 | 271 | /* 272 | /****************************************************** 273 | /* Low-level methods for reading/writing Properties; currently 274 | /* we simply delegate to `java.util.Properties` 275 | /****************************************************** 276 | */ 277 | 278 | protected Properties _loadProperties(InputStream in, IOContext ctxt) 279 | throws IOException 280 | { 281 | // NOTE: Properties default to ISO-8859-1 (aka Latin-1), NOT UTF-8; this 282 | // as per JDK documentation 283 | return _loadProperties(new Latin1Reader(ctxt, in), ctxt); 284 | } 285 | 286 | protected Properties _loadProperties(Reader r0, IOContext ctxt) 287 | throws IOException 288 | { 289 | Properties props = new Properties(); 290 | // May or may not want to close the reader, so... 291 | if (ctxt.isResourceManaged()) { 292 | try (Reader r = r0) { 293 | props.load(r); 294 | } 295 | } else { 296 | props.load(r0); 297 | } 298 | return props; 299 | } 300 | 301 | private final JsonGenerator _createJavaPropsGenerator(IOContext ctxt, 302 | int stdFeat, ObjectCodec codec, OutputStream out) throws IOException 303 | { 304 | return new WriterBackedGenerator(ctxt, _createWriter(out, null, ctxt), 305 | stdFeat, _objectCodec); 306 | 307 | } 308 | 309 | /* 310 | public static void main(String[] args) throws Exception 311 | { 312 | args = new String[] { "test.properties" }; 313 | Properties props = new Properties(); 314 | // props.load(new FileInputStream(args[0])); 315 | props.load(new ByteArrayInputStream(new byte[0])); 316 | System.out.printf("%d entries:\n", props.size()); 317 | int i = 1; 318 | for (Map.Entry entry : props.entrySet()) { 319 | System.out.printf("#%d: %s -> %s\n", i++, entry.getKey(), entry.getValue()); 320 | } 321 | }*/ 322 | } 323 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/util/JPropPathSplitter.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop.util; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | import com.fasterxml.jackson.dataformat.javaprop.JavaPropsSchema; 7 | 8 | /** 9 | * Helper class used for splitting a flattened property key into 10 | * nested/structured path that can be used to traverse and/or define 11 | * hierarchic structure. 12 | */ 13 | public abstract class JPropPathSplitter 14 | { 15 | protected final boolean _useSimpleIndex; 16 | 17 | protected JPropPathSplitter(boolean useSimpleIndex) { 18 | _useSimpleIndex = useSimpleIndex; 19 | } 20 | 21 | public static JPropPathSplitter create(JavaPropsSchema schema) 22 | { 23 | // will never be null 24 | String sep = schema.pathSeparator(); 25 | final Markers indexMarker = schema.indexMarker(); 26 | 27 | // First: index marker in use? 28 | if (indexMarker == null) { // nope, only path separator, if anything 29 | // and if no separator, can use bogus "splitter": 30 | return pathOnlySplitter(schema); 31 | } 32 | // Yes, got index marker to use. But separator? 33 | if (sep.isEmpty()) { 34 | return new IndexOnlySplitter(schema.parseSimpleIndexes(), indexMarker); 35 | } 36 | return new FullSplitter(sep, schema.parseSimpleIndexes(), 37 | indexMarker, 38 | pathOnlySplitter(schema)); 39 | } 40 | 41 | private static JPropPathSplitter pathOnlySplitter(JavaPropsSchema schema) 42 | { 43 | String sep = schema.pathSeparator(); 44 | if (sep.isEmpty()) { 45 | return NonSplitting.instance; 46 | } 47 | // otherwise it's still quite simple 48 | if (sep.length() == 1) { 49 | return new CharPathOnlySplitter(sep.charAt(0), schema.parseSimpleIndexes()); 50 | } 51 | return new StringPathOnlySplitter(sep, schema.parseSimpleIndexes()); 52 | } 53 | 54 | /** 55 | * Main access method for splitting key into one or more segments and using 56 | * segmentation to add the String value as a node in its proper location. 57 | * 58 | * @return Newly added node 59 | */ 60 | public abstract JPropNode splitAndAdd(JPropNode parent, 61 | String key, String value); 62 | 63 | /* 64 | /********************************************************************** 65 | /* Helper methods for implementations 66 | /********************************************************************** 67 | */ 68 | 69 | protected JPropNode _addSegment(JPropNode parent, String segment) 70 | { 71 | if (_useSimpleIndex) { 72 | int ix = _asInt(segment); 73 | if (ix >= 0) { 74 | return parent.addByIndex(ix); 75 | } 76 | } 77 | return parent.addByName(segment); 78 | } 79 | 80 | protected JPropNode _lastSegment(JPropNode parent, String path, int start, int end) 81 | { 82 | if (start < end) { 83 | if (start > 0) { 84 | path = path.substring(start); 85 | } 86 | parent = _addSegment(parent, path); 87 | } 88 | return parent; 89 | } 90 | 91 | protected int _asInt(String segment) { 92 | final int len = segment.length(); 93 | // do not allow ridiculously long numbers as indexes 94 | if ((len == 0) || (len > 9)) { 95 | return -1; 96 | } 97 | char c = segment.charAt(0); 98 | if ((c > '9') || (c < '0')) { 99 | return -1; 100 | } 101 | for (int i = 0; i < len; ++i) { 102 | c = segment.charAt(i); 103 | if ((c > '9') || (c < '0')) { 104 | return -1; 105 | } 106 | } 107 | return Integer.parseInt(segment); 108 | } 109 | 110 | /* 111 | /********************************************************************** 112 | /* Implementations 113 | /********************************************************************** 114 | */ 115 | 116 | /** 117 | * "No-op" implementation that does no splitting and simply adds entries 118 | * as is. 119 | */ 120 | public static class NonSplitting extends JPropPathSplitter 121 | { 122 | public final static NonSplitting instance = new NonSplitting(); 123 | 124 | private NonSplitting() { super(false); } 125 | 126 | @Override 127 | public JPropNode splitAndAdd(JPropNode parent, 128 | String key, String value) 129 | { 130 | return parent.addByName(key).setValue(value); 131 | } 132 | } 133 | 134 | /** 135 | * Simple variant where we only have path separator, and optional "segment 136 | * is index iff value is integer number" 137 | */ 138 | public static class CharPathOnlySplitter extends JPropPathSplitter 139 | { 140 | protected final char _pathSeparatorChar; 141 | 142 | public CharPathOnlySplitter(char sepChar, boolean useIndex) 143 | { 144 | super(useIndex); 145 | _pathSeparatorChar = sepChar; 146 | } 147 | 148 | @Override 149 | public JPropNode splitAndAdd(JPropNode parent, 150 | String key, String value) 151 | { 152 | JPropNode curr = parent; 153 | int start = 0; 154 | final int keyLen = key.length(); 155 | int ix; 156 | 157 | while ((ix = key.indexOf(_pathSeparatorChar, start)) >= start) { 158 | if (ix > start) { // segment before separator 159 | String segment = key.substring(start, ix); 160 | curr = _addSegment(curr, segment); 161 | } 162 | start = ix + 1; 163 | if (start == key.length()) { 164 | break; 165 | } 166 | } 167 | return _lastSegment(curr, key, start, keyLen).setValue(value); 168 | } 169 | } 170 | 171 | /** 172 | * Simple variant where we only have path separator, and optional "segment 173 | * is index iff value is integer number" 174 | */ 175 | public static class StringPathOnlySplitter extends JPropPathSplitter 176 | { 177 | protected final String _pathSeparator; 178 | protected final int _pathSeparatorLength; 179 | 180 | public StringPathOnlySplitter(String pathSeparator, boolean useIndex) 181 | { 182 | super(useIndex); 183 | _pathSeparator = pathSeparator; 184 | _pathSeparatorLength = pathSeparator.length(); 185 | } 186 | 187 | @Override 188 | public JPropNode splitAndAdd(JPropNode parent, 189 | String key, String value) 190 | { 191 | JPropNode curr = parent; 192 | int start = 0; 193 | final int keyLen = key.length(); 194 | int ix; 195 | 196 | while ((ix = key.indexOf(_pathSeparator, start)) >= start) { 197 | if (ix > start) { // segment before separator 198 | String segment = key.substring(start, ix); 199 | curr = _addSegment(curr, segment); 200 | } 201 | start = ix + _pathSeparatorLength; 202 | if (start == key.length()) { 203 | break; 204 | } 205 | } 206 | return _lastSegment(curr, key, start, keyLen).setValue(value); 207 | } 208 | } 209 | 210 | /** 211 | * Special variant that does not use path separator, but does allow 212 | * index indicator, at the end of path. 213 | */ 214 | public static class IndexOnlySplitter extends JPropPathSplitter 215 | { 216 | protected final Pattern _indexMatch; 217 | 218 | public IndexOnlySplitter(boolean useSimpleIndex, 219 | Markers indexMarker) 220 | { 221 | super(useSimpleIndex); 222 | _indexMatch = Pattern.compile(String.format("(.*)%s(\\d{1,9})%s$", 223 | Pattern.quote(indexMarker.getStart()), 224 | Pattern.quote(indexMarker.getEnd()))); 225 | } 226 | 227 | @Override 228 | public JPropNode splitAndAdd(JPropNode parent, 229 | String key, String value) 230 | { 231 | Matcher m = _indexMatch.matcher(key); 232 | // short-cut for common case of no index: 233 | if (!m.matches()) { 234 | return _addSegment(parent, key).setValue(value); 235 | } 236 | // otherwise we need recursion as we "peel" away layers 237 | return _splitMore(parent, m.group(1), m.group(2)) 238 | .setValue(value); 239 | } 240 | 241 | protected JPropNode _splitMore(JPropNode parent, String prefix, String indexStr) 242 | { 243 | int ix = Integer.parseInt(indexStr); 244 | Matcher m = _indexMatch.matcher(prefix); 245 | if (!m.matches()) { 246 | parent = _addSegment(parent, prefix); 247 | } else { 248 | parent = _splitMore(parent, m.group(1), m.group(2)); 249 | } 250 | return parent.addByIndex(ix); 251 | } 252 | } 253 | 254 | /** 255 | * Instance that supports both path separator and index markers 256 | * (and possibly also "simple" indexes) 257 | */ 258 | public static class FullSplitter extends JPropPathSplitter 259 | { 260 | protected final Pattern _indexMatch; 261 | 262 | // small but important optimization for cases where index markers are absent 263 | protected final int _indexFirstChar; 264 | protected final JPropPathSplitter _simpleSplitter; 265 | 266 | public FullSplitter(String pathSeparator, boolean useSimpleIndex, 267 | Markers indexMarker, JPropPathSplitter fallbackSplitter) 268 | { 269 | super(useSimpleIndex); 270 | String startMarker = indexMarker.getStart(); 271 | _indexFirstChar = startMarker.charAt(0); 272 | _simpleSplitter = fallbackSplitter; 273 | _indexMatch = Pattern.compile(String.format 274 | ("(%s)|(%s(\\d{1,9})%s)", 275 | Pattern.quote(pathSeparator), 276 | Pattern.quote(startMarker), 277 | Pattern.quote(indexMarker.getEnd()))); 278 | } 279 | 280 | @Override 281 | public JPropNode splitAndAdd(JPropNode parent, 282 | String key, String value) 283 | { 284 | if (key.indexOf(_indexFirstChar) < 0) { // no index start marker 285 | return _simpleSplitter.splitAndAdd(parent, key, value); 286 | } 287 | Matcher m = _indexMatch.matcher(key); 288 | int start = 0; 289 | 290 | while (m.find()) { 291 | // which match did we get? Either path separator (1), or index (2) 292 | int ix = m.start(1); 293 | 294 | if (ix >= 0) { // path separator... 295 | if (ix > start) { 296 | String segment = key.substring(start, ix); 297 | parent = _addSegment(parent, segment); 298 | } 299 | start = m.end(1); 300 | continue; 301 | } 302 | // no, index marker, with contents 303 | ix = m.start(2); 304 | if (ix > start) { 305 | String segment = key.substring(start, ix); 306 | parent = _addSegment(parent, segment); 307 | } 308 | start = m.end(2); 309 | ix = Integer.parseInt(m.group(3)); 310 | parent = parent.addByIndex(ix); 311 | } 312 | return _lastSegment(parent, key, start, key.length()).setValue(value); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/JavaPropsParser.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.*; 4 | import java.math.BigDecimal; 5 | import java.math.BigInteger; 6 | import java.util.Properties; 7 | 8 | import com.fasterxml.jackson.core.*; 9 | import com.fasterxml.jackson.core.base.ParserMinimalBase; 10 | import com.fasterxml.jackson.core.io.IOContext; 11 | import com.fasterxml.jackson.core.util.ByteArrayBuilder; 12 | import com.fasterxml.jackson.dataformat.javaprop.io.JPropReadContext; 13 | import com.fasterxml.jackson.dataformat.javaprop.util.JPropNode; 14 | import com.fasterxml.jackson.dataformat.javaprop.util.JPropNodeBuilder; 15 | 16 | public class JavaPropsParser extends ParserMinimalBase 17 | { 18 | protected final static JavaPropsSchema DEFAULT_SCHEMA = new JavaPropsSchema(); 19 | 20 | /* 21 | /********************************************************** 22 | /* Configuration 23 | /********************************************************** 24 | */ 25 | 26 | /** 27 | * Codec used for data binding when (if) requested. 28 | */ 29 | protected ObjectCodec _objectCodec; 30 | 31 | /** 32 | * Although most massaging is done later, caller may be interested in the 33 | * ultimate source. 34 | */ 35 | protected final Object _inputSource; 36 | 37 | /** 38 | * Actual {@link java.util.Properties} that were parsed and handed to us 39 | * for further processing. 40 | */ 41 | protected final Properties _sourceProperties; 42 | 43 | /** 44 | * Schema we use for parsing Properties into structure of some kind. 45 | */ 46 | protected JavaPropsSchema _schema = DEFAULT_SCHEMA; 47 | 48 | /* 49 | /********************************************************** 50 | /* Parsing state 51 | /********************************************************** 52 | */ 53 | 54 | protected JPropReadContext _readContext; 55 | 56 | protected boolean _closed; 57 | 58 | /* 59 | /********************************************************** 60 | /* Recycled helper objects 61 | /********************************************************** 62 | */ 63 | 64 | protected ByteArrayBuilder _byteArrayBuilder; 65 | 66 | protected byte[] _binaryValue; 67 | 68 | /* 69 | /********************************************************** 70 | /* Life-cycle 71 | /********************************************************** 72 | */ 73 | 74 | public JavaPropsParser(IOContext ctxt, Object inputSource, 75 | int parserFeatures, ObjectCodec codec, Properties sourceProps) 76 | { 77 | super(parserFeatures); 78 | _objectCodec = codec; 79 | _inputSource = inputSource; 80 | _sourceProperties = sourceProps; 81 | 82 | } 83 | 84 | @Override 85 | public Version version() { 86 | return PackageVersion.VERSION; 87 | } 88 | 89 | @Override 90 | public void setSchema(FormatSchema schema) 91 | { 92 | if (schema instanceof JavaPropsSchema) { 93 | _schema = (JavaPropsSchema) schema; 94 | } else { 95 | super.setSchema(schema); 96 | } 97 | } 98 | 99 | @Override 100 | public JavaPropsSchema getSchema() { 101 | return _schema; 102 | } 103 | 104 | // we do not take byte-based input, so base impl would be fine 105 | /* 106 | @Override 107 | public int releaseBuffered(OutputStream out) throws IOException { 108 | return -1; 109 | } 110 | */ 111 | 112 | // current implementation delegates to JDK `Properties, so we don't ever 113 | // see the input so: 114 | /* 115 | @Override 116 | public int releaseBuffered(Writer w) throws IOException { 117 | return -1; 118 | } 119 | */ 120 | 121 | @Override 122 | public void close() throws IOException { 123 | _closed = true; 124 | _readContext = null; 125 | } 126 | 127 | @Override 128 | public boolean isClosed() { 129 | return _closed; 130 | } 131 | 132 | /* 133 | /********************************************************** 134 | /* Public API overrides 135 | /********************************************************** 136 | */ 137 | 138 | @Override 139 | public ObjectCodec getCodec() { 140 | return _objectCodec; 141 | } 142 | 143 | @Override 144 | public void setCodec(ObjectCodec c) { 145 | _objectCodec = c; 146 | } 147 | 148 | @Override 149 | public Object getInputSource() { 150 | return _inputSource; 151 | } 152 | 153 | /* 154 | /********************************************************** 155 | /* Overrides: capability introspection methods 156 | /********************************************************** 157 | */ 158 | 159 | @Override 160 | public boolean canUseSchema(FormatSchema schema) { 161 | return schema instanceof JavaPropsSchema; 162 | } 163 | 164 | @Override 165 | public boolean requiresCustomCodec() { return false;} 166 | 167 | @Override 168 | public boolean canReadObjectId() { return false; } 169 | 170 | @Override 171 | public boolean canReadTypeId() { return false; } 172 | 173 | /* 174 | /********************************************************** 175 | /* Public API, structural 176 | /********************************************************** 177 | */ 178 | 179 | @Override 180 | public JsonStreamContext getParsingContext() { 181 | return _readContext; 182 | } 183 | 184 | @Override 185 | public void overrideCurrentName(String name) { 186 | _readContext.overrideCurrentName(name); 187 | } 188 | 189 | /* 190 | /********************************************************** 191 | /* Main parsing API, textual values 192 | /********************************************************** 193 | */ 194 | 195 | @Override 196 | public String getCurrentName() throws IOException { 197 | if (_readContext == null) { 198 | return null; 199 | } 200 | // [JACKSON-395]: start markers require information from parent 201 | if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { 202 | JPropReadContext parent = _readContext.getParent(); 203 | if (parent != null) { 204 | return parent.getCurrentName(); 205 | } 206 | } 207 | return _readContext.getCurrentName(); 208 | } 209 | 210 | @Override 211 | public JsonToken nextToken() throws IOException { 212 | 213 | if (_readContext == null) { 214 | if (_closed) { 215 | return null; 216 | } 217 | _closed = true; 218 | JPropNode root = JPropNodeBuilder.build(_schema, _sourceProperties); 219 | _readContext = JPropReadContext.create(root); 220 | 221 | // 30-Mar-2016, tatu: For debugging can be useful: 222 | /* 223 | System.err.println("SOURCE: ("+root.getClass().getName()+") <<\n"+new ObjectMapper().writerWithDefaultPrettyPrinter() 224 | .writeValueAsString(root.asRaw())); 225 | System.err.println("\n>>"); 226 | */ 227 | } 228 | 229 | while ((_currToken = _readContext.nextToken()) == null) { 230 | _readContext = _readContext.nextContext(); 231 | if (_readContext == null) { // end of content 232 | return null; 233 | } 234 | } 235 | return _currToken; 236 | } 237 | 238 | @Override 239 | public String getText() throws IOException { 240 | JsonToken t = _currToken; 241 | if (t == JsonToken.VALUE_STRING) { 242 | return _readContext.getCurrentText(); 243 | } 244 | if (t == JsonToken.FIELD_NAME) { 245 | return _readContext.getCurrentName(); 246 | } 247 | // shouldn't have non-String scalar values so: 248 | return (t == null) ? null : t.asString(); 249 | } 250 | 251 | @Override 252 | public boolean hasTextCharacters() { 253 | return false; 254 | } 255 | 256 | @Override 257 | public char[] getTextCharacters() throws IOException { 258 | String text = getText(); 259 | return (text == null) ? null : text.toCharArray(); 260 | } 261 | 262 | @Override 263 | public int getTextLength() throws IOException { 264 | String text = getText(); 265 | return (text == null) ? 0 : text.length(); 266 | } 267 | 268 | @Override 269 | public int getTextOffset() throws IOException { 270 | return 0; 271 | } 272 | 273 | @Override // since 2.8 274 | public int getText(Writer writer) throws IOException 275 | { 276 | String str = getText(); 277 | if (str == null) { 278 | return 0; 279 | } 280 | writer.write(str); 281 | return str.length(); 282 | } 283 | 284 | @SuppressWarnings("resource") 285 | @Override 286 | public byte[] getBinaryValue(Base64Variant variant) throws IOException 287 | { 288 | if (_binaryValue == null) { 289 | if (_currToken != JsonToken.VALUE_STRING) { 290 | _reportError("Current token ("+_currToken+") not VALUE_STRING, can not access as binary"); 291 | } 292 | ByteArrayBuilder builder = _getByteArrayBuilder(); 293 | _decodeBase64(getText(), builder, variant); 294 | _binaryValue = builder.toByteArray(); 295 | } 296 | return _binaryValue; 297 | } 298 | 299 | public ByteArrayBuilder _getByteArrayBuilder() 300 | { 301 | if (_byteArrayBuilder == null) { 302 | _byteArrayBuilder = new ByteArrayBuilder(); 303 | } else { 304 | _byteArrayBuilder.reset(); 305 | } 306 | return _byteArrayBuilder; 307 | } 308 | 309 | /* 310 | /********************************************************** 311 | /* Other accessor overrides 312 | /********************************************************** 313 | */ 314 | 315 | @Override 316 | public Object getEmbeddedObject() throws IOException { 317 | return null; 318 | } 319 | 320 | @Override 321 | public JsonLocation getTokenLocation() { 322 | return JsonLocation.NA; 323 | } 324 | 325 | @Override 326 | public JsonLocation getCurrentLocation() { 327 | return JsonLocation.NA; 328 | } 329 | 330 | /* 331 | /********************************************************** 332 | /* Main parsing API, textual values 333 | /********************************************************** 334 | */ 335 | 336 | @Override 337 | public Number getNumberValue() throws IOException { 338 | return _noNumbers(); 339 | } 340 | 341 | @Override 342 | public NumberType getNumberType() throws IOException { 343 | return _noNumbers(); 344 | } 345 | 346 | @Override 347 | public int getIntValue() throws IOException { 348 | return _noNumbers(); 349 | } 350 | 351 | @Override 352 | public long getLongValue() throws IOException { 353 | return _noNumbers(); 354 | } 355 | 356 | @Override 357 | public BigInteger getBigIntegerValue() throws IOException { 358 | return _noNumbers(); 359 | } 360 | 361 | @Override 362 | public float getFloatValue() throws IOException { 363 | return _noNumbers(); 364 | } 365 | 366 | @Override 367 | public double getDoubleValue() throws IOException { 368 | return _noNumbers(); 369 | } 370 | 371 | @Override 372 | public BigDecimal getDecimalValue() throws IOException { 373 | return _noNumbers(); 374 | } 375 | 376 | /* 377 | /********************************************************** 378 | /* Internal helper methods 379 | /********************************************************** 380 | */ 381 | 382 | protected T _noNumbers() throws IOException { 383 | _reportError("Current token ("+_currToken+") not numeric, can not use numeric value accessors"); 384 | return null; 385 | } 386 | 387 | @Override 388 | protected void _handleEOF() throws JsonParseException { 389 | if ((_readContext != null) && !_readContext.inRoot()) { 390 | _reportInvalidEOF(": expected close marker for "+_readContext.typeDesc(), null); 391 | } 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/JavaPropsSchema.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import com.fasterxml.jackson.core.FormatSchema; 4 | import com.fasterxml.jackson.dataformat.javaprop.util.JPropPathSplitter; 5 | import com.fasterxml.jackson.dataformat.javaprop.util.Markers; 6 | 7 | /** 8 | * Simple {@link FormatSchema} sub-type that defines details of things like: 9 | *

    10 | *
  • How are "flat" property names mapped to hierarchic POJO types, using 11 | * separator-based naming convention. 12 | *
  • 13 | *
  • What indentation (if any) is used before key values, and between key/value 14 | *
  • 15 | *
  • If and how are Array values inferred from property names 16 | *
  • 17 | *
18 | */ 19 | public class JavaPropsSchema 20 | implements FormatSchema, 21 | java.io.Serializable 22 | { 23 | private static final long serialVersionUID = 1L; // 2.5 24 | 25 | protected final static Markers DEFAULT_INDEX_MARKER = Markers.create("[", "]"); 26 | 27 | protected final static JavaPropsSchema EMPTY = new JavaPropsSchema(); 28 | 29 | /** 30 | * Since splitter instances are slightly costly to build in some cases, 31 | * we will lazily instantiate and cache them. 32 | */ 33 | protected transient JPropPathSplitter _splitter; 34 | 35 | /* 36 | /********************************************************************** 37 | /* Simple numeric properties 38 | /********************************************************************** 39 | */ 40 | 41 | /** 42 | * Specifies index number used when writing the first array entry (which 43 | * in Java has index of 0). After this initial value, additional elements 44 | * will have consecutive values, incremented by 1. 45 | * Note that this setting has no effect on reading: input indexes are only 46 | * used for sorting values, and their exact values have no meaning. 47 | *

48 | * Default value is 1. 49 | */ 50 | protected int _firstArrayOffset = 1; 51 | 52 | /* 53 | /********************************************************************** 54 | /* Formatting constants for input and output 55 | /********************************************************************** 56 | */ 57 | 58 | /** 59 | * Default path separator to use for hierarchic paths, if any; empty 60 | * String may be used to indicate that no hierarchy should be inferred 61 | * using a simple separator (although index markers may still be used, 62 | * if defined). 63 | */ 64 | protected String _pathSeparator = "."; 65 | 66 | /** 67 | * Default start marker for index access, if any; empty String may be used 68 | * to indicate no marker-based index detection should be made. 69 | *

70 | * Default value of "[" is usually combined with end marker of "]" to allow 71 | * C/Java-style bracket notation, like "settings.path[1]". 72 | */ 73 | protected Markers _indexMarker = DEFAULT_INDEX_MARKER; 74 | 75 | /* 76 | /********************************************************************** 77 | /* Formatting constants for input(-only) 78 | /********************************************************************** 79 | */ 80 | 81 | /** 82 | * Whether 'simple' index-notation is supported for path segments or not: 83 | * simple meaning that if a path segment is a textual representation of 84 | * a non-negative integer value with length of 9 or less (that is, up to 85 | * but no including one billion), it will be considered index, not property 86 | * name. 87 | *

88 | * Note that this settings does NOT control whether "start/end marker" indicated 89 | * indexes are enabled or not; those depend on {@link #_indexMarker}. 90 | *

91 | * Default value is true, "plain" index segments are 92 | * supported. 93 | */ 94 | protected boolean _parseSimpleIndexes = true; 95 | 96 | /* 97 | /********************************************************************** 98 | /* Formatting constants for output(-only) 99 | /********************************************************************** 100 | */ 101 | 102 | /** 103 | * Whether array-element paths are written using start/end markers 104 | * (see {@link #_indexMarker} or 105 | * "simple" index number: if set to true AND markers 106 | * are specified as non-empty Strings, will use sequence of 107 | *

108 |      *   startMarker index endMarker
109 |      *
110 | * to include index in path; otherwise will simply use textual representation 111 | * of the index number as path segment, prefixed by path separator as necessary. 112 | */ 113 | protected boolean _writeIndexUsingMarkers; 114 | 115 | /** 116 | * String prepended before key value, as possible indentation 117 | */ 118 | protected String _lineIndentation = ""; 119 | 120 | /** 121 | * String added between key and value; needs to include the "equals character" 122 | * (either '=' or ':', both allowed by Java Properties specification), may 123 | * also include white before and/or after "equals character". 124 | * Default value is a single '=' character with no white spaces around 125 | */ 126 | protected String _keyValueSeparator = "="; 127 | 128 | /** 129 | * String added after value, including at least one linefeed. 130 | * Default value is the 'Unix linefeed'. 131 | */ 132 | protected String _lineEnding = "\n"; 133 | 134 | /** 135 | * Optional header to prepend before any other output: typically a 136 | * comment section or so. Note that contents here are 137 | * NOT modified in any way, meaning that any comment indicators 138 | * (leading '#' or '!') and linefeeds MUST be specified by caller. 139 | */ 140 | protected String _header = ""; 141 | 142 | /* 143 | /********************************************************************** 144 | /* Construction, factories, mutant factories 145 | /********************************************************************** 146 | */ 147 | 148 | public JavaPropsSchema() { } 149 | 150 | public JavaPropsSchema(JavaPropsSchema base) { 151 | _firstArrayOffset = base._firstArrayOffset; 152 | _pathSeparator = base._pathSeparator; 153 | _indexMarker = base._indexMarker; 154 | _parseSimpleIndexes = base._parseSimpleIndexes; 155 | _writeIndexUsingMarkers = base._writeIndexUsingMarkers; 156 | _lineIndentation = base._lineIndentation; 157 | _keyValueSeparator = base._keyValueSeparator; 158 | _lineEnding = base._lineEnding; 159 | _header = base._header; 160 | } 161 | 162 | /** 163 | * Accessor for getting a {@link JPropPathSplitter} instance that does 164 | * splitting according to the settings of this instance. 165 | *

166 | * Note that instance is constructed lazily as needed, but reused afterwards 167 | * for this instance (and for these specific settings). 168 | */ 169 | public JPropPathSplitter pathSplitter() { 170 | JPropPathSplitter splitter = _splitter; 171 | if (splitter == null) { 172 | _splitter = splitter = JPropPathSplitter.create(this); 173 | } 174 | return splitter; 175 | } 176 | 177 | public JavaPropsSchema withFirstArrayOffset(int v) { 178 | if (v == _firstArrayOffset) { 179 | return this; 180 | } 181 | JavaPropsSchema s = new JavaPropsSchema(this); 182 | s._firstArrayOffset = v; 183 | return s; 184 | } 185 | 186 | /** 187 | * Mutant factory method for constructing a new instance with 188 | * specified path separator; default being comma ("."). 189 | * Note that setting separator to `null` or empty String will 190 | * basically disable handling of nesting, similar to 191 | * calling {@link #withoutPathSeparator}. 192 | */ 193 | public JavaPropsSchema withPathSeparator(String v) { 194 | if (v == null) { 195 | v = ""; 196 | } 197 | if (_equals(v, _pathSeparator)) { 198 | return this; 199 | } 200 | JavaPropsSchema s = new JavaPropsSchema(this); 201 | s._pathSeparator = v; 202 | return s; 203 | } 204 | 205 | /** 206 | * Mutant factory method for constructing a new instance that 207 | * specifies that no "path splitting" is to be done: this is 208 | * similar to default behavior of {@link java.util.Properties} 209 | * in which keys are full Strings and there is no nesting of values. 210 | */ 211 | public JavaPropsSchema withoutPathSeparator() { 212 | if ("".equals(_pathSeparator)) { 213 | return this; 214 | } 215 | JavaPropsSchema s = new JavaPropsSchema(this); 216 | s._pathSeparator = ""; 217 | return s; 218 | } 219 | 220 | public JavaPropsSchema withIndexMarker(Markers v) { 221 | if (_equals(v, _indexMarker)) { 222 | return this; 223 | } 224 | JavaPropsSchema s = new JavaPropsSchema(this); 225 | s._indexMarker = v; 226 | return s; 227 | } 228 | 229 | public JavaPropsSchema withoutIndexMarker() { 230 | if (_indexMarker == null) { 231 | return this; 232 | } 233 | JavaPropsSchema s = new JavaPropsSchema(this); 234 | s._indexMarker = null; 235 | return s; 236 | } 237 | 238 | public JavaPropsSchema withParseSimpleIndexes(boolean v) { 239 | if (v == _parseSimpleIndexes) { 240 | return this; 241 | } 242 | JavaPropsSchema s = new JavaPropsSchema(this); 243 | s._parseSimpleIndexes = v; 244 | return s; 245 | } 246 | 247 | public JavaPropsSchema withWriteIndexUsingMarkers(boolean v) { 248 | if (v == _writeIndexUsingMarkers) { 249 | return this; 250 | } 251 | JavaPropsSchema s = new JavaPropsSchema(this); 252 | s._writeIndexUsingMarkers = v; 253 | return s; 254 | } 255 | 256 | public JavaPropsSchema withLineIndentation(String v) { 257 | if (_equals(v, _lineIndentation)) { 258 | return this; 259 | } 260 | JavaPropsSchema s = new JavaPropsSchema(this); 261 | s._lineIndentation = v; 262 | return s; 263 | } 264 | 265 | /** 266 | * @since 2.8 267 | */ 268 | public JavaPropsSchema withoutLineIndentation() { 269 | return withLineIndentation(""); 270 | } 271 | 272 | public JavaPropsSchema withKeyValueSeparator(String v) { 273 | if (_equals(v, _keyValueSeparator)) { 274 | return this; 275 | } 276 | JavaPropsSchema s = new JavaPropsSchema(this); 277 | s._keyValueSeparator = v; 278 | return s; 279 | } 280 | 281 | public JavaPropsSchema withLineEnding(String v) { 282 | if (_equals(v, _lineEnding)) { 283 | return this; 284 | } 285 | JavaPropsSchema s = new JavaPropsSchema(this); 286 | s._lineEnding = v; 287 | return s; 288 | } 289 | 290 | /** 291 | * Mutant factory for constructing schema instance where specified 292 | * header section (piece of text written out right before actual 293 | * properties entries) will be used. 294 | * Note that caller must specify any and all linefeeds to use: generator 295 | * will NOT modify header String contents in any way, and will not append 296 | * a linefeed after contents (if any). 297 | */ 298 | public JavaPropsSchema withHeader(String v) { 299 | if (_equals(v, _header)) { 300 | return this; 301 | } 302 | JavaPropsSchema s = new JavaPropsSchema(this); 303 | s._header = v; 304 | return s; 305 | } 306 | 307 | /** 308 | * Convenience method, functionally equivalent to: 309 | *

310 |      *   withHeader("")
311 |      *
312 | * used to ensure that no header is prepended before actual property values 313 | * are output. 314 | * 315 | * @since 2.8 316 | */ 317 | public JavaPropsSchema withoutHeader() { 318 | return withHeader(""); 319 | } 320 | 321 | /* 322 | /********************************************************************** 323 | /* Public API, FormatSchema 324 | /********************************************************************** 325 | */ 326 | 327 | @Override 328 | public String getSchemaType() { 329 | return "JavaProps"; 330 | } 331 | 332 | public static JavaPropsSchema emptySchema() { 333 | return EMPTY; 334 | } 335 | 336 | /* 337 | /********************************************************************** 338 | /* Public API, extended, properties 339 | /********************************************************************** 340 | */ 341 | 342 | public int firstArrayOffset() { 343 | return _firstArrayOffset; 344 | } 345 | 346 | public String header() { 347 | return _header; 348 | } 349 | 350 | public Markers indexMarker() { 351 | return _indexMarker; 352 | } 353 | 354 | public String lineEnding() { 355 | return _lineEnding; 356 | } 357 | 358 | public String lineIndentation() { 359 | return _lineIndentation; 360 | } 361 | 362 | public String keyValueSeparator() { 363 | return _keyValueSeparator; 364 | } 365 | 366 | public boolean parseSimpleIndexes() { 367 | return _parseSimpleIndexes; 368 | } 369 | 370 | public String pathSeparator() { 371 | return _pathSeparator; 372 | } 373 | 374 | public boolean writeIndexUsingMarkers() { 375 | return _writeIndexUsingMarkers && (_indexMarker != null); 376 | } 377 | 378 | private boolean _equals(V a, V b) { 379 | if (a == null) { 380 | return (b == null); 381 | } 382 | return (b != null) && a.equals(b); 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/main/java/com/fasterxml/jackson/dataformat/javaprop/JavaPropsGenerator.java: -------------------------------------------------------------------------------- 1 | package com.fasterxml.jackson.dataformat.javaprop; 2 | 3 | import java.io.*; 4 | import java.math.BigDecimal; 5 | import java.math.BigInteger; 6 | import java.util.Arrays; 7 | 8 | import com.fasterxml.jackson.core.*; 9 | import com.fasterxml.jackson.core.base.GeneratorBase; 10 | import com.fasterxml.jackson.core.io.IOContext; 11 | import com.fasterxml.jackson.core.json.JsonWriteContext; 12 | import com.fasterxml.jackson.dataformat.javaprop.io.JPropEscapes; 13 | import com.fasterxml.jackson.dataformat.javaprop.io.JPropWriteContext; 14 | import com.fasterxml.jackson.dataformat.javaprop.util.Markers; 15 | 16 | public abstract class JavaPropsGenerator extends GeneratorBase 17 | { 18 | // As an optimization we try coalescing short writes into 19 | // buffer; but pass longer directly. 20 | final protected static int SHORT_WRITE = 100; 21 | 22 | /** 23 | * Since our context object does NOT implement standard write context, need 24 | * to do something like use a placeholder... 25 | */ 26 | protected final static JsonWriteContext BOGUS_WRITE_CONTEXT = JsonWriteContext.createRootContext(null); 27 | 28 | private final static JavaPropsSchema EMPTY_SCHEMA; 29 | static { 30 | EMPTY_SCHEMA = JavaPropsSchema.emptySchema(); 31 | } 32 | 33 | /* 34 | /********************************************************** 35 | /* Configuration 36 | /********************************************************** 37 | */ 38 | 39 | final protected IOContext _ioContext; 40 | 41 | /** 42 | * Definition of columns being written, if available. 43 | */ 44 | protected JavaPropsSchema _schema = EMPTY_SCHEMA; 45 | 46 | /* 47 | /********************************************************** 48 | /* Output state 49 | /********************************************************** 50 | */ 51 | 52 | /** 53 | * Current context, in form we can use it (GeneratorBase has 54 | * untyped reference; left as null) 55 | */ 56 | protected JPropWriteContext _jpropContext; 57 | 58 | /* 59 | /********************************************************** 60 | /* Output buffering 61 | /********************************************************** 62 | */ 63 | 64 | protected final StringBuilder _basePath = new StringBuilder(50); 65 | 66 | protected boolean _headerChecked; 67 | 68 | protected int _indentLength; 69 | 70 | /* 71 | /********************************************************** 72 | /* Life-cycle 73 | /********************************************************** 74 | */ 75 | 76 | public JavaPropsGenerator(IOContext ctxt, int stdFeatures, ObjectCodec codec) 77 | { 78 | super(stdFeatures, codec, BOGUS_WRITE_CONTEXT); 79 | _ioContext = ctxt; 80 | _jpropContext = JPropWriteContext.createRootContext(); 81 | } 82 | 83 | @Override 84 | public Object getCurrentValue() { 85 | return _jpropContext.getCurrentValue(); 86 | } 87 | 88 | @Override 89 | public void setCurrentValue(Object v) { 90 | _jpropContext.setCurrentValue(v); 91 | } 92 | 93 | @Override 94 | public Version version() { 95 | return PackageVersion.VERSION; 96 | } 97 | 98 | /* 99 | /********************************************************** 100 | /* Overridden methods, configuration 101 | /********************************************************** 102 | */ 103 | 104 | // // No way to indent 105 | 106 | @Override 107 | public JsonGenerator useDefaultPrettyPrinter() { 108 | // could alternatively throw exception but let it fly for now 109 | return this; 110 | } 111 | 112 | @Override 113 | public JsonGenerator setPrettyPrinter(PrettyPrinter pp) { 114 | // could alternatively throw exception but let it fly for now 115 | return this; 116 | } 117 | 118 | // public abstract getOutputTarget() 119 | 120 | // Base impl fine 121 | /* 122 | @Override 123 | public int getOutputBuffered() { 124 | return -1; 125 | } 126 | */ 127 | 128 | @Override 129 | public void setSchema(FormatSchema schema) { 130 | if (schema instanceof JavaPropsSchema) { 131 | _schema = (JavaPropsSchema) schema; 132 | // Indentation to use? 133 | if (_jpropContext.inRoot()) { 134 | String indent = _schema.lineIndentation(); 135 | _indentLength = (indent == null) ? 0 : indent.length(); 136 | if (_indentLength > 0) { 137 | _basePath.setLength(0); 138 | _basePath.append(indent); 139 | _jpropContext = JPropWriteContext.createRootContext(_indentLength); 140 | } 141 | } 142 | return; 143 | } 144 | super.setSchema(schema); 145 | } 146 | 147 | @Override 148 | public FormatSchema getSchema() { return _schema; } 149 | 150 | /* 151 | /********************************************************** 152 | /* Overrides: capability introspection methods 153 | /********************************************************** 154 | */ 155 | 156 | @Override 157 | public boolean canUseSchema(FormatSchema schema) { 158 | return schema instanceof JavaPropsSchema; 159 | } 160 | 161 | @Override 162 | public boolean canWriteObjectId() { return false; } 163 | 164 | @Override 165 | public boolean canWriteTypeId() { return false; } 166 | 167 | @Override 168 | public boolean canWriteBinaryNatively() { return false; } 169 | 170 | @Override 171 | public boolean canOmitFields() { return true; } 172 | 173 | @Override 174 | public boolean canWriteFormattedNumbers() { return true; } 175 | 176 | // No Format Features yet 177 | /* 178 | 179 | @Override 180 | public int getFormatFeatures() { 181 | return _formatFeatures; 182 | } 183 | 184 | @Override 185 | public JsonGenerator overrideFormatFeatures(int values, int mask) { } 186 | */ 187 | 188 | /* 189 | /********************************************************** 190 | /* Overridden methods: low-level I/O 191 | /********************************************************** 192 | */ 193 | 194 | // public void close() throws IOException 195 | 196 | // public void flush() throws IOException 197 | 198 | @Override 199 | public JsonStreamContext getOutputContext() { 200 | return _jpropContext; 201 | } 202 | 203 | /* 204 | /********************************************************************** 205 | /* Overridden methods; writing field names 206 | /********************************************************************** 207 | */ 208 | 209 | @Override 210 | public void writeFieldName(String name) throws IOException 211 | { 212 | if (!_jpropContext.writeFieldName(name)) { 213 | _reportError("Can not write a field name, expecting a value"); 214 | } 215 | // also, may need to output header if this would be first write 216 | if (!_headerChecked) { 217 | _headerChecked = true; 218 | String header = _schema.header(); 219 | if (header != null && !header.isEmpty()) { 220 | _writeRaw(header); 221 | } 222 | } 223 | 224 | // Ok; append to base path at this point. 225 | // First: ensure possibly preceding field name is removed: 226 | _jpropContext.truncatePath(_basePath); 227 | if (_basePath.length() > _indentLength) { 228 | String sep = _schema.pathSeparator(); 229 | if (!sep.isEmpty()) { 230 | _basePath.append(sep); 231 | } 232 | } 233 | // Note that escaping needs to be applied now... 234 | 235 | JPropEscapes.appendKey(_basePath, name); 236 | // NOTE: we do NOT yet write the key; wait until we have value; just append to path 237 | } 238 | 239 | /* 240 | /********************************************************** 241 | /* Public API: structural output 242 | /********************************************************** 243 | */ 244 | 245 | @Override 246 | public void writeStartArray() throws IOException { 247 | _verifyValueWrite("start an array"); 248 | _jpropContext = _jpropContext.createChildArrayContext(_basePath.length()); 249 | } 250 | 251 | @Override 252 | public void writeEndArray() throws IOException { 253 | if (!_jpropContext.inArray()) { 254 | _reportError("Current context not an Array but "+_jpropContext.typeDesc()); 255 | } 256 | _jpropContext = _jpropContext.getParent(); 257 | } 258 | 259 | @Override 260 | public void writeStartObject() throws IOException { 261 | _verifyValueWrite("start an object"); 262 | _jpropContext = _jpropContext.createChildObjectContext(_basePath.length()); 263 | } 264 | 265 | @Override 266 | public void writeEndObject() throws IOException 267 | { 268 | if (!_jpropContext.inObject()) { 269 | _reportError("Current context not an Ibject but "+_jpropContext.typeDesc()); 270 | } 271 | _jpropContext = _jpropContext.getParent(); 272 | } 273 | 274 | /* 275 | /********************************************************** 276 | /* Output method implementations, textual 277 | /********************************************************** 278 | */ 279 | 280 | @Override 281 | public void writeString(String text) throws IOException 282 | { 283 | if (text == null) { 284 | writeNull(); 285 | return; 286 | } 287 | _verifyValueWrite("write String value"); 288 | _writeEscapedEntry(text); 289 | } 290 | 291 | @Override 292 | public void writeString(char[] text, int offset, int len) 293 | throws IOException 294 | { 295 | _verifyValueWrite("write String value"); 296 | _writeEscapedEntry(text, offset, len); 297 | } 298 | 299 | @Override 300 | public void writeRawUTF8String(byte[] text, int offset, int len)throws IOException 301 | { 302 | _reportUnsupportedOperation(); 303 | } 304 | 305 | @Override 306 | public void writeUTF8String(byte[] text, int offset, int len) throws IOException 307 | { 308 | writeString(new String(text, offset, len, "UTF-8")); 309 | } 310 | 311 | /* 312 | /********************************************************** 313 | /* Output method implementations, unprocessed ("raw") 314 | /********************************************************** 315 | */ 316 | 317 | @Override 318 | public void writeRaw(String text) throws IOException { 319 | _writeRaw(text); 320 | } 321 | 322 | @Override 323 | public void writeRaw(String text, int offset, int len) throws IOException { 324 | _writeRaw(text.substring(offset, offset+len)); 325 | } 326 | 327 | @Override 328 | public void writeRaw(char[] text, int offset, int len) throws IOException { 329 | _writeRaw(text, offset, len); 330 | } 331 | 332 | @Override 333 | public void writeRaw(char c) throws IOException { 334 | _writeRaw(c); 335 | } 336 | 337 | @Override 338 | public void writeRaw(SerializableString text) throws IOException, JsonGenerationException { 339 | writeRaw(text.toString()); 340 | } 341 | 342 | /* 343 | /********************************************************** 344 | /* Output method implementations, base64-encoded binary 345 | /********************************************************** 346 | */ 347 | 348 | @Override 349 | public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) 350 | throws IOException 351 | { 352 | if (data == null) { 353 | writeNull(); 354 | return; 355 | } 356 | _verifyValueWrite("write Binary value"); 357 | // ok, better just Base64 encode as a String... 358 | if (offset > 0 || (offset+len) != data.length) { 359 | data = Arrays.copyOfRange(data, offset, offset+len); 360 | } 361 | String encoded = b64variant.encode(data); 362 | _writeEscapedEntry(encoded); 363 | } 364 | 365 | /* 366 | /********************************************************** 367 | /* Output method implementations, scalars 368 | /********************************************************** 369 | */ 370 | 371 | @Override 372 | public void writeBoolean(boolean state) throws IOException 373 | { 374 | _verifyValueWrite("write boolean value"); 375 | _writeUnescapedEntry(state ? "true" : "false"); 376 | } 377 | 378 | @Override 379 | public void writeNumber(int i) throws IOException 380 | { 381 | _verifyValueWrite("write number"); 382 | _writeUnescapedEntry(String.valueOf(i)); 383 | } 384 | 385 | @Override 386 | public void writeNumber(long l) throws IOException 387 | { 388 | _verifyValueWrite("write number"); 389 | _writeUnescapedEntry(String.valueOf(l)); 390 | } 391 | 392 | @Override 393 | public void writeNumber(BigInteger v) throws IOException 394 | { 395 | if (v == null) { 396 | writeNull(); 397 | return; 398 | } 399 | _verifyValueWrite("write number"); 400 | _writeUnescapedEntry(String.valueOf(v)); 401 | } 402 | 403 | @Override 404 | public void writeNumber(double d) throws IOException 405 | { 406 | _verifyValueWrite("write number"); 407 | _writeUnescapedEntry(String.valueOf(d)); 408 | } 409 | 410 | @Override 411 | public void writeNumber(float f) throws IOException 412 | { 413 | _verifyValueWrite("write number"); 414 | _writeUnescapedEntry(String.valueOf(f)); 415 | } 416 | 417 | @Override 418 | public void writeNumber(BigDecimal dec) throws IOException 419 | { 420 | if (dec == null) { 421 | writeNull(); 422 | return; 423 | } 424 | _verifyValueWrite("write number"); 425 | String str = isEnabled(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN) ? dec.toPlainString() : dec.toString(); 426 | _writeUnescapedEntry(str); 427 | } 428 | 429 | @Override 430 | public void writeNumber(String encodedValue) throws IOException 431 | { 432 | if (encodedValue == null) { 433 | writeNull(); 434 | return; 435 | } 436 | _verifyValueWrite("write number"); 437 | _writeUnescapedEntry(encodedValue); 438 | } 439 | 440 | @Override 441 | public void writeNull() throws IOException 442 | { 443 | _verifyValueWrite("write null value"); 444 | _writeUnescapedEntry(""); 445 | } 446 | 447 | /* 448 | /********************************************************** 449 | /* Implementations for methods from base class 450 | /********************************************************** 451 | */ 452 | 453 | // protected void _releaseBuffers() 454 | 455 | // protected void _flushBuffer() throws IOException 456 | 457 | @Override 458 | protected void _verifyValueWrite(String typeMsg) throws IOException 459 | { 460 | // first, check that name/value cadence works 461 | if (!_jpropContext.writeValue()) { 462 | _reportError("Can not "+typeMsg+", expecting field name"); 463 | } 464 | // and if so, update path if we are in array 465 | if (_jpropContext.inArray()) { 466 | // remove possible path remnants from an earlier sibling 467 | _jpropContext.truncatePath(_basePath); 468 | int ix = _jpropContext.getCurrentIndex() + _schema.firstArrayOffset(); 469 | if (_schema.writeIndexUsingMarkers()) { 470 | Markers m = _schema.indexMarker(); 471 | // no leading path separator, if using enclosed indexes 472 | _basePath.append(m.getStart()); 473 | _basePath.append(ix); 474 | _basePath.append(m.getEnd()); 475 | } else { 476 | // leading path separator, if using "simple" index markers 477 | if (_basePath.length() > 0) { 478 | String sep = _schema.pathSeparator(); 479 | if (!sep.isEmpty()) { 480 | _basePath.append(sep); 481 | } 482 | } 483 | _basePath.append(ix); 484 | } 485 | } 486 | } 487 | 488 | /* 489 | /********************************************************** 490 | /* Abstract methods for sub-classes 491 | /********************************************************** 492 | */ 493 | 494 | protected abstract void _writeEscapedEntry(String value) throws IOException; 495 | 496 | protected abstract void _writeEscapedEntry(char[] text, int offset, int len) throws IOException; 497 | 498 | protected abstract void _writeUnescapedEntry(String value) throws IOException; 499 | 500 | protected abstract void _writeRaw(char c) throws IOException; 501 | protected abstract void _writeRaw(String text) throws IOException; 502 | protected abstract void _writeRaw(StringBuilder text) throws IOException; 503 | protected abstract void _writeRaw(char[] text, int offset, int len) throws IOException; 504 | 505 | } 506 | --------------------------------------------------------------------------------