1 /*******************************************************************************
2 * Copyright (c) 2015 LegSem.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser Public License v2.1
5 * which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
7 *
8 * Contributors:
9 * LegSem - initial API and implementation
10 ******************************************************************************/
11 package com.legstar.cobol.gen;
12
13 import java.io.IOException;
14 import java.io.Writer;
15 import java.util.Arrays;
16
17 import org.antlr.stringtemplate.AutoIndentWriter;
18 import org.apache.commons.lang.StringUtils;
19
20 /**
21 * Writes statements that fit into 72 columns.
22 * <p/>
23 * This does not use the AutoIndentWriter indent capabilities so all indents
24 * from templates are ignored.
25 *
26 */
27 public class Copybook72ColWriter extends AutoIndentWriter {
28
29 /** Column where statements end. */
30 public static final int STATEMENTS_LAST_COLUMN = 72;
31
32 /** Sentence continuation line (start at column 12, area B). */
33 public static final String LINE_CONTINUE_SENTENCE = " ";
34
35 /** New line and literal continuation (start at column 12, area B). */
36 public static final String LINE_CONTINUE_LITERAL = " - ";
37
38 /**
39 * Alphanumeric literals are started and end with the same delimiter (either
40 * quote or apost) but might contain escaped delimiters which are sequences
41 * of double delimiters. THE MAYBE-CLOSED status corresponds to the case
42 * where we have parsed a closing delimiter but are not sure yet if it will
43 * not be followed by another delimiter which would mean the literal is not
44 * closed yet.
45 */
46 private enum AlphanumLiteralStatus {
47 NOT_STARTED, STARTED, MAYBE_CLOSED
48 };
49
50 /** Current status when alphanumeric literals are encountered. */
51 private AlphanumLiteralStatus alphanumLiteralStatus = AlphanumLiteralStatus.NOT_STARTED;
52
53 /**
54 * When an alphanumeric literal is started, this will hold the delimiter
55 * character that started the literal.
56 */
57 private char alphanumLiteralDelimiter;
58
59 /**
60 * Keeps track of the indentation so that continued sentences can have the
61 * same indentation.
62 */
63 private int indentPos;
64
65 /**
66 * Used to count tokens when we need to determine where a COBOL data item
67 * description begins.
68 */
69 private int tokenCounter = -1;
70
71 /**
72 * Non alphanumeric literals are all considered keywords (sequences of
73 * non-space characters)
74 */
75 private StringBuilder keyword = new StringBuilder();
76
77 public Copybook72ColWriter(Writer out) {
78 super(out);
79 }
80
81 /**
82 * Almost same code as AutoIndentWriter#write(String str) but prevents code
83 * from spilling beyond max column.
84 * <p/>
85 * Also fixes a bug in StringTemplate where the charPosition was incorrect
86 * on Windows following a \r\n sequence.
87 *
88 * */
89 public int write(String str) throws IOException {
90 trackIndentation(str);
91 int n = 0;
92 for (int i = 0; i < str.length(); i++) {
93 char c = str.charAt(i);
94 // found \n or \r\n newline?
95 if (c == '\r' || c == '\n') {
96 atStartOfLine = true;
97 charPosition = 0;
98 writeKeyword();
99 n += newline.length();
100 out.write(newline);
101 // skip an extra char upon \r\n
102 if ((c == '\r' && (i + 1) < str.length() && str.charAt(i + 1) == '\n')) {
103 i++; // loop iteration i++ takes care of skipping 2nd char
104 }
105 continue;
106 }
107 // normal character
108 // check to see if we are at the start of a line
109 if (atStartOfLine) {
110 atStartOfLine = false;
111 }
112 // Keep track of the status for alphanumeric literals
113 trackAlphanumLiteral(c);
114
115 // if we are about to write past column 72, break using continuation
116 // if necessary
117 if (charPosition == STATEMENTS_LAST_COLUMN) {
118 if (alphanumLiteralStatus == AlphanumLiteralStatus.NOT_STARTED) {
119 n += continueKeyword(str, indentPos);
120 } else {
121 n += continueAlphaLiteral();
122 }
123 }
124 n++;
125 writeKeywordOrAlphaLiteral(c);
126 charPosition++;
127
128 }
129 writeKeyword();
130 return n;
131 }
132
133 /**
134 * When a white space is encountered, we consider a keyword as delimited and
135 * write it out. On non space characters, if we are not in the middle of an
136 * alphanumeric literal, we consider the character as part of a keyword.
137 *
138 * @param c the character being printed
139 * @throws IOException if character cannot be printed
140 */
141 protected void writeKeywordOrAlphaLiteral(char c) throws IOException {
142 if (c == ' ') {
143 writeKeyword();
144 out.write(c);
145 } else {
146 if (alphanumLiteralStatus == AlphanumLiteralStatus.NOT_STARTED) {
147 keyword.append(c);
148 } else {
149 writeKeyword();
150 out.write(c);
151 }
152 }
153 }
154
155 /**
156 * Print a keyword.
157 *
158 * @throws IOException if writing fails
159 */
160 protected void writeKeyword() throws IOException {
161 if (keyword.length() > 0) {
162 out.write(keyword.toString().toCharArray());
163 keyword = new StringBuilder();
164 }
165 }
166
167 /**
168 * In order to indent properly continued sentences (not continued literals),
169 * we keep track of the character position of the COBOL name which follows
170 * the COBOL level number. This is tied to the StringTemplate where we
171 * assume this:
172 *
173 * <pre>
174 * $cobolDataItem.levelNumber;format="depth"$$cobolDataItem.levelNumber;format="level"$ $cobolDataItem.cobolName$
175 * </pre>
176 *
177 * @param str the string to be written
178 */
179 protected void trackIndentation(String str) {
180 if (charPosition == 0) {
181 tokenCounter = 3;
182 }
183 if (tokenCounter == 0) {
184 indentPos = charPosition;
185 }
186 switch (tokenCounter) {
187 case 3:
188 if (StringUtils.isBlank(str)) {
189 tokenCounter = 2;
190 } else {
191 tokenCounter = -1;
192 }
193 break;
194 case 2:
195 if (str.matches("\\d\\d")) {
196 tokenCounter = 1;
197 } else {
198 tokenCounter = -1;
199 }
200 break;
201 case 1:
202 if (StringUtils.isBlank(str)) {
203 tokenCounter = 0;
204 } else {
205 tokenCounter = -1;
206 }
207 break;
208 default:
209 tokenCounter = -1;
210 }
211 }
212
213 /**
214 * Detect the start and close of alphanumeric literals.
215 *
216 * @param c the current parsed character
217 */
218 protected void trackAlphanumLiteral(char c) {
219 if (c == '\'' || c == '\"') {
220 switch (alphanumLiteralStatus) {
221 case NOT_STARTED:
222 alphanumLiteralStatus = AlphanumLiteralStatus.STARTED;
223 alphanumLiteralDelimiter = c;
224 break;
225 case STARTED:
226 if (c == alphanumLiteralDelimiter) {
227 alphanumLiteralStatus = AlphanumLiteralStatus.MAYBE_CLOSED;
228 }
229 break;
230 case MAYBE_CLOSED:
231 if (c == alphanumLiteralDelimiter) {
232 alphanumLiteralStatus = AlphanumLiteralStatus.STARTED;
233 } else {
234 alphanumLiteralStatus = AlphanumLiteralStatus.NOT_STARTED;
235 }
236
237 }
238 } else {
239 if (alphanumLiteralStatus
240 .equals(AlphanumLiteralStatus.MAYBE_CLOSED)) {
241 alphanumLiteralStatus = AlphanumLiteralStatus.NOT_STARTED;
242 }
243 }
244 }
245
246 /**
247 * Literals that are about to exceed column 72 need to be continued on the
248 * next line.
249 * <p/>
250 * Alphanumeric literals are special because the continued line needs to
251 * start with the same delimiter the alphanumeric literal is using (either
252 * quote or apost).
253 *
254 * @return the number of characters written
255 * @throws IOException if writing fails
256 */
257 protected int continueAlphaLiteral() throws IOException {
258 String continueLiteral = newline + LINE_CONTINUE_LITERAL;
259 out.write(continueLiteral);
260 charPosition = LINE_CONTINUE_LITERAL.length();
261 int n = continueLiteral.length();
262 if (alphanumLiteralStatus.equals(AlphanumLiteralStatus.STARTED)) {
263 out.write(alphanumLiteralDelimiter);
264 charPosition++;
265 n++;
266 }
267 return n;
268 }
269
270 /**
271 * Put a keyword on the next line (otherwise would extend past column 72).
272 *
273 * @param str the current string (needed to detect leading space and adjust
274 * indentation)
275 * @param indentPos the indentation position
276 * @return the number of characters written
277 * @throws IOException if writing fails
278 */
279 private int continueKeyword(String str, int indentPos) throws IOException {
280 return wrap(str, indentPos);
281 }
282
283 /**
284 * Insert the wrap sequence.
285 *
286 * @param str the string to be written (this is used to reduce indent in
287 * case of leading spaces)
288 * @param indentPos the indent position
289 * @return the number of characters written
290 * @throws IOException if writing fails
291 */
292 protected int wrap(String str, int indentPos) throws IOException {
293 int n = 0;
294 String wrap = getWrap(str, indentPos);
295 // Walk wrap string and look for A\nB. Spit out A\n
296 // then spit out B.
297 for (int i = 0; i < wrap.length(); i++) {
298 char c = wrap.charAt(i);
299 if (c == '\n') {
300 n++;
301 out.write(newline);
302 charPosition = 0;
303 // continue writing any chars out
304 } else { // write A or B part
305 n++;
306 out.write(c);
307 charPosition++;
308 }
309 }
310 return n;
311 }
312
313 /**
314 * Get the wrap characters sequence including the indent for the continued
315 * line.
316 * <p/>
317 * If the string to be written starts with spaces, we reduce the indent so
318 * that the first non space character appears at the indent position.
319 *
320 * @param str the string to be printed
321 * @param indentPos the indent position of the continued line if line is
322 * wrapped
323 * @return the wrap characters sequence including the indent for the
324 * continued line
325 */
326 protected String getWrap(String str, int indentPos) {
327 int leadingSpaces = 0;
328 for (int i = 0; i < str.length(); i++) {
329 if (str.charAt(i) == ' ') {
330 leadingSpaces++;
331 } else {
332 break;
333 }
334 }
335 int indent = indentPos - leadingSpaces;
336 if (indent < 1 || indent + str.length() > STATEMENTS_LAST_COLUMN) {
337 return "\n" + LINE_CONTINUE_SENTENCE;
338 }
339 char[] chars = new char[indent];
340 Arrays.fill(chars, ' ');
341 return "\n" + new String(chars);
342 }
343
344 }