bfc-script

Introdução

O bfc-script é um framework que possibilita a integração de scripts com aplicações. O framework oferece um ambiente de desenvolvimento, compilação e execução de scripts, bem como uma linguagem amigável para usuários não técnicos e APIs utilitárias.

Linguagem

A linguagem possui poucos comandos e algumas similaridades com a linguagem Java, como os operadores (lógico, comparação, atribuição e aritimético), o uso de chaves para circundar blocos, sintaxe case sensitive, etc. Para simplificar os scripts, não é necessário usar ponto e vírgula no final de cada instrução e os comandos foram desenvolvidos no idioma português, para simplificar o seu uso à usuários sem experiência em linguagens de programação.

Comandos

imprimir

Exibe uma mensagem no console.

imprimir 'John Doe'
percorrer

Permite iterar valores e obter o índice atual da iteração.

percorrer(ate:5){
    imprimir indice
}

O comando disponibiliza uma variável implícita chamada indice que representa o índice corrente da iteração.

percorrer(de:3, ate:5){
    imprimir indice
}

A variável indice pode ser atribuída a uma outra variável com um nome personalizado, permitindo que a mesma seja acessada em diversos níveis do comando percorrer.

percorrer(de:3, ate:5){ idPrincipal ->
    imprimir idPrincipal
    percorrer(de:6, ate:8){ idSecundario ->
        imprimir 'ID princial: ' + idPrincipal
        imprimir 'ID secundário: ' + idSecundario
    }
}

É possível também iterar pelos valores de um lista informando ou não o índice inicial e final da iteração

numeros = [1,2,3]
percorrer(de:1, ate:3, itens: numeros){ 
    imprimir item
}
// Irá imprimir os números 1, 2 e 3
letras = ['A','B','C']
percorrer(de:2, ate:3, itens: letras){ 
    imprimir item
}
// Irá imprimir as letras B e C, ou seja, do índice 2 até o índice 3
  • Caso o índice inicial não seja informado irá utilizar como padrão o valor 1 (primeiro item).
  • Caso o índice final não seja informado irá utilizar como padrão o índice final da lista (último item).
Controle do fluxo de execução

O fluxo de execução de um comando percorrer pode ser alterado utilizando as funções parar() e continuar(). Este controle é especialmente útil quando se deseja interromper a execução de um comando percorrer devido à um condição específica ou avançar para um próximo item do laço.

parar()

A função parar interrompe um comando percorrer em determinada condição, e continua a execução do script sem passar pelas repetições seguintes.

O seguinte código irá imprimir os números 1, 2 e a palavra Ok:

numeros = [1,2,3,4]
percorrer(numeros){
  se(item == 3){
    parar()
  } 
  imprimir item
}

//As instruções fora do comando percorrer serão executadas normalmente, somente o comando percorrer 
//será interrompido e não todo o script
imprimir 'Ok'
continuar()

A função continuar faz com que o comando percorrer passe para a próxima repetição/item. Este função é especialmente útil em situações em que se deseje ignorar o processamento de um item com base em uma condição.

O seguinte código irá imprimir os números 1, 2 e 4 ignorando o número 3:

numeros = [1,2,3,4]
percorrer(numeros){
  se(item == 3){
    continuar()
  } 
  imprimir item
}
Nomeando os comandos percorrer

Em situações onde são utilizados mais de um comando percorrer aninhados e se deseje parar ou continuar um percorrer específico, a atribuição de um nome ao comando se faz necessário. Para nomear o comando basta preencher a propriedade nome na declaração da instrução:

numeros = [1,2,3,4]
percorrer(itens: numeros, nome: 'p1'){ 
  imprimir 'p1: ' + item 
  percorrer(de: 1, ate: 5){
    se (indice == 3){
      parar 'p1'
    }
    imprimir 'p2: ' + indice
  }
}

A saída do script será:

p1: 1 // primeiro item da lista de números do percorrer 'p1'
p2: 1 // primeiro índice do segundo percorrer
p2: 2 // ...
p2: 3 // ...
p2: 4 // No índice 4 o comando parar 'p1' foi executado interrompendo a execução de todos os comandos percorrer até o 'p1'

A mesma regra se aplica ao comando continuar(), porém ao invés de interromper a execução do comando percorrer irá avançar para o próximo índice/item.

numeros = [1,2,3]
percorrer(itens: numeros, nome: 'p1'){ 
  imprimir 'p1: ' + item
  percorrer(de: 1, ate: 5, nome: 'p2'){
    se (indice == 2){
      continuar 'p1'
    }
    imprimir 'p2: ' + indice
  }
}

A saída do script será:

p1: 1 // primeiro item da lista de números do percorrer 'p1'
p2: 1 // primeiro índice do segundo percorrer 
      // Ao passar pelo segundo ídice o comando continuar 'p1' foi execuado parando a execução do percorrer atual e avançando o item do 
      // percorrer 'p1' para a próxima iteração, neste caso o número 2
p1: 2 
p2: 1
p1: 3
p2: 1
retornar

Permite submeter dados como retorno de um script.

Conforme podemos observar, a variável valor é submetida como retorno do script utilizando o comando retornar.

valor = 10 * 2 * 3
retornar valor

No exemplo abaixo, os valores são retornados pelo comando retornar através de um mapa. As chaves valor e nome recebem seus respectivos valores e são submetidas ao comando retornar e serão resgatadas programaticamente através de um java.util.Map.

retornar valor:10 * 2 * 3, nome:'David Crosby'
se

Permite criar expressões condicionais.

se(10 == 10){
   imprimir 'verdadeiro'
}

se(9 <= 20){
    imprimir 'verdadeiro'
}senao{
    imprimir 'falso'
}
tentar/tratar/finalizar

Às vezes, a execução de uma ação pode acarretar em uma exceção. Quando isto ocorre, é possível que seja realizado um tratamento para que o script continue sua execução realizando alguma outra ação, como por exemplo, notificar outros usuários, tratar alguns retornos conhecidos, tentar realizar a ação novamente, etc.

No bloco tentar devem ser colocadas as ações onde pode ocorrer um erro. Por exemplo, chamar um serviço HTTP, SOAP, escrever em um arquivo, etc.

Dentro do bloco tratar deve ser especificado o código que será executado em caso de erro dentro do bloco tentar. Nele, existe uma variável chamada excecao que é uma representação do erro ocorrido. Ela possui os seguintes atributos:

  • codigo - É um identificador alfanumérico erro para facilitar sua localização nos manuais e documentações
  • mensagem - Uma mensagem passada pela API ou linguagem que representa a descrição do erro ocorrido
  • linha - Linha onde ocorreu o erro
tentar {
  resultado = Http.servico('https://endereco-nao-existe').GET()
  imprimir resultado.codigo()
} tratar {
   // Ao mandar imprimir o objeto excecao, será impresso o código, mensagem e linha (se existir)
   imprimir 'Estou tratando uma exceção: ' + excecao
}

Ainda é possível que independentemente de ocorrer uma falha ou não, ao final da execução do método seja executada alguma ação. Para isso, existe também o bloco finalizar. Onde tudo o que for definido neste bloco, será executado independentemente de ocorrer um erro ou não.

tentar {
  resultado = Http.servico('https://endereco-nao-existe').GET()
  imprimir resultado.codigo()
} tratar {
   // Ao mandar imprimir o objeto excecao, será impresso o código, mensagem e linha (se existir)
   imprimir 'Estou tratando uma exceção: ' + excecao
} finalizar {
    imprimir 'O conteúdo deste bloco sempre será executado'
}

Algumas observações referentes à este recurso:

  • O bloco finalizar sempre é executado
  • Dentro do bloco finalizar, a variável excecao já não está disponível
  • O comando suspender não é tratado
suspender

Permite suspender a execução de um script com uma exceção.

se(codigo != 10){
   suspender "O código $codigo é inválido"
}
esperar

O comando esperar permite que a execução de um script entre em modo de pausa por um determinado intervalo tempo.

Este comando se mostra útil quando o acesso a um serviço é limitado por um intervalo de tempo.

imprimir Datas.hoje()
esperar 2000 //O script irá pausar a execução neste ponto durante 2000 millisegundos
imprimir Datas.hoje()

É possível informar o intervalo de tempo utilizando a forma simplificada de tempo/datas da engine:

imprimir Datas.hoje()
esperar 1.segundo //O script irá pausar a execução neste ponto durante 1 segundo
imprimir Datas.hoje()

O tempo máximo de espera permitido por comando é de 60 segundos. Essa função está disponíveis ao usuário final apenas através da Ferramenta de Scripts.

exportar

Comando utilizado para exportar símbolos declarados no escopo atual. Aceita como parâmetro um mapa com o nome externo e uma referência ao recursos exportado.

// exportando uma constante
exportar PI_APROXIMADO: 3.1415

// exportando múltiplos símbolos
exportar(
	nomeExterno: referencia,
	outroNome: outraReferencia
)

Este comando está disponível apenas na Ferramenta de Scripts.

importar

Comando utilizado para importar recursos de um componente. Aceita como parâmetro uma String com o nome do identificador do componente desejado, e retorna um objeto com os recursos importados.

log = importar("log")
math = importar("calculadora")
counter = importar("contador")

log.info("Iniciando execução")
acum = 0

percorrer(ate: 20) {
  counter.incrementar()
  acum = math.somar(acum, indice)
  log.info("Executando operação #$indice")
}

Este comando está disponível apenas na Ferramenta de Scripts.

Listas

Também conhecida como Arrays, podemos trabalhar com listas de maneira bem simplificada.

//Instancia uma lista com valores
valores = [0, 1, 2, 3, 4]

//Instancia uma lista vazia
nomes = []

//Adiciona um item na lista
nomes << 'Chuck Norris'

//Itera uma lista
percorrer(valores){
    imprimir item
}

O comando percorrer possui uma variável implícita para obter o valor da iteração chamada item.

A variável item e indice podem ser atribuídas à outras variáveis com nomes personalizados utilizando a seguinte sintaxe do comando percorrer:

percorrer(valores){ valor ->
    imprimir valor
}

percorrer(valores){ valor, posicao ->
    imprimir valor // Equivale a item
    imprimir posicao // Equivale a indice
}

percorrer(valores){ item, posicao ->
    imprimir item
    percorrer(outros){
        imprimir item //Equivale ao item do percorrer principal pois utilizou o mesmo nome da variável implícita
    }
}

Obtendo e atribuindo valores em posições específicas da lista. A primeira posição da lista tem o índice 0 (zero), a segunda posição tem o índice 1, e assim por diante. Os dados são acessados assim: lista[indice]

//Instancia uma lista com valores
nomes = ['Harrison Reid', 'Thomas Sharpe', 'Louie Hill']

//nomes[0] contém o valor 'Harrison Reid'
//nomes[1] contém o valor 'Thomas Sharpe'
//nomes[2] contém o valor 'Louie Hill'

//Obtendo o nome da segunda posição da lista:
nomeDaSegundaPosicao = nomes[1]

Mapas

É possível criar mapas simplificados e acessar seu valores de forma explicita.

pessoa = [nome:'João da Silva', idade:25, profissao: 'Contador']
 
imprimir pessoa.nome
imprimir pessoa.idade
imprimir pessoa.profissao

Observe o exemplo abaixo, usando uma lista de mapas:

//Declaração da lista. Será composta por um mapa que contém o nome, a idade e a profissão.
dadosPessoais = []

//Adicionando dados à lista
dadosPessoais << [nome:'Luca Ingram', idade:25, profissao: 'Process pipeline drafter']
dadosPessoais << [nome:'Jack Young', idade:30, profissao: 'Industrial economist']
dadosPessoais << [nome:'Jake Sullivan', idade:31, profissao: 'Gastroenterology nurse']

//Imprimindo os dados...
percorrer(dadosPessoais) {
  imprimir 'Posição: ' + indice + ' -> Dados:' + item 
}

//Temos o resultado:
Posição: 0 -> Dados:[nome:Luca Ingram, idade:25, profissao:Process pipeline drafter]
Posição: 1 -> Dados:[nome:Jack Young, idade:30, profissao:Industrial economist]
Posição: 2 -> Dados:[nome:Jake Sullivan, idade:31, profissao:Gastroenterology nurse]

//Agora, vamos alterar última posição da lista com novos dados. 
//Jake Sullivan trocou de profissão e precisamos atualizar seu dado profissional. Fazemos isso substituindo um mapa por outro:
dadosPessoais[2] = [nome:'Jake Sullivan', idade:31, profissao: 'Administrative leader']

//Voltamos a imprimir. Resultado:
Posição: 0 -> Dados:[nome:Luca Ingram, idade:25, profissao:Process pipeline drafter]
Posição: 1 -> Dados:[nome:Jack Young, idade:30, profissao:Industrial economist]
Posição: 2 -> Dados:[nome:Jake Sullivan, idade:31, profissao:Administrative leader]

//Outro exemplo: Preciso saber a idade do segundo profissional da lista, Jack Young:
imprimir dadosPessoais[1].idade
//Resultado: 30    

Intervalos

Intervalos permitem que você crie uma lista de valores sequenciais podendo serem utilizados como listas. A notação .. define um intervalo, do primeiro item até o último. Intervalos definidos com a notação ..< incluem o primeiro valor, mas não o último valor.

//Cria um intervalo
dias = 1..3

//Percorre um intervalo    
percorrer(dias){
    imprimir item
}

//Percorre um intervalo        
percorrer(4..7){
    imprimir item    
}

//Percorre um intervalo de forma decrescente
percorrer(8..7){
    imprimir item    
}

//Percorre um intervalo decrescente desconsiderando o último valor
percorrer(6..<4){
    imprimir item    
}

O comando percorrer possui uma variável implícita para obter o valor da iteração chamada item.

Datas

A linguagem permite trabalhar com datas de forma bem simplificada. Várias funções estão embutidas nos elementos de data facilitando muito o uso, além de tornar as implementações bem intuitivas. O exemplo abaixo demonstra o uso de algumas funções para datas da API padrão e como utilizar estas funções de forma simplificada, além de demonstrar formas nativas para somar datas/horas/etc.

//Funções para obter-se uma data/dataHora
hoje = Datas.hoje()
primeiroDiaDoAno = Datas.data(hoje.ano,1 ,1 )
ultimoDiaDoAno = Datas.dataHora(hoje.ano, 12, 31, 23, 59)

imprimir Datas.adicionaSegundos(hoje, 10)
imprimir hoje + 10.segundos

imprimir Datas.adicionaMinutos(hoje, 10)
imprimir hoje + 10.minutos

imprimir Datas.adicionaHoras(hoje, 10)
imprimir hoje + 10.horas

imprimir Datas.adicionaDias(hoje, 10)
imprimir hoje + 10.dias

imprimir Datas.adicionaMeses(hoje, 10)
imprimir hoje + 10.meses

imprimir hoje + 1.segundo
imprimir hoje + 1.minuto
imprimir hoje + 1.hora
imprimir hoje + 1.dia
imprimir hoje + 1.mes

imprimir hoje + 10.anos + 9.meses + 8.semanas + 7.dias + 6.horas + 5.minutos + 4.segundos + 3.milesegundos

imprimir Datas.ano(hoje)
imprimir hoje.ano
imprimir Datas.mes(hoje)
imprimir hoje.mes
imprimir Datas.dia(hoje)
imprimir hoje.dia
imprimir Datas.hora(hoje)
imprimir hoje.hora
imprimir Datas.minuto(hoje)
imprimir hoje.minuto
imprimir Datas.segundo(hoje)
imprimir hoje.segundo

imprimir Datas.diaSemana(hoje)
imprimir hoje.diaSemana

imprimir Datas.removeDias(hoje, 10)
imprimir hoje - 10.dias

imprimir Datas.removeMeses(hoje, 10)
imprimir hoje - 10.meses

imprimir Datas.extenso(hoje)
imprimir hoje.extenso

imprimir Datas.nomeDiaSemana(hoje)
imprimir hoje.nomeDiaSemana

imprimir Datas.nomeMes(hoje)
imprimir hoje.nomeMes

imprimir Datas.ehData('01/01/2010')

imprimir Datas.diferencaAnos(primeiroDiaDoAno, ultimoDiaDoAno)
imprimir Datas.diferencaDias(primeiroDiaDoAno, ultimoDiaDoAno)
imprimir Datas.diferencaHoras(primeiroDiaDoAno, ultimoDiaDoAno)
imprimir Datas.diferencaMeses(primeiroDiaDoAno, ultimoDiaDoAno)
imprimir Datas.diferencaMinutos(primeiroDiaDoAno, ultimoDiaDoAno)
imprimir Datas.diferencaSegundos(primeiroDiaDoAno, ultimoDiaDoAno)

amanha = hoje + 1.dia

//Podemos criar intervalos com datas
percorrer(hoje..amanha){
    imprimir item.extenso
}

percorrer(hoje+1.semana..<amanha+2.semanas){
    imprimir item.extenso
}

//Podemos obter uma data apartir de uma expressão como esta
semanaQueVem = 7.dias.apartirDe.hoje

imprimir semanaQueVem + 5.dias

É importante notar que os valores numéricos informados nas funções de data para representar ano, mês, dias, horas e segundos, diferentemente da formatação Brasileira, não devem conter zeros à esquerda:

//Correto
Datas.data(2017, 8, 5)

//Incorreto (Erro de compilação)
Datas.data(2017, 08, 05)

Valores nulos

Em programação de computadores, null é um valor especial para um ponteiro (ou qualquer outro tipo de referência) que indica que este ponteiro, intencionalmente, não se refere a um objeto (ponteiro nulo) - Wikipedia.

Este recurso se mostra útil para identificar quando um valor não esta disponível, assumindo um valor próprio para este comportamento, o nulo ou vazio.

A palavra reservada nulo representa o valor para este comportamento, de modo que no exemplo abaixo estamos dizendo que a variável valorCusto é igual a nulo ou em outras palavras, que seu valor é vazio.

    valorCusto = nulo

Um script pode receber variáveis com valores nulo, onde em certas ocasiões é necessário checar se o valor da variável é nulo ou não. Podemos realizar as checagens de duas formas:

    valorCusto = nulo

    // Forma 1
    se (valorCusto != nulo){
        imprimir ("A variável valorCusto não está nula.")
    }

    // Forma 2
    se (valorCusto){
        imprimir ("A variável valorCusto não está nula.")
    }

    se (!valorCusto){
        imprimir ("A variável valorCusto está nula.")
    }

    se (valorCusto && funcionario.agenciaBancaria){
        imprimir ("A variável valorCusto a a agência bancária do funcionário não estão nulas.")
    }

O acesso à referências nulas gera erros durante a execução de um script. No exemplo abaixo, suponhamos que a agência bancária do funcionário esteja nula:

    nomeFuncionario = funcionario.agenciaBancaria.nome

Ao tentar obter o nome de uma agência bancária nula, recebemos um erro de execução: A propriedade nome não é acessível em um elemento nulo. Para evitar este comportamento, podemos utilizar o operador de navegação segura (?):

    nomeFuncionario = funcionario.agenciaBancaria?.nome

O operador deve ser utilizado em diversos níveis de uma referência, caso seja apropriado:

    nomeFuncionario = funcionario.agenciaBancaria?.municipio?.nome

    // valor impresso 'null'
    imprimir(nomeFuncionario)

No exemplo acima, caso não utilizassemos o operador (?) no município (municipio?.nome), receberiamos um erro durante a execução, devido ao fato de que a agenciaBancária esta nula. Como resultado final o valor da variável nomeFuncionario é nula.

Em algumas ocasiões, gostariamos de considerar um valor padrão onde o resultado seria nulo. Para este propósito utilizamos a expressão ternária (?:), como podemos observar abaixo:

    nomeFuncionario = funcionario.agenciaBancaria?.municipio?.nome

    imprimir(nomeFuncionario?:'Sem nome')

No exemplo acima, quando o valor da variável nomeFuncionario for nulo, o valor retornado será Sem nome.

Poderiamos usar esta expressão diretamente, conforme o exemplo abaixo:

    nomeFuncionario = funcionario.agenciaBancaria?.municipio?.nome?:'Sem nome'

    imprimir(nomeFuncionario)

API Padrão

A engine padrão disponibiliza uma API com várias funções utilitárias para manipulação de datas, caracteres e números. As funções são separadas por classes e são invocadas como métodos. Alguns métodos para manipulação de datas e caracteres podem ser utilizados de maneira direta, invocando o método apartir do próprio elemento, não necessitando a invocação através da classe. Durante a explanação das funções, serão sinalizados, as que possuem alternativa de uso direta. Esta sessão abordará detalhes de cada função da API padrão. Essas funções estarão disponíveis ao usuário final, e serão absorvidas plenamente conforme a utilização. Sinta-se a vontade para pular esta sessão neste primeiro momento.

Caracteres

capitaliza

Por meio desta função é possível colocar a primeira letra de cada palavra de um texto emmaiúsculo e as outras letras para minúsculo.

Caracteres.capitaliza(texto)

Alternativa

texto.capitaliza
direita

Obtem uma quantidade específica de caracteres iniciados da direita para esquerda.

Caracteres.direita(texto, quantidade)

Alternativa

texto.direita(quantidade)
equivalente

Verifica se uma expressão esta contida em um texto. Mais sobre expressões regulares

Caracteres.equivalente(texto, expressao)

Alternativa

texto.equivalente(expressao)
esquerda

Obtem uma quantidade específica de caracteres iniciados da esquerda para direita.

Caracteres.esquerda(texto, quantidade)

Alternativa

texto.esquerda(quantidade)
maisculo

Converte todos os caracteres de um texto em maiúsculo.

Caracteres.maiusculo(texto)

Alternativa

texto.maiusculo
minusculo

Converte todos os caracteres de um texto em minusculo.

Caracteres.minusculo(texto)

Alternativa

texto.minusculo
posicao

Obtem a posição onde um caracter se encontra em uma texto.

Caracteres.posicao(texto, expressao, posicaoInicio)

Alternativa

texto.posicao(expressao, posicaoInicio)
posicao

Obtem a posição inicial de uma expressão regular em uma texto.

Caracteres.posicao(texto, expressao regular, posicaoInicio)

Alternativa

texto.posicao(expressao regular, posicaoInicio)
posicaoFinal

Obtem a posição final de uma expressão regular em uma texto.

Caracteres.posicaoFinal(texto, expressao regular, posicaoInicio)

Alternativa

texto.posicaoFinal(expressao regular, posicaoInicio)
removeEspacos

Remover o excesso de espaços de um texto.

Caracteres.removeEspacos(texto)

Alternativa

texto.removeEspacos
removeEspacosDireita

Remove o excesso de espaços de um texto à esquerda.

Caracteres.removeEspacosDireita(texto)

Alternativa

texto.removeEspacosDireita
removeEspacosEsquerda

Remove o excesso de espaços de um texto à direita.

Caracteres.removeEspacosEsquerda(texto)

Alternativa

texto.removeEspacosEsquerda
repetir

Repete um texto especificado de acordo com uma quantidade definida.

Caracteres.repetir(texto, repeticao)

Alternativa

texto.repetir(repeticao)

Um exemplo prático de utilização é para completar os caracteres de um campo de um leiaute bancário. Por exemplo, no leiaute tem o campo nome com 100 caracteres, porém, se o nome não tiver 100 caracteres, então, o campo deve ser preenchido com espaços em branco à direita.

Segue um exemplo de preenchimento de um campo do tipo String:

//Script convencional
arquivo = Arquivo.novo('arq.txt', 'txt', [encoding: 'iso-8859-1']);

dadosFuncionarios = Dados.dubai.v1.funcionarios

percorrer(dadosFuncionarios.busca()){
  arquivo.escrever(item.nome)
  arquivo.escrever(Caracteres.repetir(" ", 100 - item.nome.tamanho()))
  arquivo.escrever(item.rg)
  arquivo.escrever(Caracteres.repetir(" ", 10 - item.rg.tamanho()))
  arquivo.escrever('ABCDE')
  arquivo.novaLinha()
}

