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.java.eclipse;
11  
12  import java.util.ArrayList;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Objects;
16  import java.util.SortedSet;
17  
18  import org.apache.commons.collections4.CollectionUtils;
19  import org.apache.commons.lang3.tuple.Pair;
20  import org.eclipse.jdt.core.ToolFactory;
21  import org.eclipse.jdt.core.formatter.CodeFormatter;
22  import org.eclipse.jface.text.Document;
23  import org.eclipse.jface.text.IDocument;
24  import org.eclipse.jface.text.IRegion;
25  import org.eclipse.jface.text.Region;
26  import org.eclipse.text.edits.TextEdit;
27  import org.netbeans.api.annotations.common.CheckForNull;
28  import org.netbeans.api.annotations.common.NonNull;
29  
30  import de.funfried.netbeans.plugins.external.formatter.exceptions.CannotLoadConfigurationException;
31  import de.funfried.netbeans.plugins.external.formatter.exceptions.ConfigReadException;
32  import de.funfried.netbeans.plugins.external.formatter.exceptions.FormattingFailedException;
33  import de.funfried.netbeans.plugins.external.formatter.exceptions.ProfileNotFoundException;
34  
35  /**
36   * Wrapper class to the Eclipse formatter implementation.
37   *
38   * @author bahlef
39   */
40  public final class EclipseJavaFormatterWrapper {
41  	/** Use to specify the kind of the code snippet to format. */
42  	private static final int FORMATTER_OPTS = CodeFormatter.K_COMPILATION_UNIT | CodeFormatter.F_INCLUDE_COMMENTS /* + CodeFormatter.K_CLASS_BODY_DECLARATIONS + CodeFormatter.K_STATEMENTS */;
43  
44  	/**
45  	 * Package private Constructor for creating a new instance of {@link EclipseJavaFormatterWrapper}.
46  	 */
47  	EclipseJavaFormatterWrapper() {
48  	}
49  
50  	/**
51  	 * Formats the given {@code code} with the given configurations and returns
52  	 * the formatted code.
53  	 *
54  	 * @param formatterFile the path to the formatter configuration file
55  	 * @param formatterProfile the name of the formatter configuration profile
56  	 * @param code the unformatted code
57  	 * @param lineFeed the line feed to use for formatting
58  	 * @param sourceLevel the source level to use for formatting
59  	 * @param changedElements a {@link SortedSet} containing ranges as {@link Pair} objects defining the offsets which should be formatted
60  	 *
61  	 * @return the formatted code
62  	 *
63  	 * @throws ConfigReadException if there is an issue parsing the formatter configuration
64  	 * @throws ProfileNotFoundException if the given {@code profile} could not be found
65  	 * @throws CannotLoadConfigurationException if there is any issue accessing or reading the formatter configuration
66  	 * @throws FormattingFailedException if the external formatter failed to format the given code
67  	 */
68  	@CheckForNull
69  	public String format(String formatterFile, String formatterProfile, String code, String lineFeed, String sourceLevel, SortedSet<Pair<Integer, Integer>> changedElements)
70  			throws ConfigReadException, ProfileNotFoundException, CannotLoadConfigurationException, FormattingFailedException {
71  		if (code == null) {
72  			return null;
73  		}
74  
75  		int codeLength = code.length();
76  
77  		List<IRegion> regions = new ArrayList<>();
78  		if (changedElements == null) {
79  			regions.add(new Region(0, codeLength));
80  		} else if (!CollectionUtils.isEmpty(changedElements)) {
81  			for (Pair<Integer, Integer> changedElement : changedElements) {
82  				int length = (changedElement.getRight() - changedElement.getLeft()) + 1;
83  				if (length > codeLength) {
84  					length = codeLength;
85  				}
86  
87  				regions.add(new Region(changedElement.getLeft(), length));
88  			}
89  		} else {
90  			// empty changed elements means nothing's left which can be formatted due to guarded sections
91  			return code;
92  		}
93  
94  		Map<String, String> allConfig = EclipseFormatterConfig.parseConfig(formatterFile, formatterProfile, sourceLevel);
95  
96  		CodeFormatter formatter = ToolFactory.createCodeFormatter(allConfig, ToolFactory.M_FORMAT_EXISTING);
97  		//see http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.jdt.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fjdt%2Fcore%2Fformatter%2FCodeFormatter.html&anchor=format(int,
98  
99  		return format(formatter, code, regions.toArray(IRegion[]::new), lineFeed);
100 	}
101 
102 	/**
103 	 * Formats the given {@code code} with the given configurations and returns
104 	 * the formatted code.
105 	 *
106 	 * @param formatter the {@link CodeFormatter}
107 	 * @param code the unformatted code
108 	 * @param lineFeed the line feed to use for formatting
109 	 * @param regions an array containing {@link IRegion} objects defining the offsets which should be formatted
110 	 *
111 	 * @return the formatted code
112 	 *
113 	 * @throws FormattingFailedException if the external formatter failed to format the given code
114 	 * @throws IllegalArgumentException if the given {@code regions} are invalid
115 	 */
116 	@CheckForNull
117 	private String format(@NonNull CodeFormatter formatter, @NonNull String code, @NonNull IRegion[] regions, String lineFeed) throws FormattingFailedException, IllegalArgumentException {
118 		String formattedCode = null;
119 
120 		try {
121 			TextEdit te = formatter.format(FORMATTER_OPTS, code, regions, 0, lineFeed);
122 			if (te != null && te.getChildrenSize() > 0) {
123 				IDocument dc = new Document(code);
124 				te.apply(dc);
125 
126 				formattedCode = dc.get();
127 
128 				if (Objects.equals(code, formattedCode)) {
129 					return null;
130 				}
131 			}
132 		} catch (IllegalArgumentException ex) {
133 			throw ex;
134 		} catch (Exception ex) {
135 			throw new FormattingFailedException(ex);
136 		}
137 
138 		return formattedCode;
139 	}
140 }