View Javadoc
1   /*
2    * Copyright (c) 2020 bahlef.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v2.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v20.html
7    * Contributors:
8    * bahlef - initial API and implementation and/or initial documentation
9    */
10  package de.funfried.netbeans.plugins.external.formatter.json.jackson;
11  
12  import java.util.Objects;
13  
14  import org.apache.commons.lang3.StringUtils;
15  import org.netbeans.api.annotations.common.CheckForNull;
16  import org.netbeans.api.annotations.common.NonNull;
17  
18  import com.fasterxml.jackson.core.JsonFactory;
19  import com.fasterxml.jackson.core.JsonParser;
20  import com.fasterxml.jackson.core.JsonProcessingException;
21  import com.fasterxml.jackson.core.util.DefaultIndenter;
22  import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
23  import com.fasterxml.jackson.core.util.Separators;
24  import com.fasterxml.jackson.databind.ObjectMapper;
25  import com.fasterxml.jackson.databind.SerializationFeature;
26  
27  import de.funfried.netbeans.plugins.external.formatter.exceptions.FormattingFailedException;
28  
29  /**
30   * Wrapper class to the revelc.net formatter implementation.
31   *
32   * @author bahlef
33   */
34  public final class JacksonJsonFormatterWrapper {
35  	/**
36  	 * Package private Constructor for creating a new instance of {@link JacksonJsonFormatterWrapper}.
37  	 */
38  	JacksonJsonFormatterWrapper() {
39  	}
40  
41  	/**
42  	 * Formats the given {@code code} with the given configurations and returns
43  	 * the formatted code.
44  	 *
45  	 * @param code     the unformatted code
46  	 * @param lineFeed the line feed to use for formatting
47  	 * @param options  the {@link Options}
48  	 *
49  	 * @return the formatted code
50  	 */
51  	@CheckForNull
52  	public String format(String code, String lineFeed, Options options) throws FormattingFailedException {
53  		if (code == null) {
54  			return null;
55  		}
56  
57  		if (lineFeed == null) {
58  			lineFeed = System.getProperty("line.separator");
59  		}
60  
61  		if (options == null) {
62  			options = new Options();
63  		}
64  
65  		int indentSize = options.getIndentSize();
66  		int spacesPerTab = options.getSpacesPerTab();
67  		boolean spaceBeforeSeparator = options.isSpaceBeforeSeparator();
68  		boolean expandTabsToSpaces = options.isExpandTabsToSpaces();
69  
70  		String indentString;
71  		if (expandTabsToSpaces) {
72  			indentString = StringUtils.repeat(" ", indentSize);
73  		} else {
74  			indentString = StringUtils.repeat("\t", indentSize / spacesPerTab) + StringUtils.repeat(" ", indentSize % spacesPerTab);
75  		}
76  
77  		DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(indentString, lineFeed);
78  
79  		DefaultPrettyPrinter printer = new JacksonPrettyPrinter(spaceBeforeSeparator);
80  		printer.withSeparators(DefaultPrettyPrinter.DEFAULT_SEPARATORS);
81  		printer.indentObjectsWith(indenter);
82  		printer.indentArraysWith(indenter);
83  
84  		ObjectMapper objectMapper = new ObjectMapper();
85  		objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
86  		objectMapper.setDefaultPrettyPrinter(printer);
87  
88  		JsonFactory factory = objectMapper.getFactory();
89  		factory.enable(JsonParser.Feature.ALLOW_COMMENTS);
90  		factory.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
91  		factory.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
92  		factory.enable(JsonParser.Feature.ALLOW_YAML_COMMENTS);
93  
94  		return format(objectMapper, code);
95  	}
96  
97  	/**
98  	 * Formats the given {@code code} with the given {@link ObjectMapper} and returns
99  	 * the formatted code.
100 	 *
101 	 * @param objectMapper the {@link ObjectMapper}
102 	 * @param code         the unformatted code
103 	 *
104 	 * @return the formatted code
105 	 *
106 	 * @throws FormattingFailedException if the given code could not be formatted
107 	 *                                   with this formatter
108 	 */
109 	@CheckForNull
110 	private String format(ObjectMapper objectMapper, @NonNull String code) throws FormattingFailedException {
111 		String formattedCode;
112 		try {
113 			Object json = objectMapper.readValue(code, Object.class);
114 			formattedCode = objectMapper.writer().writeValueAsString(json);
115 			if (Objects.equals(code, formattedCode)) {
116 				return null;
117 			}
118 		} catch (JsonProcessingException ex) {
119 			throw new FormattingFailedException(ex);
120 		}
121 
122 		return formattedCode;
123 	}
124 
125 	/**
126 	 * Custom implementation of the {@link DefaultPrettyPrinter}.
127 	 */
128 	private static class JacksonPrettyPrinter extends DefaultPrettyPrinter {
129 		private static final long serialVersionUID = 1L;
130 
131 		private final boolean spaceBeforeSeparator;
132 
133 		/**
134 		 * Creates a new instance of {@link JacksonPrettyPrinter}.
135 		 *
136 		 * @param spaceBeforeSeparator {@code true} to add a space between the
137 		 *                             key and before the value separator
138 		 */
139 		public JacksonPrettyPrinter(boolean spaceBeforeSeparator) {
140 			super();
141 			this.spaceBeforeSeparator = spaceBeforeSeparator;
142 		}
143 
144 		/**
145 		 * {@inheritDoc}
146 		 */
147 		@Override
148 		public DefaultPrettyPrinter createInstance() {
149 			return new DefaultPrettyPrinter(this);
150 		}
151 
152 		/**
153 		 * {@inheritDoc}
154 		 */
155 		@Override
156 		public DefaultPrettyPrinter withSeparators(Separators separators) {
157 			this._separators = separators;
158 			this._objectFieldValueSeparatorWithSpaces = (this.spaceBeforeSeparator ? " " : "") + separators.getObjectFieldValueSeparator() + " ";
159 
160 			return this;
161 		}
162 	}
163 
164 	public static class Options {
165 		private int indentSize = 2;
166 
167 		private int spacesPerTab = 2;
168 
169 		private boolean spaceBeforeSeparator = false;
170 
171 		private boolean expandTabsToSpaces = true;
172 
173 		public int getIndentSize() {
174 			return indentSize;
175 		}
176 
177 		public void setIndentSize(int indentSize) {
178 			this.indentSize = indentSize;
179 		}
180 
181 		public int getSpacesPerTab() {
182 			return spacesPerTab;
183 		}
184 
185 		public void setSpacesPerTab(int spacesPerTab) {
186 			this.spacesPerTab = spacesPerTab;
187 		}
188 
189 		public boolean isSpaceBeforeSeparator() {
190 			return spaceBeforeSeparator;
191 		}
192 
193 		public void setSpaceBeforeSeparator(boolean spaceBeforeSeparator) {
194 			this.spaceBeforeSeparator = spaceBeforeSeparator;
195 		}
196 
197 		public boolean isExpandTabsToSpaces() {
198 			return expandTabsToSpaces;
199 		}
200 
201 		public void setExpandTabsToSpaces(boolean expandTabsToSpaces) {
202 			this.expandTabsToSpaces = expandTabsToSpaces;
203 		}
204 	}
205 }