View Javadoc

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 }