Resultado.arquivo(arquivo)


//Script otimizado
preencherComEspacos = { texto, tamanho -> 
   texto + Caracteres.repetir(" ", tamanho - texto.tamanho())
}

arquivo = Arquivo.novo('arq.txt', 'txt', [encoding: 'iso-8859-1']);

dadosFuncionarios = Dados.dubai.v1.funcionarios

percorrer(dadosFuncionarios.busca()){
  arquivo.escrever(preencherComEspacos(item.nome,100))
  arquivo.escrever(preencherComEspacos(item.rg, 10))
  arquivo.escrever('ABCDE')
  arquivo.novaLinha()
}

Resultado.arquivo(arquivo)

Segue um outro exemplo, utilizando o mesmo leiaute bancário, de preenchimento com zeros à esquerda para um determinado campo do tipo numérico:

//Função para preencher os espaços do número
formatarNumero = { numero, tamanho ->
 
  numeroTxt = "$item.id"
 
  //Observe que o preenchimento do campo está à esquerda, no caso, antes do Código.
  Caracteres.repetir('X', tamanho - numeroTxt.tamanho()) + numeroTxt
  
  //Observe que o preenchimento do campo está à direita, no caso, depois do Código.
  // numeroTxt + Caracteres.repetir('X', tamanho - numeroTxt.tamanho())
}

arquivo = Arquivo.novo('arq.txt', 'txt', [encoding: 'iso-8859-1']);

dadosFuncionarios = Dados.dubai.v1.funcionarios

percorrer(dadosFuncionarios.busca()){
  idString = "$item.id"
  //Aqui os números são preenchidos com os caracteres desejados sem utilizar a função formatarNumero()
  imprimir item.nome + Caracteres.repetir(" ", 100 - item.nome.tamanho()) + 
    Caracteres.repetir("X", 10 - idString.tamanho()) + item.id + '-ABCDE'


  arquivo.escrever(item.nome)
  arquivo.escrever(Caracteres.repetir(" ", 100 - item.nome.tamanho()))
  //Aqui é utilizado o recurso da função de preenchimento dos espaços - formatarNumero(a,b)
  arquivo.escrever(formatarNumero(item.id,10))
  arquivo.escrever('-ABCDE')
  arquivo.novaLinha()
}

Resultado.arquivo(arquivo)
sobrepor

Sobrepõe um texto em outro em uma determinada posição e com uma quantidade especifica de caracteres.

Caracteres.sobrepor(texto, posicaoInicial, quantidadeSobrepor, textoSobrepor)

Alternativa

texto.sobrepor(posicaoInicial, quantidadeSobrepor, textoSobrepor)
substituir

Substitui as ocorrências de uma expressão localizada em um texto por outra expressão.

Caracteres.substituir(texto, textoLocalizar, textoSubstituir)

Alternativa

texto.substituir(textoLocalizar, textoSubstituir)
subTexto

Obtém um número específico de caracteres de uma posição especifica de um texto.

Caracteres.subTexto(texto, inicio, tamanho)

Alternativa

texto.subTexto(inicio, tamanho)
tamanho

Obtem o tamanho de um texto.

Caracteres.tamanho(texto)

Alternativa

texto.tamanho
vazio

Verifica se uma palavra esta vazia.

Caracteres.vazio(valor)

Alternativa

valor.vazio
dividir

Esta função divide um texto de acordo com a expressão regular informada.

Caracteres.dividir("boa|tarde", ~/\|/) //[boa, tarde]

Alternativa

valor.dividir(~/\|/)

Expressões regulares

A API de Caracteres suporta o uso de expressões regulares para realizar diversas operações baseadas em um padrão em textos.

Caracteres.expressao(texto, expressao)
//ou
"texto".expressao(expressao)

As expressões são representadas na linguagem de scripts utilizando o seguinte padrão: ~/expressão/

Exemplo:

expNumeros = ~/\d+/
expLetras = ~/(?i)[a-z]/

Caracteres.expressao("1235", ~/[a-z]/)
"1235".expressao(~/[a-z]/)

As operações disponíveis em um expressão são:

equivalente()

Verifica se o texto é totalmente equivalente a expressão.

//verdadeiro pois todo o texto equivale a expressão regular informada
Caracteres.expressao("123", ~/\d+/).equivalente()

//falso pois apesar do texto conter números o valor não é totalmente equivalente à expressão.
"123AB".expressao(~/\d+/).equivalente()
totalGrupos()

Retorna o total de grupos da expressão regular.

expBoasVindas = "boa-tarde".expressao(~/boa-(tarde|noite)/)
imprimir expBoasVindas.totalGrupos() // 1
dividir()

Esta função divide um texto de acordo com a expressão regular informada retornando uma lista com os valores separados.

partes = "boa|tarde".expressao(~/\|/).dividir()
percorrer(partes){
   imprimir item
}

//Saída:
//boa
//tarde
substituirPor(valor)

Realiza a substituição de todos os valores da expressão encontrados no texto pelo valor informado no parâmetro.

valor = "A1B2".expressao(~/[0-9]+/).substituirPor("*")
imprimir valor // A*B*
substituirPrimeiroPor(valor)

Realiza a substituição do primeiro valor da expressão encontrado no texto pelo valor informado no parâmetro.

valor = "A1B2".expressao(~/[0-9]+/).substituirPrimeiroPor("*")
imprimir valor // A*B2
encontrouPadrao()

Indica se o padrão da expressão foi encontrado no texto.

encontrouAlgo = "1235A".expressao(~/[0-9]+/).encontrouPadrao()
imprimir encontrouAlgo // true
concatenarValoresEncontrados()

Retorna todos os valores encontrados no texto pela expressão concatenados.

imprimir "B30th45".expressao(~/[0-9]+/).concatenarValoresEncontrados() // 3045
concatenarValoresEncontrados(separador)

Retorna todos os valores encontrados no texto pela expressão concatenados com o separador informado no parâmetro.

imprimir "B30th45".expressao(~/[0-9]+/).concatenarValoresEncontrados(",") // 30,45
Caractere de escape

A seguinte linha de código provoca um erro de sintaxe:

imprimir "These "names" sets apply to this country: American"

//Resultado:
Há um erro de sintaxe. (1:18) 

Isto corre pois o compilador interpreta os caracteres de aspas duplas dentro da cadeia de caracteres como delimitadores. Para eliminar o problema emprega-se o caractere de escape \ (barra contrária, ou backslash) antes das aspas:

imprimir "These \"names\" sets apply to this country: American"

//Resultado:
These "names" sets apply to this country: American

Veja este caso:

imprimir "C:\Temp\PDF\Leitura"

//Resultado:
Há um erro de sintaxe. (1:13) 

Isso ocorre porque o compilador interpreta as barras contrárias como caracteres de escape. Mas não é o que queremos. Queremos imprimir um caminho de pastas usando, literalmente, as barras contrárias:

//Adicione mais um caractere de escape em cada barra
imprimir "C:\\Temp\\PDF\\Leitura"

//Resultado:
C:\Temp\PDF\Leitura 

Dentro das expressões regulares, é necessário inserir, no caractere de escape, um til entre as barras contrárias:

~\caractere de escape aqui~\

Veja abaixo um exemplo usando caractere de escape. Repare que, dentro do último comando "expressao", há uma solcitiação de substituir uma barra por uma string vazia.

//O código abaixo remove caracteres especiais
caracteres = "Caráctéres\$  Es\$,pêc-i_a*i/s. fôrám removídós\$: !@#¨&*^´()\n"

