Como ler um campo COMP-3 usando o Java?

Neste laboratório vamos codificar e executar um programa em Java para ler e “descompactar” campos COMP-3 gerados pelo COBOL. Se antes de acompanhar o passo a passo você quiser entender mais sobre os conceitos do COMP-3, pode dar uma olhada nesse artigo que publiquei aqui.

Gerando um arquivo com campos compactados

Vou codificar um programa muito simples em COBOL para gerar um arquivo sequencial com três registros e três campos COMP-3 com pictures diferentes:

identification division.
program-id. gtc006.

environment division.
input-output section.
file-control.
    select arquivo assign to "arquivo.txt"
    organization is line sequential
    file status is wt-st.

data division.
file section.
fd arquivo.
01 registro.
    03 campo-1  pic  x(004).
    03 campo-2  pic s9(007) comp-3.
    03 campo-3  pic s9(013)v9(002) comp-3.
    03 campo-4  pic  9(005) comp-3.

working-storage section.
01 wt-st        pic  x(002) value spaces.

procedure division.
inicio.
    open output arquivo
    
    move "reg1"      to campo-1
    move -129        to campo-2
    move 23456.78    to campo-3
    move 1           to campo-4
    write registro

    move "reg2"      to campo-1
    move zeros       to campo-2
    move -9876543.21 to campo-3
    move 2           to campo-4
    write registro

    move "reg3"      to campo-1
    move +256        to campo-2
    move 0.98        to campo-3
    move 3           to campo-4
    write registro

    close arquivo

    stop run.

O uso de pictures diferentes é importante para confirmar que o programa Java é suficientemente genérico para trabalhar com qualquer tamanho de variável e com diferentes configurações de sinal.

O que se vê na PROCEDURE é que o arquivo gerado terá os seguintes registros:

#campo-1campo-2campo-3campo-4
1reg1-12923456.781
2reg209876543.212
3reg3+2560.983

Se olharmos o conteúdo desse arquivo com um editor hexadecimal, o que veremos será o seguinte:

Repare, por exemplo, que na primeira linha podemos ver o valor do campo-2 do primeiro registro (-129). Os dois primeiros dígitos “12” estão ocupando um só byte, o “9” ocupa o primeiro nibble do byte seguinte, e “d” (no segundo nibble) nos mostra que esse é um campo negativo.

Lendo esse arquivo num programa Java

Basicamente um campo COMP-3 armazena cada dígito num nibble, exceto pelo último byte, que é formado por um dígito e um marcador de sinal (C para positivo, D para negativo e F para sem sinal).

Então vamos codificar o programa Java passo a passo para relacionar os conceitos com a solução que vamos adotar.

public class Comp3 {
  
// Le o arquivo de entrada (com campos comp-3) e exibe o conteudo dos campos ja' descompactados
public static void main(String[] args) throws Exception {

        String linha = new String("ISO-8859-1");            
        String campo1 = null;
        long campo2 = 0;
        long campo3 = 0;
        long campo4 = 0;

        try {
            FileReader arquivo = new FileReader("arquivo.txt");
            BufferedReader arquivoBuffer = new BufferedReader(arquivo);
            linha = arquivoBuffer.readLine();
            while (linha != null) {
                campo1 = linha.substring(0, 4);
                campo2 = Comp3.converte(linha.substring(4, 8).getBytes("ISO-8859-1"));
                campo3 = Comp3.converte(linha.substring(8, 16).getBytes("ISO-8859-1"));
                campo4 = Comp3.converte(linha.substring(16, 19).getBytes("ISO-8859-1"));
                System.out.println("[" + campo1 + "] [" + campo2 + "] [" + campo3 + "] [" + campo4 + "]");
                linha = arquivoBuffer.readLine();
            }
            arquivoBuffer.close();
        } 
        catch (FileNotFoundException e) {
            throw new Exception("Arquivo nao encontrado."); }
        catch (IOException e) {
            throw new Exception("Erro na leitura do arquivo"); }

}

No parágrafo main vamos ler o arquivo gerado pelo COBOL e exibir o conteúdo de cada campo na tela.

Aqui é importante chamar a atenção para dois pontos:

