blob: 8e305b3b1c7b429a3d4eae14df34e2c39ee4c595 [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.eis.cobol;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;
import org.eclipse.persistence.internal.eis.cobol.helper.Helper;
/**
* <p>
* <b>Purpose</b>: This class is a parser for Cobol Copy books. It will take a stream as
* an argument in its constructor and parse the stream when <code>parse()</code> is called.
* The <code>parse()</code> method returns a <code>Vector</code> of <code>RecordMetaData</code>
* one for each "01" level record description in the stream.
*/
public class CopyBookParser {
/** the maximum nested levels allowed, or in other words, the highest level number allowed */
private static final int maximumNestingLevels = 50;
/** the line the parser is currently on */
private String currentLine;
/** the current line number */
private int currentLineNumber;
/**
* Default constructor
*/
public CopyBookParser() {
currentLine = null;
currentLineNumber = 0;
}
/**
* This method is the primary public api for this class, it takes an <code>InputStream</code>
* as an argument then parses this stream looking for "01" level record entries. It returns
* a <code>Vector</code> containing <code>RecordMetaData</code> for each "01" record defintion
* encountered in the stream.
*/
public Vector<RecordMetaData> parse(InputStream stream) throws Exception {
Vector<RecordMetaData> records;
currentLineNumber = 0;
//read file and prepare for parsing
try {
//FileInputStream fileInput = new FileInputStream(myParseFile);
byte[] bytes = new byte[stream.available()];
stream.read(bytes);
String copyBookString = new String(bytes);
//parse through string and determine hierarchy and field sizes
records = buildStructure(copyBookString);
//calculate the field offsets from the records
for (int i = 0; i < records.size(); i++) {
setOffsetsForComposite(records.elementAt(i), 0);
}
} catch (IOException exception) {
throw CopyBookParseException.ioException(exception);
}
return records;
}
/**
* This is a private method that handles the actual parsing of the string tokens, it scans
* each line looking for a "01" level record definition, when it is encountered, it builds the
* hierarchical structure for the <code>RecordMetaData</code>
*/
private Vector<RecordMetaData> buildStructure(String fileString) throws Exception {
Vector<RecordMetaData> records = new Vector<>();
StringTokenizer lineTokenizer = new StringTokenizer(fileString, System.getProperty("line.separator"), false);
RecordMetaData record = new RecordMetaData();
Vector<String> recordLines = new Vector<>();
Vector<Integer> lineNums = new Vector<>();
//first pass removes all non-record data and brings all lines together
while (lineTokenizer.hasMoreTokens() && !"procedure division.".equalsIgnoreCase(currentLine)) {
currentLine = lineTokenizer.nextToken();
currentLineNumber++;
if (!currentLine.trim().startsWith("*") && (currentLine.trim().length() > 0)) {
StringTokenizer lineTokens = new StringTokenizer(currentLine);
String firstToken = lineTokens.nextToken();
if (firstToken.endsWith(".")) {
firstToken = firstToken.substring(0, currentLine.lastIndexOf('.'));
}
Integer levelNumber = Helper.integerFromString(firstToken);
if ((levelNumber != null) && (levelNumber < 50)) {
//assure we've gotten entire line
while (!currentLine.trim().endsWith(".")) {
currentLine += lineTokenizer.nextToken();
currentLineNumber++;
}
currentLine = currentLine.substring(0, currentLine.lastIndexOf('.'));
recordLines.addElement(currentLine);
lineNums.addElement(currentLineNumber);
}
}
}
//second pass will build the structure
int nestingLevel = maximumNestingLevels;
Stack<CompositeObject> parents = new Stack<>();
Hashtable<Object, Integer> parentsToLevels = new Hashtable<>();
Enumeration<String> recordsEnum = recordLines.elements();
Enumeration<Integer> recordLineNums = lineNums.elements();
while (recordsEnum.hasMoreElements()) {
currentLine = recordsEnum.nextElement();
currentLineNumber = recordLineNums.nextElement();
StringTokenizer lineTokens = new StringTokenizer(currentLine);
if (lineTokens.hasMoreTokens()) {
String firstToken = lineTokens.nextToken();
Integer levelNumber = Helper.integerFromString(firstToken);
Object component;
//process record
if (levelNumber == 1) {
nestingLevel = maximumNestingLevels;
parents = new Stack<>();
parentsToLevels = new Hashtable<>();
component = buildRecord(lineTokens);
record = (RecordMetaData)component;
records.addElement(record);
}
//process subordinate field
else if (levelNumber >= nestingLevel) {
component = buildField(lineTokens);
parents.peek().addField((FieldMetaData)component);
}
//field is no longer subordinate skip back to original level
else {
while (parentsToLevels.get(parents.peek()) >= levelNumber) {
parents.pop();
}
component = buildField(lineTokens);
parents.peek().addField((FieldMetaData)component);
}
nestingLevel = levelNumber;
if (component instanceof FieldMetaData) {
((FieldMetaData)component).setRecord(record);
}
if (component instanceof CompositeObject) {
parents.push((CompositeObject) component);
parentsToLevels.put(component, levelNumber);
}
}
}
return records;
}
/**
* This method cascades down through the records built in the <code>buildStructure</code>
* method setting the offsets for the fields, this must be done now, because the sizes
* for the fields must allready be determined.
*/
private void setOffsetsForComposite(CompositeObject object, int offset) {
int currentOffset = offset;
int previousFieldSize = 0;
Vector<FieldMetaData> fields = object.getFields();
Enumeration<FieldMetaData> fieldEnum = fields.elements();
FieldMetaData previousField = null;
//loop through fields setting their offsets and redefines if it applies
while (fieldEnum.hasMoreElements()) {
FieldMetaData field = fieldEnum.nextElement();
//if its a redefine, must first see if it larger and reset offset accordingly
if (field.isFieldRedefine()) {
field.setFieldRedefined(previousField);
if (field instanceof CompositeObject) {
setOffsetsForComposite((CompositeObject)field, previousField.getOffset());
}
field.setOffset(previousField.getOffset());
if (previousFieldSize < field.getSize()) {
currentOffset += (field.getSize() - previousFieldSize);
previousFieldSize = field.getSize();
}
} else {
if (field instanceof CompositeObject) {
setOffsetsForComposite((CompositeObject)field, currentOffset);
}
field.setOffset(currentOffset);
currentOffset += field.getSize();
previousField = field;
previousFieldSize = field.getSize();
}
}
}
/**
* This method processes "01" level lines building a <code>RecordMetaData</code> and
* returning it.
*/
private RecordMetaData buildRecord(StringTokenizer lineTokens) {
RecordMetaData record;
if (lineTokens.hasMoreTokens()) {
String recordName = lineTokens.nextToken();
record = new RecordMetaData(recordName);
return record;
} else {
throw invalidCopyBookException("The record has no name.");
}
}
/**
* This method handles all "02" through "49" level lines building a field from the line.
* it returns <code>FieldMetaData</code> for either composite or elementary fields
* appropriately.
*/
private FieldMetaData buildField(StringTokenizer lineTokens) throws Exception {
FieldMetaData field;
String fieldName;
boolean redefine = false;
int arraySize = -1;
String[] tokens = new String[lineTokens.countTokens()];
int index = 0;
String dependentField = "";
//build token array first so that backtracking can be done if necessary
while (lineTokens.hasMoreTokens()) {
tokens[index++] = lineTokens.nextToken();
}
index = 0;
if (tokens.length > 0) {
if (isKeyWord(tokens[index])) {
fieldName = "filler";
} else {
fieldName = tokens[index];
}
}
//composite without name
else {
field = new CompositeFieldMetaData();
field.setName("filler");
return field;
}
//find the pic statement
for (int i = 0;
(i < tokens.length) && !tokens[index].equalsIgnoreCase("pic") && !tokens[index].equalsIgnoreCase("picture");
i++) {
if (tokens[index].equalsIgnoreCase("redefines")) {
redefine = true;
}
if (tokens[index].equalsIgnoreCase("occurs")) {
arraySize = handleOccursStatement(tokens, ++index);
index--;
}
if (tokens[index].equalsIgnoreCase("depending")) {
dependentField = handleDependeningStatement(tokens, ++index);
index--;
}
index++;
}
//elementary field
if ((index < tokens.length) && (tokens[index].equalsIgnoreCase("pic") || tokens[index].equalsIgnoreCase("picture"))) {
field = buildElementaryField(fieldName, tokens, index);
}
//composite field
else {
field = new CompositeFieldMetaData();
field.setName(fieldName);
}
field.setIsFieldRedefine(redefine);
field.setArraySize(arraySize);
field.setDependentFieldName(dependentField);
return field;
}
/**
* This method handles the "depending" statement returning the name of the field that
* this field depends on.
*/
private String handleDependeningStatement(String[] tokens, int index) {
String fieldName = null;
try {
if (index < tokens.length) {
fieldName = tokens[index];
if (fieldName.equalsIgnoreCase("on")) {
fieldName = tokens[++index];
}
}
//there was no name following the depending clause or on clause
} catch (ArrayIndexOutOfBoundsException exception) {
throw invalidCopyBookException("There was no field name following the depending clause.", exception);
}
return fieldName;
}
/**
* This method handles the "occurs" statment, returning the maximum number of times this field
* should occur.
*/
private int handleOccursStatement(String[] tokens, int index) throws Exception {
try {
int size = 0;
if (index < tokens.length) {
size = Helper.integerFromString(tokens[index]);
if (tokens[++index].equalsIgnoreCase("to")) {
int newSize = Helper.integerFromString(tokens[++index]);
if (size > 0) {
newSize = newSize - size;
}
size = newSize;
}
}
if (size < 1) {
throw invalidCopyBookException("Must occur at least once.");
}
return size;
//there was no integer following the occurs statment or one after the to statement
} catch (ArrayIndexOutOfBoundsException exception) {
throw invalidCopyBookException("Occurs clause must be folowed by and integer.", exception);
}
}
/**
* This method build elementary fields getting the pertinent size
* information from the pic statement
*/
private FieldMetaData buildElementaryField(String fieldName, String[] tokens, int index) throws Exception {
FieldMetaData field = new ElementaryFieldMetaData();
String picStatment;
int size = 0;
try {
field.setName(fieldName);
picStatment = tokens[++index];
if (picStatment.equalsIgnoreCase("is")) {
picStatment = tokens[++index];
}
//either the pic statement didn't follow the pic clause or didn't follow the is clause
} catch (ArrayIndexOutOfBoundsException exception) {
throw invalidCopyBookException("Picture clause must be followed by a pic statement.", exception);
}
//set type and calculate size
if (picStatment.toUpperCase().startsWith("A")) {
field.setType(FieldMetaData.ALPHABETIC);
size = calculateSizeOfAlphaNumeric(picStatment, field);
} else if (picStatment.toUpperCase().startsWith("X")) {
field.setType(FieldMetaData.ALPHA_NUMERIC);
size = calculateSizeOfAlphaNumeric(picStatment, field);
} else if (picStatment.toUpperCase().startsWith("9") || picStatment.toUpperCase().startsWith("V") || picStatment.toUpperCase().startsWith("Z") || picStatment.startsWith("+") || picStatment.startsWith("-") || picStatment.toUpperCase().startsWith("S")) {
field.setType(FieldMetaData.NUMERIC);
size = calculateSizeOfNumeric(picStatment, tokens, index, field);
}
field.setSize(size);
return field;
}
/**
* This method calculates the size of alphanumeric pic statments that begin with "A" or "X"
*/
private int calculateSizeOfAlphaNumeric(String picStatement, FieldMetaData field) throws Exception {
//parse through statement and determine size
char[] picChars = picStatement.toCharArray();
int length = picChars.length;
int index = 0;
int size = 0;
while (index < length) {
char currentChar = picChars[index];
switch (currentChar) {
case '(':
StringBuilder number = new StringBuilder();
index++;
size--;
currentChar = picChars[index];
while ((index < length) && (currentChar != ')')) {
number.append(currentChar);
index++;
currentChar = picChars[index];
}
try {
int value = Integer.parseInt(number.toString());
size += value;
} catch (NumberFormatException exception) {
throw invalidCopyBookException("In pic statement a valid integer must be enclosed by the parenthesis.", exception);
}
if (currentChar == ')') {
index++;
} else {
throw invalidCopyBookException("An open parenthesis must be followed by a close parenthesis.");
}
break;
case 'p':
case 'P':
case 'Z':
case 'z':
case '+':
case '-':
case 'x':
case 'X':
case 'a':
case 'A':
case '9':
size++;
index++;
break;
case 'V':
case 'v':
field.setDecimalPosition(size);
index++;
break;
case 'S':
case 's':
index++;
break;
case '.':
if (index == (length - 1)) {
return size;
} else {
index++;
}
size++;
break;
default:
throw invalidCopyBookException("Invalid character: " + currentChar + " in pic statement.");
}
}
return size;
}
/**
* This method calculates the size of numeric fields in which the pic statement begins with "9"
*/
private int calculateSizeOfNumeric(String picStatement, String[] tokens, int index, FieldMetaData field) throws Exception {
int size = 0;
int digits = 0;
if (picStatement.toUpperCase().startsWith("S")) {
field.setIsSigned(true);
}
for (int i = index; i < tokens.length; i++, index++) {
String nextStatement = tokens[i];
//determine if there is usage other than display default
if (nextStatement.equalsIgnoreCase("comp") || nextStatement.equalsIgnoreCase("computational")) {
digits = calculateSizeOfAlphaNumeric(picStatement, field);
field.setType(FieldMetaData.BINARY);
if (digits < 3) {
size = 1;
} else if (digits < 5) {
size = 2;
} else if (digits < 8) {
size = 3;
} else if (digits < 10) {
size = 4;
} else if (digits < 13) {
size = 5;
} else if (digits < 15) {
size = 6;
} else if (digits < 17) {
size = 7;
} else if (digits < 20) {
size = 8;
}
if (field.isSigned() && ((digits == 7) || (digits == 12) || (digits == 19))) {
size++;
}
} else if (nextStatement.equalsIgnoreCase("comp-2") || nextStatement.equalsIgnoreCase("computational-2")) {
//size of float, should not be encountered
field.setType(FieldMetaData.MANTISSA);
return 4;
} else if (nextStatement.equalsIgnoreCase("comp-3") || nextStatement.equalsIgnoreCase("computational-3") || nextStatement.equalsIgnoreCase("packed-decimal")) {
int tempSize = calculateSizeOfAlphaNumeric(picStatement, field);
field.setType(FieldMetaData.PACKED_DECIMAL);
size = (tempSize + 1) / 2;
if (((tempSize + 1) % 2) > 0) {
size++;
}
} else if (nextStatement.equalsIgnoreCase("seperate")) {
size = calculateSizeOfAlphaNumeric(picStatement, field);
size++;
} else {
field.setType(FieldMetaData.ALPHA_NUMERIC);
size = calculateSizeOfAlphaNumeric(picStatement, field);
}
}
return size;
}
/**
* This method returns true if the string word equals one of a list of keywords.
*/
private boolean isKeyWord(String word) {
String[] keyWords = { "pic", "picture", "redefines", "blank", "external", "global", "justified", "just", "occurs" };
for (int i = 0; i < keyWords.length; i++) {
if (word.equalsIgnoreCase(keyWords[i])) {
return true;
}
}
return false;
}
/**
* This method will create a invalid copybook exception and add the line number and line
* at which the exception occurred
*/
private CopyBookParseException invalidCopyBookException(String message) {
return CopyBookParseException.invalidCopyBookException(message + " Error occrured on line " + currentLineNumber + ":" + currentLine);
}
/**
* This method adds the internal exception if it is provided
*/
private CopyBookParseException invalidCopyBookException(String message, Exception internalException) {
CopyBookParseException exception = invalidCopyBookException(message);
exception.setInternalException(internalException);
return exception;
}
}