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
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)).

## [2.22.2] - 2022-02-09
### Changed
Expand Down
4 changes: 4 additions & 0 deletions README.md
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.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.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/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
107 changes: 107 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/json/GsonStep.java
@@ -0,0 +1,107 @@
/*
* 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;

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;
import com.diffplug.spotless.json.gson.*;

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, " "));
}
}

}
@@ -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;

public class GsonBuilderWrapper extends GsonWrapperBase {

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

public 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");
}

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

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

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

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

}
48 changes: 48 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapper.java
@@ -0,0 +1,48 @@
/*
* 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;

public class GsonWrapper extends GsonWrapperBase {

private final Constructor<?> constructor;
private final Method fromJsonMethod;
private final Method toJsonMethod;

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

public Object createGson() {
return newInstance(constructor);
}

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

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

}
@@ -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;

public abstract class GsonWrapperBase {

public static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Gson; maybe you set an incompatible version?";
public 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());
}
}

}
@@ -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;

public class JsonElementWrapper extends GsonWrapperBase {

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

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

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

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

}