  • É preciso que você conheça o layout do arquivo para saber onde começa e termina cada campo. No trecho acima, usamos o método substring para separar cada um dos quatro campos do registro;
  • Os campos campo-2, campo-3 e campo-4 serão transformados em long, e para isso criaremos um método chamado converte que receberá um array de bytes gerado a partir de cada substring;

Mas o detalhe que acaba complicando a vida de quem tenta essa solução é ignorar algumas particularidades do Java tanto para o tratamento de strings quanto para a transformação de strings em bytes.

Repare que depois de aplicar o método substring para separar os campos, também utilizamos o método getBytes() para trasformar esse substring num array de bytes. Acontece que o Java faz isso com base no charset configurado na JVM, ou informado em tempo de execução. Se você não informar nada como parâmetro para o getBytes() ele assumirá que deve fazer a conversão com base em, digamos, UTF-8.

Mas como nossos campos COMP-3 guardam seus conteúdos em nibbles, pode acontecer que uma determinada combinação de bits não tenha representação no charset default. Por exemplo:

Um campo s9(007) comp-3 com valor -129 estará representado fisicamente assim (cada par de números representa um byte):

00 00 12 9d

O UTF-8 não tem um caracter associado ao hexadecimal 9d. Logo, o getBytes() vai transformar o 9d em algo como 0x3f (ou 63 em notação decimal), indicando que houve um “erro” na tentativa de traduzir esse byte.

A solução para isso é “bypassar” o charset default e indicar explicitamente, o nome de um charset que contemple todas as combinações hexadecimais possíveis. Foi isso que fizemos ao indicar “ISO-8859-1” tanto na criação da String quanto na chamada a getBytes().

O método de conversão

Vamos começar definindo algumas constantes:

//Converte campo COMP-3 para LONG
public static long converte(byte[] entrada) throws Exception {

    final int Positivo = 0x0C;      // ultimo nibble de campo positivo
    final int Negativo = 0x0D;      // ultimo nibble de campo negativo
    final int SemSinal = 0x0F;      // ultimo nibble de campo sem sinal
    final int GetHO    = 0x0F;      // para obter os High Order bits
    final int GetLO    = 0x0F;      // para obter os Low Order bits

As três primeiras constantes serão usadas quando precisarmos comparar o último nibble de um campo com um dos indicadores de sinal que o COMP-3 utiliza.

As constantes GetHO e GetLO são usados em operações com bits para obter respectivamente, os quatro primeiros bits (High Order Bits) e os quatro últimos bits (Low Order Bits) de cada byte.

O próximo passo será definir variáveis de trabalho que serão usadas nesse método e também a variável que guardará o valor convertido:

long saida = 0;                 // Valor convertido 
int digito1 = 0;                // Variavel de trabalho
int digito2 = 0;                // Variavel de trabalho

Agora precisaremos varrer todos os bytes informados via argumento e quebrar cada um deles em nibble 1 e nibble 2 (que chamei de dígito1 e dígito2). Para isso, usaremos a operação shift e o operador AND:

for(int i=0; i < entrada.length; i++) {
   digito1 = (entrada[i] >> 4) & GetHO;
   if (i == entrada.length - 1) {   // se for o último dígito  
      saida = (saida * 10) + digito1; 
      digito2 = entrada[i] & GetLO;  
      if (digito2 == Negativo) {
         saida = -saida;
      } else {
         if(digito2 != Positivo && digito2 != SemSinal) {
            throw new Exception("Campo nao e' comp-3");
         }
      }
   } else {                           
      saida = (saida * 10) + digito1;
      digito2 = entrada[i] & GetLO;        
      saida = (saida * 10) + digito2;
   }
}

A operação shift é a que vemos no início do loop, representada pelo sinal “>> 4”. Isso significa que estamos deslocando todo o byte quatro bits para a direita preenchendo com zeros os bits da esquerda. Na prática, acabamos de gerar um byte com o valor que antes estava representado por meio byte. Além disso, nessa mesma operação estamos “somando” (AND) o nosso novo byte com o hexadecimal 0xFF (a constante GetHO). Isso faz com que se elimine qualquer extensão de sinal do byte que acabamos de gerar. Esse conceito de “extensão de sinal” tem a ver com a plataforma, nada a ver com o sinal do campo que comentamos antes.

Se já estivermos no último byte (i == entrada.length – 1) então significa que o último nibble está ocupado por um indicador de sinal (C, D ou F). Tudo o que temos que fazer é transformar o último meio-byte em um byte (digito2 = entrada[i] & GetLO) e compará-lo com uma daquelas constantes que criamos no início do método.

Agora repare que cada vez que obtemos um novo dígito, nós multiplicamos a variável de saída por 10 e depois somamos o dígito a essa variável. Isso faz com que os dígitos “andem” da direita para a esquerda à medida em que a gente vai descobrindo cada um deles.

O programa completo

import java.text.*;
import java.util.*;
import java.io.*;

public class Comp3 {
  
// Le o arquivo de entrada (com campos comp-3) e exibe o conteudo dos campos ja' descompactados
public static void main(String[] args) throws Exception {

        String linha = new String("ISO-8859-1");            
        String campo1 = null;
        long campo2 = 0;
        long campo3 = 0;
        long campo4 = 0;

        try {
            FileReader arquivo = new FileReader("arquivo.txt");
            BufferedReader arquivoBuffer = new BufferedReader(arquivo);
            linha = arquivoBuffer.readLine();
            while (linha != null) {
                campo1 = linha.substring(0, 4);
                campo2 = Comp3.converte(linha.substring(4, 8).getBytes("ISO-8859-1"));
                campo3 = Comp3.converte(linha.substring(8, 16).getBytes("ISO-8859-1"));
                campo4 = Comp3.converte(linha.substring(16, 19).getBytes("ISO-8859-1"));
                System.out.println("[" + campo1 + "] [" + campo2 + "] [" + campo3 + "] [" + campo4 + "]");
                linha = arquivoBuffer.readLine();
            }
            arquivoBuffer.close();
        } 
        catch (FileNotFoundException e) {
            throw new Exception("Arquivo nao encontrado."); }
        catch (IOException e) {
            throw new Exception("Erro na leitura do arquivo"); }

}

//Converte campo COMP-3 para LONG
public static long converte(byte[] entrada) throws Exception {

    final int Positivo = 0x0C;      // ultimo nibble de campo positivo
    final int Negativo = 0x0D;      // ultimo nibble de campo negativo
    final int SemSinal = 0x0F;      // ultimo nibble de campo sem sinal
    final int GetHO    = 0x0F;      // para obter os High Order bits
    final int GetLO    = 0x0F;      // para obter os Low Order bits
    
    long saida = 0;                 // Valor convertido 

    int digito1 = 0;                 // Guarda o valor do primeiro nibble
    int digito2 = 0;                  // Guarda o valor do segundo nibble

    for(int i=0; i < entrada.length; i++) {
       digito1 = (entrada[i] >> 4) & GetHO;
       if (i == entrada.length - 1) {     
          saida = (saida * 10) + digito1; 
          digito2 = entrada[i] & GetLO;  
          if (digito2 == Negativo) {
             saida = -saida;
          } else {
             if(digito2 != Positivo && digito2 != SemSinal) {
                throw new Exception("Campo nao e' comp-3");
             }
          }
       } else {                           // Nao e' o ultimo digito
          saida = (saida * 10) + digito1;
          digito2 = entrada[i] & GetLO;        // Obtem o ultimo nibble
          saida = (saida * 10) + digito2;
       }
    }

    return saida;
} 

Executando o programa

Há mais um detalhe importante: ao executar o programa é necessário que se indique explicitamente qual o charset que será utilizado, ou seja, tanto a chamada ao programa, quanto a criação da String que receberá o registro do arquivo, quanto a chamada ao método getBytes() para transformar o registro num array de bytes devem fazer referência ao mesmo charset. ISO-8859-1 é o que funciona na minha instalação, mas pode ser que existam outros.

Sendo assim, usamos o comando abaixo para executar o programa…

java -Dfile.encoding=ISO-8859-1 Comp3

…e ele exibirá na tela os seguintes resultados…

[reg1] [-129] [2345678] [1]
[reg2] [0] [-987654321] [2]
[reg3] [256] [98] [3]

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *