diff --git a/build.gradle b/build.gradle index 1bc6a8fd..141de6f6 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { dependencies { classpath 'me.champeau.gradle:gradle-javadoc-hotfix-plugin:0.1' classpath 'com.github.jengelman.gradle.plugins:shadow:0.8' + classpath 'me.champeau.gradle:japicmp-gradle-plugin:0.1.0' } } @@ -73,4 +74,5 @@ task wrapper(type: Wrapper) { } //Task used by Heroku for staging -task stage(dependsOn: [':json-path-web-test:clean', 'json-path-web-test:shadowJar']) {} \ No newline at end of file +task stage(dependsOn: [':json-path-web-test:clean', 'json-path-web-test:shadowJar']) {} +apply from: "$rootDir/gradle/binaryCompatibility.gradle" \ No newline at end of file diff --git a/changelog.md b/changelog.md index 0e881069..e55688bd 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ In The Pipe * Negate exist checks in inline filters (not defined or null) `parse(JSON_DOCUMENT).read("$.store.book[?(!@.isbn)]")` * Improved object mapping with Jackson and Gson (now handles generic types) +* JacksonTreeJsonProvider supporting path operations on `com.fasterxml.jackson.databind.JsonNode` * Exceptions thrown by JsonPath.compile are now wrapped in an InvalidPathException * Fixed Deep scanning issue (#60) diff --git a/gradle/binaryCompatibility.gradle b/gradle/binaryCompatibility.gradle new file mode 100644 index 00000000..5f9de439 --- /dev/null +++ b/gradle/binaryCompatibility.gradle @@ -0,0 +1,138 @@ +/* + * Copyright 2003-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import groovy.text.markup.MarkupTemplateEngine +import groovy.text.markup.TemplateConfiguration + + +buildscript { + // this block should not be necessary, but for some reason it fails without! + repositories { + jcenter() + } + + dependencies { + classpath 'me.champeau.gradle:japicmp-gradle-plugin:0.1.0' + } +} + + +task checkBinaryCompatibility { + description = 'Generates binary compatibility reports' +} + + +if (JavaVersion.current().isJava7Compatible()) { + apply plugin: 'me.champeau.gradle.japicmp' + + def referenceMinorVersion = '1.1.0' + + def reportGenerator = { model -> + outputProcessor { + + def skipClass = { c -> c.fullyQualifiedName.startsWith('com.jayway.jsonpath.internal') || c.fullyQualifiedName.contains('$')} + def skipMethod = { c, m -> skipClass(c) || m.name =~ /access\$[0-9]+/ } + def violations = [:].withDefault { + // key = class name + // value = map of violations + [:].withDefault { [] } + } + removedMethod { c, m -> + if (!skipMethod(c, m)) { + def level = m.name.startsWith('super$') ? 'warning' : 'error' + violations[c.fullyQualifiedName][level] << "Method ${m.name} has been removed" + } + } + removedClass { c -> + if (!skipClass(c)) { + violations[c.fullyQualifiedName].error << "Class has been removed" + } + } + + modifiedMethod { c, m -> + if (!skipMethod(c, m)) { + violations[c.fullyQualifiedName].warning << """

Method ${m.name} has been modified

+

From

${m.oldMethod.get()?.longName}
to
${m.newMethod.get()?.longName}

""" + } + } + + newClass { c -> + if (!skipClass(c)) { + violations[c.fullyQualifiedName].info << "Class has been added" + } + } + newMethod { c, m -> + if (!skipMethod(c, m)) { + violations[c.fullyQualifiedName].info << """

Method ${m.name} has been added

+

Signature:

${m.newMethod.get()?.longName}

