Skip to content

Commit c9ee54c

Browse files
authored
Merge pull request #836 from capitalone-contributions/multisegment-header-trailer
Multisegment Header Trailer Removal
2 parents dc80bd4 + b81c59f commit c9ee54c

14 files changed

Lines changed: 418 additions & 23 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ output
4949
# syntax: regexp
5050
# ^\.pc/
5151

52+
.vscode/

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1642,7 +1642,8 @@ The output looks like this:
16421642
| .option("enable_index_cache", "true") | When true (default), calculated indexes are cached in memory for later use. This improves performance of processing when same files are processed more than once. |
16431643
| .option("input_split_records", 50000) | Specifies how many records will be allocated to each split/partition. It will be processed by Spark tasks. (The default is not set and the split will happen according to size, see the next option) |
16441644
| .option("input_split_size_mb", 100) | Specify how many megabytes to allocate to each partition/split. (The default is 100 MB) |
1645-
1645+
| .option("record_header_name", "HEADER") | Assuming a copybook definition represents a header, offsets the file read start by the number of bytes in that header and excludes the record definition from the output schema. |
1646+
| .option("record_trailer_name", "TRAILER") | Assuming a copybook definition represents a trailer, offsets the file read end by the number of bytes in that trailer and excludes the record definition from the output schema. |
16461647
##### Helper fields generation options
16471648
16481649
| Option (usage example) | Description |

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/extractors/record/RecordExtractors.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ object RecordExtractors {
7171
activeSegmentRedefine: String = "",
7272
generateInputFileField: Boolean = false,
7373
inputFileName: String = "",
74-
handler: RecordHandler[T]
74+
handler: RecordHandler[T],
75+
recordsToExclude: Set[String] = Set.empty
7576
): Seq[Any] = {
7677
val dependFields = scala.collection.mutable.HashMap.empty[String, Either[Int, String]]
7778
val corruptFields = new ArrayBuffer[CorruptField]
@@ -202,7 +203,7 @@ object RecordExtractors {
202203

203204
val records: ListBuffer[T] = ListBuffer.empty[T]
204205

205-
for (record <- rootRecords) yield {
206+
for (record <- rootRecords if !recordsToExclude.contains(record.name.toUpperCase)) yield {
206207
val (size, values) = getGroupValues(nextOffset, record.asInstanceOf[Group])
207208
if (!record.isRedefined) {
208209
nextOffset += size
@@ -259,7 +260,8 @@ object RecordExtractors {
259260
recordId: Long = 0,
260261
generateInputFileField: Boolean = false,
261262
inputFileName: String = "",
262-
handler: RecordHandler[T]
263+
handler: RecordHandler[T],
264+
recordsToExclude: Set[String] = Set.empty
263265
): Seq[Any] = {
264266
val isAstFlat = ast.children.exists(_.isInstanceOf[Primitive])
265267

@@ -422,7 +424,7 @@ object RecordExtractors {
422424

423425
val records: ListBuffer[T] = ListBuffer.empty[T]
424426

425-
rootRecords.collect { case grp: Group if grp.parentSegment.isEmpty =>
427+
rootRecords.collect { case grp: Group if grp.parentSegment.isEmpty && !recordsToExclude.contains(grp.name.toUpperCase) =>
426428
val (size, values) = getGroupValues(nextOffset, grp, segmentsData(0)._2, 0, segmentsData(0)._1 :: Nil)
427429
nextOffset += size
428430
records += values

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/iterator/FixedLenNestedRowIterator.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ class FixedLenNestedRowIterator[T: ClassTag](
9696
generateCorruptFields = generateCorruptFields,
9797
generateCorruptFieldsAsHex = generateCorruptFieldsAsHex,
9898
activeSegmentRedefine = activeSegmentRedefine,
99-
handler = handler
99+
handler = handler,
100+
recordsToExclude = readerProperties.recordsToExclude
100101
)
101102

102103
// Advance byte index to the next record

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/iterator/VarLenHierarchicalIterator.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ final class VarLenHierarchicalIterator[T: ClassTag](cobolSchema: Copybook,
149149
recordIndex,
150150
generateInputFileName,
151151
dataStream.inputFileName,
152-
handler
152+
handler,
153+
readerProperties.recordsToExclude
153154
)
154155
}
155156

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/iterator/VarLenNestedIterator.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ final class VarLenNestedIterator[T: ClassTag](cobolSchema: Copybook,
109109
activeSegmentRedefine = segmentRedefine,
110110
generateInputFileName,
111111
dataStream.inputFileName,
112-
handler
112+
handler,
113+
readerProperties.recordsToExclude
113114
))
114115

115116
recordFetched = true

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,7 @@ case class CobolParameters(
111111
debugLayoutPositions: Boolean,
112112
enableSelfChecks: Boolean,
113113
metadataPolicy: MetadataPolicy,
114+
recordHeaderName: Option[String] = None,
115+
recordTrailerName: Option[String] = None,
114116
options: Map[String, String]
115117
)

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParametersParser.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ object CobolParametersParser extends Logging {
6060
val PARAM_RECORD_END_OFFSET = "record_end_offset"
6161
val PARAM_FILE_START_OFFSET = "file_start_offset"
6262
val PARAM_FILE_END_OFFSET = "file_end_offset"
63+
val PARAM_RECORD_HEADER_NAME = "record_header_name"
64+
val PARAM_RECORD_TRAILER_NAME = "record_trailer_name"
6365
val PARAM_IS_XCOM = "is_xcom"
6466
val PARAM_IS_TEXT = "is_text"
6567

@@ -310,6 +312,8 @@ object CobolParametersParser extends Logging {
310312
params.getOrElse(PARAM_DEBUG_LAYOUT_POSITIONS, "false").toBoolean,
311313
params.getOrElse(PARAM_ENABLE_SELF_CHECKS, "false").toBoolean,
312314
MetadataPolicy(params.getOrElse(PARAM_METADATA, "basic")),
315+
params.get(PARAM_RECORD_HEADER_NAME).map(_.trim).filter(_.nonEmpty),
316+
params.get(PARAM_RECORD_TRAILER_NAME).map(_.trim).filter(_.nonEmpty),
313317
params.getMap
314318
)
315319
validateSparkCobolOptions(params, recordFormat, validateRedundantOptions)
@@ -412,6 +416,9 @@ object CobolParametersParser extends Logging {
412416
CorruptFieldsPolicy.Disabled
413417
}
414418

419+
val recordsToExclude = (parameters.recordHeaderName.map(n => CopybookParser.transformIdentifier(n).toUpperCase).toSet ++
420+
parameters.recordTrailerName.map(n => CopybookParser.transformIdentifier(n).toUpperCase).toSet)
421+
415422
ReaderParameters(
416423
recordFormat = parameters.recordFormat,
417424
isEbcdic = parameters.isEbcdic,
@@ -470,6 +477,7 @@ object CobolParametersParser extends Logging {
470477
varLenParams.reAdditionalInfo,
471478
varLenParams.inputFileNameColumn,
472479
parameters.metadataPolicy,
480+
recordsToExclude,
473481
parameters.options
474482
)
475483
}
@@ -982,6 +990,22 @@ object CobolParametersParser extends Logging {
982990
params.contains(PARAM_ENABLE_INDEX_CACHE) && params(PARAM_ENABLE_INDEX_CACHE).toBoolean)
983991
throw new IllegalArgumentException(s"When '$PARAM_ENABLE_INDEXES' = false, '$PARAM_ENABLE_INDEX_CACHE' cannot be true.")
984992

993+
if (params.contains(PARAM_RECORD_HEADER_NAME) && params.contains(PARAM_FILE_START_OFFSET)) {
994+
throw new IllegalArgumentException(s"Options '$PARAM_RECORD_HEADER_NAME' and '$PARAM_FILE_START_OFFSET' cannot be used together.")
995+
}
996+
997+
if (params.contains(PARAM_RECORD_TRAILER_NAME) && params.contains(PARAM_FILE_END_OFFSET)) {
998+
throw new IllegalArgumentException(s"Options '$PARAM_RECORD_TRAILER_NAME' and '$PARAM_FILE_END_OFFSET' cannot be used together.")
999+
}
1000+
1001+
if (params.contains(PARAM_RECORD_HEADER_NAME) && params.contains(PARAM_RECORD_TRAILER_NAME)) {
1002+
val headerName = params(PARAM_RECORD_HEADER_NAME).trim
1003+
val trailerName = params(PARAM_RECORD_TRAILER_NAME).trim
1004+
if (headerName.equalsIgnoreCase(trailerName)) {
1005+
throw new IllegalArgumentException(s"Options '$PARAM_RECORD_HEADER_NAME' and '$PARAM_RECORD_TRAILER_NAME' cannot refer to the same record ('$headerName').")
1006+
}
1007+
}
1008+
9851009
if (validateRedundantOptions && unusedKeys.nonEmpty) {
9861010
val unusedKeyStr = unusedKeys.mkString(",")
9871011
val msg = s"Redundant or unrecognized option(s) to 'spark-cobol': $unusedKeyStr."

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/ReaderParameters.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,5 +139,6 @@ case class ReaderParameters(
139139
reAdditionalInfo: String = "",
140140
inputFileNameColumn: String = "",
141141
metadataPolicy: MetadataPolicy = MetadataPolicy.Basic,
142+
recordsToExclude: Set[String] = Set.empty,
142143
options: Map[String, String] = Map.empty
143144
)

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/schema/CobolSchema.scala

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,24 @@ class CobolSchema(val copybook: Copybook,
5555
val corruptSchemaPolicy: CorruptFieldsPolicy,
5656
val generateSegIdFieldsCnt: Int = 0,
5757
val segmentIdProvidedPrefix: String = "",
58-
val metadataPolicy: MetadataPolicy = MetadataPolicy.Basic) extends Serializable {
58+
val metadataPolicy: MetadataPolicy = MetadataPolicy.Basic,
59+
val recordsToExclude: Set[String] = Set.empty) extends Serializable {
5960

6061
val segmentIdPrefix: String = if (segmentIdProvidedPrefix.isEmpty) getDefaultSegmentIdPrefix else segmentIdProvidedPrefix
6162

6263
def getCobolSchema: Copybook = copybook
6364

64-
lazy val getRecordSize: Int = copybook.getRecordSize
65+
lazy val getRecordSize: Int = {
66+
if (recordsToExclude.isEmpty) {
67+
copybook.getRecordSize
68+
} else {
69+
val excludedSize = copybook.ast.children
70+
.filter(r => recordsToExclude.contains(r.name.toUpperCase))
71+
.map(_.binaryProperties.actualSize)
72+
.sum
73+
copybook.getRecordSize - excludedSize
74+
}
75+
}
6576

6677
def isRecordFixedSize: Boolean = copybook.isRecordFixedSize
6778

@@ -147,7 +158,8 @@ object CobolSchema {
147158
readerParameters.corruptFieldsPolicy,
148159
segIdFieldCount,
149160
segmentIdPrefix,
150-
readerParameters.metadataPolicy
161+
readerParameters.metadataPolicy,
162+
readerParameters.recordsToExclude
151163
)
152164
}
153165

0 commit comments

Comments
 (0)