caracteres.expressao(~/[à|á|ã]/).substituirPor("a")
          .expressao(~/[Á|Ã|À]/).substituirPor("A")
          .expressao(~/[é|ê]/).substituirPor("e")
          .expressao(~/[É|É]/).substituirPor("E")
          .expressao(~/[É|É]/).substituirPor("")
          .expressao(~/[í]/).substituirPor("i")  
          .expressao(~/[Í]/).substituirPor("I")  
          .expressao(~/[õ|ó|ô]/).substituirPor("o")  
          .expressao(~/[Õ|Ó|Ô]/).substituirPor("O")
          .expressao(~/[ü|ú]/).substituirPor("u")
          .expressao(~/[Ú|Ü]/).substituirPor("U")
          .expressao(~/[ç]/).substituirPor("c")
          .expressao(~/[Ç]/).substituirPor("C")
          .expressao(~/  /).substituirPor(" ")
          .expressao(~/[,|~\\n~\|~\/~\|-|_|*|.|!|@|#|$|%|¨|&|*|^|´|.|~|:|;|)|(|%|~\-|]/).substituirPor("")

//Resultado:
Caracteres Especiais foram removidos 

Múltiplas ocorrências e grupos

Uma expressão pode encontrar diversas ocorrências de um padrão em um texto. Estes valores podem ser organizados por grupos ou simplesmente por valor localizado.

Percorrendo todas as ocorrências encontradas em um texto:

achaNumeros = "B30th45".expressao(~/[0-9]+/)
percorrer(achaNumeros){
  imprimir item.valorEncontrado()
}
//Saída:
// 30
// 45
posicaoInicial()

Retorna a posição inicial do texto que coincida com a expressão localizada. Caso o padrão não seja encontrado retorna -1.

posicaoFinal()

Retorna a posição final do texto que coincida com a expressão localizada. Caso o padrão não seja encontrado retorna -1.

achaNumeros = "B30th45".expressao(~/[0-9]+/)
percorrer(achaNumeros){
  valor = item.valorEncontrado()
  inicio = item.posicaoInicial()
  fim = item.posicaoFinal()
  imprimir "O valor $valor inicia na posição $inicio e termina na posição $fim"
}

// Saída:
//O valor 30 inicia na posição 1 e termina na posição 3
//O valor 45 inicia na posição 5 e termina na posição 7
posicaoFinal()

Retorna a posição final do texto que coincida totalmente com a expressão utilizada. Caso não encontrado retorna -1.

imprimir "B30th".expressao(~/\d+/).posicaoFinal() //3
valoresGrupos()

Retorna uma lista com todos os valores encontrados pelos grupos especificados na expressão.

exp = "boa-tarde".expressao(~/boa-(tarde|noite)/)
percorrer(exp){
  imprimir item.valoresGrupos() // [tarde]
}
valorGrupo(indice)

Retorna o valor do grupo encontrado conforme índice e expressão.

exp = "bom-dia".expressao(~/(boa|bom)-(dia|tarde|noite)/)
percorrer(exp){
  imprimir item.valorGrupo(0) //bom
  imprimir item.valorGrupo(1) //dia
}

exp2 = "boa-tarde".expressao(~/(boa|bom)-(dia|tarde|noite)/)
percorrer(exp2){
  imprimir item.valorGrupo(0) //boa
  imprimir item.valorGrupo(1) //tarde
}
valorEncontrado()

Retorna o conteúdo do texto encontrado de acordo com a expressão/grupo utilizado.

exp = "Muito boa-tarde respeitável público.. Ops, acho que seria boa-noite!".expressao(~/(boa|bom)-(dia|tarde|noite)/)
percorrer(exp){
    imprimir item.valorEncontrado()
}

//Saída:
// boa-tarde
// boa-noite
concatenarValoresGrupos()

Retorna os valores dos grupos da expressão concatenados.

exp = "boa-Tarde".expressao(~/(boa|bom)-(dia|(t|T)arde|noite)/)
percorrer(exp){
  imprimir item.concatenarValoresGrupos() // boaTardeT
}
concatenarValoresGrupos(separador)

Retorna os valores dos grupos da expressão concatenados com o separador informado no parâmetro.

exp = "boa-Tarde".expressao(~/(boa|bom)-(dia|(t|T)arde|noite)/)
percorrer(exp){
  imprimir item.concatenarValoresGrupos("-") // boa-Tarde-T
}

Datas

adicionaDias

Adiciona uma quantidade especificada de dias à uma data.

Datas.adicionaDias(data, quantidadeDias)

Alternativa

data.adicionaDias(quantidadeDias)
adicionaHoras

Adiciona uma quantidade especificada de horas em uma data/hora.

Datas.adicionaHoras(data, quantidadeHoras)
adicionaMeses

Adiciona uma quantidade especificada de meses em uma data. Caso o dia da data especificadanão seja um dia válido para o mês resultante, Ex: 31/10/2011, adiciona 1 mês, valor inválido para nova data 31/11/2011, a diferença de dias será acrescentada na nova data, Ex:31/10/2011, adiciona 1 mês fica 01/12/2011.

Datas.adicionaMeses(data, quantidadeMeses)

Alternativa

data.adicionaMeses(quantidadeMeses)
adicionaMinutos

Adiciona uma quantidade especificada de minutos em uma data/hora.

Datas.adicionaMinutos(data, quantidadeMinutos)

Alternativa

data.adicionaMinutos(quantidadeMinutos)
adicionaSegundos

Adiciona uma quantidade especificada de segundos em uma data/hora.

Datas.adicionaSegundos(data, quantidadeMinutos)

Alternativa

data.adicionaSegundos(quantidadeMinutos)
ano

Obtem o ano em que se encontra uma determinada data.

Datas.ano(data)

Alternativa

data.ano
data

Gera uma data sem hora de acordo com o dia, mês e ano passados por parâmetro

Datas.data(ano, mes, dia)

Alternativa

ano.data(mes, dia)
dataHora

Gera uma data com a hora de acordo com o dia, mês, ano, hora e minuto passados por parâmetro

Datas.dataHora(ano, mes, dia, hora, minuto)

Alternativa

ano.dataHora(mes, dia, hora, minuto)
dia

Obtem o dia em que se encontra uma determinada data.

Datas.dia(data)

Alternativa

data.dia
diaSemana

Obtem o dia da semana em que se encontra uma determinada data, considerando-se o domingo comoprimeiro dia e o sábado como o sétimo dia.

Datas.diaSemana(data)

Alternativa

data.diaSemana
diferencaAnos

Calcula a diferença em anos entre duas datas.

Datas.diferencaAnos(menorData, maiorData)

Alternativa

menorData.diferencaAnos(maiorData)
diferencaDias

Calcula a diferença em dias entre duas datas.

Datas.diferencaDias(menorData, maiorData)

Alternativa

menorData.diferencaDias(maiorData)
diferencaHoras

Calcula a diferença em horas entre duas datas.

Datas.diferencaHoras(menorData, maiorData)

Alternativa

menorData.diferencaHoras(maiorData)
diferencaMeses

Calcula a diferença em meses entre duas datas.

Datas.diferencaMeses(menorData, maiorData)

Alternativa

menorData.diferencaMeses(maiorData)
diferencaMinutos

Calcula a diferença em minutos entre duas datas/hora.

Datas.diferencaMinutos(menorData, maiorData)

Alternativa

menorData.diferencaMinutos(maiorData)
diferencaSegundos

Calcula a diferença em segundos entre duas datas/hora

Datas.diferencaSegundos(menorData, maiorData)

Alternativa

menorData.diferencaSegundos(maiorData)
ehData

Verifica se um texto é uma data válida.

Datas.ehData(texto)

Alternativa

texto.ehData
extenso

Obtem a data por extenso.

Datas.extenso(data)

Alternativa

data.extenso
hoje

Obtem a data e hora do sistema operacional.

Datas.hoje()

Alternativa

Datas.hoje(data)
hora

Obtem a hora em que se encontra uma determinada data/hora.

Datas.hora(data)

Alternativa

data.hora
mes

Obtem o mês em que se encontra uma determinada data.

Datas.mes(data)

Alternativa

data.mes
minuto

Obtem os minutos referentes a uma determinada data/hora

Datas.minuto(data)

Alternativa

data.minuto
nomeDiaSemana

Obtem o nome do dia da semana.

Datas.nomeDiaSemana(data)

Alternativa

data.nomeDiaSemana
nomeMes

Obtem o nome do mês de uma data.

Datas.nomeMes(data)

Alternativa

data.nomeMes
periodo

Cria um período com data inicial e data final.

Datas.periodo(dataInicial, dataFinal)
removeDias

Remove uma quantidade especificada de dias de uma data.

Datas.removeDias(data, quantidadeDias)

Alternativa

data.removeDias(quantidadeDias)
removeMeses

Remove uma quantidade especificada de meses de uma data. Caso o dia da data especificada não seja um dia válido para o mês resultante, Ex: 31/12/2011, remove 1 mês, valor inválido para nova data 31/11/2011, a diferença de dias será acrescentada da nova data, Ex: 31/12/2011,remove 1 mês fica 01/12/2011.

Datas.removeMeses(data, quantidadeMeses)

Alternativa

data.removeMeses(quantidadeMeses)
segundo

Obtem os segundos referentes a uma determinada data/hora.

Datas.segundo(data)

Alternativa

data.segundo
formatar

Obtem o valor de uma data formatado de acordo com um padrão especificado.

Datas.formatar(data, formato)

Alternativa

data.formatar

Exemplos:

//data definida como dia 26/05/2017
imprimir data.formatar('yyyy-MM-dd') // 2017-05-26
imprimir data.formatar('MM/yyyy') // 05/2017
imprimir data.formatar('EEEE') // Sexta-feira

Padrões para formatação:

Letra Descrição Exemplos
y Ano 2009; 09
M Mês do ano Julho; Jul; 07
w Semana no ano 27
W Semana no mês 2
D Dia no ano 189
d Dia no mês 10
F Dia da semana no mês 2
E Nome do dia da semana Segunda-feira, Seg
u Número do dia da semana (1=Segunda.7=Domingo) 1
a Indicador de AM/PM AM
H Hora no dia (0-23) 0
k Hora no dia (1-24) 24
K Hora no dia (0-11) 0
h Hora no dia (1-12) 12
m Minuto na hora 55
s Segundos no minuto 30
S Milissegundo 978

Numeros

absoluto

Calcula o valor absoluto de um número.

Numeros.absoluto(valor)
arredonda

Arredonda um valor.

Numeros.arredonda(valor, casasDecimais)
coseno

Calcula o co-seno de um ângulo.

Numeros.coseno(valor)
decimal

Converte o valor de um texto em um número decimal de alta precisão.

Numeros.decimal(texto)
ehNumero

Verifica se um texto é um número válido.

Numeros.ehNumero(texto)
exponencial

Obtem o exponencial de um número específico.

Numeros.exponencial(numero)
fatorial

Obtem o fatorial de um número.

Numeros.fatorial(numero)
inteiro

Converte o valor de um texto em um número inteiro. Caso o texto represente um número decimal, este será truncado para um inteiro, ou seja, a parte decimal será descartada.

Numeros.inteiro(texto)
logaritmo

Informa o logaritmo natural de um número.

Numeros.logaritmo(valor)
logaritmo10

Informa logaritmo de base 10 de um número.

Numeros.logaritmo10(valor)
maximo

Obtem o maior valor entre dois números.

Numeros.maximo(valor1, valor2)
minimo

Obtem o menor valor entre dois números.

Numeros.minimo(valor1, valor2)
numero

Converte o valor de um texto em um número, retornando o tipo Long para números inteiros e Double para números decimais.

IMPORTANTE! Esta função NÃO deve ser utilizada para trabalhar com valores monetários. O tipo Double não é adequado para esse fim e vai resultar em imprecisões que ao longo de um cálculo podem alterar de forma significativa o resultado. A função adequada para este fim é a Numeros.decimal.

Numeros.numero(texto)
pi

Multiplica o valor de PI pelo número especificado.

Numeros.pi(valorMultiplicar)
piso

Obtem o maior número que é menor ou igual ao número espedificado, sendo este número inteiro.

Numeros.piso(valor)
raiz

Calcula a raiz quadrada de um número.

Numeros.raiz(valor)
randomico

Obtem um número aleatório entre 1 a um valor limite especificado.

Numeros.randomico(numeroDelimitador)
resto

Retorna o resto da divisão realizada entre o dividendo e o divisor, que são passados porparâmetro.

Numeros.resto(valorDividendo, valorDivisor)
seno

Calcula o seno de um ângulo.

Numeros.seno(valor)
seZero

Testa os valores passados como parâmetro e retorna o primeiro diferente de zero.

Numeros.seZero(valor1, valor2, valorN)
tangente

Calcula a tangente de um ângulo.

Numeros.tangente(valor)
teto

Obtem o menor número que é maior ou igual ao número espedificado, sendo este número inteiro.

Numeros.teto(valor)
trunca

Trunca um valor de acordo com o número de casas decimais especificadas.

Numeros.trunca(valor, casasDecimais)

JSON

ler

Converte um json em um mapa

pessoa = JSON.ler('{"nome":"João da Silva"}')
imprimir pessoa.nome // imprimie João da Silva
escrever

Converte um objeto em json

json = JSON.escrever([nome: "João da Silva"])
imprimir json // imprimie {"nome":"João da Silva"}

API de Arquivos

O bfc-script disponibiliza uma API para leitura e escrita de arquivos. As funções são separadas por tipo de arquivo e são invocadas como métodos. Esta sessão abordará o uso de cada função da API de arquivos e os detalhes de cada implementação. Essas funções estarão disponíveis ao usuário final apenas através da Ferramenta de Scripts, e serão absorvidas plenamente conforme a utilização.

Leitura de arquivos

A leitura de arquivos está disponível através da função Arquivo.ler(). Esta função irá retornar uma implementação específica com operações distintas para realizar a leitura do arquivo conforme tipo. Esta função contém algumas variações para permitir diferentes origens e a passagem de parâmetros para as implementações:

  • Arquivo.ler(arquivo ou conteúdo): Realiza a leitura do arquivo utilizando a implementação padrão para arquivos texto.
  • Arquivo.ler(arquivo ou conteúdo, tipo do arquivo): Realiza a leitura do arquivo utilizando a implementação própria para o tipo de arquivo informado.
  • Arquivo.ler(arquivo ou conteúdo, tipo do arquivo, parametros): Realiza a leitura de arquivo utilizando a implementação própria para o tipo do arquivo informado permitindo a passagem de parâmetros específicos da implementação.

Exemplo de utilização:

    arquivoTxt = Arquivo.ler(origem, 'txt');
    arquivoCsv = Arquivo.ler('Bob|Esponja', 'csv', [delimitador:'|']);

Escrita de arquivos

A criação de um novo arquivo está disponível através da função novo. Esta função irá retornar uma implementação específica com operações distintas para realizar a escrita do arquivo conforme tipo. A função novo contem algumas variações:

  • Arquivo.novo(nome do arquivo): Cria um novo arquivo utilizando a implementação padrão para arquivos texto.
  • Arquivo.novo(nome do arquivo, tipo do arquivo): Cria um novo arquivo arquivo utilizando a implementação própria para o tipo de arquivo informado.
  • Arquivo.novo(nome do arquivo, tipo do arquivo, parametros): Cria um novo arquivo utilizando a implementação própria para o tipo de arquivo informado permitindo a passagem de parâmetros específicos da implementação.

Exemplo de utilização:

    arquivoTxt = Arquivo.novo('Jones.txt')
    arquivoCsv = Arquivo.novo('Spencer.csv', 'csv', [entreAspas: 'N'])

Download dos arquivos

Por padrão a API de arquivos não disponibiliza para download os arquivos criados pelas implementações. Para que os mesmos sejam incluídos no arquivo zip de resultado da execução de um script através da Ferramenta de Scripts é necessário adicionar estes arquivos ao resultado.

txt = Arquivo.novo('teste.txt', 'txt')

//Adiciona o arquivo txt no zip do resultado
Resultado.arquivo(txt)

//Personaliza o nome do arquivo no zip do resultado
Resultado.arquivo(txt, 'meu_arquivo.txt')

//Personaliza o nome do arquivo e do diretório no zip do resultado
Resultado.arquivo(txt, 'meu_arquivo.txt', 'Arquivos texto')

//Personaliza o nome do arquivo
Resultado.nome('meu_resultado.zip')

Implementações

Arquivos Texto (txt)

Leitura

arquivo = Arquivo.ler(arquivo, 'txt')

Lendo um arquivo com um Encoding específico (Por padrão utiliza UTF-8):

arquivo = Arquivo.ler(arquivo, 'txt', [ encoding: 'iso-8859-1' ]);
lerLinha()

Realiza a leitura de uma linha do arquivo e retorna o conteúdo lido.

texto = arquivo.lerLinha()
contemProximaLinha()

Verificar se o arquivo sendo lido contém uma próxima linha

percorrer(enquanto: { arquivo.contemProximaLinha() }) {
    imprimir arquivo.lerLinha()
}

Escrita

arquivo = Arquivo.novo('FendaDoBiquine.txt')

Criando um novo arquivo com um Encoding específico (Por padrão utiliza UTF-8):

arquivo = Arquivo.novo('FendaDoBiquine.txt', 'txt', [ encoding: 'iso-8859-1' ]);
escrever(texto)

Realiza a escrita de um conteúdo no arquivo

arquivo.escrever('Jonathan Peters')
novaLinha()

Realiza a escrita de uma quebra de linha no arquivo

arquivo.escrever('Jonathan Peters')
arquivo.novaLinha()
arquivo.escrever('Oscar Randall')

Arquivos CSV (csv)

Leitura

arquivo = Arquivo.ler(arquivo, 'csv')

Lendo um arquivo com um Encoding específico (Por padrão utiliza UTF-8):

arquivo = Arquivo.ler(arquivo, 'txt', [ encoding: 'iso-8859-1' ]);

Parâmetros de leitura:

  • delimitador: Caracter que delimita os valores do arquivo CSV. Por padrão utiliza uma virgula.
  • encoding: Determina o encoding a ser usado na leitura do arquivo. Por padrão utiliza UTF-8.
lerLinha()

Realiza a leitura de uma linha do arquivo e retorna o conteúdo lido.

texto = arquivo.lerLinha()
contemProximaLinha()

Verificar se o arquivo sendo lido contém uma próxima linha

percorrer(enquanto: { arquivo.contemProximaLinha() }) {
    imprimir arquivo.lerLinha()
}
pularLinhas(int linhas)

Ignora a leitura da quantidade de linhas informado no parâmetro com base na linha atual

arquivo = Arquivo.ler('Nome,Endereco\nBob,Fenda do biquine', 'csv')
arquivo.pularLinhas(1)
imprimir arquivo.lerLinha() //Bob,Fenda do biquine
lerProximoValor()

Realiza a leitura do próximo valor do arquivo considerando o delimitador. Esta função realiza a leitura de todo o arquivo e não somente da linha atual.

arquivo = Arquivo.ler('Plancton,Mar\nBob,Fenda do Biquine', 'csv')
arquivo.lerProximoValor() //Plancton
arquivo.lerProximoValor() //Mar
arquivo.lerProximoValor() //Bob
contemProximoValor()

Indica se existe um próximo valor a ser lido no arquivo atual.

se(arquivo.contemProximoValor()){
    arquivo.lerProximoValor()
}

Escrita

arquivo = Arquivo.novo('FendaDoBiquine.csv', 'csv')

Criando um novo arquivo com um Encoding específico (Por padrão utiliza UTF-8):

arquivo = Arquivo.novo('FendaDoBiquine.csv', 'csv', [ encoding: 'iso-8859-1' ]);

Parâmetros de escrita:

  • delimitador: Caracter para delimitar os valores do arquivo CSV. Por padrão utiliza uma virgula.
  • entreAspas: Indica se os valores escritos no arquivo deverão estar entre aspas duplas. Utilizar S para Sim e N para Não.
  • encoding: Determina o encoding a ser usado na criação do arquivo. Por padrão utiliza UTF-8.
escrever(texto)

Realiza a escrita de um conteúdo no arquivo utilizando o delimitador parametrizado.

arquivo.escrever('Jonathan Peters')
novaLinha()

Realiza a escrita de uma quebra de linha no arquivo

arquivo.escrever('Jonathan Peters')
arquivo.novaLinha()
arquivo.escrever('Oscar Randall')

Arquivos XML (xml)

Os documentos XML são lidos e escritos por item/evento. Cada parte do documento é considerado um item e possui caracteristicas diferentes. No exemplo abaixo podemos afirmar que o documento XML possui 5 itens/eventos:

<pessoa completo="N">João da Silva</pessoa>

dos quais:

  1. Início de documento

  2. <pessoa : Início do elemento. Este tipo de item contem um nome(pessoa), pode conter atributos, namespaces etc.

  3. João da Silva: Texto do elemento. Este é um tipo de item considerado TEXTO. Diferente de um início de elemento ele não possui um nome e nem atributos.

  4. </pessoa> : Fim do elemento

  5. Fim de documento

Implementação

Leitura

arquivo = Arquivo.ler(arquivo, 'xml')
tipo()

Retorna o tipo do item sendo lido. Os valores disponíveis são:

  • INICIO_DOCUMENTO
  • FIM_DOCUMENTO
  • INICIO_ELEMENTO
  • FIM_ELEMENTO
  • ATRIBUTO
  • DTD
  • CDATA
  • NAMESPACE
  • TEXTO
  • COMENTARIO
  • ESPACO
  • DECLARACAO_NOTACAO
  • DECLARACAO_ENTIDADE
  • REFERENCIA_ENTIDADE
valor()

Retorna o valor texto do item corrente. Caso o item seja um elemento complexo irá retornar em branco. Tipos suportados: INICIO_ELEMENTO, FIM_ELEMENTO, ATRIBUTO, REFERENCIA_ENTIDADE, DECLARACAO_ENTIDADE.

contemValor()

Indica se o item atual contém um valor do tipo texto e se o mesmo é diferente de vazio.

se(arquivo.contemValor()){
    imprimir arquivo.valor()
}
contemNome()

Indica se o item atual contém um nome e se o mesmo é diferente de vazio.

nome()

Retorna o nome do item atual. Caso o item não seja suportado irá retornar em branco. Tipos suportados: INICIO_ELEMENTO, FIM_ELEMENTO, ATRIBUTO, REFERENCIA_ENTIDADE, DECLARACAO_ENTIDADE.

xml()

Retorna o item atual no formato XML.

namespaces()

Retorna uma lista contendo os namespaces presentes no item atual. Tipos suportados: INICIO_ELEMENTO, FIM_ELEMENTO, ATRIBUTO, NAMESPACE

Um Namespace contem as seguintes informações:

  • prefixo(): Prefixo do namespace

  • namespace(): Valor do namespace

contemNamespace(namespace)

Indica se o item atual contém um namespace declarado igual ao informado por parâmetro.

contemNamespace(namespace, prefixo)

Indica se o item atual contém um namespace declarado com valor e prefixo igual aos parâmetros.

atributos()

Retorna uma lista contendo os atributos presentes no item atual. Tipos suportados: INICIO_ELEMENTO

Um Atributo contem as seguintes informações:

  • prefixo(): Prefixo do namespace

  • namespace(): Namespace do atributo

  • nome(): nome do atributo

  • valor(): valor do atributo

percorrer(arquivo.atributos()){
    imprimir item.nome()
}
contemAtributo(nome)

Indica se o elemento atual contém um atributo com o nome informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO

contemAtributo(nome, namespace)

Indica se o elemento atual contém um atributo com o nome e namespace informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO

contemAtributo(nome, namespace, prefixo)

Indica se o elemento atual contém um atributo com o prefixo, nome e namespace informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO

atributo(nome)

Retorna o atributo do elemento com base no nome informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO

atributo(nome, namespace)

Retorna o atributo do elemento com base no namespace e nome informado nos parâmetros. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO

atributo(nome, namespace, prefixo)

Retorna o atributo do elemento com base no prefixo, namespace e nome informado nos parâmetros. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO

ehTipoInicioDocumento

Indica se o tipo do item atual é igual a INICIO_DOCUMENTO

ehTipoFimDocumento

Indica se o tipo do item atual é igual a FIM_DOCUMENTO

ehTipoTexto

Indica se o tipo do item atual é igual a TEXTO

ehTipoComentario

Indica se o tipo do item atual é igual a COMENTARIO

ehTipoFimElemento

Indica se o tipo do item atual é igual a FIM_ELEMENTO

ehTipoInicioElemento

Indica se o tipo do item atual é igual a INICIO_ELEMENTO

ehTipoEspaco

Indica se o tipo do item atual é igual a ESPACO

ehTipoCData

Indica se o tipo do item atual é igual a CDATA

ehTipoAtributo

Indica se o tipo do item atual é igual a ATRIBUTO

ehTipoNamespace

Indica se o tipo do item atual é igual a NAMESPACE

contemProximo()

Indica se o documento atual contem um próximo item a ser lido.

tipoProximo()

Retorna o tipo do próximo item caso a leitura do documento não tenha sido finalizada.

proximo()

Itera no documento passando a leitura para o próximo item. Tem como retorno o tipo do novo item.

percorrer(enquanto: { xml.contemProximo() }) { 
  
  xml.proximo()
  
  imprimir '#Dados do elemento: ' + conta
  imprimir 'Tipo: ' + xml.tipo()
              
  se(xml.contemValor()){
  	 imprimir 'Valor: ' + xml.valor()
  }
  
  se(xml.contemProximo()){
	imprimir 'Tipo do proximo elemento: ' + xml.tipoProximo()
  }
  
  se(xml.ehTipoInicioElemento()){ 
      imprimir 'Nome: ' + xml.nome()   
      percorrer(xml.atributos()){
  		imprimir 'Atributo: ' + item.nome() + '=' + item.valor()
  	}
  }
}
proximaTag()

Itera no documento passando a leitura para o próxima tag encontrada (INICIO_ELEMENTO ou FIM_ELEMENTO). Tem como retorno o tipo do novo item atual.

contemProximoElemento()

Indica se o documento atual contem um próximo elemento a ser lido.

proximoElemento()

Itera no documento passando a leitura para o próximo elemento (INICIO_ELEMENTO). Tem como retorno um valor booleano indicando se a leitura do próximo elemento foi realizada ou se o documento chegou ao fim.

xml = Arquivo.ler('<root><!-- Nomes dos colaboradores --><nome completo="N" composto="S">Marcos da Silva</nome><nome completo="S">Maria Joana Amaral</nome></root>', 'xml');

//Leitura baseada em elementos
percorrer(enquanto: { xml.proximoElemento() }) { 

  imprimir '#Dados do elemento:'
  imprimir 'Tipo: ' + xml.tipo()
  imprimir 'Nome: ' + xml.nome()   
  imprimir 'Valor: ' + xml.valor()

  percorrer(xml.atributos()){
	imprimir 'Atributo: ' + item.nome() + '=' + item.valor()
  }
}
proximoElemento(nome)

Itera no documento passando a leitura para o próximo elemento cujo nome seja igual ao valor informado no parâmetro. Tem como retorno um valor booleano indicando se a leitura do elemento foi realizada ou se o documento chegou ao fim e o mesmo não foi encontrado.

proximoElemento(nome, namespace)

Itera no documento passando a leitura para o próximo elemento cujo nome e namespace sejam iguais aos valores informados nos parâmetros. Tem como retorno um valor booleano indicando se a leitura do elemento foi realizada ou se o documento chegou ao fim e o mesmo não foi encontrado.

Escrita

arquivo = Arquivo.novo('FendaDoBiquine.xml', 'xml')

Parâmetros de escrita:

  • encoding: Encoding do documento XML. Por padrão utiliza UTF-8.
  • indentar: Indica se o documento será escrito indentado ou de forma linear. Utilizar S para Sim e N para Não.
namespacePadrao(namespace)

Define o namespace padrão do elemento XML.

prefixo(prefixo, uri)

Define o prefixo de uma URI.

escreverAtributo(nome, valor)

Escreve um novo atributo no elemento atual.

xml.escreverInicioElemento('pessoa')
xml.escreverAtributo('idade', '18')
//Saída: <pessoa idade="18">
escreverAtributo(nome, valor, namespace)

Escreve um novo atributo no elemento atual informando também o namespace.

escreverAtributo(nome, valor, namespace, prefixo)

Escreve um novo atributo no elemento atual informando também o namespace.

escreverCData(data)

Escreve uma área com conteúdo CDATA no documento XML.

escreverTexto(texto)

Escreve texto no item atual.

xml.escreverInicioElemento('pessoa')
xml.escreverTexto('João')
xml.escreverFimElemento('pessoa')
    
//Saída: <pessoa>João</pessoa>
escreverComentario(comentario)

Escreve um comentário no documento XML.

<!-- comentário -->
escreverNamespace(namespace)

Escreve um namespace no elemento atual.

escreverNamespace(namespace, prefixo)

Escreve um namespace com prefixo no elemento atual.

escreverDTD(dtd)

Escreve um DTD no documento XML.

escreverElementoVazio(elemento)

Escreve um elemento vazio no documento XML:

xml.escreverElementoVazio('vazio')
//Saída: <vazio />
escreverElementoVazio(elemento, namespace)

Escreve um elemento vazio e namespace no documento XML.

escreverElementoVazio(elemento, namespace, prefixo)

Escreve um elemento vazio e namespace com prefixo no documento XML.

escreverInicioDocumento()

Escreve a declaração de um documento XML.

escreverInicioDocumento(encoding)

Escreve a declaração de um documento XML informando o encoding.

escreverInicioDocumento(encoding, versao)

Escreve a declaração de um documento XML informando o encoding e versão.

xml.escreverInicioDocumento('UTF-8', '1.0')
//Saída: <?xml version='1.0' encoding='UTF-8'?>
escreverInicioDocumento(encoding, versao, standalone)

Escreve a declaração de um documento XML informando o encoding, versão e standalone.

escreverFimDocumento()

Escreve o fim do documento XML.

escreverInicioElemento(nome)

Escreve o início de um elemento no documento XML.

xml.escreverInicioElemento('pessoa')   
//Saída: <pessoa>
escreverInicioElemento(nome, namespace)

Escreve o início de um elemento com namespace no documento XML.

escreverInicioElemento(nome, namespace, prefixo)

Escreve o início de um elemento com namespace e prefixo no documento XML.

escreverFimElemento()

Escreve o fim do elemento atual.

nomeElementoAtual()

Retorna o nome do elemento sendo editado.

xml.escreverInicioElemento('pessoa')
xml.escreverInicioElemento('endereco')
imprimir xml.nomeElementoAtual() //Saída: endereco
xml.escreverFimElemento()
imprimir xml.nomeElementoAtual() //Saída: pessoa
escreverFimElementos()

Escreve o fim de todos os elementos atualmente abertos.

xml.escreverInicioElemento('pessoa')
xml.escreverInicioElemento('endereco')
xml.escreverFimElementos()

//Saída: <pessoa><endereco></endereco></pessoa>
escreverFimElementos(elementoParar)

Escreve o fim de todos os elementos abertos abaixo do elemento informado no parâmetro.

xml.escreverInicioElemento('pessoa')
xml.escreverInicioElemento('endereco')
xml.escreverInicioElemento('cidade')
xml.escreverInicioElemento('bairro')

xml.escreverFimElementos('endereco')

//Saída: <pessoa><endereco><cidade><bairro></bairro></cidade></endereco>
escreverElementoTexto(nome, valor)

Escreve um elemento texto no documento XML.

xml.escreverElementoTexto('nome', 'Maria')
//Saída: <nome>Maria</nome>
escreverElementoTexto(nome, valor, namespace)

Escreve um elemento texto com namespace.

escreverElementoTexto(nome, valor, namespace, prefixo)

Escreve um elemento texto com namespace e prefixo.

escrever(xml)

Escreve um bloco XML no arquivo atual.

xml.escrever('<nome>José</nome>')
contemElementoAberto()

Indica se o documento atual contém algum início de elemento sem um fim declarado.

escreverReferencia(nome, id, valor)

Escreve a referência à uma entidade no documento XML.

escreverInstrucaoProcessamento(target, conteudo)

Escreve as instruções de processamento no documento.

escreverEspaco(conteudo)

Escreve um item texto do tipo ESPACO

escreverEspacoIgnoravel(conteudo)

Escreve um item texto do tipo ESPACO ignorável.

Arquivos JSON (json)

Os documentos JSON são lidos e escritos por item/evento. Cada parte do documento é considerado um item e possui caracteristicas diferentes. No exemplo abaixo podemos afirmar que o documento JSON possui 4 itens:

{
    "nome": "João da Silva"
}

dos quais:

  1. { : Início do objeto. Este tipo de item pode conter propriedades.

  2. "nome": Nome do campo. Este é um tipo de item considerado TEXTO.

  3. "João da Silva"" : Valor do campo

  4. } Fim de objeto

Implementação

Leitura

json = Arquivo.ler('''
{
    "nome": "João da Silva",
    "telefones": [
            "4899999999999",
            "4899999999992"
    ]
}
''', 'json')

json.proximo() // inicio do objeto
json.proximo() // nome do primeiro campo

nomePessoa = ""
telefonesPessoa = []

percorrer(enquanto: { !json.ehFimObjeto() }) {

    nomeCampo = json.texto()

    se(nomeCampo == "nome"){
        nomePessoa = json.proximo().texto()
    }

    se(nomeCampo == "telefones"){

        json.proximo() // inicio do array
        json.proximo() // primero item do array

        telefones = []
        percorrer(enquanto: { !json.ehFimMatriz() }) {
            telefonesPessoa<<json.texto()
            json.proximo() // proximo item
        }
    }

    json.proximo() // avança para o proximo campo
}

pessoa = [nome: nomePessoa, telefones: telefonesPessoa]

proximo()

Avança a leitura para o próximo item

ehInicioObjeto()

Retorna um valor booleano indicando se o item atual é o inicio de um objeto

ehInicioMatriz()

Retorna um valor booleano indicando se o item atual é o inicio de uma matriz (array)

ehFimObjeto()

Retorna um valor booleano indicando se o item atual é o fim de um objeto

ehFimMatriz()

Retorna um valor booleano indicando se o item atual é o fim de uma matriz (array)

ehNomeCampo()

Retorna um valor booleano indicando se o item atual é o nome de um campo

ehTexto()

Retorna um valor booleano indicando se o item atual é um texto

ehNumero()

Retorna um valor booleano indicando se o item atual é um número

ehBooleano()

Retorna um valor booleano indicando se o item atual é um booleano

ehNulo()

Retorna um valor booleano indicando se o item atual é nulo

texto()

Retorna o valor do item atual como texto

numero()

Retorna o valor do item atual como número

booleano()

Retorna o valor do item atual como booleano

jsonParser()

Retorna a implementação nativa do parse que está sendo usado, para possibilitar implementações avançadas, acesse https://fasterxml.github.io/jackson-core/javadoc/2.5/com/fasterxml/jackson/core/JsonParser.html para mais informações.

Escrita

json = Arquivo.novo('pessoa.json', 'json')

json.escreverInicioObjeto()
json.escreverNomeCampo("nome")
json.escreverTexto("João da Silva")

json.escreverNomeCampo("telefones")
json.escreverInicioMatriz()

json.escreverTexto("4899999999999")
json.escreverTexto("4899999999992")

json.escreverFimMatriz()
json.escreverFimObjeto()

escreverInicioMatriz()

Escreve o inicio de uma matriz (array)

escreverFimMatriz()

Escreve o fim de uma matriz (array)

escreverInicioObjeto()

Escreve o inicio de um objeto

escreverFimObjeto()

Escreve o fim de um objeto

escreverNomeCampo(nome)

Escreve o nome do campo

{
    "valor": ""
}
escreverTexto(texto)

Escreve um texto

{
    "valor": "abc"
}
escreverNumero(numero)

Escreve um número

{
    "valor": 123
}
escreverBooleano(booleano)

Escreve um boolenao

{
    "valor": true
}
escreverNulo()

Escreve o valor nulo

{
    "valor": null
}
escreverObjeto(objeto)

Escreve um objeto completo

json.escreverObjeto([nome: "AAA"])
{
    "valor": {
        "nome": "AAA"
    }
}
escreverCampoTexto(nome, texto)

Escreve o nome e o valor de campo do tipo texto

escreverCampoBooleano(nome, booleano)

Escreve o nome e o valor de campo do tipo booleano

escreverCampoNulo(nome)

Escreve o nome e o valor de campo nulo

escreverCampoNumero(nome, numero)

Escreve o nome e o valor de campo do tipo numero

escreverCampoInicioMatriz(nome)

Escreve o nome e o inicio de um campo do tipo matriz (array)

escreverCampoInicioObjeto(nome)

Escreve o nome e o inicio de um campo do tipo objeto

escreverCampoObjeto(nome, objeto)

Escreve o nome e o valor de um campo do tipo objeto

jsonGenerator()

Retorna a implementação nativa do jsonGenerator que está sendo utilizado, para mais informações acesse https://fasterxml.github.io/jackson-core/javadoc/2.8/com/fasterxml/jackson/core/JsonGenerator.html

Arquivos ZIP (zip)

Leitura

A leitura de arquivos .zip está disponível através da seguinte chamada:

zip = Arquivo.ler('arquivo.zip', 'zip')

Onde iterando, é possível navegar entre os arquivos:

percorrer(zip) {
    
    //Imprime o nome do arquivo, sem considerar extensão
    imprimir item.nome;

    imprimir item.extensao;
    
    //Imprime o nome inteiro do arquivo, incluindo extensão e a sua estrutura de pastas
    imprimir item.nomeAbsoluto;
    
    //É possivel identificar se o item em questão representa um diretório
    imprimir item.ehDiretorio();
    
    //Recupera uma referência de um arquivo ao conteúdo do zip.
    arquivo = item.arquivo;
    
    //Pode ser utilizado outras apis para trabalhar no arquivo descompactado...
    Arquivo.ler(arquivo, 'txt');
}

Obs: Caso durante a geração do zip, seja utilizado um encoding diferente do UTF-8, o valor deve ser informado. Por exemplo:

zip = Arquivo.ler('arquivo.zip', 'zip', [encoding: 'CP437'])

Escrita

arquivo = Arquivo.novo('relatorios.zip', 'zip')
criarDiretorio

Realiza a criação de um diretório em branco no arquivo zip

arquivo = Arquivo.novo('relatorios.zip', 'zip')
arquivo.criarDiretorio('2016')
relatorios.zip
└─ 2016
adicionar(Arquivo)

Adiciona um arquivo no diretório raiz do arquivo zip

arquivo = Arquivo.novo('relatorios.zip', 'zip')
arquivo.adicionar(parametros.arquivo.valor)
adicionar(Arquivo, Nome do arquivo no zip)

Adiciona um arquivo no diretório raiz do arquivo zip utilizando o nome informado no segundo parâmetro

arquivo = Arquivo.novo('relatorios.zip', 'zip')
arquivo.adicionar(parametros.arquivo.valor, 'despesas.pdf')
relatorios.zip
└─ despesas.pdf
adicionar(Arquivo, Nome do arquivo no zip, Diretório)

Adiciona um arquivo em um determinado diretório do arquivo zip utilizando o nome informado como parâmetro. Caso o diretório não exista no arquivo zip o mesmo será criado.

arquivo = Arquivo.novo('relatorios.zip', 'zip')
arquivo.adicionar(parametros.arquivo.valor, 'despesas.pdf', '2016')
relatorios.zip
└─ 2016
    └─ despesas.pdf

É possível criar subdiretórios utilizando o separador / no nome do diretório:

arquivo = Arquivo.novo('relatorios.zip', 'zip')
arquivo.adicionar(parametros.arquivo.valor, 'despesas.pdf', '2016/Despesas')
relatorios.zip
└─ 2016
    └─ Despesas
        └─ despesas.pdf
adicionar(Lista de arquivos)

Adiciona um ou mais arquivos no diretório raiz do arquivo zip.

arquivo = Arquivo.novo('relatorios.zip', 'zip')
arquivo.adicionar([parametros.despesas.valor, parametros.receitas.valor])
relatorios.zip
├─ Despesas.pdf
└─ Receitas.pdf
adicionar(Lista de arquivos, Diretório)

Adiciona um ou mais arquivos no diretório informado no parâmetro do arquivo zip. Caso o diretório não exista o mesmo será criado.

arquivo = Arquivo.novo('relatorios.zip', 'zip')
arquivo.adicionar([parametros.despesas.valor, parametros.receitas.valor], 'Arquivos')
relatorios.zip
└─ Arquivos
   ├─ Despesas.xml
   └─ Receitas.xml
comentario(Comentário)

Adiciona um comentário às informações do arquivo zip

Arquivo.novo('relatorios.zip', 'zip').comentario('Arquivo contendo os relatórios')

API de E-mail

O bfc-script disponibiliza uma API para envio de mensagens de e-mails. Essas funções estarão disponíveis ao usuário final apenas através da Ferramenta de Scripts, e serão absorvidas plenamente conforme a utilização.

Para criar uma nova mensagem de e-mail a ser enviada deve-se utilizar a função Email.novo():

msg = Email.novo()

Uma mensagem contem diversas caracteristicas que serão apresentadas a seguir, uma vez configurada a mensagem, o envio é realizado através da função enviar() da mensagem.

msg = Email.novo()
//configurações da mensagem
msg.enviar()

Mensagem

de(email)

Define o email do remetente da mensagem

de(email, nome)

Define o email e nome do remetente da mensagem

para(email)

Adiciona um email de destinatário na mensagem

para(email, nome)

Adiciona um email e nome de destinatário na mensagem

copiaPara(email)

Adiciona um email de cópia na mensagem

copiaPara(email, nome)

Adiciona um email e nome de cópia na mensagem

copiaOcultaPara(email)

Adiciona um email de cópia oculta na mensagem

copiaOcultaPara(email, nome)

Adiciona um email e nome de cópia oculta na mensagem

responderPara(email)

Adiciona um email no qual a mensagem deverá ser respondia pelos destinatários

responderPara(email, nome)

Adiciona um email e nome no qual a mensagem deverá ser respondia pelos destinatários

mensagem(mensagem)

Define o conteúdo da mensagem

mensagemHtml(mensagem)

Define o conteúdo da mensagem como HTML

assunto(assunto)

Define o assunto da mensagem

cabecalho(nome, valor)

Adiciona um cabecalho no e-mail

autenticacao([usuario, senha, porta, host])

Autentica o envio do email. Exemplo de uso:

.autenticacao([ usuario: joao, senha: joao, porta: 587, host: smtp.live.com])

enviar()

Envia a mensagem de e-mail

Anexos

A API conta com 2 tipos de anexos disponíveis, sendo um baseado em fonte de arquivos e outro em URL. Exemplos de fonte de arquivos são os parâmetros do script do tipo Arquivo e artefatos gerados pela API de arquivos.

Criando anexos:

A criação de anexos pode ser realizada através de funções específicas da API ou de forma simplificada pela mensagem. Anexos criados pelas funções da API devem ser manualmente adicionados à mensagem. Por motivo de performance, caso o mesmo anexo do tipo Arquivo tenha que ser enviado para vários destinatários em mensagens diferentes, este deverá ser criado pela API uma única vez no processo.

Funções de anexo da API

Email.criarAnexoUrl()

Cria um novo anexo do tipo URL.

Email.criarAnexoUrl(url)

Cria um novo anexo do tipo URL informando o endereço.

Email.criarAnexoUrl(url, nome)

Cria um novo anexo do tipo URL informando o endereço e o nome a ser utilizado na mensagem.

Email.criarAnexoArquivo()

Cria um novo anexo do tipo arquivo.

Email.criarAnexoArquivo(arquivo)

Cria um anexo do tipo arquivo com base em uma fonte de arquivos.

 csv = Arquivo.novo('teste.csv', 'csv')
 csv.escrever('Valor')
 csv.fechar()
 
 //Arquivo criado pela API de arquivos
 anexoArquivo = Email.criarAnexoArquivo(csv)
 
 //Parâmetro do script tipo Arquivo
 anexoParametro = Email.criarAnexoArquivo(parametros.xml.valor)
 
 Email.novo()
    .anexar(anexoArquivo)
    .anexar(anexoParametro)
 
Email.criarAnexoArquivo(arquivo, nome)

Cria um anexo do tipo arquivo com base em uma fonte de arquivos. O nome do anexo será o mesmo informado no parâmetro nome.

Propriedades dos anexos

nome(nome)

Nome do anexo

descricao(descricao)

Descrição do anexo

dispostoParaVisualizacao()

Define a disposição do anexo para visualização INLINE no corpo do e-mail.

dispostoComoAnexo()

Define a disposição do anexo como ATTACHMENT (arquivo anexado).

incorporado()

Define o anexo como incorporado. Esta opção irá gerar um CID com base no nome do anexo e poderá ser utilizado no corpo da mensagem para referenciar o anexo.

naoIncorporado()

Define o anexo como não incorporado. Por padrão todos os anexos criados não são incorporados.

url(url)

Define a url para anexos do tipo URL.

arquivo(arquivo)

Define a fonte do arquivo para anexos do tipo Arquivo.

Funções de anexo da Mensagem

anexar(anexo)

Adiciona um anexo criado a partir das funções Email.criarAnexoXX() à mensagem

 anexo = Email.criarAnexoUrl('http://cnd.fgv.br/sites/cnd.fgv.br/files/teste_2.pdf', 'Nome do arquivo.pdf')
 msg.anexar(msg)
anexarArquivo(origem)

Adiciona um anexo à mensagem com base em uma fonte de arquivo. Exemplos de fontes são o valor de um parâmetro do script do tipo arquivo e um arquivo criado pela API de arquivos.

 csv = Arquivo.novo('teste.csv', 'csv')
 csv.escrever('Valor')
 csv.fechar()
 
 //Arquivo criado pela API de arquivos
 msg.anexarArquivo(csv)
 
 //Parâmetro do script tipo Arquivo
 msg.anexarArquivo(parametros.xml.valor) 
anexarArquivo(origem, nome)

Adiciona um anexo à mensagem com base em uma fonte de arquivo. O nome do anexo na mensagem será o mesmo informado no parâmetro nome.

anexarArquivo(anexoArquivo)

Adiciona um anexo criado a partir da função Email.criarAnexoArquivo à mensagem

anexarUrl(url)

Adiciona um anexo à mensagem com base em uma URL. O download do anexo será realizado e adicionado a mensagem.

anexarUrl(url, nome)

Adiciona um anexo à mensagem com base em uma URL. O download do anexo será realizado e um anexo com o nome parametrizado será adicionado.

 msg.anexarUrl('http://cnd.fgv.br/sites/cnd.fgv.br/files/teste_2.pdf', 'Nome do arquivo.pdf')
anexarUrl(anexoUrl)

Adiciona um anexo criado a partir da função Email.criarAnexoUrl à mensagem

Exemplos

Envio de e-mail autenticado

Email.novo()
	.autenticacao([ usuario: 'usuario', senha: 'senha', porta: 'porta', host: 'smtp.live.com' ])
	.de('betha@betha.com.br','Betha Sistemas')
	para(destinatario, 'Destinatário')
	.mensagem('Testando envio de email')
	.assunto('Envio cloud job')
	.enviar();

Envio de e-mail com anexos

email = Email.novo()
   
imagemAssinatura = Email.criarAnexoUrl('http://www.betha.com.br/site/images/betha-2.png', 'logo.png')
                        .dispostoParaVisualizacao()
                        .incorporado()
   
destinatario = 'destinatario@betha.com.br'
   
email.de('betha@betha.com.br','Betha Sistemas')
     .para(destinatario, 'Destinatário')
     .copiaPara(destinatario, 'Destinatário de cópia')
     .copiaOcultaPara(destinatario)
     .responderPara(destinatario, 'Nome para Resposta')
     .assunto('Teste de e-mail')
     .mensagemHtml('Este é um teste de e-mail. <br/><br/>' +
                      'Att, <br/><br/>' +
                      'Betha Sistemas <p/><p/>' + 
                      '<img src="cid:logo.png" width="319" height="51" />')
     .anexar(imagemAssinatura)
     .anexarUrl('http://cnd.fgv.br/sites/cnd.fgv.br/files/teste_2.pdf', 'Nome do arquivo.pdf')
     .enviar()
               
   
imprimir 'E-mail enviado com sucesso!'

Enviar arquivos dos parâmetros do script e API de Arquivos

csv = Arquivo.novo('teste.csv', 'csv')
csv.escrever('Valor')

anexoCsv = Email.criarAnexoArquivo(csv, 'teste.csv')
 
Email.novo()
     .de('betha@betha.com.br','Betha Sistemas')
     .para('destinatario@betha.com.br', 'Destinatário')
     .assunto('Teste de e-mail')
     .mensagem('Novo arquivo enviado.')
     .anexar(anexoCsv)
     .anexarArquivo(parametros.arquivo.valor, 'Arquivo.xml')
     .enviar()

API de Notificações

O bfc-script disponibiliza uma API para envio de notificações aos usuários do sistema. Essas funções estarão disponíveis ao usuário final apenas através da Ferramenta de Scripts, e serão absorvidas plenamente conforme a utilização.

Para criar uma nova notificação deve-se utilizar a função Notificacao.nova():

msg1 = Notificacao.nova()
msg2 = Notificacao.nova('Seja bem vindo!') //Define a mensagem na inicialização

Uma mensagem contem diversas caracteristicas que serão apresentadas a seguir, uma vez configurada a mensagem, o envio é realizado através da função enviar() da mensagem.

msg = Notificacao.nova()
//configurações da mensagem
msg.enviar()

Mensagem

para(usuarios)

Adiciona um usuário como destinatário da notificação. É possível adicionar vários usuários:

Notificacao.nova('Seja bem vindo!')
           .para('joao.silva', 'maria.silva')
           .enviar()

Também é possível adicionar o usuário logado como destinatário da notificação usando o identificador usuario.id:

Notificacao.nova('Seja bem vindo!')
           .para(usuario.id)
           .enviar()

Mais um exemplo, usando o usuário logado e outros usuários:

Notificacao.nova('Seja bem vindo!')
           .para(usuario.id, 'joao.silva', 'maria.silva')
           .enviar()

mensagem(mensagem)

Define a mensagem a ser enviada pela notificação

link(href)

Define o link a ser enviado pela notificação

link(href, titulo)

Define o link com título a ser enviado pela notificação

link(href, titulo, label)

Define o link, titulo e label a ser enviado pela notificação

enviar()

Realiza o envio da notificação aos destinatários.

Exemplos

Notificacao.nova('Seja bem vindo!')
           .para('usuario.betha', 'suite.betha')
           .enviar()
Notificacao.nova()
           .para('suite.betha')
           .mensagem('Obrigado!')
           .enviar()

API de Mensagens

O bfc-script disponibiliza uma API para envio de mensagens aos usuários do sistema. Essas funções estarão disponíveis ao usuário final apenas através da Ferramenta de Eventos, e serão absorvidas plenamente conforme a utilização.

A API possibilita o envio de quatro tipos de mensagens: erro, aviso, informação e sucesso, conforme o exemplo abaixo::

Mensagens.erro('Mensagem de erro')
Mensagens.aviso('Mensagem de aviso')
Mensagens.info('Mensagem de informação')
Mensagens.sucesso('Mensagem de sucesso')

É possível ainda adicionar mensagens parametrizadas:

Mensagens.erro('Mensagem de %s %s', 'erro', 'parametrizada')

API de SOAP

O bfc-script disponibiliza uma API para consumo de serviços web no padrão SOAP. Essas funções estarão disponíveis ao usuário final apenas através da Ferramenta de Scripts, e serão absorvidas plenamente conforme a utilização.

A API conta com algumas representações básicas de funcionamento sendo elas o Serviço, a Mensagem e a Resposta. O Serviço trata-se de um webservice SOAP a ser consumido, cada interação com esse webservice é realizado através de uma Mensagem. O produto desta mensagem quando executada é uma Resposta, essa resposta pode ser transformada em vários tipos de saídas, sendo elas um Leitor de XML da API de arquivo, Uma fonte de arquivo para utilização em conjunto com outras APIs (E-mail, Arquivos), O conteúdo XML da resposta em sí ou simplesmente a impressão da resposta no console do script.

Fluxo da API de SOAP

Serviço

Para criar um novo serviço SOAP à ser consumido deve-se utilizar a função Soap.servico, as opções disponíveis para criação e configurações dos serviços são:

Soap.servico(url)

Cria um novo servico com base na URL informada por parâmetro.

servico = Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx')

Soap.servico(url, namespace, prefixo)

Cria um novo serviço com base na URL informada por parâmetro. O namespace e prefixo (target namespace) informados serão utilizados como padrão na montagem da mensagem e manipulação dos elementos.

Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx',
             'http://ws.cdyne.com/',
             'ws')

Soap.servico(url, namespace, prefixo, usuario, senha)

Esta opção dá suporte a criação de serviços que utilizem mecanismos de autenticação HTTP básico permitindo informar também o usuário e senha de conexão como parâmetros.

Soap.criarNamespace(namespace, prefixo)

Cria uma representação de um namespace e prefixo para ser reutilizado nas funções da API, desta forma não é necessário informar namespace e prefixo a cada função.

Os valores podem ser acessados através das funções: namespace() e prefixo() da representação criada.

namespace = Soap.criarNamespace('http://ws.cdyne.com/', 'ws')
servico = Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx', namespacePadrao)

imprimir namespace.namespace()
imprimir namespace.prefixo()

cabecalho(nome, valor)

Adiciona um cabeçalho(header) HTTP na requisição SOAP do serviço.

servico = Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx')
servico.cabecalho('Authorization','Bearer 239e3d8e-1e93-4f2b-beff-c430103b9287')

cookie(nome, valor, path)

Adiciona um cookie na requisição SOAP do serviço.

servico = Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx')
servico.cookie('nome', 'valor', 'path')

detalhe: path é opcional e pode ser omitido.

mensagem()

Cria uma nova mensagem (envelope) à ser enviado ao serviço.

mensagem(namespace, prefixo)

Cria uma nova mensagem (envelope) à ser enviado ao serviço sobrescrevendo o namespace e prefixo padrão informados na criação do serviço.

mensagem(namespace)

Cria uma nova mensagem (envelope) à ser enviado ao serviço sobrescrevendo o namespace e prefixo padrão informados na criação do serviço. Esta opção deve ser utilizada com namespaces criados com a função Soap.criarNamespace.

tempoLimite(valor)

Define o tempo limite para a execução das requisições. O valor deve ser informado em milissegundos.

Mensagem

A representação de uma mensagem é composta basicamente de um cabeçalho e de um corpo. Ambos podem conter diversos elementos que por sua vez podem possuir outros elementos. Esta estrutura de árvore é baseada no padrão XML e é através dela que os parâmetros de entrada da funcionalidade do webservice são informados. Em resumo, uma mensagem destina-se à execução de um método/função de um serviço SOAP.

As opções disponíveis para montagem da mensagem são:

namespace(namespace, prefixo)

Adiciona um namespace a mensagem. Este namespace será adicionado ao envelope SOAP da mensagem e poderá ser utilizado na declaração dos elementos.

namespace(namespace)

Adiciona um namespace a mensagem. Este namespace será adicionado ao envelope SOAP da mensagem e poderá ser utilizado na declaração dos elementos.

namespaces()

Retorna uma lista com os namespaces declarados na mensagem.

namespacePadrao()

Retorna o namespaces definido como padrão da mensagem.

corpo()

Retorna o elemento do corpo da mensagem.

corpo(conteudo)

Escreve o conteúdo XML no corpo da mensagem. É importante lembrar que o conteúdo deverá ser um XML válido e utilizar os namespaces declarados na mensagem/corpo.

servico.mensagem().corpo('<ws:VerifyEmail><ws:email>teste@test.com</ws:email><ws:LicenseKey>example</ws:LicenseKey></ws:VerifyEmail>')

corpo(arquivo)

Escreve o conteúdo XML no corpo da mensagem com base em uma fonte de arquivo. É importante lembrar que o conteúdo do arquivo deverá ser um XML válido e utilizar os namespaces declarados na mensagem/corpo.

xml = Arquivo.novo('msg.xml', 'xml')
xml.fechar()

servico.mensagem().corpo(xml)

cabecalho()

Retorna o elemento do cabeçalho da mensagem.

cabecalho(conteudo)

Escreve o conteúdo XML no cabeçalho da mensagem. É importante lembrar que o conteúdo deverá ser um XML válido e utilizar os namespaces declarados na mensagem/corpo.

servico.mensagem().cabecalho('<ws:VerifyEmail><ws:email>teste@test.com</ws:email><ws:LicenseKey>example</ws:LicenseKey></ws:VerifyEmail>')

cabecalho(arquivo)

Escreve o conteúdo XML no cabecalho da mensagem com base em uma fonte de arquivo. É importante lembrar que o conteúdo do arquivo deverá ser um XML válido e utilizar os namespaces declarados na mensagem/corpo.

xml = Arquivo.novo('msg.xml', 'xml')
xml.fechar()

servico.mensagem().cabecalho(xml)

operacao(operacao)

Define a operação/método/função a ser executada no webservice. Este valor será informado no cabeçalho HTTP SOAPAction da mensagem.

executar()

Executa a chamada ao método/função do serviço SOAP e retorna uma representação da resposta.

executar(operacao)

Executa a chamada ao método/função do serviço SOAP e retorna uma representação da resposta. Esta opção permite que o a operação à ser executada seja informada diretamente pela função executar() sem a necessidade de pré-definir o valor através da função operacao().

requisicao()

Retorna uma representação para leitura da requisição SOAP realizada. Esta leitor de mensagem contém as mesmas funcionalidades da resposta e poderá ser utilizado para fins de depuração.

Elementos

Um elemento pode ser considerado como uma representação baseada em árvore para descrever uma informação na mensagem. Os elementos de uma mensagem (corpo, cabeçalho) SOAP utilizam o padrão XML.

As opções disponíveis para manipulação dos elementos são:

nome()

Retorna o nome do elemento.

namespace()

Retorna o namespace do elemento.

adicionarElemento(nome)

Adiciona/cria um novo elemento. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

elemEnderecos = servico.mensagem()
                       .corpo()
                       .adicionarElemento('endereco')
                           
elemEnderecos.adicionarElemento('residencial')    
elemEnderecos.adicionarElemento('comercial')                                     

O elemento elemEnderecos irá produzir o seguinte XML na mensagem:

<endereco>
    <residencial></residencial>
    <comercial></comercial>
</endereco>

adicionarElemento(nome, namespace, prefixo)

Adiciona/cria um novo elemento informando também o namespace e prefixo de declaração do elemento. É importante lembrar que o namespace utilizado deve estar declarado na mensagem ou elemento pai. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

adicionarElemento(nome, namespace)

Adiciona/cria um novo elemento informando também o namespace de declaração do elemento. É importante lembrar que o namespace utilizado deve estar declarado na mensagem ou elemento pai. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

adicionarTexto(texto)

Adiciona conteúdo do tipo texto à um elemento. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

elemEnderecos.adicionarElemento('residencial')
             .adicionarTexto('Rua geral')

produzirá:

<residencial>Rua geral</residencial>

adicionarElementoTexto(nome, texto)

Adiciona um novo elemento com conteúdo do tipo texto. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

elemEnderecos.adicionarElementoTexto('residencial', 'Rua geral')

produzirá:

<residencial>Rua geral</residencial>

adicionarElementoTexto(nome, texto, namespace, prefixo)

Adiciona um novo elemento com conteúdo do tipo texto indicando também o namespace e prefixo que o elemento está vinculado. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

adicionarElementoTexto(nome, texto, namespace)

Adiciona um novo elemento com conteúdo do tipo texto indicando também o namespace e prefixo que o elemento está vinculado. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

adicionarAtributo(nome, valor)

Adiciona um novo atributo ao elemento atual. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

elemEnderecos.adicionarElemento('residencial')
             .adicionarAtributo('principal','N')

produzirá:

<residencial principal="N">Rua geral</residencial>

adicionarAtributo(nome, valor, namespace, prefixo)

Adiciona um novo atributo ao elemento atual indicando também o namespace e prefixo ao qual o atributo está vinculado. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

adicionarAtributo(nome, valor, namespace)

Adiciona um novo atributo ao elemento atual indicando também o namespace e prefixo ao qual o atributo está vinculado. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

adicionarNamespace(namespace, prefixo)

Adiciona a declaração de um namespace ao elemento atual. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

adicionarNamespace(namespace)

Adiciona a declaração de um namespace ao elemento atual. O retorno desta função é a representação do novo elemento criado e poderá ser utilizado para adicionar elementos filhos caso necessário.

elementoAnterior()

Utilizado para navegação entre os elementos criados de uma mesma família. Esta opção contém a representação do elemento pai do elemento corrente.

servico.mensagem()
       .corpo()
       .adicionarElemento('pessoas')
       .adicionarElementoTexto('nome', 'João')
       .elementoAnterior() //O elemento pai do elemento nome é pessoas
       .adicionarElementoTexto('nome', 'Maria')
                

produzirá:

<pessoas>
    <nome>João</nome>
    <nome>Maria</nome>
</pessoas>

contemElementoAnterior()

Indica se o elemento atual possui um próximo elemento.

proximoElemento()

Utilizado para navegação entre os elementos criados de uma mesma família. Esta opção contém a representação do primeiro elemento filho do item corrente.

contemProximoElemento()

Indica se o elemento atual possui um elemento anterior.

elemPessoas = servico.mensagem()
                     .corpo()
                     .adicionarElemento('pessoas')

imprimir elemPessoas.contemElementoAnterior() // true pois corpo() e cabecalho() são elementos da mensagem            
imprimir elemPessoas.contemProximoElemento() // false

elemPessoas.adicionarElementoTexto('nomePessoa', 'João')

imprimir elemPessoas.contemProximoElemento() // true
elemPessoas.proximoElemento().nome() // nomePessoa

Métodos

Métodos são representações auxiliares para simplificar a montagem dos elementos da mensagem nos casos em que o webservice contém apenas parâmetros simples como entrada.

As opções disponíveis para criação de métodos a partir da mensagem são:

metodo(operacao)

Cria um novo método para a operação/função informado no parâmetro.

metodo(operacao, namespace, prefixo)

Cria um novo método utilizando a operação, namespace e prefixo informados nos parâmetros.

metodo(operacao, namespace)

Cria um novo método utilizando a operação e namespaces informados nos parâmetros.

Exemplo:

metodoVerificaEmail = servico.mensagem().metodo('VerifyEmail')

As funções disponíveis na representação de métodos são:

parametro(nome, valor) Adiciona um parâmetro de entrada ao método atual.

parametro(nome, valor, namespace) Adiciona um parâmetro de entrada ao método atual utilizando o namespace informado.

operacao()

Retorna a operação do método atual.

namespace()

Retorna o namespace do método atual.

elemento()

Retorna o elemento que representa o método atual.

requisicao()

Retorna uma representação para leitura da requisição SOAP realizada. Esta leitor de mensagem contém as mesmas funcionalidades da resposta e poderá ser utilizado para fins de depuração.

executar()

Executa a chamada do método atual e retorna uma representação da resposta.

mensagem()

Retorna a representação da mensagem em que o método foi criado.

Exemplo:

servico = Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx',
                       'http://ws.cdyne.com/', 'ws')
                       
