Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON formatting using Gson #1125

Merged
merged 14 commits into from
Feb 15, 2022
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