""" + } + } + after { + model.violations = violations + } + } + } + +// using a global engine for all tasks in order to increase performance + def configDir = file("$rootDir/gradle") + def templateFile = 'binarycompat-report.groovy' + def templateConfiguration = new TemplateConfiguration() + templateConfiguration.with { + autoIndent = true + autoNewLine = true + } + def engine = new MarkupTemplateEngine(this.class.classLoader, configDir, templateConfiguration) + + + subprojects { + + task japicmp(type: me.champeau.gradle.ArtifactJapicmpTask) { + dependsOn jar + //baseline = "com.jayway.jsonpath:${project.name}:${referenceMinorVersion}@jar" + baseline = "com.jayway.jsonpath:${project.name}:+@jar" + to = jar.archivePath + accessModifier = 'protected' + onlyModified = true + failOnModification = false + txtOutputFile = file("$buildDir/reports/japi.txt") + + def htmlReportFile = file("${buildDir}/reports/binary-compat-${project.name}.html") + inputs.file file("$configDir/$templateFile") + inputs.file templateFile + outputs.file htmlReportFile + + def model = [title : "Binary compatibility report for ${project.name}", + project : project, + baseline: baseline, + archive : to.name] + outputProcessor(reportGenerator.curry(model)) + + doLast { + htmlReportFile.withWriter('utf-8') { wrt -> + engine.createTemplateByPath(templateFile).make(model).writeTo(wrt) + } + } + + } + } + + subprojects { + check.dependsOn(checkBinaryCompatibility) + tasks.withType(me.champeau.gradle.ArtifactJapicmpTask) { task -> + checkBinaryCompatibility.dependsOn(task) + } + } +} \ No newline at end of file diff --git a/gradle/binarycompat-report.groovy b/gradle/binarycompat-report.groovy new file mode 100644 index 00000000..9e3e6633 --- /dev/null +++ b/gradle/binarycompat-report.groovy @@ -0,0 +1,153 @@ +/* + * Copyright 2003-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A template which generates an HTML report from the bincompat XML report + */ +modelTypes = { + String title + String baseline + String archive + Map>> violations +} + +def severityMapping = [ + error : 'danger', + warning: 'warning', + info : 'info', + ignore : 'success' +] + +yieldUnescaped '' + + +html { + head { + meta 'charset': "utf-8" + meta 'http-equiv': "content-type", content: "text/html; charset=utf-8" + meta 'http-equiv': "X-UA-Compatible", content: "IE=edge" + meta name: "viewport", content: "width=device-width, initial-scale=1" + + title(title) + link href: "http://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css", rel: "stylesheet" + link href: "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css", rel: "stylesheet" + link href: "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css", rel: "stylesheet" + } + + body { + div(class:'navbar navbar-inverse navbar-fixed-top', role:'navigation') { + div(class:'container') { + div(class:'navbar-header') { + button(type:'button', class:'navbar-toggle', 'data-toggle':'collapse', 'data-target':'navbar-collaspe') { + span(class:'sr-only', 'Toggle navigation') + span(class:'icon-bar'){} + span(class:'icon-bar'){} + span(class:'icon-bar'){} + } + a(class:'navbar-brand',href:'#', 'Binary compatibility report') + } + div(class:'navbar-collapse collapse') { + ul(class:"nav navbar-nav") { + li(class: 'dropdown') { + a(id: 'severityDropdown', href: '#', class: 'dropdown-toggle', 'data-toggle': 'dropdown', 'Severity ') + ul(class: "dropdown-menu dropdown-severity", role: "menu") { + li(role: 'presentation', class: 'active') { + a(role: 'menuitem', tabindex: '-1', href: '#', 'All levels') + } + li(role: 'presentation') { a(role: 'menuitem', tabindex: '-1', href: '#', 'Error') } + li(role: 'presentation') { a(role: 'menuitem', tabindex: '-1', href: '#', 'Warning') } + li(role: 'presentation') { a(role: 'menuitem', tabindex: '-1', href: '#', 'Info') } + } + } + + } + } + } + } + + + div(class: 'container') { + div(class:'jumbotron') { + div(class:'container') { + div(class: 'page-header') { + h1 'Binary compatibility' + p "Comparing ${archive} to reference ${baseline}" + p { + yield "Be warned that this report is not perfect and depends on what " + a(href: 'https://github.com/siom79/japicmp', 'JApicmp') + yield " is capable to detect." + } + } + } + } + violations.each { fqcn, classViolations -> + def errors = classViolations.keySet() + def severities = errors.collect { "severity-${it}" } + div(class: "panel panel-default ${severities.join(' ')}") { + div(class: "panel-heading") { + h3(class: 'panel-title', "Class $fqcn") + } + div(class: 'panel-body') { + table(class: "table table-striped table-bordered") { + tbody { + classViolations.each { err, list -> + list.each { item -> + tr(class: "bincompat-error severity-${err}") { + td { + h4 { + span(class: "label label-${severityMapping[err]}", err.capitalize()) + } + } + td { span(item) } + } + } + } + } + } + } + } + } + + script(src: "http://code.jquery.com/jquery-1.11.0.min.js") {} + script(src: "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js") {} + script { + yieldUnescaped ''' +$(document).ready(function () { + var severity = null; + doFilter(); + function doFilter() { + var severityClass = "severity-" + severity; + $('.panel').hide(); + $('.bincompat-error').hide(); + $('.bincompat-error').filter(function () { + return (severity==null || $(this).hasClass(severityClass)); + }).show(); + $('.panel').filter(function () { + return (severity==null || $(this).hasClass(severityClass)); + }).show(); + } + $(".dropdown-severity li a").click(function() { + severity = $(this).text().toLowerCase(); + if (severity==="all levels") { + severity = null; + } + doFilter(); + }); +});''' + } + } + } +} \ No newline at end of file diff --git a/json-path/build.gradle b/json-path/build.gradle index caf7fa1e..15bdaaab 100644 --- a/json-path/build.gradle +++ b/json-path/build.gradle @@ -1,5 +1,6 @@ apply from: "$rootDir/gradle/publishMaven.gradle" + displayName = "Json Path" description = "Java port of Stefan Goessner JsonPath." @@ -13,9 +14,9 @@ jar { dependencies { compile libs.jsonSmart + compile libs.slf4jApi compile libs.jacksonDatabind, optional compile libs.gson, optional - compile libs.slf4jApi testCompile libs.test } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/mapper/JsonSmartMappingProvider.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/mapper/JsonSmartMappingProvider.java index 8f3751f1..bd02d67f 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/mapper/JsonSmartMappingProvider.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/mapper/JsonSmartMappingProvider.java @@ -42,9 +42,13 @@ public class JsonSmartMappingProvider implements MappingProvider { static { DEFAULT.registerReader(Long.class, new LongReader()); + DEFAULT.registerReader(long.class, new LongReader()); DEFAULT.registerReader(Integer.class, new IntegerReader()); + DEFAULT.registerReader(int.class, new IntegerReader()); DEFAULT.registerReader(Double.class, new DoubleReader()); + DEFAULT.registerReader(double.class, new DoubleReader()); DEFAULT.registerReader(Float.class, new FloatReader()); + DEFAULT.registerReader(float.class, new FloatReader()); DEFAULT.registerReader(BigDecimal.class, new BigDecimalReader()); DEFAULT.registerReader(String.class, new StringReader()); DEFAULT.registerReader(Date.class, new DateReader()); @@ -94,7 +98,7 @@ public class JsonSmartMappingProvider implements MappingProvider { @Override public T map(Object source, TypeRef targetType, Configuration configuration) { - throw new UnsupportedOperationException("Not supported! Use Jackson or Gson provider"); + throw new UnsupportedOperationException("Json-smart provider does not support TypeRef! Use a Jackson or Gson based provider"); } private static class StringReader extends JsonReaderI { @@ -116,7 +120,9 @@ public class JsonSmartMappingProvider implements MappingProvider { if(src == null){ return null; } - if (Long.class.isAssignableFrom(src.getClass())) { + if(Integer.class.isAssignableFrom(src.getClass())){ + return (Integer) src; + } else if (Long.class.isAssignableFrom(src.getClass())) { return ((Integer) src).intValue(); } else if (Double.class.isAssignableFrom(src.getClass())) { return ((Double) src).intValue(); @@ -138,7 +144,9 @@ public class JsonSmartMappingProvider implements MappingProvider { if(src == null){ return null; } - if (Integer.class.isAssignableFrom(src.getClass())) { + if(Long.class.isAssignableFrom(src.getClass())){ + return (Long) src; + } else if (Integer.class.isAssignableFrom(src.getClass())) { return ((Integer) src).longValue(); } else if (Double.class.isAssignableFrom(src.getClass())) { return ((Double) src).longValue(); @@ -160,7 +168,9 @@ public class JsonSmartMappingProvider implements MappingProvider { if(src == null){ return null; } - if (Integer.class.isAssignableFrom(src.getClass())) { + if(Double.class.isAssignableFrom(src.getClass())){ + return (Double) src; + } else if (Integer.class.isAssignableFrom(src.getClass())) { return ((Integer) src).doubleValue(); } else if (Long.class.isAssignableFrom(src.getClass())) { return ((Long) src).doubleValue(); @@ -182,7 +192,9 @@ public class JsonSmartMappingProvider implements MappingProvider { if(src == null){ return null; } - if (Integer.class.isAssignableFrom(src.getClass())) { + if(Float.class.isAssignableFrom(src.getClass())){ + return (Float) src; + } else if (Integer.class.isAssignableFrom(src.getClass())) { return ((Integer) src).floatValue(); } else if (Long.class.isAssignableFrom(src.getClass())) { return ((Long) src).floatValue(); @@ -215,10 +227,11 @@ public class JsonSmartMappingProvider implements MappingProvider { if(src == null){ return null; } - if(Long.class.isAssignableFrom(src.getClass())){ + if(Date.class.isAssignableFrom(src.getClass())){ + return (Date) src; + } else if(Long.class.isAssignableFrom(src.getClass())){ return new Date((Long) src); - } - else if(String.class.isAssignableFrom(src.getClass())){ + } else if(String.class.isAssignableFrom(src.getClass())){ try { return DateFormat.getInstance().parse(src.toString()); } catch (ParseException e) { diff --git a/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java new file mode 100644 index 00000000..67a118f2 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java @@ -0,0 +1,91 @@ +package com.jayway.jsonpath; + +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonProviderTest extends BaseTest { + + private static final String JSON = + "[" + + "{\n" + + " \"foo\" : \"foo0\",\n" + + " \"bar\" : 0,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp0\"}" + + "}," + + "{\n" + + " \"foo\" : \"foo1\",\n" + + " \"bar\" : 1,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp1\"}" + + "}," + + "{\n" + + " \"foo\" : \"foo2\",\n" + + " \"bar\" : 2,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp2\"}" + + "}" + + "]"; + + @Test + public void strings_are_unwrapped() { + assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); + assertThat(using(JACKSON_TREE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); + assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); + assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); + } + + @Test + public void integers_are_unwrapped() { + assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); + assertThat(using(JACKSON_TREE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); + assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); + assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void ints_are_unwrapped() { + assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); + assertThat(using(JACKSON_TREE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); + assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); + assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void list_of_numbers() { + + TypeRef> typeRef = new TypeRef>() {}; + + assertThat(using(JACKSON_TREE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + } + + @Test + public void test_type_ref() throws IOException { + TypeRef>> typeRef = new TypeRef>>() {}; + + assertThat(using(JACKSON_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); + assertThat(using(JACKSON_TREE_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); + assertThat(using(GSON_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); + } + + + public static class FooBarBaz { + public T gen; + public String foo; + public Long bar; + public boolean baz; + } + + + public static class Sub { + public String prop; + } + +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/JsonSmartMappingProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/JsonSmartMappingProviderTest.java deleted file mode 100644 index 7c7b4ad9..00000000 --- a/json-path/src/test/java/com/jayway/jsonpath/JsonSmartMappingProviderTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.jayway.jsonpath; - -import org.junit.Test; - -import java.io.IOException; -import java.util.List; - -import static com.jayway.jsonpath.JsonPath.parse; -import static com.jayway.jsonpath.JsonPath.using; -import static org.assertj.core.api.Assertions.assertThat; - -public class JsonSmartMappingProviderTest { - - private static final String JSON = - "[" + - "{\n" + - " \"foo\" : \"foo0\",\n" + - " \"bar\" : 0,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"eric\" : \"yepp\"}" + - "}," + - "{\n" + - " \"foo\" : \"foo1\",\n" + - " \"bar\" : 1,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"eric\" : \"yepp\"}" + - "}," + - "{\n" + - " \"foo\" : \"foo2\",\n" + - " \"bar\" : 2,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"eric\" : \"yepp\"}" + - "}" + - "]"; - - @Test - public void class_mapping() { - - Object map = Configuration.defaultConfiguration().jsonProvider().createMap(); - Configuration.defaultConfiguration().jsonProvider().setProperty(map, "eric", "eric-val"); - - - Gen gen = parse(map).read("$", Gen.class); - - assertThat(gen.eric).isEqualTo("eric-val"); - - } - - @Test(expected = UnsupportedOperationException.class) - public void test_type_ref() throws IOException { - - TypeRef> typeRef = new TypeRef>() {}; - - List gen = parse(JSON).read("$", typeRef); - } - - - public static class FooBarBaz { - public Gen gen; - public String foo; - public Long bar; - public boolean baz; - } - - - public static class Gen { - public String eric; - } -}