resposta = servico.mensagem()
                  .metodo('VerifyEmail')
                  .parametro('email', parametros.email.valor)
                  .parametro('LicenseKey', '123')
                  .executar()

Resposta

Uma mensagem quando executada tem como retorno uma representação de resposta. Esta resposta pode ser processada/lida de maneiras distintas conforme necessidade.

As opções de processamento disponíveis são:

imprimir()

Imprime o conteúdo XML da resposta no console do editor de scripts.

conteudo()

Retorna o conteúdo XML da resposta no formato caracter.

resposta = servico.metodo('VerifyEmail')
       .parametro('email', parametros.email.valor)
       .parametro('LicenseKey', '123')
       .executar()

imprimir resposta.conteudo()

arquivo()

Retorna uma fonte de arquivo que contém o conteúdo da resposta. Esta opção deverá ser utilizada em conjunto com as demais APIs da engine de scripts.

resposta = servico.metodo('VerifyEmail')
       .parametro('email', parametros.email.valor)
       .parametro('LicenseKey', '123')
       .executar()

//Utilizando com a API de arquivos
xml = Arquivo.ler(resposta.arquivo(), 'xml')

//Utilizando o retorno como anexo da API de Email
Email.novo()
     .de('webservice@betha.com.br')
     .para('usuarios@betha.com.br')
     .assunto('Arquivo de resposta do webservice!')
     .anexarArquivo(resposta.arquivo(), 'resposta.xml')
     .enviar()     

xml()

Retorna um leitor de arquivos XML da API de Arquivos para o conteúdo da resposta.

resposta = servico.metodo('VerifyEmail')
       .parametro('email', parametros.email.valor)
       .executar()

xml = resposta.xml()
xml.proximoElemento('GoodEmail')

se(xml.valor() == 'true'){
  imprimir 'O endereço ' + parametros.email.valor + ' é um e-mail válido!'
} senao {
  imprimir 'O endereço ' + parametros.email.valor + ' é inválido!'
}

anexos()

Retorna uma lista com os anexos presentes na mensagem de retorno do serviço.

retorno = servico.mensagem().corpo(xml).executar()
anexos = retorno.anexos()

percorrer(anexos) {
    imprimir item.id()
    imprimir item.tipo()
    imprimir item.cabecalho("Content-Type") // mesmo valor do item.tipo()
    
    arquivo = item.arquivo()
}

anexo(id)

Localiza e retorna um anexo de acordo com o seu id

retorno = servico.mensagem().corpo(xml).executar()

anexo = retorno.anexo(xmlRetorno.atributo("href").valor());
Resultado.arquivo(anexo.arquivo());

Base64

Base64 é um método para codificação de dados para transferência na Internet. É utilizado frequentemente para transmitir dados binários por meios de transmissão que lidam apenas com texto, como por exemplo para enviar arquivos anexos por e-mail. Uma abordagem muito comum, inclusive praticada por alguns tribunais de contas, é o uso do Base64 para envio de arquivos binários em webservices SOAP.

Segue um exemplo de como utilizar a API provida no BFCScript para este propósito:

textoCodificado = BaseCodec.padrao64().codifica('Meu texto').texto()
textoDecodificado = BaseCodec.padrao64().decodifica(textoCodificado).texto()

É possível codificar um arquivo zip gerado em um script:

xml = Arquivo.novo('teste.xml', 'xml', [indentar:'S'])

xml.escreverInicioDocumento("UTF-8")
xml.escreverInicioElemento('root')

xml.escreverComentario('Nomes dos colaboradores sem setor')
xml.escreverInicioElemento('nome')
xml.escreverAtributo('completo', 'N')
xml.escreverAtributo('composto', 'S')
xml.escreverTexto('Marcos da Silva')
xml.escreverFimElemento()

xml.escreverInicioElemento('nome')
xml.escreverAtributo('completo', 'S')
xml.escreverTexto('Maria Joana Amaral')
xml.escreverFimElemento()

xml.escreverInicioElemento('setor')
xml.escreverAtributo('nome', 'Desenvolvimento')
xml.escreverInicioElemento('pessoas')
xml.escreverElementoTexto('nome', 'Javanildo Soares')
xml.escreverFimElemento()
xml.escreverFimElemento()

xml.escreverFimElemento()
xml.escreverFimDocumento()

arquivo = Arquivo.novo('arquivo.zip', 'zip')
arquivo.adicionar(xml)

arquivoCodificado = BaseCodec.padrao64().codifica(arquivo).texto()
arquivoDecodificado = BaseCodec.padrao64().decodifica(arquivoCodificado).texto()

Ou ainda codificar um arquivo recebido por parâmetro:

arquivoCodificado = BaseCodec.padrao64().codifica(parametros.arquivo.valor).texto()
arquivoDecodificado = BaseCodec.padrao64().decodifica(arquivoCodificado).texto()

É possível ainda definir o encoding para codificação e/ou decodificação (o encoding padrão é UTF-8):

textoCodificado = BaseCodec.padrao64().codifica('áé', 'iso-8859-1').texto()
textoDecodificado =  BaseCodec.padrao64().decodifica(textoCodificado).texto('iso-8859-1')

Também é possível decodificar para um arquivo de forma que o mesmo possa ser utilizado na API de Arquivos:

conteudoDecodificado = BaseCodec.padrao64().decodifica(zipCodificado)
zip = Arquivo.ler(conteudoDecodificado.arquivo(), 'zip')

pecorrer(zip) { item ->
	imprimir item.nome
}

A função BaseCodec está disponível ao usuário final apenas através da Ferramenta de Scripts.

Atenção: existe um limite cumulativo de 30MB para codificar/decodificar conteúdo Base64 durante uma execução de script. A soma de todo o conteúdo processado deve ficar sempre abaixo deste limite. Lembrando que este limite incinde sobre a saída gerada pela API, e não sobre as entradas utilizadas.

Contornando a limitação cumulativa

Caso seja necessário codificar ou decodificar diversos conteúdos e esteja sendo esbarrado no limite cumulativo, pode-se utilizar métodos que não retornam como o resposta o conteúdo (não alocando no contexto global), e sim disponibilizam ele dentro de uma closure, que acaba liberando a memória alocada mais rapidamente:

input = 'Meu texto'; //poderia ser um arquivo
BaseCodec.padrao64().codifica(input) { conteudo ->
   imprimir conteudo.texto();
}

inputEncoded = 'YWJjZGVmZ2hp'; //poderia ser um arquivo
BaseCodec.padrao64().decodifica(inputEncoded) { conteudo ->
   imprimir conteudo.texto();
}

Fazendo o uso dessas chamadas, o limite passa a ser de 30mb de forma individual e não descontando do limite cumulativo global.

É recomendado que não se atríbua essa variável interna para um contexto maior, permitindo assim que a memória alocada seja liberada

Hash

Uma função de hash pode ser utilizada para verificar a integridade de arquivos, uma vez que uma hash é gerada com base em um texto ou arquivo, não existe uma maneira de fazer o caminho contrário. Uma abordagem muito comum, inclusive praticada por alguns tribunais de contas, é o uso de verificações de integridade de arquivos binários enviados em webservices SOAP.

Existem alguns algoritmos popularmente utilizados para gerar uma hash de algo, são eles MD5, SHA-1, SHA-256 e SHA-512:

padraoMD5()

hashMD5 = Hash.padraoMD5().codifica("qualquer texto").texto()

Quando utilizado um texto, é possível definir o charset para codificação (o charset padrão é UTF-8):

hashMD5 = Hash.padraoMD5().codifica("ë", "iso-8859-1").texto()

Quando utilizado um arquivo, não é possível definir o charset para codificação, o charset utilizado é o do arquivo:

xml = Arquivo.novo('teste.xml', 'xml', [indentar:'S'])
 
xml.escreverInicioDocumento("UTF-8")
xml.escreverInicioElemento('root')
 
xml.escreverComentario('Nomes dos colaboradores sem setor')
xml.escreverInicioElemento('nome')
xml.escreverAtributo('completo', 'N')
xml.escreverAtributo('composto', 'S')
xml.escreverTexto('Marcos da Silva')
xml.escreverFimElemento()
 
xml.escreverInicioElemento('nome')
xml.escreverAtributo('completo', 'S')
xml.escreverTexto('Maria Joana Amaral')
xml.escreverFimElemento()
 
xml.escreverInicioElemento('setor')
xml.escreverAtributo('nome', 'Desenvolvimento')
xml.escreverInicioElemento('pessoas')
xml.escreverElementoTexto('nome', 'Javanildo Soares')
xml.escreverFimElemento()
xml.escreverFimElemento()
 
xml.escreverFimElemento()
xml.escreverFimDocumento()

hashMD5 = Hash.padraoMD5().codifica(xml).texto()

padraoSHA1()

hashSHA1 = Hash.padraoSHA1().codifica("qualquer texto").texto()

Quando utilizado um texto, é possível definir o charset para codificação (o charset padrão é UTF-8):

hashSHA1 = Hash.padraoSHA1().codifica("ë", "iso-8859-1").texto()

Quando utilizado um arquivo, não é possível definir o charset para codificação, o charset utilizado é o do arquivo:

imprimir Hash.padraoSHA1().codifica(parametros.arquivo.valor).texto()

padraoSHA256()

hashSHA256 = Hash.padraoSHA256().codifica("qualquer texto").texto()

Quando utilizado um texto, é possível definir o charset para codificação (o charset padrão é UTF-8):

hashSHA256 = Hash.padraoSHA256().codifica("ë", "iso-8859-1").texto()

Quando utilizado um arquivo, não é possível definir o charset para codificação, o charset utilizado é o do arquivo:

zip = Arquivo.novo('arquivos.zip', 'zip');

zip.adicionar(parametros.arquivo.valor, 'teste.xml');

imprimir Hash.padraoSHA256().codifica(zip).texto()

padraoSHA512()

hashSHA512 = Hash.padraoSHA512().codifica("qualquer texto").texto()

Quando utilizado um texto, é possível definir o charset para codificação (o charset padrão é UTF-8):

hashSHA512 = Hash.padraoSHA512().codifica("ë", "iso-8859-1").texto()

Quando utilizado um arquivo, não é possível definir o charset para codificação, o charset utilizado é o do arquivo:

zip = Arquivo.novo('arquivos.zip', 'zip');
 
zip.adicionar(parametros.arquivo.valor, 'teste.xml');
 
imprimir Hash.padraoSHA512().codifica(zip).texto()

padraoHex()

hexadecimal = Hash.padraoHex().codifica("qualquer texto").texto()

Quando utilizado um texto, é possível definir o charset para codificação (o charset padrão é UTF-8):

hexadecimal = Hash.padraoHex().codifica("ë", "iso-8859-1").texto()

Quando utilizado um arquivo, não é possível definir o charset para codificação, o charset utilizado é o do arquivo:

zip = Arquivo.novo('arquivos.zip', 'zip');

zip.adicionar(parametros.arquivo.valor, 'teste.xml');

imprimir Hash.padraoHex().codifica(zip).texto()

Exemplos

Exemplo 1:

servico = Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx',
                       'http://ws.cdyne.com/', 'ws')

resposta = servico.mensagem()
                  .metodo('VerifyEmail')
                  .parametro('email', parametros.email.valor)
                  .parametro('LicenseKey', '123')
                  .executar()

xml = resposta.xml()
xml.proximoElemento('GoodEmail')

se(xml.valor() == 'true'){
  imprimir 'O endereço ' + parametros.email.valor + ' é um e-mail válido!'
} senao {
  imprimir 'O endereço ' + parametros.email.valor + ' é inválido!'
}

Exemplo 2:

servico = Soap.servico('https://www3.bcb.gov.br/wssgs/services/FachadaWSSGS',
                       'http://publico.ws.casosdeuso.sgs.pec.bcb.gov.br', 'ws')

resposta = servico.mensagem()
                  .metodo('getUltimoValorVO')
                  .parametro('in0', '1')
                  .executar()

xml = resposta.xml()
xml.proximoElemento('svalor')

notificacao = 'A cotação do dolar para hoje é de R$ ' + Caracteres.substituir(xml.valor(), '.', ',')

Notificacao.nova()
           .mensagem(notificacao)
           .para('usuario')
           .enviar()

Exemplo 3:

servico = Soap.servico('https://demo.voxtecnologia.com.br/servicos/ws-consulta-previa-localizacao',
                       'https://demo.voxtecnologia.com.br/servicos/ws-consulta-previa-localizacao', 'ws')

mensagem = servico.mensagem()
elemDadosConsulta = mensagem.corpo().adicionarElemento('dadosConsultaPreviaLocalizacao')
elemMensagem = elemDadosConsulta.adicionarElemento('mensagem')

elemMensagem.adicionarElemento('controle')  
            .adicionarElementoTexto('hash', '6cd61adb859800aa565d97290f798e01')
            .elementoAnterior()
            .adicionarElementoTexto('categMens', 'dadosConsultaPreviaLocalizacao')
            .elementoAnterior()
            .adicionarElementoTexto('data', '2015-10-28 09:54:44')
            .elementoAnterior()
            .adicionarElementoTexto('versao', '1.0')
		
elemMensagem.adicionarElemento('dadosConsultaPreviaLocalizacao')
            .adicionarElementoTexto('co_protocolo', 'PRP1512623548')
            .elementoAnterior()
            .adicionarElementoTexto('dt_evento', '2016-05-23 12:00:00')
            .elementoAnterior()
            .adicionarElementoTexto('is_indeferido', 'false')
		
resposta = mensagem.executar()

email = Email.novo()
             .de('remetente@betha.com.br')
             .para('destinatario@betha.com.br')
             .assunto('Arquivo de resposta do webservice!')
             .mensagem('Segue arquivo em anexo do retorno do webservice')
             .anexarArquivo(mensagem.requisicao().arquivo(), 'envio.xml')
             .anexarArquivo(resposta.arquivo(), 'resposta.xml')
             .enviar()

Exemplo 4:

Armazenar o retorno em formato de anexo de um serviço.

xml = """
      <ser:entregarManifestacaoProcessual>
         <tip:idManifestante></tip:idManifestante>
         <tip:senhaManifestante></tip:senhaManifestante>
         <tip:numeroProcesso></tip:numeroProcesso>         
	 <tip:documento idDocumento="27" tipoDocumento="13" mimetype="application/pdf" descricao="Petição Inicial" tipoDocumentoLocal="14">
	 	<int:conteudo></int:conteudo>
	 </tip:documento>
	 	<tip:documento idDocumento="28" tipoDocumento="14" mimetype="application/pdf" descricao="Petição Inicial" tipoDocumentoLocal="14">
	 	<int:conteudo></int:conteudo>
 		</tip:documento>
         <tip:dataEnvio></tip:dataEnvio>
      </ser:entregarManifestacaoProcessual>
""";

imprimir xml;

servico = Soap.servico("https://eproc1ghml.tjsc.jus.br/homologa1g/ws/controlador_ws.php?srv=intercomunicacao2.2",
            "http://www.cnj.jus.br/servico-intercomunicacao-2.2.2/", "ser")

retorno = servico.mensagem()
                .namespace("http://www.cnj.jus.br/tipos-servico-intercomunicacao-2.2.2", "tip")
                .namespace("http://www.cnj.jus.br/servico-intercomunicacao-2.2.2/", "ser")
  				.namespace("http://www.cnj.jus.br/servico-intercomunicacao-2.2.2/", "int")
                .corpo(xml)
                .executar()

xmlRetorno = retorno.xml()

se (xmlRetorno.proximoElemento("Body") && xmlRetorno.proximoElemento("entregarManifestacaoProcessualResposta") && xmlRetorno.proximoElemento("recibo") && xmlRetorno.proximoElemento("Include")) {
  anexo = retorno.anexo(xmlRetorno.atributo("href").valor());
  Resultado.arquivo(anexo.arquivo());
}

imprimir retorno.conteudo()

API de Fonte de Dados

O bfc-script disponibiliza uma API para consumo das fonte de dados registradas pelas aplicações no catálogo de dados da Betha Sistemas. Essas funções estarão disponíveis ao usuário final apenas através da Ferramenta de Scripts, e serão absorvidas plenamente conforme a utilização.

Uma fonte de dados é composta por um Ativo, um Tema, e as Operações propriamente ditas. A API de script utiliza a seguinte estrutura para representar estes componentes:

Dados.ativo.versao.tema.operacao()

Ativo

Um Ativo pode ser considerado como a fonte/origem das informações. É nele que as operações de busca e manipulação de dados serão executadas.

Tema

Um tema pertence à um Ativo e representa um grupo de informação em comum. Exemplos de possíveis temas do Ativo folha seriam funcionarios, feriados, etc.

Operações

Operações são funções de busca/manipulação de dados disponibilizadas através de um Tema. Considerando o exemplo do tema funcionário do ativo folha, teriamos como possíveis operações a criação de um funcionário, busca dos registros, cálculo da folha, etc.

As Operações básicas de um Tema são:

  • Operação de busca
  • Operação de criação
  • Operação de atualização
  • Operação de exclusão

A disponibilidade de cada operação depende do ativo e tema utilizado.

Operação de busca

A operação de busca padrão conta com os seguintes parâmetros:

  • criterio: Parâmetro utilizado para filtrar os dados da busca.
tema.busca(criterio:"nome = 'Maria' and idade > 18")
  • ordernacao: Parâmetro utilizado para informar a ordem do resultado da busca. o valor deste parâmetro deve ser preenchido com o nome dos campos separados por virgula seguido da orientação (asc - Ascendente, desc - Descendente).
tema.busca(ordenacao:"nome,sobrenome asc, cidade desc")
  • campos: Parâmetro utilizado para informar quais campos do registro devem estar no resultado da busca.
tema.busca(campos:"nome, sobrenome, cidade(nome, uf)")
  • parametros: Utilizado para informar os valores dos parâmetros da operação. O nome dos parâmetros pode variar conforme ativo, tema e operação utilizada.
ativo.telefones.busca(parametros:[codigoFuncionario:15])
  • consolidado: Utilizado para informar se os valores retornados devem ser consolidados de acordo com o cadastro de contextos compartilhados. Caso seja passado true, será executado uma consulta na fonte de dados para cada entidade e database configurados, ignorando a entidade e database atual.
ativo.telefones.busca(consolidado:true)

Exemplo:

dadosFuncionarios = Dados.dubai.v1.funcionarios

percorrer(dadosFuncionarios.busca(ordenacao:"rg desc", campos:"nome, rg, id, dataAdmissao, dataNascimento")){ 
    imprimir "##Funcionario" 
    imprimir item.dataNascimento
    imprimir item
    
    imprimir "##Telefones: "   
    percorrer(dadosFuncionarios.telefones.busca(parametros:[codigoFuncionario:item.id])){ 
        imprimir item
    }  
}

Para retornar um único item, deve-se utilziar o parâmetro primeiro:true na operação de busca.

dadosFuncionarios.busca(ordenacao:"rg desc", campos:"nome, rg, id, dataAdmissao, dataNascimento", primeiro:true) 
  • valorPadrao: Parâmetro utilizado para ativar/desativar o valor padrão dos campos quando nulos. Caso este parâmetro seja verdadeiro os registros das fontes de dados poderão conter propriedades nulas que deverão ser tratadas pelo próprio script.
tema.busca(valorPadrao: falso)

Operação de criação

A operação de criação padrão conta com os seguintes parâmetros:

  • parametros: Utilizado para informar os valores dos parâmetros da operação. O nome dos parâmetros pode variar conforme ativo, tema e operação utilizada.
  • conteudo: Dados do registro a ser criado.
dadosFuncionarios = Dados.dubai.v1.funcionarios

telefone = [
  sequencial: 10,
  telefone: '488817858',
  tipo: 'OUTRO',
  tipoNumero: 'CELULAR',
  descricao: "Outro Celular"
]

telefoneCriado = dadosFuncionarios.telefones.cria(parametros: [codigoFuncionario:12], conteudo: telefone)

Operação de atualização

A operação de atualização padrão conta com os seguintes parâmetros:

  • parametros: Utilizado para informar os valores dos parâmetros da operação. O nome dos parâmetros pode variar conforme ativo, tema e operação utilizada.
  • conteudo: Dados do registro a ser atualizado.
dadosFuncionarios = Dados.dubai.v1.funcionarios

telefone = [
  telefone: '488817858'
]

telefoneAlterado = dadosFuncionarios.telefones.atualiza(parametros: [codigoFuncionario: 12, codigoTelefone: 15], conteudo: telefone)

Operação de exclusão

A operação de exclusão padrão conta com os seguintes parâmetros:

  • parametros: Utilizado para informar os valores dos parâmetros da operação. O nome dos parâmetros pode variar conforme ativo, tema e operação utilizada.
dadosFuncionarios.telefones.exclui(parametros: [codigoFuncionario: 12, codigoTelefone: 15])

API de Scripts

O bfc-script disponibiliza uma API que provê suporte à chamada entre scripts. Esta funcionalidade permite que uma lógica comum seja reutilizada por diversos scripts e está disponivel ao usuário final apenas através da Ferramenta de Scripts.

Para que seja possível invocar um script através da API é necessário configurar o identificador único do script à ser executado.

Executando scripts

A execução de um script é realizada utilizando a palavra reservada Scripts, seguida do identificador e método de execução:

Scripts.identificador.executar(parametros)

Scripts.calculos.somar.executar(parametros)
Scripts.dubai.consultas.funcionarios.executar(parametros)

identificador

Identificador único do script.

executar(parametros)

Função responsável pela execução do script.

parametros = [ p1 : 100, p2 : 200]

Scripts.somar.executar(parametros)

variaveis(Map)

Função responsável por enviar variáveis para outro script sem a necessidade de ter parâmetros no script que recebe essas variáveis.

O script de exemplo tem dois parâmetros do tipo arquivo, com os nomes arquivo e arquivo2, esses arquivos são enviados para o segundo script identificado por scriptb:

arquivos = [arquivo1: parametros.arquivo.valor, arquivo2: parametros.arquivo2.valor, val3:'teste teste 123']

Scripts.scriptb.variaveis(arquivos).executar();

No script que recebe as variáveis, os valores estão disponíveis da seguinte maneira:

Resultado.arquivo(variaveis.arquivo1)
Resultado.arquivo(variaveis.arquivo2)

imprimir variaveis.val3

O scriptb não precisa ter nenhum parâmetro para receber as variáveis e qualquer tipo pode ser passado por variaveis().

Outro exemplo seria um script principal que gera um arquivo csv com valores obtidos em execuções de outros três scripts:

Script principal:

//Escrita
csv = Arquivo.novo('teste.csv', 'csv', [delimitador:';'])

csv.escrever('ID')
csv.escrever('NOME')
csv.escrever('SALARIO')

Scripts.geraarquivo.funcionarios1.variaveis(arquivo:csv).executar()


geraArquivo = {col1, col2, col3 ->
  csv.novaLinha()
  
  csv.escrever(col1)
  csv.escrever(col2)
  csv.escrever(col3)
}

Scripts.geraarquivo.funcionarios2.variaveis(retorno:geraArquivo).executar()

retorno = Scripts.componente3.executar()

imprimir retorno.variaveis.geraArquivo2();  // imprime o valor teste2 no log de execução do script principal

retorno.variaveis.geraArquivo(csv)

Resultado.arquivo(csv)

Script com o identificador geraarquivo.funcionarios1:

csv = variaveis.arquivo
criterio = Criterio.onde('nome').igual('Paulo Henrique')

dadosFuncionarios = Dados.dubai.v1.funcionarios

percorrer(dadosFuncionarios.busca(campos:"nome, id, salario", criterio: criterio)){ 
    csv.novaLinha()
  	csv.escrever(item.id)
	csv.escrever(item.nome)
	csv.escrever(item.salario)
}

Script com o identificador geraarquivo.funcionarios2:

dadosFuncionarios = Dados.dubai.v1.funcionarios
criterio = Criterio.onde('nome').igual('Wellington')

id = '';
nome = '';
salario = '';

percorrer(dadosFuncionarios.busca(campos:"nome, id, salario", criterio: criterio)){ 
  	id = item.id
	nome = item.nome
	salario = item.salario
}

variaveis.retorno(id, nome, salario)

Script com o identificador componente3:

geraArquivo2 = {
  retornar "teste2"
}

geraArquivo = {csv ->
  csv.novaLinha()
  
  csv.escrever('Valor 31')
  csv.escrever('Valor 32')
  csv.escrever('Valor 33')
  
}

Ao executar o script principal o arquivo csv é gerado.

Retorno dos scripts

A execução de um script através da API sempre irá produzir um resultado, Este resultado é representado da seguinte forma:

resultado = Scripts.somar.executar([ p1 : 100, p2 : 200])

As opções disponíveis em um resultado são:

valor

