Skip to content

Commit

Permalink
JSON formatting using Gson (#1125)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Feb 15, 2022
2 parents 7331070 + 2b249b4 commit 4333885
Show file tree
Hide file tree
Showing 26 changed files with 2,195 additions and 89 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).

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

### Changed

Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ It's easy to build such a function, but there are some gotchas and lots of integ
## Current feature matrix

<!---freshmark matrix
function lib(className) { return '| [`' + className + '`](lib/src/main/java/com/diffplug/spotless/' + className.replace('.', '/') + '.java) | ' }
function extra(className) { return '| [`' + className + '`](lib-extra/src/main/java/com/diffplug/spotless/extra/' + className.replace('.', '/') + '.java) | ' }
function lib(className) { return '| [`' + className + '`](lib/src/main/java/com/diffplug/spotless/' + className.replaceAll('\\.', '/') + '.java) | ' }
function extra(className) { return '| [`' + className + '`](lib-extra/src/main/java/com/diffplug/spotless/extra/' + className.replaceAll('\\.', '/') + '.java) | ' }
// | GRADLE | MAVEN | SBT | (new) |
output = [
Expand Down Expand Up @@ -61,6 +61,8 @@ lib('java.ImportOrderStep') +'{{yes}} | {{yes}}
lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('json.gson.GsonStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
lib('json.JsonSimpleStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('kotlin.DiktatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
Expand Down Expand Up @@ -102,6 +104,8 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
| [`java.PalantirJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`json.gson.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
| [`json.JsonSimpleStep`](lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
| [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2022 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless.json.gson;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import com.diffplug.spotless.JarState;

class GsonBuilderWrapper extends GsonWrapperBase {

private final Constructor<?> constructor;
private final Method serializeNullsMethod;
private final Method disableHtmlEscapingMethod;
private final Method createMethod;

GsonBuilderWrapper(JarState jarState) {
Class<?> clazz = loadClass(jarState.getClassLoader(), "com.google.gson.GsonBuilder");
this.constructor = getConstructor(clazz);
this.serializeNullsMethod = getMethod(clazz, "serializeNulls");
this.disableHtmlEscapingMethod = getMethod(clazz, "disableHtmlEscaping");
this.createMethod = getMethod(clazz, "create");
}

Object createGsonBuilder() {
return newInstance(constructor);
}

Object serializeNulls(Object gsonBuilder) {
return invoke(serializeNullsMethod, gsonBuilder);
}

Object disableHtmlEscaping(Object gsonBuilder) {
return invoke(disableHtmlEscapingMethod, gsonBuilder);
}

Object create(Object gsonBuilder) {
return invoke(createMethod, gsonBuilder);
}

}
106 changes: 106 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2022 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless.json.gson;

import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Objects;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.JarState;
import com.diffplug.spotless.Provisioner;

public class GsonStep {
private static final String MAVEN_COORDINATES = "com.google.code.gson:gson";

public static FormatterStep create(int indentSpaces, boolean sortByKeys, boolean escapeHtml, String version, Provisioner provisioner) {
Objects.requireNonNull(provisioner, "provisioner cannot be null");
return FormatterStep.createLazy("gson", () -> new State(indentSpaces, sortByKeys, escapeHtml, version, provisioner), State::toFormatter);
}

private static final class State implements Serializable {
private static final long serialVersionUID = -1493479043249379485L;

private final int indentSpaces;
private final boolean sortByKeys;
private final boolean escapeHtml;
private final JarState jarState;

private State(int indentSpaces, boolean sortByKeys, boolean escapeHtml, String version, Provisioner provisioner) throws IOException {
this.indentSpaces = indentSpaces;
this.sortByKeys = sortByKeys;
this.escapeHtml = escapeHtml;
this.jarState = JarState.from(MAVEN_COORDINATES + ":" + version, provisioner);
}

FormatterFunc toFormatter() {
JsonWriterWrapper jsonWriterWrapper = new JsonWriterWrapper(jarState);
JsonElementWrapper jsonElementWrapper = new JsonElementWrapper(jarState);
JsonObjectWrapper jsonObjectWrapper = new JsonObjectWrapper(jarState, jsonElementWrapper);
GsonBuilderWrapper gsonBuilderWrapper = new GsonBuilderWrapper(jarState);
GsonWrapper gsonWrapper = new GsonWrapper(jarState, jsonElementWrapper, jsonWriterWrapper);

Object gsonBuilder = gsonBuilderWrapper.serializeNulls(gsonBuilderWrapper.createGsonBuilder());
if (!escapeHtml) {
gsonBuilder = gsonBuilderWrapper.disableHtmlEscaping(gsonBuilder);
}
Object gson = gsonBuilderWrapper.create(gsonBuilder);

return inputString -> {
String result;
if (inputString.isEmpty()) {
result = "";
} else {
Object jsonElement = gsonWrapper.fromJson(gson, inputString, jsonElementWrapper.getWrappedClass());
if (jsonElement == null) {
throw new AssertionError(GsonWrapperBase.FAILED_TO_PARSE_ERROR_MESSAGE);
}
if (sortByKeys && jsonElementWrapper.isJsonObject(jsonElement)) {
jsonElement = sortByKeys(jsonObjectWrapper, jsonElementWrapper, jsonElement);
}
try (StringWriter stringWriter = new StringWriter()) {
Object jsonWriter = jsonWriterWrapper.createJsonWriter(stringWriter);
jsonWriterWrapper.setIndent(jsonWriter, generateIndent(indentSpaces));
gsonWrapper.toJson(gson, jsonElement, jsonWriter);
result = stringWriter + "\n";
}
}
return result;
};
}

private Object sortByKeys(JsonObjectWrapper jsonObjectWrapper, JsonElementWrapper jsonElementWrapper, Object jsonObject) {
Object result = jsonObjectWrapper.createJsonObject();
jsonObjectWrapper.keySet(jsonObject).stream().sorted()
.forEach(key -> {
Object element = jsonObjectWrapper.get(jsonObject, key);
if (jsonElementWrapper.isJsonObject(element)) {
element = sortByKeys(jsonObjectWrapper, jsonElementWrapper, element);
}
jsonObjectWrapper.add(result, key, element);
});
return result;
}

private String generateIndent(int indentSpaces) {
return String.join("", Collections.nCopies(indentSpaces, " "));
}
}

}
41 changes: 41 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2022 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless.json.gson;

import java.lang.reflect.Method;

import com.diffplug.spotless.JarState;

class GsonWrapper extends GsonWrapperBase {

private final Method fromJsonMethod;
private final Method toJsonMethod;

GsonWrapper(JarState jarState, JsonElementWrapper jsonElementWrapper, JsonWriterWrapper jsonWriterWrapper) {
Class<?> clazz = loadClass(jarState.getClassLoader(), "com.google.gson.Gson");
this.fromJsonMethod = getMethod(clazz, "fromJson", String.class, Class.class);
this.toJsonMethod = getMethod(clazz, "toJson", jsonElementWrapper.getWrappedClass(), jsonWriterWrapper.getWrappedClass());
}

Object fromJson(Object gson, String json, Class<?> type) {
return invoke(fromJsonMethod, gson, json, type);
}

void toJson(Object gson, Object jsonElement, Object jsonWriter) {
invoke(toJsonMethod, gson, jsonElement, jsonWriter);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2022 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless.json.gson;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

abstract class GsonWrapperBase {

static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Gson; maybe you set an incompatible version?";
static final String FAILED_TO_PARSE_ERROR_MESSAGE = "Unable to format JSON";

protected final Class<?> loadClass(ClassLoader classLoader, String className) {
try {
return classLoader.loadClass(className);
} catch (ClassNotFoundException cause) {
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
}
}

protected final Constructor<?> getConstructor(Class<?> clazz, Class<?>... argumentTypes) {
try {
return clazz.getConstructor(argumentTypes);
} catch (NoSuchMethodException cause) {
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
}
}

protected final Method getMethod(Class<?> clazz, String name, Class<?>... argumentTypes) {
try {
return clazz.getMethod(name, argumentTypes);
} catch (NoSuchMethodException cause) {
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
}
}

protected final <T> T newInstance(Constructor<T> constructor, Object... args) {
try {
return constructor.newInstance(args);
} catch (InstantiationException | IllegalAccessException cause) {
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
} catch (InvocationTargetException cause) {
throw new AssertionError(FAILED_TO_PARSE_ERROR_MESSAGE, cause.getCause());
}
}

protected Object invoke(Method method, Object targetObject, Object... args) {
try {
return method.invoke(targetObject, args);
} catch (IllegalAccessException cause) {
throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
} catch (InvocationTargetException cause) {
throw new AssertionError(FAILED_TO_PARSE_ERROR_MESSAGE, cause.getCause());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2022 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless.json.gson;

import java.lang.reflect.Method;

import com.diffplug.spotless.JarState;

class JsonElementWrapper extends GsonWrapperBase {

private final Class<?> clazz;
private final Method isJsonObjectMethod;

JsonElementWrapper(JarState jarState) {
this.clazz = loadClass(jarState.getClassLoader(), "com.google.gson.JsonElement");
this.isJsonObjectMethod = getMethod(clazz, "isJsonObject");
}

boolean isJsonObject(Object jsonElement) {
return (boolean) invoke(isJsonObjectMethod, jsonElement);
}

Class<?> getWrappedClass() {
return clazz;
}

}
Loading

0 comments on commit 4333885

Please sign in to comment.