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.
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.
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
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.
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'
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 }
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
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'
Permite criar expressões condicionais.
se(10 == 10){ imprimir 'verdadeiro' } se(9 <= 20){ imprimir 'verdadeiro' }senao{ imprimir 'falso' }
À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:
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:
Permite suspender a execução de um script com uma exceção.
se(codigo != 10){ suspender "O código $codigo é inválido" }
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.
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.
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.
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]
É 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 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.
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)
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)
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.
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
Obtem uma quantidade específica de caracteres iniciados da direita para esquerda.
Caracteres.direita(texto, quantidade)
Alternativa
texto.direita(quantidade)
Verifica se uma expressão esta contida em um texto. Mais sobre expressões regulares
Caracteres.equivalente(texto, expressao)
Alternativa
texto.equivalente(expressao)
Obtem uma quantidade específica de caracteres iniciados da esquerda para direita.
Caracteres.esquerda(texto, quantidade)
Alternativa
texto.esquerda(quantidade)
Converte todos os caracteres de um texto em maiúsculo.
Caracteres.maiusculo(texto)
Alternativa
texto.maiusculo
Converte todos os caracteres de um texto em minusculo.
Caracteres.minusculo(texto)
Alternativa
texto.minusculo
Obtem a posição onde um caracter se encontra em uma texto.
Caracteres.posicao(texto, expressao, posicaoInicio)
Alternativa
texto.posicao(expressao, posicaoInicio)
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)
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)
Remover o excesso de espaços de um texto.
Caracteres.removeEspacos(texto)
Alternativa
texto.removeEspacos
Remove o excesso de espaços de um texto à esquerda.
Caracteres.removeEspacosDireita(texto)
Alternativa
texto.removeEspacosDireita
Remove o excesso de espaços de um texto à direita.
Caracteres.removeEspacosEsquerda(texto)
Alternativa
texto.removeEspacosEsquerda
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)
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)
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)
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:
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()
Retorna o total de grupos da expressão regular.
expBoasVindas = "boa-tarde".expressao(~/boa-(tarde|noite)/) imprimir expBoasVindas.totalGrupos() // 1
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
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*
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
Indica se o padrão da expressão foi encontrado no texto.
encontrouAlgo = "1235A".expressao(~/[0-9]+/).encontrouPadrao() imprimir encontrouAlgo // true
Retorna todos os valores encontrados no texto pela expressão concatenados.
imprimir "B30th45".expressao(~/[0-9]+/).concatenarValoresEncontrados() // 3045
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
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
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
Retorna a posição inicial do texto que coincida com a expressão localizada. Caso o padrão não seja encontrado retorna -1.
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
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
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] }
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 }
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
Adiciona uma quantidade especificada de dias à uma data.
Datas.adicionaDias(data, quantidadeDias)
Alternativa
data.adicionaDias(quantidadeDias)
Adiciona uma quantidade especificada de horas em uma data/hora.
Datas.adicionaHoras(data, quantidadeHoras)
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)
Adiciona uma quantidade especificada de minutos em uma data/hora.
Datas.adicionaMinutos(data, quantidadeMinutos)
Alternativa
data.adicionaMinutos(quantidadeMinutos)
Adiciona uma quantidade especificada de segundos em uma data/hora.
Datas.adicionaSegundos(data, quantidadeMinutos)
Alternativa
data.adicionaSegundos(quantidadeMinutos)
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)
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)
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
Calcula a diferença em anos entre duas datas.
Datas.diferencaAnos(menorData, maiorData)
Alternativa
menorData.diferencaAnos(maiorData)
Calcula a diferença em dias entre duas datas.
Datas.diferencaDias(menorData, maiorData)
Alternativa
menorData.diferencaDias(maiorData)
Calcula a diferença em horas entre duas datas.
Datas.diferencaHoras(menorData, maiorData)
Alternativa
menorData.diferencaHoras(maiorData)
Calcula a diferença em meses entre duas datas.
Datas.diferencaMeses(menorData, maiorData)
Alternativa
menorData.diferencaMeses(maiorData)
Calcula a diferença em minutos entre duas datas/hora.
Datas.diferencaMinutos(menorData, maiorData)
Alternativa
menorData.diferencaMinutos(maiorData)
Calcula a diferença em segundos entre duas datas/hora
Datas.diferencaSegundos(menorData, maiorData)
Alternativa
menorData.diferencaSegundos(maiorData)
Obtem a hora em que se encontra uma determinada data/hora.
Datas.hora(data)
Alternativa
data.hora
Obtem os minutos referentes a uma determinada data/hora
Datas.minuto(data)
Alternativa
data.minuto
Obtem o nome do dia da semana.
Datas.nomeDiaSemana(data)
Alternativa
data.nomeDiaSemana
Remove uma quantidade especificada de dias de uma data.
Datas.removeDias(data, quantidadeDias)
Alternativa
data.removeDias(quantidadeDias)
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)
Obtem os segundos referentes a uma determinada data/hora.
Datas.segundo(data)
Alternativa
data.segundo
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 |
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)
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)
Obtem o maior número que é menor ou igual ao número espedificado, sendo este número inteiro.
Numeros.piso(valor)
Obtem um número aleatório entre 1 a um valor limite especificado.
Numeros.randomico(numeroDelimitador)
Retorna o resto da divisão realizada entre o dividendo e o divisor, que são passados porparâmetro.
Numeros.resto(valorDividendo, valorDivisor)
Testa os valores passados como parâmetro e retorna o primeiro diferente de zero.
Numeros.seZero(valor1, valor2, valorN)
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.
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:
Exemplo de utilização:
arquivoTxt = Arquivo.ler(origem, 'txt'); arquivoCsv = Arquivo.ler('Bob|Esponja', 'csv', [delimitador:'|']);
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:
Exemplo de utilização:
arquivoTxt = Arquivo.novo('Jones.txt') arquivoCsv = Arquivo.novo('Spencer.csv', 'csv', [entreAspas: 'N'])
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')
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' ]);
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:
Realiza a leitura de uma linha do arquivo e retorna o conteúdo lido.
texto = arquivo.lerLinha()
Verificar se o arquivo sendo lido contém uma próxima linha
percorrer(enquanto: { arquivo.contemProximaLinha() }) { imprimir arquivo.lerLinha() }
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
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
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:
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:
Início de documento
<pessoa : Início do elemento. Este tipo de item contem um nome(pessoa), pode conter atributos, namespaces etc.
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.
</pessoa> : Fim do elemento
Fim de documento
arquivo = Arquivo.ler(arquivo, 'xml')
Retorna o tipo do item sendo lido. Os valores disponíveis são:
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.
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() }
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.
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
Indica se o item atual contém um namespace declarado igual ao informado por parâmetro.
Indica se o item atual contém um namespace declarado com valor e prefixo igual aos parâmetros.
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() }
Indica se o elemento atual contém um atributo com o nome informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO
Indica se o elemento atual contém um atributo com o nome e namespace informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO
Indica se o elemento atual contém um atributo com o prefixo, nome e namespace informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO
Retorna o atributo do elemento com base no nome informado no parâmetro. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO
Retorna o atributo do elemento com base no namespace e nome informado nos parâmetros. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO
Retorna o atributo do elemento com base no prefixo, namespace e nome informado nos parâmetros. Tipos suportados: INICIO_ELEMENTO, ATRIBUTO
Indica se o tipo do item atual é igual a INICIO_DOCUMENTO
Indica se o tipo do item atual é igual a FIM_DOCUMENTO
Indica se o tipo do item atual é igual a TEXTO
Indica se o tipo do item atual é igual a COMENTARIO
Indica se o tipo do item atual é igual a FIM_ELEMENTO
Indica se o tipo do item atual é igual a INICIO_ELEMENTO
Indica se o tipo do item atual é igual a ESPACO
Indica se o tipo do item atual é igual a CDATA
Indica se o tipo do item atual é igual a ATRIBUTO
Indica se o tipo do item atual é igual a NAMESPACE
Retorna o tipo do próximo item caso a leitura do documento não tenha sido finalizada.
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() } } }
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.
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() } }
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.
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.
arquivo = Arquivo.novo('FendaDoBiquine.xml', 'xml')
Parâmetros de escrita:
Escreve um novo atributo no elemento atual.
xml.escreverInicioElemento('pessoa') xml.escreverAtributo('idade', '18') //Saída: <pessoa idade="18">
Escreve um novo atributo no elemento atual informando também o namespace.
Escreve um novo atributo no elemento atual informando também o namespace.
Escreve texto no item atual.
xml.escreverInicioElemento('pessoa') xml.escreverTexto('João') xml.escreverFimElemento('pessoa') //Saída: <pessoa>João</pessoa>
Escreve um DTD no documento XML.
Escreve um elemento vazio no documento XML:
xml.escreverElementoVazio('vazio') //Saída: <vazio />
Escreve um elemento vazio e namespace com prefixo no documento XML.
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'?>
Escreve a declaração de um documento XML informando o encoding, versão e standalone.
Escreve o início de um elemento no documento XML.
xml.escreverInicioElemento('pessoa') //Saída: <pessoa>
Escreve o início de um elemento com namespace no documento XML.
Escreve o início de um elemento com namespace e prefixo no documento XML.
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
Escreve o fim de todos os elementos atualmente abertos.
xml.escreverInicioElemento('pessoa') xml.escreverInicioElemento('endereco') xml.escreverFimElementos() //Saída: <pessoa><endereco></endereco></pessoa>
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>
Escreve um elemento texto no documento XML.
xml.escreverElementoTexto('nome', 'Maria') //Saída: <nome>Maria</nome>
Escreve um elemento texto com namespace e prefixo.
Indica se o documento atual contém algum início de elemento sem um fim declarado.
Escreve a referência à uma entidade no documento XML.
Escreve as instruções de processamento no documento.
Escreve um item texto do tipo ESPACO
Escreve um item texto do tipo ESPACO ignorável.
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:
{ : Início do objeto. Este tipo de item pode conter propriedades.
"nome": Nome do campo. Este é um tipo de item considerado TEXTO.
"João da Silva"" : Valor do campo
} Fim de objeto
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]
Retorna um valor booleano indicando se o item atual é o inicio de uma matriz (array)
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.
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()
Escreve um objeto completo
json.escreverObjeto([nome: "AAA"])
{ "valor": { "nome": "AAA" } }
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
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'])
arquivo = Arquivo.novo('relatorios.zip', 'zip')
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
Adiciona um arquivo no diretório raiz do arquivo zip
arquivo = Arquivo.novo('relatorios.zip', 'zip') arquivo.adicionar(parametros.arquivo.valor)
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
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
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
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
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()
Adiciona um email e nome no qual a mensagem deverá ser respondia pelos destinatários
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.
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.
Cria um novo anexo do tipo URL informando o endereço e o nome a ser utilizado na mensagem.
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)
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.
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)
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)
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.
Adiciona um anexo criado a partir da função Email.criarAnexoArquivo à mensagem
Adiciona um anexo à mensagem com base em uma URL. O download do anexo será realizado e adicionado a mensagem.
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();
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!'
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()
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()
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()
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')
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.
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:
Cria um novo servico com base na URL informada por parâmetro.
servico = Soap.servico('http://ws.cdyne.com/emailverify/Emailvernotestemail.asmx')
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')
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.
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()
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')
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.
Cria uma nova mensagem (envelope) à ser enviado ao serviço sobrescrevendo o namespace e prefixo padrão informados na criação do serviço.
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.
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:
Adiciona um namespace a mensagem. Este namespace será adicionado ao envelope SOAP da mensagem e poderá ser utilizado na declaração dos elementos.
Adiciona um namespace a mensagem. Este namespace será adicionado ao envelope SOAP da mensagem e poderá ser utilizado na declaração dos elementos.
Retorna uma lista com os namespaces declarados na mensagem.
Retorna o namespaces definido como padrão da mensagem.
Retorna o elemento do corpo da mensagem.
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>')
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)
Retorna o elemento do cabeçalho da mensagem.
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>')
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)
Define a operação/método/função a ser executada no webservice. Este valor será informado no cabeçalho HTTP SOAPAction da mensagem.
Executa a chamada ao método/função do serviço SOAP e retorna uma representação da resposta.
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().
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.
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 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()
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:
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()
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()
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!' }
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() }
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.
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
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()
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()
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()
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.
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 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:
A disponibilidade de cada operação depende do ativo e tema utilizado.
A operação de busca padrão conta com os seguintes parâmetros:
tema.busca(criterio:"nome = 'Maria' and idade > 18")
tema.busca(ordenacao:"nome,sobrenome asc, cidade desc")
tema.busca(campos:"nome, sobrenome, cidade(nome, uf)")
ativo.telefones.busca(parametros:[codigoFuncionario:15])
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)
tema.busca(valorPadrao: falso)
A operação de criação padrão conta com os seguintes parâmetros:
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)
A operação de atualização padrão conta com os seguintes parâmetros:
dadosFuncionarios = Dados.dubai.v1.funcionarios telefone = [ telefone: '488817858' ] telefoneAlterado = dadosFuncionarios.telefones.atualiza(parametros: [codigoFuncionario: 12, codigoTelefone: 15], conteudo: telefone)
A operação de exclusão padrão conta com os seguintes parâmetros:
dadosFuncionarios.telefones.exclui(parametros: [codigoFuncionario: 12, codigoTelefone: 15])
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.
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)
Função responsável pela execução do script.
parametros = [ p1 : 100, p2 : 200] Scripts.somar.executar(parametros)
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.
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:
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' }
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
O retorno de um script é realizado utilizando o comando retornar:
retornar 'Retornando uma mensagem'
ret = [valor: 1, mensagem: 'Retornando um mapa'] retornar ret
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])
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:
Por que utilizar componentes?
São algumas vantagens na utilização dos componentes:
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:
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()
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.
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:
Adiciona um cookie na requisição HTTP informando também o caminho/path, domínio e versão do cookie.
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' ])
Adiciona um cabeçalho/header com vários valores na requisição HTTP.
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()
Cria uma cópia da requisição atual adicionando um ou mais parâmetros de query na requisição.
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
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
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:
servico.aceitarTipoMidia(Http.JSON) servico.aceitarTipoMidia([Http.JSON, Http.XML])
Configura a autenticação básica HTTP no serviço.
servico.credenciais('user', '123')
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)
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'])
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()
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()
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()
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'])
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])
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])
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])
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)
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:
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()
Adiciona um parâmetro ao formulário atual.
formulario = servico.criarFormulario() .parametro("nome", "Betha")
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)
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()
Envia o formulário utilizando como tipo de método o verbo POST.
resposta = servico.criarFormulario().PUT()
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()
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
Envia o formulário utilizando como tipo de método o verbo POST.
resposta = servico.criarFormularioMultipart().POST()
Envia o formulário utilizando como tipo de método o verbo PUT.
resposta = servico.criarFormularioMultipart().PUT()
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:
Retorna o código HTTP da resposta.
resposta = ... se (resposta.codigo() == 404) { imprimir 'Servidor não encontrado'; }
Retorna o tipo de mídia da resposta
imprimir 'A resposta é do tipo ' + resposta.tipoMidia()
Indica se o tipo de mídia da resposta é do tipo JSON
se (resposta.ehJson()){ imprimir 'A resposta é um JSON' }
Indica se o código resposta é considerado como sucesso segundo o padrão HTTP.
se (resposta.sucesso()){ imprimir 'Tudo OK com a chamada HTTP' }
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.
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.
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.
Recupera um cookie da resposta com base no nome. Um cookie pode conter as seguintes informações:
usaEnterComoTab = resposta.cookie('UsaEnterComoTab') imprimir 'O usuário utiliza enter como TAB: ' + usaEnterComoTab.valor()
Recupera uma lista contendo os cookies retornados na resposta.
percorrer(resposta.cookies()){ imprimir item.nome() + ':' + item.valor() }
Recupera um cabeçalho(Header) da resposta com base no nome. Um cabeçalho poder conter as seguintes informações:
imprimir resposta.cabecalho('Authorization').valor()
Indica se a resposta contém um cabeçalho (Header) com o nome informado no parâmetro.
Recupera uma lista contendo os cabeçalhos retornados na resposta.
percorrer(resposta.cabecalhos()){ imprimir item.nome() + ':' + item.valor() }
Retorna o conteúdo da resposta no formato de caracteres.
resposta = Http.servico('https://www.betha.com.br/users').GET() imprimir resposta.conteudo()
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()
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.
Exemplo de script de assinatura de um documento pdf:
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);
Exemplo de script de assinatura de um lote de documentos xml:
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);
Exemplo de script de assinatura de um documento PDF com múltiplos assinantes:
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);
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:
E por último o identificador que também foi definido no script de assinatura no item script do objeto de opcoes
A criação de um novo critério acontece das seguintes maneiras:
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');
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"'
Após a criação do critério, é possível fazer uso um conjunto de operações de comparação.
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);
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.
Para as operações contidoEm e naoContidoEm pode ser informado um delimitador para os valores que não são String.
numero = [1,2,5] criterio = Criterio.onde('idade').contidoEm(numero, [delimitador: '"']);
O critério acima, gera o seguinte filtro:
idade in ("1", "2", "5")
numero = [1,2,5] criterio = Criterio.onde('idade').contidoEm(numero);
O critério acima, gera o seguinte filtro:
idade in (1, 2, 5)
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')
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"'
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)
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:
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
O bfc-script disponibiliza uma API para a consulta das informações relacionadas a execução.
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.
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')
Adiciona um novo valor no cache, com a chave informada e o tempo de expiração (opicional).
Cache.adicionar('meu-token', 'aaabbbccc', 2.horas)
O bfc-script pode ser integrado em qualquer aplicação Java SE ou EE.
A configuração pode variar de acordo com a stack utilzada.
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.
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.
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);
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.
É 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.
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")
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:
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.
É 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);
É 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);
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.
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; }
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.
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.
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.
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.
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.
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.
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.
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.
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:
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:
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:
Arquitetura evolucionária e design emergente: Interfaces fluentes
Esta sessão será utilizada para responder perguntas freqüentes.
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.
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.