Recupera o valor retornado pelo script executado.

imprimir resultado.valor()

vazio

Verifica se o script invocado retornou algum resultado.

resultado = Scripts.somar.executar([ p1 : 100, p2 : 200])
 
se(resultado.vazio){
   imprimir 'Nenhum resultado foi retornado pelo script somar'
}

variavel(nome)

Recupera o valor de uma variável declarada no script invocado.

resultado = Scripts.somar.executar([ p1 : 100, p2 : 200]) 
imprimir resultado.variavel('log') //Realizando soma

Note que a variável log foi declarada no conteúdo do script somar:

log = "Realizando soma"  
retornar parametros.p1.valor + parametros.p2.valor

retornar

O retorno de um script é realizado utilizando o comando retornar:

retornar 'Retornando uma mensagem'
ret = [valor: 1, mensagem: 'Retornando um mapa']
retornar ret

Executando scripts em lote

A execução de scripts em lote pode ser realizada através da criação de um script centralizador. Este script será responsável por orquestrar a execução dos demais scripts obedecendo a ordem cronológica de execução. Exemplo:

resultado = Scripts.somar.executar([ p1 : 100, p2 : 200])

Scripts.enviarEmailSoma.executar([valor: resultado.valor])

Scripts.enviarSmsSoma.executar([valor: resultado.valor])

Componentes

Quando utilizar componentes?

Quando para resolver uma dada situação seja mais indicado fragmentar o script, seja por uma questão de organização ou de praticidade em ter funções com finalidades específicas devidamente separadas.

Por exemplo: Consideremos que um dado Tribunal de Contas exige bimestralmente a informação de todos os fornecedores de uma entidade via Web Service. Neste caso, poderiam existir os seguintes recursos:

  • Script principal, onde recebe os parâmetros para gerar as informações e enviá-las ao Tribunal de Contas;
  • Componente para identificar todos os fornecedores passíveis de envio ao Tribunal de Contas;
  • Componente para gerar o arquivo a ser enviado ao Tribunal de Contas;
  • Componente para enviar o arquivo ao Tribunal de Contas via Web Service;

Por que utilizar componentes?

São algumas vantagens na utilização dos componentes:

  • Melhor performance na execução do script: Por conta do recurso Exportar e Importar e também porque os componentes são executados exclusivamente a partir de um script que o invoca;
  • Organização de código: Por meio da fragmentação de código orientada ao objetivo fim do componente;
  • Privacidade de variáveis e closures (funções): Num componente é possível declarar variáveis e closures localmente, e então escolher quais serão expostos para quando o componente for importado por outro script;
  • Reutilização de código: Pode ser utilizado o mesmo componente em mais de um script;

Exemplos de utilização:

Exemplo 1: Este exemplo demonstra um componente que abstrai uma API REST.

Segue o código de um componente cujo identificador foi denominado restapi e que será chamado pelo script pai:

// variável local e privada ao componente
api = Http.servico('https://jsonplaceholder.typicode.com')
 
// closure privada ao componente
extractContent = { response ->
  conteudo = -1
  
  se (response.sucesso() && response.contemResultado()) {
    conteudo = response.conteudo()
  }
  
  retornar conteudo
}
 
getPost = { id ->
  extractContent(api.caminho("posts/$id").GET())
}
 
getComments = { idPost ->
  extractContent(api.caminho("posts/$idPost/comments").GET())
}
 
Scripts.exportar(
  buscarPublicacao: getPost,
  buscarComentarios: getComments
)

O comando exportar é utilizado para definir quais recursos serão expostos pelo componente. Repare que as declarações que não foram exportadas permanecem privadas ao componente, permitindo assim uma modelagem mais robusta de funcionalidades comuns a vários scripts.

Segue o código do script pai que chama o componente acima:

api = Scripts.restapi.importar()

publicacao = api.buscarPublicacao(1)

se (publicacao != -1) {
  imprimir "Publicacao: " + publicacao
}

comentarios = api.buscarComentarios(1)

se (comentarios != -1) {
  imprimir "Comentarios: " + comentarios
}

A importação dos recursos de um componente é realizada utilizando a palavra reservada Scripts, seguido do identificador e o método de importação. O resultado é atribuído a uma variável, a partir da qual os recursos importados podem ser acessados.

Alternativamente, pode-se utilizar a palavra reservada importar, passando como parâmetro o identificador do componente, no caso, fica assim:

api = importar "restapi"

Exemplo 2: Este exemplo trata de múltiplos componentes para um único Script Pai:

Existem 3 componentes:

  • Que simula uma calculadora;
  • Que executa um contador;
  • Que gera um log;

Componente 1: identificador ped.calculadora

add = { p1, p2 ->
  p1 + p2
}

sub = { p1, p2 ->
  p1 - p2
}

mul = { p1, p2 ->
  p1 * p2
}

div = { p1, p2 ->
  p1 / p2
}

exportar(
  somar: add,
  subtrair: sub,
  multiplicar: mul,
  dividir: div
)

Componente 2: identificador ped.contador

current = 0

inc = { amount = 1 -> current += amount }
dec = { amount = 1 -> current -= amount }
reset = { current = 0 }
get = { current }

Scripts.exportar(
        incrementar: { inc(1) },
        incrementarEm: inc,
        decrementar: { dec(1) },
        decrementarEm: dec,
        zerar: reset,
        atual: get
)

Componente 3: identificador ped.log

logFile = Arquivo.novo('log.txt')

logInfo = { text ->
  logFile.escrever("INFO: $text")
  logFile.novaLinha()
}

logError = { text ->
  logFile.escrever("ERROR: $text")
  logFile.novaLinha()
}

commit = {
  imprimir "Adicionado log no resultado"
  Resultado.arquivo(logFile, 'log.txt')
}

exportar(
  info: logInfo,
  erro: logError,
  salvar: commit
)

Finalmente, o Script Pai:

calculadora = importar "ped.calculadora"
contador = importar "ped.contador"

imprimir "2 + 2 = ${calculadora.somar(2, 2)}"
imprimir "2 - 2 = ${calculadora.subtrair(2, 2)}"
imprimir "2 * 2 = ${calculadora.multiplicar(2, 2)}"
imprimir "2 / 2 = ${calculadora.dividir(2, 2)}"

imprimir "Inicio: " + contador.atual()
imprimir "+1: " + contador.incrementar()
imprimir "+10: " + contador.incrementarEm(calculadora.somar(5, 5))
imprimir "Zerado: " + contador.zerar()

//------------------------------------------------
log = importar "ped.log"

log.info("Iniciando execução")

percorrer(ate: 5) {
  log.info("Executando passo #$indice")
}

log.erro("Erro na conversão dos valores")
log.salvar()

API de HTTP

O bfc-script disponibiliza uma API para consumo de serviços web HTTP. Essas funções estarão disponíveis ao usuário final apenas através da Ferramenta de Scripts, e serão absorvidas plenamente conforme a utilização.

A API conta com algumas representações básicas de funcionamento sendo elas a Requisição e a Resposta. A Requisição trata-se da definição de uma ou várias chamadas à um serviço HTTP comum, cada interação com esse serviço é realizado através dos métodos/verbos padrões do HTTP. O produto desta interação quando executada é uma Resposta, essa resposta pode ser transformada em vários tipos e saídas, sendo elas um JSON no formato de Mapa (quando a resposta do serviço for JSON), Uma fonte de arquivo para utilização em conjunto com outras APIs (E-mail, Arquivos), O conteúdo da resposta no formato de texto em sí ou simplesmente a impressão da resposta no console do script.

Fluxo da API de HTTP

Requisição

Para criar uma nova requisição HTTP deve-se utilizar a função Http.servico, a função recebe como parâmetro a URL do serviço a ser consumido:

servico = Http.servico('https://jsonplaceholder.typicode.com/posts')

A URL informada pode conter marcadores, estes marcadores serão substituídos por um determinado valor no momento da execução de um método HTTP.

servico = Http.servico('https://jsonplaceholder.typicode.com/posts/{ano}/{id}')

As opções disponíveis na API para interagir com um serviço são:

cookie(nome, valor)

Adiciona um cookie na requisição HTTP.

cookie(nome, valor, caminho, domínio, versão)

Adiciona um cookie na requisição HTTP informando também o caminho/path, domínio e versão do cookie.

cookie(Mapa[nome, valor])

Adiciona um ou vários cookies (nome/valor) na requisição HTTP com base em uma mapa.

servico = Http.servico('https://jsonplaceholder.typicode.com/posts')
servico.cookie('Usuario', 'João da Silva')
servico.cookie('Usuario', 'João da Silva', '/usuarios', 'betha.com.br', 1)
servico.cookie([ Usuario: 'João da Silva', Accesso: 'sim' ])

cabecalho(nome, valor)

Adiciona um cabeçalho/header na requisição HTTP.

cabecalho(nome, lista de valores)

Adiciona um cabeçalho/header com vários valores na requisição HTTP.

cabecalho(Mapa[nome, valor])

Adiciona um ou vários cabeçalhos/headers (nome/valor) na requisição HTTP com base em uma mapa.

servico.cabecalho('Authorization', 'Basic ABCF5065FGC==')
servico.cabecalho('User', ['murphy', 'luiz.silva'])
servico.cabecalho([ User: 'alex.o.leao'])

Parâmetros e caminhos

Para permitir que um mesmo serviço atenda as mais diversas situações encontradas em APIs HTTP, as funções caminho() e parametro() possuem um comportamento diferenciado das demais. Estas funções quando executadas criam uma cópia da requisição atual adicionado os respectivos parâmetro(s)/caminho(s). Desta forma a requisição original não é alterada e pode ser reutilizada para outras invocações no mesmo serviço.

Exemplo:

servico = Http.servico('https://jsonplaceholder.typicode.com')

//Gera uma chamada GET ao endereço https://jsonplaceholder.typicode.com/posts?id=5
servico.caminho('posts')
       .parametro('id', 5)
       .GET()

//Gera uma chamada GET ao endereço https://jsonplaceholder.typicode.com/users/admins?id=5
servico.caminho('users')
       .caminho('admins')
       .parametro('id', 2)
       .GET()

//Caso se deseje alterar a requisição original basta associar a cópia ao serviço original, desta forma
//todas as requisições criadas à partir deste serviço utilizaram estes caminhos
servico = servico.caminho('users')
                 .caminho('admins')

//Gera uma chamada GET ao endereço https://jsonplaceholder.typicode.com/users/admins/ativos?id=5
servico.caminho('ativos')
       .parametro('id', 5)
       .GET()

parametro(nome, valor)

Cria uma cópia da requisição atual adicionando um parâmetro de query.

parametro(nome, lista de valores)

Cria uma cópia da requisição atual adicionando um ou mais parâmetros de query na requisição.

parametro(Mapa[nome, valor])

Cria uma cópia da requisição atual adicionando um ou mais parâmetros de query na requisição com base no mapa informado no parâmetro.

servico.parametro('id', '1') //posts?id=1
servico.parametro('id', ['1', '2', '5']) //posts?id=1&id=2&id=5
servico.parametro([ id: '5', nome: 'joao']) //posts?id=1&nome=joao
servico = Http.servico(...)
servico.parametro('id', 1)
servico.parametro('id', 2)

imprimir servico.parametros() //[] - Vazio pois como cria uma cópia o serviço original não é alterado
servico = servico.parametro('id', 1)
                 .parametro('id', 2)

imprimir servico.parametros() //[id : [1,2]] - pois atribuímos a cópia o serviço original

caminho(caminho)

Cria uma cópia da requisição atual adicionando um caminho/path na URL corrente.

servico = Http.servico('https://jsonplaceholder.typicode.com')

servico.caminho('posts')
       .caminho('listar') //URL: https://jsonplaceholder.typicode.com/posts/listar

aceitarTipoMidia(midias)

Informa qual tipo de mídia é aceito pela requisição. O valor padrão é Http.TODOS.

servico.aceitarTipoMidia('application/json')

