Skip to content

Commit 4333885

Browse files
authored
JSON formatting using Gson (#1125)
2 parents 7331070 + 2b249b4 commit 4333885

26 files changed

+2195
-89
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Added
14+
* Added support for JSON formatting based on [Gson](https://github.com/google/gson) ([#1125](https://github.com/diffplug/spotless/pull/1125)).
1315

1416
### Changed
1517

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ It's easy to build such a function, but there are some gotchas and lots of integ
3030
## Current feature matrix
3131

3232
<!---freshmark matrix
33-
function lib(className) { return '| [`' + className + '`](lib/src/main/java/com/diffplug/spotless/' + className.replace('.', '/') + '.java) | ' }
34-
function extra(className) { return '| [`' + className + '`](lib-extra/src/main/java/com/diffplug/spotless/extra/' + className.replace('.', '/') + '.java) | ' }
33+
function lib(className) { return '| [`' + className + '`](lib/src/main/java/com/diffplug/spotless/' + className.replaceAll('\\.', '/') + '.java) | ' }
34+
function extra(className) { return '| [`' + className + '`](lib-extra/src/main/java/com/diffplug/spotless/extra/' + className.replaceAll('\\.', '/') + '.java) | ' }
3535
3636
// | GRADLE | MAVEN | SBT | (new) |
3737
output = [
@@ -61,6 +61,8 @@ lib('java.ImportOrderStep') +'{{yes}} | {{yes}}
6161
lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
6262
lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
6363
extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
64+
lib('json.gson.GsonStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
65+
lib('json.JsonSimpleStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
6466
lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
6567
lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
6668
lib('kotlin.DiktatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -102,6 +104,8 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
102104
| [`java.PalantirJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
103105
| [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
104106
| [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
107+
| [`json.gson.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
108+
| [`json.JsonSimpleStep`](lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
105109
| [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
106110
| [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
107111
| [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2022 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.json.gson;
17+
18+
import java.lang.reflect.Constructor;
19+
import java.lang.reflect.Method;
20+
21+
import com.diffplug.spotless.JarState;
22+
23+
class GsonBuilderWrapper extends GsonWrapperBase {
24+
25+
private final Constructor<?> constructor;
26+
private final Method serializeNullsMethod;
27+
private final Method disableHtmlEscapingMethod;
28+
private final Method createMethod;
29+
30+
GsonBuilderWrapper(JarState jarState) {
31+
Class<?> clazz = loadClass(jarState.getClassLoader(), "com.google.gson.GsonBuilder");
32+
this.constructor = getConstructor(clazz);
33+
this.serializeNullsMethod = getMethod(clazz, "serializeNulls");
34+
this.disableHtmlEscapingMethod = getMethod(clazz, "disableHtmlEscaping");
35+
this.createMethod = getMethod(clazz, "create");
36+
}
37+
38+
Object createGsonBuilder() {
39+
return newInstance(constructor);
40+
}
41+
42+
Object serializeNulls(Object gsonBuilder) {
43+
return invoke(serializeNullsMethod, gsonBuilder);
44+
}
45+
46+
Object disableHtmlEscaping(Object gsonBuilder) {
47+
return invoke(disableHtmlEscapingMethod, gsonBuilder);
48+
}
49+
50+
Object create(Object gsonBuilder) {
51+
return invoke(createMethod, gsonBuilder);
52+
}
53+
54+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2022 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.json.gson;
17+
18+
import java.io.IOException;
19+
import java.io.Serializable;
20+
import java.io.StringWriter;
21+
import java.util.Collections;
22+
import java.util.Objects;
23+
24+
import com.diffplug.spotless.FormatterFunc;
25+
import com.diffplug.spotless.FormatterStep;
26+
import com.diffplug.spotless.JarState;
27+
import com.diffplug.spotless.Provisioner;
28+
29+
public class GsonStep {
30+
private static final String MAVEN_COORDINATES = "com.google.code.gson:gson";
31+
32+
public static FormatterStep create(int indentSpaces, boolean sortByKeys, boolean escapeHtml, String version, Provisioner provisioner) {
33+
Objects.requireNonNull(provisioner, "provisioner cannot be null");
34+
return FormatterStep.createLazy("gson", () -> new State(indentSpaces, sortByKeys, escapeHtml, version, provisioner), State::toFormatter);
35+
}
36+
37+
private static final class State implements Serializable {
38+
private static final long serialVersionUID = -1493479043249379485L;
39+
40+
private final int indentSpaces;
41+
private final boolean sortByKeys;
42+
private final boolean escapeHtml;
43+
private final JarState jarState;
44+
45+
private State(int indentSpaces, boolean sortByKeys, boolean escapeHtml, String version, Provisioner provisioner) throws IOException {
46+
this.indentSpaces = indentSpaces;
47+
this.sortByKeys = sortByKeys;
48+
this.escapeHtml = escapeHtml;
49+
this.jarState = JarState.from(MAVEN_COORDINATES + ":" + version, provisioner);
50+
}
51+
52+
FormatterFunc toFormatter() {
53+
JsonWriterWrapper jsonWriterWrapper = new JsonWriterWrapper(jarState);
54+
JsonElementWrapper jsonElementWrapper = new JsonElementWrapper(jarState);
55+
JsonObjectWrapper jsonObjectWrapper = new JsonObjectWrapper(jarState, jsonElementWrapper);
56+
GsonBuilderWrapper gsonBuilderWrapper = new GsonBuilderWrapper(jarState);
57+
GsonWrapper gsonWrapper = new GsonWrapper(jarState, jsonElementWrapper, jsonWriterWrapper);
58+
59+
Object gsonBuilder = gsonBuilderWrapper.serializeNulls(gsonBuilderWrapper.createGsonBuilder());
60+
if (!escapeHtml) {
61+
gsonBuilder = gsonBuilderWrapper.disableHtmlEscaping(gsonBuilder);
62+
}
63+
Object gson = gsonBuilderWrapper.create(gsonBuilder);
64+
65+
return inputString -> {
66+
String result;
67+
if (inputString.isEmpty()) {
68+
result = "";
69+
} else {
70+
Object jsonElement = gsonWrapper.fromJson(gson, inputString, jsonElementWrapper.getWrappedClass());
71+
if (jsonElement == null) {
72+
throw new AssertionError(GsonWrapperBase.FAILED_TO_PARSE_ERROR_MESSAGE);
73+
}
74+
if (sortByKeys && jsonElementWrapper.isJsonObject(jsonElement)) {
75+
jsonElement = sortByKeys(jsonObjectWrapper, jsonElementWrapper, jsonElement);
76+
}
77+
try (StringWriter stringWriter = new StringWriter()) {
78+
Object jsonWriter = jsonWriterWrapper.createJsonWriter(stringWriter);
79+
jsonWriterWrapper.setIndent(jsonWriter, generateIndent(indentSpaces));
80+
gsonWrapper.toJson(gson, jsonElement, jsonWriter);
81+
result = stringWriter + "\n";
82+
}
83+
}
84+
return result;
85+
};
86+
}
87+
88+
private Object sortByKeys(JsonObjectWrapper jsonObjectWrapper, JsonElementWrapper jsonElementWrapper, Object jsonObject) {
89+
Object result = jsonObjectWrapper.createJsonObject();
90+
jsonObjectWrapper.keySet(jsonObject).stream().sorted()
91+
.forEach(key -> {
92+
Object element = jsonObjectWrapper.get(jsonObject, key);
93+
if (jsonElementWrapper.isJsonObject(element)) {
94+
element = sortByKeys(jsonObjectWrapper, jsonElementWrapper, element);
95+
}
96+
jsonObjectWrapper.add(result, key, element);
97+
});
98+
return result;
99+
}
100+
101+
private String generateIndent(int indentSpaces) {
102+
return String.join("", Collections.nCopies(indentSpaces, " "));
103+
}
104+
}
105+
106+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2022 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.json.gson;
17+
18+
import java.lang.reflect.Method;
19+
20+
import com.diffplug.spotless.JarState;
21+
22+
class GsonWrapper extends GsonWrapperBase {
23+
24+
private final Method fromJsonMethod;
25+
private final Method toJsonMethod;
26+
27+
GsonWrapper(JarState jarState, JsonElementWrapper jsonElementWrapper, JsonWriterWrapper jsonWriterWrapper) {
28+
Class<?> clazz = loadClass(jarState.getClassLoader(), "com.google.gson.Gson");
29+
this.fromJsonMethod = getMethod(clazz, "fromJson", String.class, Class.class);
30+
this.toJsonMethod = getMethod(clazz, "toJson", jsonElementWrapper.getWrappedClass(), jsonWriterWrapper.getWrappedClass());
31+
}
32+
33+
Object fromJson(Object gson, String json, Class<?> type) {
34+
return invoke(fromJsonMethod, gson, json, type);
35+
}
36+
37+
void toJson(Object gson, Object jsonElement, Object jsonWriter) {
38+
invoke(toJsonMethod, gson, jsonElement, jsonWriter);
39+
}
40+
41+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2022 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.json.gson;
17+
18+
import java.lang.reflect.Constructor;
19+
import java.lang.reflect.InvocationTargetException;
20+
import java.lang.reflect.Method;
21+
22+
abstract class GsonWrapperBase {
23+
24+
static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Gson; maybe you set an incompatible version?";
25+
static final String FAILED_TO_PARSE_ERROR_MESSAGE = "Unable to format JSON";
26+
27+
protected final Class<?> loadClass(ClassLoader classLoader, String className) {
28+
try {
29+
return classLoader.loadClass(className);
30+
} catch (ClassNotFoundException cause) {
31+
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
32+
}
33+
}
34+
35+
protected final Constructor<?> getConstructor(Class<?> clazz, Class<?>... argumentTypes) {
36+
try {
37+
return clazz.getConstructor(argumentTypes);
38+
} catch (NoSuchMethodException cause) {
39+
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
40+
}
41+
}
42+
43+
protected final Method getMethod(Class<?> clazz, String name, Class<?>... argumentTypes) {
44+
try {
45+
return clazz.getMethod(name, argumentTypes);
46+
} catch (NoSuchMethodException cause) {
47+
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
48+
}
49+
}
50+
51+
protected final <T> T newInstance(Constructor<T> constructor, Object... args) {
52+
try {
53+
return constructor.newInstance(args);
54+
} catch (InstantiationException | IllegalAccessException cause) {
55+
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
56+
} catch (InvocationTargetException cause) {
57+
throw new AssertionError(FAILED_TO_PARSE_ERROR_MESSAGE, cause.getCause());
58+
}
59+
}
60+
61+
protected Object invoke(Method method, Object targetObject, Object... args) {
62+
try {
63+
return method.invoke(targetObject, args);
64+
} catch (IllegalAccessException cause) {
65+
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
66+
} catch (InvocationTargetException cause) {
67+
throw new AssertionError(FAILED_TO_PARSE_ERROR_MESSAGE, cause.getCause());
68+
}
69+
}
70+
71+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2022 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.json.gson;
17+
18+
import java.lang.reflect.Method;
19+
20+
import com.diffplug.spotless.JarState;
21+
22+
class JsonElementWrapper extends GsonWrapperBase {
23+
24+
private final Class<?> clazz;
25+
private final Method isJsonObjectMethod;
26+
27+
JsonElementWrapper(JarState jarState) {
28+
this.clazz = loadClass(jarState.getClassLoader(), "com.google.gson.JsonElement");
29+
this.isJsonObjectMethod = getMethod(clazz, "isJsonObject");
30+
}
31+
32+
boolean isJsonObject(Object jsonElement) {
33+
return (boolean) invoke(isJsonObjectMethod, jsonElement);
34+
}
35+
36+
Class<?> getWrappedClass() {
37+
return clazz;
38+
}
39+
40+
}

0 commit comments

Comments
 (0)