A API conta com algumas constantes para os tipos de mídia comuns:

  • Http.JSON: application/json
  • Http.XML: application/xml
  • Http.TEXTO_XML: text/xml
  • Http.XHTML: application/xhtml+xml
  • Http.TEXTO_HTML: text/html
  • Http.TEXTO: text/plain
  • Http.FORMULARIO: application/x-www-form-urlencoded
  • Http.MULTIPART: multipart/form-data
  • Http.ARQUIVO: application/octet-stream
  • Http.TODOS: */*
servico.aceitarTipoMidia(Http.JSON)
servico.aceitarTipoMidia([Http.JSON, Http.XML])

credenciais(usuario, senha)

Configura a autenticação básica HTTP no serviço.

servico.credenciais('user', '123')

tempoLimite(valor)

Define o tempo limite para requisições executadas a partir do serviço. O valor deve ser informado em milissegundos.

// define timeout de 30 segundos
servico.tempoLimite(30000)

GET (Mapa[marcadores])

Executa uma chamada do tipo GET ao serviço retornando uma representação de resposta.

resposta = Http.servico('https://www.betha.com.br/users').GET()

svcUsuarios = Http.servico('https://www.betha.com.br/usuarios/{uf}/{nome}')
svcUsuarios.GET([uf: 'SC', nome: 'joao'])
svcUsuarios.GET([uf: 'SP', nome: 'maria'])

OPTIONS (Mapa[marcadores])

Executa uma chamada do tipo OPTIONS ao serviço retornando uma representação de resposta.

resposta = Http.servico('https://www.betha.com.br/users').OPTIONS()

HEAD (Mapa[marcadores])

Executa uma chamada do tipo HEAD ao serviço retornando uma representação de resposta.

resposta = Http.servico('https://www.betha.com.br/users').HEAD()

TRACE (Mapa[marcadores])

Executa uma chamada do tipo TRACE ao serviço retornando uma representação de resposta.

resposta = Http.servico('https://www.betha.com.br/users').TRACE()

DELETE ()

DELETE (Mapa[marcadores])

DELETE (dados, Mapa[marcadores])

DELETE (dados, Tipo de mídia)

DELETE (dados, Tipo de mídia, Mapa[marcadores])

Executa uma chamada do tipo DELETE ao serviço retornando uma representação de resposta. Os dados da requisição são enviados conforme tipo de mídia informado no parâmetro sendo Http.JSON (application/json) o valor padrão.

dados = [
    id: "5",
    author: "Uncle Bob",
]

resposta = Http.servico('https://www.betha.com.br/users').DELETE()
resposta = Http.servico('https://www.betha.com.br/users/{id}').DELETE([id: '5'])
resposta = Http.servico('https://www.betha.com.br/users/{id}').DELETE(dados, [id: '5'])
resposta = Http.servico('https://www.betha.com.br/users/{id}').DELETE(dados, Http.JSON)
resposta = Http.servico('https://www.betha.com.br/users/{id}').DELETE(dados, Http.JSON, [id: '5'])

POST (dados)

POST (dados, Mapa[marcadores])

POST (dados, Tipo de mídia)

POST (dados, Tipo de mídia, Mapa[marcadores])

POST (formulário)

POST (formulário, Mapa[marcadores])

Executa uma chamada do tipo POST ao serviço retornando uma representação de resposta. Os dados/formulário da requisição são enviados conforme tipo de mídia informado no parâmetro sendo Http.JSON (application/json) o valor padrão.

conteudo = [
    title: "The rabbit in the topper",
    author: "Mr. M",
]

resposta = Http.servico('https://www.betha.com.br/users')
               .POST(conteudo, Http.JSON)

resposta = Http.servico('https://www.betha.com.br/users')
               .POST('{"nome":"Test"}', Http.JSON)

resposta = Http.servico('https://www.betha.com.br/users/{id}')
               .POST(conteudo, Http.JSON, [id: 5])

PUT (dados)

PUT (dados, Mapa[marcadores])

PUT (dados, Tipo de mídia)

PUT (dados, Tipo de mídia, Mapa[marcadores])

PUT (formulário)

PUT (formulário, Mapa[marcadores])

Executa uma chamada do tipo PUT ao serviço retornando uma representação de resposta. Os dados/formulário da requisição são enviados conforme tipo de mídia informado no parâmetro sendo Http.JSON (application/json) o valor padrão.

conteudo = [
    title: "The rabbit in the topper",
    author: "Mr. M",
]

resposta = Http.servico('https://www.betha.com.br/users')
               .PUT(conteudo, Http.JSON)

resposta = Http.servico('https://www.betha.com.br/users')
               .PUT('{"nome":"Test"}', Http.JSON)

resposta = Http.servico('https://www.betha.com.br/users/{id}')
               .PUT(conteudo, Http.JSON, [id: 5])

PATCH (dados)

PATCH (dados, Mapa[marcadores])

PATCH (dados, Tipo de mídia)

PATCH (dados, Tipo de mídia, Mapa[marcadores])

PATCH (formulário)

PATCH (formulário, Mapa[marcadores]

Executa uma chamada do tipo PATCH ao serviço retornando uma representação de resposta. Os dados/formulário da requisição são enviados conforme tipo de mídia informado no parâmetro sendo Http.JSON (application/json) o valor padrão.

conteudo = [
    title: "The rabbit in the topper",
    author: "Mr. M",
]

resposta = Http.servico('https://www.betha.com.br/users')
               .PATCH(conteudo, Http.JSON)

resposta = Http.servico('https://www.betha.com.br/users')
               .PATCH('{"nome":"Test"}', Http.JSON)

resposta = Http.servico('https://www.betha.com.br/users/{id}')
               .PATCH(conteudo, Http.JSON, [id: 5])

METODO (método)

METODO (Mapa[marcadores], método)

METODO (método, dados)

METODO (método, dados, Mapa[marcadores])

METODO (método, tipo de mídia)

METODO (método, dados, tipo de mídia)

METODO (método, dados, tipo de mídia, Mapa[marcadores])

METODO (formulário)

METODO (formulário, Mapa[marcadores]

A função METODO é utilizada para realizar chamadas utilizando métodos personalizados. Tem como retorno uma representação de resposta. Os dados/formulário da requisição quando existentes são enviados conforme tipo de mídia informado no parâmetro sendo Http.JSON (application/json) o valor padrão.

dados = [
    title: "The rabbit in the topper",
    author: "Mr. M",
]

resp = Http.servico('https://www.betha.com.br/users')
           .METODO('GET'); //GET sem corpo

resp = Http.servico('https://www.betha.com.br/users/{id}')
           .METODO([id: 'joao'], 'GET'); //Marcadores de URL

resp = Http.servico('https://www.betha.com.br/users')
           .METODO('POST', dados)

Formulários

Para permitir o uso de serviços web onde os dados são recebidos no formato de formulários (form), a API conta com os seguintes facilitadores:

Formulario

Para criar um novo formulário de dados deve-se utilizar as funções criarFormulario() ou criarFormulario(tipo de mídia formulário). Estas funções criam um novo formulário utilizando como tipo de mídia o valor Http.FORMULARIO (application/x-www-form-urlencoded). A mídia pode ser alterada informando no parâmetro da função o tipo de mídia desejada.

servico = Http.servico('https://www.betha.com.br/users')
formulario = servico.criarFormulario()
parametro(nome, valor)

Adiciona um parâmetro ao formulário atual.

formulario = servico.criarFormulario()
                    .parametro("nome", "Betha")
parametro(Mapa[nome, valor])

Adiciona um ou mais parâmetros ao formulário atual bom base em uma mapa.

parametros = [nome: "Betha", site: "www.betha.com.br"]

formulario = servico.criarFormulario()
                    .parametro(parametros)
POST()
POST(Mapa[marcadores])

Envia o formulário utilizando como tipo de método o verbo POST.

parametros = [nome: "Betha", site: "www.betha.com.br"]

resposta  = servico.criarFormulario()
                    .parametro(parametros)
                    .POST()
PUT()
PUT(Mapa[marcadores])

Envia o formulário utilizando como tipo de método o verbo POST.

resposta  = servico.criarFormulario().PUT()
PATCH()
PATCH(Mapa[marcadores])

Envia o formulário utilizando como tipo de método o verbo POST.

resposta  = servico.criarFormulario().PATCH()
METODO(método)
METODO(método, Mapa[marcadores])

Envia o formulário utilizando como tipo de método um valor personalizado.

resposta  = servico.criarFormulario().METODO('POST')
Formulario Multipart

Para criar um novo formulário multipart deve-se utilizar as funções criarFormularioMultipart() ou criarFormularioMultipart(tipo de mídia formulário). Estas funções criam um novo formulário multipart utilizando como tipo de mídia o valor Http.MULTIPART (multipart/form-data). A mídia padrão pode ser alterada informando no parâmetro da função o tipo de mídia desejada.

Formulários multipart geralmente são utilizados para envio de dados e arquivos (upload) em um requisição.

servico = Http.servico('https://www.betha.com.br/users')
formulario = servico.criarFormularioMultipart()
parametro (nome, valor)
parametro (nome, valor, tipo de mídia)
parametro (Mapa[nome:valor])
parametro (Mapa[nome:valor])

Adiciona um ou mais parâmetro(s) ao formulário atual. Este parâmetro pode conter um tipo de mídia diferente ao do formulário.

servico = Http.servico('https://www.betha.com.br/users')
formulario = servico.criarFormularioMultipart()
                    .parametro("nome", "João")
                    .parametro("foto", arquivo, Http.ARQUIVO) //Fonte de arquivo da API de arquivos
                    .parametro([nome: "Betha", site: "www.betha.com.br"]) //valores baseados em um mapa
POST()
POST(Mapa[marcadores])

Envia o formulário utilizando como tipo de método o verbo POST.

resposta  = servico.criarFormularioMultipart().POST()
PUT()
PUT(Mapa[marcadores])

Envia o formulário utilizando como tipo de método o verbo PUT.

resposta  = servico.criarFormularioMultipart().PUT()
PATCH()
PATCH(Mapa[marcadores])

Envia o formulário utilizando como tipo de método o verbo PATCH.

resposta  = servico.criarFormularioMultipart().PATCH()
METODO(método)
METODO(método, Mapa[marcadores])

Envia o formulário utilizando como tipo de método um valor personalizado.

resposta  = servico.criarFormularioMultipart().METODO('POST')

Resposta

Uma chamada à um serviço web quando executada tem como retorno uma representação de resposta. Esta resposta pode ser processada/lida de maneiras distintas conforme necessidade.

As opções disponíveis são:

codigo()

Retorna o código HTTP da resposta.

resposta = ...

se (resposta.codigo() == 404) {
    imprimir 'Servidor não encontrado';
}

tipoMidia()

Retorna o tipo de mídia da resposta

imprimir 'A resposta é do tipo ' + resposta.tipoMidia()

ehJson()

Indica se o tipo de mídia da resposta é do tipo JSON

se (resposta.ehJson()){
    imprimir 'A resposta é um JSON'
}

sucesso()

Indica se o código resposta é considerado como sucesso segundo o padrão HTTP.

se (resposta.sucesso()){
    imprimir 'Tudo OK com a chamada HTTP'
}

tamanho()

Retorna o tamanho do conteúdo segundo o cabeçalho da resposta (Content-Length) recebido. Caso o valor não seja retornado pelo servidor ou seja inválido retorna -1.

contemResultado()

Indica se a requisição retornou algum resultado. Esta verificação é realizada com base no conteudo() e no tamanho() da mensagem e pode ou não estar presente dependendo de cada serviço utilizado.

ultimaModificacao()

Retorna a data da ultima modificação do resultado. Este valor é obtido do resultado da requisição e pode ou não estar presente dependendo de cada serviço utilizado.

cookie(nome)

Recupera um cookie da resposta com base no nome. Um cookie pode conter as seguintes informações:

  • nome(): Nome do cookie
  • valor(): Valor do cookie
  • valorCodificado(): Valor do cookie codificado no padrão URL
  • valorDecodificado(): Valor do cookie no padrão URL decodificado
  • caminho(): Caminho/Path do cookie
  • dominio(): Domínio do cookie
  • versao(): Versão do cookie (número)
  • comentario(): Comentário do cookie
  • idadeLimite(): Idade limite/tempo de duração do cookie (maxAge)
  • expira(): Data de expiração do cookie
  • ehSeguro(): Indica se é seguro
  • ehSomenteHttp(): Indica se é somente HTTP
usaEnterComoTab = resposta.cookie('UsaEnterComoTab')
imprimir 'O usuário utiliza enter como TAB: ' + usaEnterComoTab.valor()

contemCookie(nome)

Indica se a resposta contém um cookie com o nome informado no parâmetro.

cookies()

Recupera uma lista contendo os cookies retornados na resposta.

percorrer(resposta.cookies()){
    imprimir item.nome() + ':' + item.valor()
}

cabecalho(nome)

Recupera um cabeçalho(Header) da resposta com base no nome. Um cabeçalho poder conter as seguintes informações:

  • nome(): Nome do cabeçalho
  • valores(): Uma lista contendo todos os valores do cabeçalho
  • valor(): O primeiro valor encontrado do cabeçalho
imprimir resposta.cabecalho('Authorization').valor()

contemCabecalho(nome)

Indica se a resposta contém um cabeçalho (Header) com o nome informado no parâmetro.

cabecalhos()

Recupera uma lista contendo os cabeçalhos retornados na resposta.

percorrer(resposta.cabecalhos()){
    imprimir item.nome() + ':' + item.valor()
}

cookie(nome)

Retorna o cookie (caso definido)

resposta.cookie('nome').valor();

imprimir()

Imprime o conteúdo da resposta no console do editor de scripts.

conteudo()

Retorna o conteúdo da resposta no formato de caracteres.

resposta = Http.servico('https://www.betha.com.br/users').GET()
imprimir resposta.conteudo()

arquivo()

Retorna uma fonte de arquivo que contém o conteúdo da resposta. Esta opção deverá ser utilizada em conjunto com as demais APIs da engine de scripts.

resposta = Http.servico('https://www.betha.com.br/users').GET()

//Utilizando com a API de arquivos
texto = Arquivo.ler(resposta.arquivo(), 'txt')

//Utilizando o retorno como anexo da API de Email
Email.novo()
     .de('webservice@betha.com.br')
     .para('usuarios@betha.com.br')
     .assunto('Arquivo de resposta do webservice!')
     .anexarArquivo(resposta.arquivo(), 'resposta.txt')
     .enviar()

json()

Retorna o resultado de uma serviço JSON como Mapa/Lista de mapa.

resposta = Http.servico('https://www.betha.com.br/users/5').GET()

json = resposta.json()

imprimir json.id
imprimir json.nome

API de Assinatura

A assinatura de um documento por meio do script pode ser feita por arquivo ou por lote, apenas documentos xml e pdf podem ser assinados. Todo processo de assinatura envolve a criação de dois scripts, um para chamada da api de assinatura e outro de callback que é executado após a assinatura e pode receber o arquivo assinado e o arquivo original como parâmetro.

  • Observação: Para assinar qualquer documento, é necessário que a ferramenta de assinatura esteja rodando na maquina do usuário.

Assinando Documento

Exemplo de script de assinatura de um documento pdf:

assinar(arquivo, opcoes)
arquivo = parametros.arquivo.valor;

opcoes = [
    nome: 'pdfassinado', // nome do arquivo enviado por parâmetro para o script de callback
    tipo: 'pdf',
    lote: false,  // indica se é lote ou não
    retorno: [
        script: 'callback_assinatura',  // identificador do script de callback que deve ser executado após assinar o documento/lote
        parametro: 'assinado', // nome do parâmetro do tipo arquivo no script de callback que recebe o arquivo assinado  
        notificacao: true
    ],
    parametros: [
        original: arquivo
    ]
];

Assinador.assinar(arquivo, opcoes);

Exemplo de script de assinatura de um documento xml:

arquivo = parametros.arquivo.valor;

opcoes = [
    nome: 'xmlassinado', // nome do arquivo enviado por parâmetro para o script de callback
    tipo: 'xml',
    lote: false,  // indica se é lote ou não
    tagAssinatura: 'TEXTO', // nome da tag do xml que deve ser assinada
    retorno: [
        script: 'callback_assinatura',  // identificador do script de callback que deve ser executado após assinar o documento/lote
        parametro: 'assinado', // nome do parâmetro do tipo arquivo no script de callback que recebe o arquivo assinado  
        notificacao: true
    ],
    parametros: [
        original: arquivo
    ]
];

Assinador.assinar(arquivo, opcoes);

Assinando lote de documentos

Exemplo de script de assinatura de um lote de documentos xml:

assinar(arquivo, opcoes)
arquivo = parametros.arquivo.valor;
zip = Arquivo.novo('arquivos.zip', 'zip');
zip.adicionar(arquivo, 'a.xml');
zip.adicionar(arquivo, 'b.xml');

opcoes = [
    nome: 'xmlassinado', // nome do arquivo enviado por parâmetro para o script de callback
    tipo: 'xml',
    lote: true,  // indica se é lote ou não
    tagAssinatura: 'TEXTO', // nome da tag do xml que deve ser assinada
    retorno: [
        script: 'callback_assinatura',  // identificador do script de callback que deve ser executado após assinar o documento/lote
        parametro: 'assinado', // nome do parâmetro do tipo arquivo no script de callback que recebe o arquivo assinado  
        notificacao: true
    ],
    parametros: [
        original: zip
    ]
];

Assinador.assinar(zip, opcoes);

Múltiplos assinantes

Exemplo de script de assinatura de um documento PDF com múltiplos assinantes:

  • Observação: Apenas documentos do tipo PDF podem utilizar o recurso de múltiplos assinantes.
assinar(arquivo, opcoes)
arquivo = parametros.arquivo.valor;

opcoes = [
    nome: 'pdfassinado',
    tipo: 'pdf',
    lote: false,  
    assinantes: [
        incluirUsuarioCorrente: true, //indica se o usuário que solicitou a execução do script deve assinar o documento
        usuarios: [ // lista de assinantes
            'joao',
            'maria.silva'
        ]
    ],
    retorno: [
        script: 'callback_assinatura',
        parametro: 'assinado',  
        notificacao: true
    ],
    parametros: [
        original: arquivo
    ]
];

Assinador.assinar(arquivo, opcoes);

Script de callback

O script de callback é o script executado automaticamente após o a execução do script que chama a api de assinatura com Assinador.assinar(arquivo, opcoes).

Com base no script de assinatura do exemplo anterior, o script de callback deve ter dois parâmetros do tipo arquivo, que são:

  • assinado que foi definido no script de assinatura no item retorno.parametro do objeto de opcoes
  • original que também foi definido no script de assinatura no item parametros do objeto de opcoes

E por último o identificador que também foi definido no script de assinatura no item script do objeto de opcoes

API de Critério

Criação

A criação de um novo critério acontece das seguintes maneiras:

Iniciando uma expressão
Criterio.onde(campo);​

​Esse é o caso mais comum, onde a configuração padrão do critério já atende. Através dessa forma, o critério é criado já se iniciando uma expressão.

Criterio.onde('nome').igual('João');​
Configurando o critério

Configuração de parâmetro obrigatório:

Criterio.​novo( [ parametros: Criterio.obrigatorio() ] );

Configuração padrão:

Criterio.​novo( [ parametros: Criterio.​opcional() ] );​

Aceita um template que vai ser utilizado na geração da mensagem de erro quando um parâmetro de uma condição não tiver seu valor informado:

Criterio.​novo( [ parametros: Criterio.obrigatorio(​template_mensagem_validacao​) ] );​

​Por padrão, os valores utilizados como parâmetros, são opcionais, e caso seu valor seja nulo, a expressão é desconsiderada na geração do filtro:

​valorIdade = nulo; 
​valorNome = 'João';​ 
Criterio.onde('idade').igual(valorIdade).e('nome').igual(valorNome);

​O critério a acima geraria o seguinte filtro:

'nome = "João"'

Operações

Após a criação do critério, é possível fazer uso um conjunto de operações de comparação.

Operações de comparação
  • igual(valor)
  • diferenteDe(valor)
  • maiorQue(valor)
  • maiorOuIgualQue(valor)
  • menorQue(valor)
  • menorOuIgualQue(valor)
  • ehNulo() nome is null
  • naoEhNulo() nome is not null
  • ehVerdadeiro() responsavel is true
  • ehFalso() responsavel is false
  • naoEhVerdadeiro() responsavel is false
  • naoEhFalso() responsavel is true
  • comecaCom(valor) nome like "P%"
  • naoComecaCom(valor) nome not like "P%"
  • terminaCom(valor) nome like " %P"
  • naoTerminaCom(valor) nome not like " %P"
  • contem(valor) nome like "%João%"
  • naoContem(valor) nome not like "%João%"
  • contidoEm(valores) nome in ("João", "Maria")
  • naoContidoEm(valores) nome not in ("João", "Maria")

Exemplo de utilização:

Criterio.onde('idade').igual(40); 
Criterio.onde('casado).ehFalso(); 
Criterio.padrao().onde('agencia').ehNulo(); 
​Criterio.novo([ parametros: Criterio.obrigatorio('É necessário informar um valor para o campo ${campo}') ]).onde('idade').igual(27);

Obrigatoriedade dos valores

Caso seja necessário configurar a obrigatoriedade dos valores de forma pontual, é possível configurar em qualquer operação que aceite um valor como parâmetro:

Criterio.onde('idade').igual(valor, [ parametro: Criterio.obrigatorio() ]); 
Criterio.novo([ parametros: Criterio.obrigatorio('É necessário informar um valor para o campo ${campo}') ]).onde('idade').igual(27).e('nome').igual(valorNome, [ parametro: Criterio.opcional() ]);

Quando se configura a obrigatoriedade para todos os parâmetros do critério, a propriedade se chama parametros. Porém quando é configurado a nível de operação, se chama parametro.

Delimitador nas operações contidoEm/naoContidoEm

Para as operações contidoEm e naoContidoEm pode ser informado um delimitador para os valores que não são String.

Com delimitador

numero = [1,2,5] 
criterio = Criterio.onde('idade').contidoEm(numero, [delimitador: '"']);

O critério acima, gera o seguinte filtro:

idade in ("1", "2", "5")

Sem delimitador

numero = [1,2,5] 
criterio = Criterio.onde('idade').contidoEm(numero);

O critério acima, gera o seguinte filtro:

idade in (1, 2, 5)

Com formatação e delimitador

data1 = Datas.data(2017, 1, 11) 
data2 = Datas.data(2016, 1, 10) 
datas = [ data1, data2 ] 
criterio = Criterio.onde('aniversario').contidoEm(datas, [formatacao: "data", delimitador: "'"])

O critério acima, gera o seguinte filtro:

aniversario in ('2017-01-11', '2016-01-10')

Filtros compostos

Os filtros podem ser utilizados de forma composta, fazendo uso do e ou do ou:

Criterio.onde('idade').igual(40).e('nome').igual('João').ou('bairro').igual('centro');

O critério acima, gera o seguinte filtro:

'idade = 40 and nome = "João" ou bairro = "Centro"'

Agrupamento

O critério tem suporte a grupos, através da seguinte chamada:

Criterio.grupo(CriterioInterno);
Criterio.onde('a').igual(12).e(Criterio.grupo(Criterio.onde('b').igual(24).ou('c').igual(30)));

O critério acima, gera o seguinte filtro:

a = 12 e (b = 24 or c = 30)

Datas

Atualmente a engine de scripts tem suporte a datas através de seu formato completo (data e hora), e no critério ela é representada através do formato ISO 8601

Criterio.onde('dataNascimento').igual(Datas.hoje());

O critério acima, gera um filtro parecido com:

dataNascimento = 2018-02-05T09:37:09.009

Caso queira submeter a data no filtro utilizando outros formatos, as seguintes opções estão disponíveis:

​Informar uma das formatações pré-definidas (data e hora)

​​Criterio.onde('dataNascimento').igual(Datas.hoje()​, [formatacao: 'data']​); 
Criterio.onde('​horaAlmoco').igual(Datas.hoje()​, [formatacao: 'hora']​);​ ​​

Os critérios acima, geram os seguintes filtros:

dataNascimento = ​2018-02-05 
horaAlmoco = 09:43:45.045

​Informar uma ​formatação customizada

​​Criterio.onde('dataNascimento').igual(Datas.hoje(), [ formatacao: 'dd/MM/yyyy']);

API de Execução

O bfc-script disponibiliza uma API para a consulta das informações relacionadas a execução.

Consultar o protocolo da execução

Execucao.atual.protocolo

Consultar se a execução foi cancelada pelo usuário

Execucao.atual.foiCancelada

Exemplos

Monitorar o cancelamento e interromper a execução:

percorrer(...) {

  // processamento da aplicação

  // verifica e interompe caso a execução atual foi cancelada
  se(Execucao.atual.foiCancelada){
    interomper 'Execucao cancelada'
  }
 
}

API de Cache

O bfc-script disponibiliza uma API para armazenar valores pequenos em cache (máximo de 10kb), como dados de autenticação de serviços externos. O valor é armazenado usando o contexto de sistema, database e entidade.

Cache.adicionar(chave, valor)

Adiciona um novo valor no cache, com a chave informada e o tempo de expiração padrão de 12 horas.

Cache.adicionar('meu-token', 'aaabbbccc')

Cache.adicionar(chave, valor, expirarEm)

Adiciona um novo valor no cache, com a chave informada e o tempo de expiração (opicional).

Cache.adicionar('meu-token', 'aaabbbccc', 2.horas)

Cache.recuperar(chave, valorPadrao)

Recupera um valor colocado previamente no cache, ou retorna o valor padrão.

Cache.recuperar('meu-token', '')

Cache.contem(chave)

Verifica se o cache ontem algum valor para a chave informada.

Cache.remover(chave)

Remove o valor para chave informada.

Integrando com uma aplicação

O bfc-script pode ser integrado em qualquer aplicação Java SE ou EE.

Configurando a aplicação

A configuração pode variar de acordo com a stack utilzada.

Stack Dubai

O bfc-script esta disponível desde a versão 1.7.0 da stack.

Deve-se mapear as seguintes dependências:

        <dependency>
            <groupId>com.betha.bfc.commons</groupId>
            <artifactId>bfc-rest-script</artifactId>
        </dependency>
        <dependency>
            <groupId>com.betha.bfc.core</groupId>
            <artifactId>bfc-script-standard-engine</artifactId>
        </dependency>
        <dependency>
            <groupId>com.betha.bfc.commons</groupId>
            <artifactId>bfc-script-standard-engine-production</artifactId>
        </dependency>        

Para projetos web, não é necessário mapear a dependência bfc-rest-script.

Em projetos que possuem explicitamente o uso do cglib, ou alguma dependência que o utiliza, é necessário remove-la do projeto, para não gerar conflito com o bfc-script.

Executando um script

A execução de scripts em uma aplicação ocorre pela utilização da API do bfc-script. A classe ScriptManager contem os métodos necessários para executarmos nossos scripts, isso pode ser feito de duas formas:

1 - Para executarmos um script literal, basta utilizarmos o método evaluate da classe ScriptEngineContext conforme veremos abaixo.

ScriptEngineContext engineContext = ScriptManager.createContext();
engineContext.evaluate("imprimir 'Ola!!'");

2 - Porem em um ambiente real, nossos scripts não estarão transcritos estaticamente no código, conforme vimos no exemplo acima, mas estarão armazenados em um banco de dados, havendo a possibilidade desses serem editados a qualquer momento.

O armazenamento dos scripts deve ser realizado pela aplicação, que deverá apenas implementar uma interface de acesso ao script, para que a engine saiba como resgata-lo durante a execução. É indicado que a aplicação possua uma tabela para armazenamento de scripts independente do caso de uso, onde esta se relacionará com a tabela que representa o caso de uso.

Vejo o exemplo das tabelas abaixo:

A tabela SCRIPTS armazena scripts de qualquer natureza.

SCRIPTS
------------------
ID NUMBER PRIMARY KEY
SCRIPT CLOB
DH_ALTERACAO TIMESTAMP

A tabela EVENTOS representa possíveis eventos do cálculo da folha de pagamento, onde cada evento tem respectivamente um script. Esta tabela é aplicavel para o caso de uso calculo da folha.

EVENTOS
------------------
ID NUMBER PRIMARY KEY
DESCRICAO VARCHAR2
I_SCRIPT NUMBER FOREIGN KEY SCRIPTS

A tabela CRITICAS_USUARIOS representa possíveis critérios de validação configurados pelo usuário final. As criticas podem ser definidas para diversos cadastros, onde para cada crítica há um script. Esta tabela é aplicavel para o caso de uso críticas de usuário.

CRITICAS_USUARIOS
------------------
ID NUMBER PRIMARY KEY
ID_SCRIPT NUMBER FOREIGN KEY SCRIPTS
DESTINO NUMBER (0 - CLIENTE, 1 - BAIRROS, ..)

Embora as tabelas EVENTOS e CRITICAS_USUARIOS representam casos de usos distintos, a tabela SCRIPTS, armazena scripts de ambas naturezas.

No momento da execução dos scripts, a aplicação deve informar o id do script persistido, onde a engine se encarregará de carregar o script do banco de dados, compilar e executar. A engine ainda controla todo mecanismo de cache e reload de scripts, caso esses tenham sido alterados.

Digamos que o script abaixo esta armazenado na tabela SCRIPTS e o seu id é 25.

valor = 10 * 3
 
imprimir valor / 2

Para executarmos o script acima, basta invocar o método run passando o id do script por parâmetro.

ScriptEngineContext engineContext = ScriptManager.createContext();
engineContext.run("25");

A engine carregará o script do banco de dados, compilará, executará e armazenará o script compilado em cache para ser usado posteriormente.

Stack Dubai

Os scripts devem ser persistidos no banco da própria aplicação. Para execução de um script é necessário definir a entidade JPA que representa o script bem como seu respectivo repositório, estendendo as seguintes classes, com.betha.bfc.script.engine.standard.AbstractScript e com.betha.bfc.script.engine.standard.AbstractScriptRepository.

Segue um exemplo de implementação da entidade Script e seu repositório, estendendo as devidas classes:

@Entity
@Table(schema = "esquema da app", name = "VW_SCRIPTS")
@SequenceGenerator(name = "SEQ_SCRIPTS", schema = "BFCIT_OWNER", sequenceName = "SEQ_SCRIPTS", allocationSize = 1)
public class Script extends AbstractScript {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_SCRIPTS")
    private Long id;

    @Column(name = "DESCRICAO", length = 50)
    private String descricao;

    @Lob
    private String script = "//inicio";

    protected Script() {
    }

    public String getDescricao() {
        return descricao;
    }

    public String getScript() {
        return script;
    }

    @Override
    public Long getId() {
        return id;
    }

    protected void setDescricao(String descricao) {
        this.descricao = descricao;
    }

    protected void setScript(String script) {
        this.script = script;
    }

    @Override
    public InputStream getScriptAsStream() {
        return new ByteArrayInputStream(getScript().getBytes(StandardCharsets.UTF_8));
    }

    public static final class Builder extends AbstractEntityBuilder<Script> {

        private Builder(final Script entity, final AbstractEntityBuilder.EntityState state) {
            super(entity, state);
        }

        public static Script.Builder create() {
            return new Script.Builder(new Script(), AbstractEntityBuilder.EntityState.NEW);
        }

        public static Script.Builder from(final Script entity) {
            return new Script.Builder(entity, AbstractEntityBuilder.EntityState.BUILT);
        }

        public Builder descricao(String descricao) {
            entity.setDescricao(descricao);
            return this;
        }

        public Builder script(String script) {
            entity.setScript(script);
            return this;
        }

        @Override
        protected void validate(Validator validator) {
            super.validate(validator);
        }
    }
}

É necessário definir na implementação do repositório, a tipagem da entidade que representa o script, neste caso Script.

@Dependent
public class ScriptRepository extends AbstractScriptRepository\<Script\> {

}

A classe AbstractScript, contém o método execute, que possibilita sua execução a partir de um contexto no o ambiente de produção para engine padrão:

    VariaveisScriptFolha = variaveisScript = new VariaveisScriptFolha(funcionario, evento, usuarioLogado);

    ScriptEngineContext engineContext = ScriptManager.createContext();

    engineContext.inject(Injectables.byAnnotation(variaveisScript));

    CriticasUsuario criticasUsuarioCliente = criticasUsuarioRepository.findByDestino(Destino.CLIENTE);
    
    Script script = criticasUsuarioCliente.getScript();
    script.execute(engineContext);

Passando parâmetros para um script

Para passar informações para nossos scripts, precisamos seta-las como variáveis dentro da engineContext. Observe o script abaixo. Daremos um id à este script, 30.

valor = valorSalarioHora * 8 * 22
 
imprimir valor

Executamos o script acima passando o id do script para o método run.

ScriptEngineContext engineContext = ScriptManager.createContext();
 
engineContext.setVariable("valorSalarioHora", 66.35);
engineContext.run("30");

O script de código 30 utiliza uma variável chamada valorSalarioHora para efetuar um calculo, esta variável é setada no momento da execução desse script, conforme vimos acima.

Beans

É possível ainda passar um objeto como parâmetro. Este objeto deve seguir as convenções JavaBean, como seus atributos privados e métodos de leitura e gravação.

valor = funcionario.valorSalarioHora * 8 * 22
 
imprimir valor

Executamos o script acima, porem precisamos passar uma instância de uma classe Funcionario como podemos ver abaixo.

Funcionario funcionario = new Funcionario();
funcionario.setValorSalarioHora(66.35);

ScriptEngineContext engineContext = ScriptManager.createContext();

engineContext.setVariable("funcionario", funcionario);
engineContext.run("30");

Objetos passados por parâmetros podem ser simples pojos bem como, objetos que realizam acesso a um banco de dados, etc.

Coleções

Podemos passar coleções de JavaBeans. O comando percorrer itera coleções e disponibiliza uma variável implícita que representa o item iterado chamada item.

percorrer(funcionarios){
    valor = item.valorSalarioHora * 8 * 22

    imprimir valor
}

Executamos o script acima passando uma instância de classe que implemente a interface java.util.Collection ou alguma de suas subclasses. É necessário que os objetos dentro da coleção sejam JavaBeans.

Funcionario funcionario1 = new Funcionario();
funcionario.setValorSalarioHora(66.35);

Funcionario funcionario2 = new Funcionario();
funcionario.setValorSalarioHora(70.23);

List<Funcionario> funcionarios = new ArrayList<Funcionario>();
funcionarios.add(funcionario1);
funcionarios.add(funcionario2);

ScriptEngineContext engineContext = ScriptManager.createContext();

engineContext.setVariable("funcionarios", funcionarios);
engineContext.run("30")
NullsafeProxy

A engine por padrão, não permite que valores nulos sejam utilizados dentro dos scripts. Caso tentar passar um parâmetro nulo como variável, a engine irá disparar um NullpointerException e caso alguma propriedade dentro de um JavaBean estiver nula, mesmo que este bean esteja dentro de uma coleção, este atributo assumira um valor padrão:

  • 0 para números,
  • vazio para caracteres
  • 01/01/1800 para datas

A engine encapsula os objetos complexos passados como variáveis em um nullsafeProxy. Este proxy atua recursivamente para qualquer possível atributo de tipo complexo, encapsulando este também como um nullsafeProxy. É importante que classes dos objetos passados por parâmetro para um script, não sejam final e possuam um construtor público.

Esse comportamento pode ser desabilitado de duas maneiras.

@NoProxy

É possível anotar uma classe com @NoProxy, onde esta não herdará o comportamento NullSafeProxy.

    @ScriptInjectable.NoProxy
    public class Funcionario{
        // ...
    }

Funcionario funcionario = new Funcionario();

engineContext.setVariable(funcionario, funcionario);

$nullSafe

É possível setar uma variável para desabilitar o comportamento NullSafeProxy para um contexto por setar uma variável boleana atribuída à chave $nullSafe.

engineContext.setVariable($nullSafe, false);

Funcionario funcionario = new Funcionario();

engineContext.setVariable(funcionario, funcionario);

Anotações

Com o objetivo de simplificar a manipulação de variáveis injetadas em um script e ainda trazer mais integridade, é possível utilizar anotações.

@ScriptInjectable
public class VariaveisScriptFolha {

    @ScriptInjectable.Variable
    private Funcionario funcionario;

    @ScriptInjectable.Variable(name = "eventoFolha")
    private Evento evento;

    @ScriptInjectable.Variable(name = "usuarioLogado")
    private String usuario;

    public VariaveisScriptFolha(Funcionario funcionario, Evento evento, String usuario) {
        this.funcionario = funcionario;
        this.evento = evento;
        this.usuario = usuario;
    }
    
}

A classe VariaveisScriptFolha por exemplo, relaciona todas as variáveis que devem ser injetadas na execução do script de um suposto processo de calculo da folha.

A anotação @ScriptInjectable qualifica a classe como portadora de atributos passíveis de serem injetados em um script. A anotação @ScriptInjectable.Variable, marca um atributo da classe, para ser escaneado e o configura como uma variável em um script. O atributo name, define o nome que será utilizado para a variável no script. Se o valor do atributo não for setado, será utilizado como nome da variável dentro do script, o próprio nome do atributo com a anotação @ScriptInjectable.Variable.

Para setar na engine as variáveis baseadas na configuração da classe VariaveisScriptFolha, é necessário antes, submeter a instancia da classe para o utilitário Injectables.byAnnotation, retornando uma instância de Injectables, que poderá ser setado no contexto, conforme o exemplo abaixo:

	Funcionario funcionario = ...;
	Evento evento = ...;
	String usuarioLogado = ...;
	
	VariaveisScriptFolha = variaveisScript = new VariaveisScriptFolha(funcionario, evento, usuarioLogado);
	
	ScriptEngineContext engineContext = ScriptManager.createContext();
	
	engineContext.inject(Injectables.byAnnotation(variaveisScript));

A classe VariaveisScriptFolha garante que todas as variáveis que devem ser injetadas no script serão setadas, pois o construtor da classe exige o preenchimento de todos os valores. Essa abordagem pode ser combinada com técnicas mais sofisticadas, como builders ou mecanismos de validação como o Beans Validation.

Metadados

As anotações possuem duplo propósito, configuração de variáveis a serem injetadas em um script e coleta de meadados, utilizados para construção do ambiente de desenvolvimento de um script para o usuário final, onde o usuário precisará conhecer quais variáveis estão disponíveis, bem como outros detalhes, como parâmetros, retornos e documentação.

@ScriptInjectable
public class VariaveisScriptFolha {

    @ScriptInjectable.Variable(doc = @ScriptInjectable.Doc("Funcionário para o qual o calculo esta sendo executado"))
    private Funcionario funcionario;

    @ScriptInjectable.Variable(name = "eventoFolha", doc = @ScriptInjectable.Doc("Fornece os dados sobre o evento da folha que esta sendo calculado"))
    private Evento evento;

    @ScriptInjectable.Variable(name = "usuarioLogado", doc = @ScriptInjectable.Doc("Usuário logado no sistema"))
    private String usuario;

    public VariaveisScriptFolha(Funcionario funcionario, Evento evento, String usuario) {
        this.funcionario = funcionario;
        this.evento = evento;
        this.usuario = usuario;
    }

}

É possível configurar os metadados de variáveis de tipos complexos, como por exemplo o Funcionario.

public class Funcionario {

    private String nome;

    private Integer idade;

    private BigDecimal salario;

    private Date dataAdmissao;

    @ScriptInjectable.Method(
            doc = @ScriptInjectable.Doc("Nome do funcionário"),
            ret = @ScriptInjectable.Return(
                    doc = @ScriptInjectable.Doc("O nome do funcionário")))
    public String getNome() {
        return nome;
    }

    @ScriptInjectable.Method(
            doc = @ScriptInjectable.Doc("Idade do funcionário"),
            ret = @ScriptInjectable.Return(
                    doc = @ScriptInjectable.Doc("A idade do funcionário")))
    public Integer getIdade() {
        return idade;
    }

    @ScriptInjectable.Method(
            doc = @ScriptInjectable.Doc("Salário do funcionário"),
            ret = @ScriptInjectable.Return(
                    doc = @ScriptInjectable.Doc("O salário do funcionário")))
    public BigDecimal getSalario() {
        return salario;
    }

    @ScriptInjectable.Method(
            doc = @ScriptInjectable.Doc("Data de admissão do funcionário"),
            ret = @ScriptInjectable.Return(
                    doc = @ScriptInjectable.Doc("A data de admissão do funcionário")))
    public Date getDataAdmissao() {
        return dataAdmissao;
    }

}

Essa configuração não é recursiva, suportando apenas o primeiro nível, a partir da classe com a anotação @ScriptInjectable.

É possível configurar a forma como o usuário vai interagir com uma variável. Podemos configurar uma variável como uma classe de funções, que na pratica, apenas altera a forma como o usuário visualizará a variável em tempo de desenvolvimento.

@ScriptInjectable
public class VariaveisScriptFolha {
    
    // outros atributos
    
    @ScriptInjectable.Class(name = "RepositorioFuncionarios", doc = @ScriptInjectable.Doc("Classe de funções para consulta e manipulação de funcionários"))
    private RepositorioFuncionario repositorioFuncionario;

}


Obtendo dados de um script

Em alguns casos iremos precisar obter o retorno de um script ou o valor de alguma variável específica. A engine fornece métodos para a obtenção de variáveis registradas dentro da engine ou ainda para recuperar os dados de retorno de um script.

Observe o script abaixo cujo o id é 30.

idadeFuncionario = 60
dataCalculo = Datas.hoje()

valor = valorSalarioHora * 8 * 22
nomeFuncionario = 'Robert Plant'

retornar salario:valor, nome: nomeFuncionario

No exemplo acima estamos utilizando a variável injetada valorSalarioHora para fazermos um suposto calculo salarial. Criamos a variável nomeFuncionario, e atribuímos o nome para o funcionário e utilizamos o comando retornar para submetermos estes dados como valor de retorno para o script, atribuindo uma chave para cada valor. Estes valores serão resgatados através de uma java.util.Map como veremos abaixo.

ScriptEngineContext engineContext = ScriptManager.createContext();

engineContext.setVariable("valorSalarioHora", 66.35);
Result result = engineContext.run("30");

Map returnData = result.getValue();

Number salario = (Number)returnData.get("salario");
String nomeFuncionario = (String)returnData.get("nome");

Todo script executado gera um retorno. Este tipo de retorno precisa ser previamente reconhecido pelo executor do script, pois será necessário fazer um cast para obtermos o valor de retorno. Para o exemplo acima, o retorno é obtido através de um mapa, através das chaves previamente reconhecidas, obtemos os valores requeridos.

Podemos resgatar os valores das variáveis de outra forma. Para o script executado acima, existem duas outras variáveis declaradas que não foram submetidas ao comando retornar, poderíamos passar elas normalmente para o comando, assim como fizemos com as variáveis salario e nome, ou podemos recuperar seus valores conforme veremos abaixo:

ScriptEngineContext engineContext = ScriptManager.createContext();

engineContext.setVariable("valorSalarioHora", 66.35);
Result result = engineContext.run("30");

int idade = result.getVariable("idadeFuncionario");
Date dataCalculo = result.getVariable("dataCalculo");

A classe Result fornece um método para obtermos as variáveis usadas dentro do script. Através do método getVariable, podemos resgatar qualquer variável registrada no script, inclusive aquelas que submetemos ao comando retornar. Devido ao uso de generics no retorno do método getVariable, não precisamos fazer o cast para o tipo requerido, pois o cast é feito implicitamente.

Entendo Contextos

As aplicações possuem apenas uma instância de uma determinada engine. Isso possibilita compartilhar scripts carregados e já compilados em outras execuções, diminuindo o custo de execução dos scripts utilizados por outros processos. Porem é necessário que durante a execução dos scripts exista isolamento, para que uma execução não gere efeitos colaterais em outras.

Embora exista apenas uma instância da engine para uma aplicação, a execução dos scripts é dividida por contextos. Os contextos garantem que os dados envolvidos na execução de um script, como parâmetros passados para o script, instância de classes e outros recursos, estejam restritos a execução corrente, não influenciando execuções paralelas.

Por outro lado, um contexto permite o compartilhamento de informações durante o seu tempo de vida. Podemos executar um número ilimitado de scripts em um único contexto, sendo útil quando precisamos passar os mesmos parâmetros para vários scripts a serem executados ou manter um estado entre eles.

Devemos ter em mente que o estado dos objetos possuem escopo de contexto, qualquer modificação de um atributo será compartilhado a todos os scripts executados dentro de um mesmo contexto, possibilitando por exemplo, agrupar informações entre várias execuções conseguintes ou trocar informações entre scripts.

Manter o escopo reduzido a um contexto de execução pode ser perigoso quando utilizado de maneira involuntária, compartilhando por exemplo, um escopo entre várias execuções sem o conhecimento do ciclo de vida da engine de script.

Ambiente de desenvolvimento - Editor de script

O editor de scripts fornece uma forma amigável para codificação de scripts que serão compilados e executados pela engine. A responsabilidade sobre a validação da sintaxe, compilação, execução de scripts e geração de informações relacionadas a erros de compilação são geridos inteiramente pela engine.

O projeto

Atualmente a maioria dos softwares foram desenhados para operar nos data-centers das empresas, com contratos e licenças específicas. Porem em um ambiente cloud, onde aplicações podem ser oferecidas como serviço, é necessário que todos os recursos computacionais sejam compartilhados. Clientes precisarão adaptar a aplicação às suas características específicas, e é necessário garantir que as particularidades de cada um não impactem em alterações de software e não gerem conflito à outros clientes.,

A Betha através do projeto cloud, esta adaptando sua infraestrutura web para fornecer aos seus clientes, soluções compatíveis com o este novo paradigma tecnológico, que emergiu junto com o conceito de cloud computer. O bfc-script é uma parte importante deste novo modelo, e irá se integrar com outras soluções subseqüentes. Ele possibilitará que as aplicações sejam personalizadas através de scripts de usuário e ainda que os sistemas FolhaRh e Tributos, que fazem uso de scripts de customização, sejam migrados para o ambiente web.

Arquitetura

O bfc-script foi projetado para ser intergrado à aplicações Java SE e EE. O framework centraliza toda sua especificação em um módulo core, possibilitando a criação de diversas engines dentro de um mesmo ambiente de execução.

Arquitetura

A engine de execução

Foi estudado a viabilidade de se implementar uma engine de execução para o bfc-script, porém, foi constatado que existem diversas engines disponíveis para JVM, algumas amplamente utilizadas pela comunidade. Após uma primeira avaliação, foram elencadas 3 engines para testes: Jruby (Ruby), Rhino (Java Script) e Groovy (Mix Java/Ruby).

Os principais requisitos para a seleção de uma engine eram: tipagem dinamica, boa performance, favorecer a criação de DSLs. A engine do groovy foi a que melhor preencheu estes requisitos, se tornando a engine de execução do bfc-script.

Registro de engines

Várias engines podem ser registradas em um mesmo ambiente de execução. Isso poderá ser útil caso seja necessário criar engines que resgatam scripts de lugares específicos, ou que ainda precisem restringir ou liberar recursos específicos. As engines são registradas através do mecanismo de service/lookup do java. O ScriptManager faz lookup das engines registradas no classpath, através de uma identificação atribuida à engine no momento em que esta é registrada, constituida por um nome e um Environment (PRODUCTION ou DEVELOPMENT). Portanto podemos ter duas engines com o mesmo nome mas para ambientes diferentes. O Environment possibilita que as engines implementem caracteristicas desejáveis para os ambientes distintos, sendo estas caracteristicas perceptiveis na compilação e execução do scripts.

Para maioria dos casos de uso não será necessário criar uma engine e sim utilizar a engine engine padrão.

Engine padrão

A engine de execução padrão foi desenvolvida para dar suporte às necessidades de praticamente todas as aplicações da Betha. A engine permite que pacotes de funções utilitárias sejam plugados, personalizando o box de funções utilitárias da engine para um caso de uso desejado.

A engine padrão, possui um conjunto de funções utilitárias para manipulação de datas, caracteres, números, e também instruções de iteração e condicionais, que definem a linguagem e utilitários da engine.

Groovy

Ao contrário do Microsoft .Net, Java (linguagem e plataforma) não foi criada visando o uso de várias linguagens para gerar código interpretado (bytecodes em Java, MSIL - Microsoft Intermediate Language - em .Net). Com os anos, a comunidade Java criou suas próprias ferramentas para compensar este fato, na forma de linguagens que geram bytecodes compatíveis com os produzidos pelo compilador da linguagem Java. Groovy é uma destas linguagens, talvez a mais conhecida. É um projeto de Software Livre hospedado na Codehaus responsável por outros projetos como XStream, Pico/Nano Container, AspectWerkz, ActiveMQ, JMock, Drools e tantos outros. A linguagem Groovy é padronizada pela JSR 241.

O grande foco de Groovy é a produção de scripts, como os feitos em Bash ou Perl, mas a linguagem é poderosa o suficiente para ir muito além. Alguns programas podem exigir que rotinas sejam configuráveis e a maioria dos grandes sistemas empresariais, como ERPs e outros sistemas vendidos como produtos, permite um alto nível de personalização por cliente. Imagine um software de frente de caixa que precisa aceitar promoções definidas pelo time de marketing várias vezes por ano.

Na maioria das vezes estas configurações são implementadas com algum nível de parametrização em arquivos de configuração, mas quando existe uma mudança grande (como uma promoção compre dois produtos do lote ABC-001 e leve mais um) geralmente o desenvolvedor precisa codificar esta em Java e fazer outro deploy da aplicação.

Fonte: Groovy: Linguagem de Script para Java

Mais informações:

Wikipedia Groovy

Codehaus

Linguagens de Script para JVM

Quem tem acompanhado as ferramentas de desenvolvimento de software durante a última década sabe que o termo Java se refere a um par de tecnologias: a linguagem de programação Java e do Java Virtual Machine (JVM). A linguagem Java é compilada em bytecodes que são executados na JVM. Através deste projeto, Java oferece a sua portabilidade como grande diferencial.

A linguagem e a JVM, no entanto, têm caminhado cada vez mais em direções opostas. A linguagem tornou-se mais complexa, enquanto a JVM se tornou uma das plataformas de execução mais rápida e eficiente disponível. Em muitos benchmarks, Java iguala o desempenho do código binário gerado por linguagens compiladas como C e C + +. A crescente complexidade da linguagem e do notável desempenho, portabilidade e escalabilidade da JVM criaram uma abertura para uma nova geração de linguagens de programação. Essas linguagens oferecem possibilidades que carecem na linguagem Java, além de serem geralmente mais sucintas e objetivas.

Tecnólogos divergem sobre o que exatamente é uma linguagem de script. Na sua definição mais estrita, é uma linguagem que permite ao desenvolvedor escrever programas rápidos.

Fonte: Top five scripting languages the jvm

Mais informações:

Linguagens dinâmicas na JVM

Java Scripting Linguagens Interpretadas pelo java

List of Java Virtual Machines

Linguagens de domínio especifico

Artigo no formato de perguntas e repostas formulado por Martin Fowler.

O que é uma Domain Specific Language? (ou linguagem de domínio especifico, ou ainda DSL)

Uma Domain Specific Language (DSL) é uma linguagem de programação de expressividade limitada, focada num domínio particular. A maioria das linguagens que você conhece são linguagens de propósito geral (General Purpose Languages), que podem lhe dar com a maioria das coisas que você encontra durante um projeto de sistema. Cada DSL pode agir somente em um aspecto especifico do sistema.

Então você não poderia escrever todo projeto em uma DSL?

Não. A maioria dos projetos irão usar uma linguagem de propósito geral e muitas DSLs.

Essa idéia é nova?

Não totalmente. DSLs tem sido usadas extensamente nos círculos de usuários do Unix desde os primórdios desse sistema. A comunidade Lisp discute a criação de DSLs em Lisp e então usa a DSL para implementar a lógica. A maioria dos projetos de TI usam muitas DSLs – você já deve ter ouvido de coisas como CSS, SQL, expressão regular e etc.

Então porque este assunto está fazendo tanto barulho só agora?

Provavelmente é por causa do Ruby and Rails. Ruby é uma linguagem com muitas características que tornam fácil o desenvolvimento de DSLs e as pessoas que estão envolvidas na comunidade Ruby têm se sentido mais a vontades com essa abordagem, então eles levam vantagem dessas características. Em particular o Rails usa muitas DSLs que o deixam mais fácil de usar. Isto, por sua vez, tem incentivado mais pessoas a seguir essas idéias. Outra razão é que muitos sistemas feitos em Java ou C# precisam ter muito de seu comportamento definido de forma mais dinâmica. Isto conduziu a arquivos XML complexos que são difíceis de compreender que, por sua vez, levou as pessoas a explorar DSLs novamente.

Então DSLs podem ser usadas com outras linguagens além do Ruby?

Sim, como eu já disse, as DSLs já estavam por ai há muito tempo, mais do que o Ruby. Ruby tem uma sintaxe não obstrutiva e também características de meta-programação que a torna mais fácil para criar elegantes DSLs internas, mais do que outras linguagens como C# e Java. Mas existe DSLs internas uteis feitas em Java e C#

Qual a diferença entre DSL interna e DSL externa?

Uma DSL interna é apenas um idioma particular de escrever código na linguagem hospedeira. Então uma DSL interna feita em Ruby é um código Ruby, escrito num estilo particular que deixa a linguagem mais próxima da linguagem hospede. Tais como elas podem ser chamadas de Interface Fluente ou DSL embutida. Uma DSL externa é uma linguagem completamente separada que é traduzida para que a linguagem hospedeira possa entender.

Por que as pessoas estão interessadas nas DSLs?

Eu vejo que as DSLs possuem dois principais benefícios. O benefício mais comum é fazer que certos tipos de códigos fiquem mais fáceis de compreender, que se tornem mais fáceis de modificar, assim melhorando a produtividade do programador. Isto é válido para todos interessados e relativamente fácil de atingir. O benefício mais interessante, todavia, é que uma DSL bem projetada pode ser entendível por pessoas do negocio, permitindo-lhes compreender diretamente o código que implementa suas regras de negócios.

Então este é o gancho – pessoas do negócio escrevendo suas próprias regras?

Em geral eu não penso assim. É muito trabalhoso criar um ambiente que permita as pessoas do negocio escrever suas próprias regras. Você tem que fazer boas ferramentas de edição, depuração, testes e etc. Você tem a maioria dos benefícios das DSLs apenas permitindo que pessoas do negocio sejam capazes de ler as regras. Então eles podem revê-las para aperfeiçoa-las, falar sobre elas com os desenvolvedores e propor mudanças corretas da implementação. Ter DSLs legíveis ao contexto negócios é de longe menos esforço do que ter DSLs escrevíveis pelas pessoas do negócios, mas os ganhos ainda são bons. Existem momentos onde vale a pena o esforço para fazer DSLs escrevíveis por pessoas do negocio, mas esse é um objetivo mais avançado.

Você precisa de ferramentas especiais (leia-se caras)?

Normalmente, não. DSLs internas são apenas facilidades da linguagem de programação que você está usando. DSLs externas requerem que você use algumas ferramentas especiais – mas essas são open source e muito maduras. O maior problema com essas ferramentas é que a maioria dos desenvolvedores não estão acostumados com elas e acreditam que elas são complicadas de usar mais do que elas realmente são (um problema exacerbado pela pobre documentação). Todavia há exceções no horizonte. Existe uma classe de ferramentas que eu chamo LanguageWorkbench. Essas ferramentas permitem você definir DSLs mais facilmente e também provem sofisticados editores para elas. Ferramentas assim tornam mais viáveis a construção de DSL para os negócios.

Então isto é a repetição do sonho do desenvolvimento de software sem programação (ou programadores)?

Esta foi a intenção do COBOL e não penso que há alguma razão para que as DSLs tenham sucesso onde o COBOL (e tantas outras falharam). Eu penso que é importante que DSLs permitam pessoas do negocio e desenvolvedores colaborarem mais eficientemente porque eles podem falar sobre um conjunto de regras que também são códigos executáveis.

Quando eu deveria considerar a hipótese de criar uma DSL?

Quando você está trabalhando num aspecto do sistema com ricas regras de negócios ou work-flows. Uma DSL bem escrita deveria permitir que os clientes entendessem as regras pelas quais o sistema funciona.

Isto não vai levar a uma cacofonia de linguagens que as pessoas acharam mais difíceis de aprender?

Nós já temos uma cacofonia de frameworks que os programadores tem que aprender. Isto é uma inevitável conseqüência de sistemas reusáveis, é o único jeito que temos de lhe dar com todas essas coisas que os sistemas tem que fazer hoje em dia. Na essência uma DSL não é nada mais do que uma fachada chique sobre um framework. Como resultado elas contribuem um pouquinho com a complexidade que já havia. Na verdade uma boa DSL deveria fazer as coisas melhores tornando esses frameworks mais fáceis de usar.

Mas as pessoas não vão criar muitas DSLs de baixa qualidade?

Claro, assim como pessoas criam frameworks de baixa qualidade. Mas, novamente, eu gostaria de argumentar que DSLs de baixa qualidade não causam mais danos se comparados aos que os frameworks mal projetados causam

Fonte: DSL Interna e externa perguntas e respostas

Original: DSL Questions & Answers

Mais informações:

Domain Specific Languages

Arquitetura evolucionária e design emergente: Interfaces fluentes

Groovy SQL Builder

Refatorando para Fluent-Interface

FAQ

Esta sessão será utilizada para responder perguntas freqüentes.

Por quê não utilizar uma linguagem baseada em SQL assim como o Tributos?

  1. Porque toda infraestrutura de compilação e execução de scripts é dependente de um banco de dados, precisaríamos estar fortemente acoplados a um sistema de banco de dados.
  2. Em aplicações web, é importante reduzir o número de acessos a um banco de dados para não prejudicar a performance das aplicações, criar uma solução acoplada a um banco de dados refletiria negativamente nesse aspecto.
  3. Linguagens PL/SQL são muito rígidas e difíceis de se compreender por usuários sem conhecimentos em linguagens de programação. A intenção é que a codificação de scripts seja simples e intuitiva.
  4. Comprometeria a segurança, pois habilitaria a visibilidade de todas as tabelas do banco de dados ao usuário, permitindo que sejam feitas manipulações de dados não desejados.

Por quê a API padrão não fornece métodos para consultar uma tabela no banco de dados?

Como a intenção é que a codificação de scripts seja simples e intuitiva e aberta à pessoas que não possuem conhecimentos em linguagens de programação, deixar essa possibilidade aberta para esses usuários, pode acarretar em consultas pouco otimizadas, prejudicando todos os usuários de um banco de dados, levando até mesmo a indisponibilidade de toda a aplicação.

Por que não devo passar objetos que possuem responsabilidade internas em uma aplicação como parâmetros em scripts?

O bfc-script permite que sejam fornecidos parâmetros que serão utilizados pelo usuário dentro do script. Por exemplo, posso criar um script no qual o salário líquido será passado por parâmetro, ou ainda um objeto que represente um funcionário, onde o usuário terá acesso a nome, sexo e o salário líquido.

Quando você passa por parâmetro um objeto, alterações na classe do objeto impactam diretamente os scripts que o utilizam. Existe uma dependência forte entre a classe que representa o objeto e os scripts. Não é prudente fornecer um objeto que possui responsabilidades internas em sua aplicação, como por exemplo VOs.

VOs constituem o modelo da aplicação e estão suscetíveis a mudanças extremas. Medir o impacto da alteração de um VO por si só já é altamente complexo, se estes VOs forem utilizados em scripts, pode inviabilizar a alteração do modelo e prejudicar a evolução do sistema.