Utilizando código Java em banco de dados Oracle

Propaganda
bd oracle_
UTILIZANDO CÓDIGO JAVA EM
BANCO DE DADOS ORACLE
Como criar procedimentos e funções em banco de dados Oracle utilizando Java
O SGBD (Sistema de Gerenciamento de Banco de Dados) da Oracle
possui, a partir da versão 8i, uma JVM (Java Virtual Machine) embutida, tornando-o assim capaz de armazenar e executar códigos
Java. Neste artigo, será explicado como funciona esta estrutura e
mostrado como criar uma classe Java dentro da base de dados, habilitando assim que seus métodos sejam executados como procedimentos nativos.
A
Oracle em 1999 lançou a versão 8i de seu gerenciador de banco de dados. A grande novidade em
relação às versões anteriores era a implementação
de uma máquina virtual Java interna, exceto nas
versões Express. Esta mudança tornaria os bancos
de dados a partir desta versão capazes de armazenar internamente classes Java. Os métodos públicos
de cada classe poderiam ser compatibilizados com o
!"#$%&'()*+(&,!-./%0&-%#,!,"%1%2&!34225/4.2&3%6%&
se fossem funções ou procedimentos.
A implantação de uma máquina virtual Java interna nos bancos de dados Oracle traz alguns recursos interessantes, dentre os quais podemos destacar:
opção para o desenvolvedor Java criar soluções nas
plataformas da Oracle; possibilidade de integrar no
banco de dados aplicações Java já existentes; tirar
proveito das medidas de segurança providas pela
6789.,!&/.#-9!:&40& %#&;60&!:%3!<$%&"4&646=#.!&46&
separado para cada sessão de usuário.
Este texto irá explicar como é a arquitetura da
máquina virtual dentro do banco de dados, como é
/ 58
feito o armazenamento das classes e o gerenciamento da sessão de cada usuário. Será mostrado também
um exemplo para criar procedimentos em Java dentro do banco de dados.
APLICAÇÕES JAVA EM SERVIDOR
BIBLIOTECAS ESPECÍFICAS DA ORACLE
BIBLIOTECAS JAVA PADRÃO
MÁQUINA VIRTUAL JAVA DO SGBD
BIBLIOTECAS NATIVAS DO ORACLE
SISTEMA OPERACIONAL
Figura 1. Arquitetura Java interna no gerenciador de banco de dados.
Edgard Resende Bilharinho | [email protected]
Graduado em Ciência de Computação, especializado em administração de banco de dados Oracle e em desenvolvimento Java. Trabalha como desenvolvedor PL/SQL utilizando Oracle Forms/Reports.
ARQUIVOS
.java
ARQUIVOS
.jar
ARQUIVOS
.class
CLIENTE
1
LOAD JAVA
CLIENTE
2
RBDMS
CLIENTE
3
CÓDIGO
FONTE
CLASSE
JAVA
CLASSE
JAVA
RECURSOS
EM JAVA
schema
COMPILADOR
JAVA
TABELA DE
OPÇÕES
Figura 2. Gerenciamento dos códigos em Java.
Arquitetura
Dentro de cada gerenciador de banco de dados
Oracle nas versões 8i e posteriores estão implemen-!"!2&!2&>'?2&"!&42 43.;3!<$%&@!/!&*AB&C%6%&6%2-#!&
!&;D9#!&E0&!:F6&"!2&>'?2&G72.3!2&4H.2-46&-!6GF6&!2&
42 435;3!2&"!&I#!3:4&894&J%#!6&.,3:95"!2& !#!&.,-4grar o Java ao banco de dados. Esta arquitetura é que
possibilita que o Oracle seja capaz de armazenar e
executar códigos Java compatibilizando-os com seus
3=".D%2&,!-./%2&46&'()*+(B
Pode-se utilizar a máquina virtual Java do banco de dados para compilar códigos. Porém, no Oracle
há uma particularidade. Ao contrário das máquinas
virtuais que rodam em sistemas operacionais, o Oracle não gera arquivos .jar e nem arquivos .class. Ao
3%6 .:!#&96&3=".D%&@!/!&42-4& !22!&!&;3!#&.,-4D#!"%&
dentro do banco de dados como um objeto interno.
>&;D9#!&K&.:92-#!&3%6%&24#.!6&!2&489./!:L,3.!2&4,-#4&
os objetos em banco e os arquivos geralmente criados em uma máquina virtual padrão.
O equivalente ao arquivo executável .jar se torna
Para cada
sessão de
usuário
MVJ
MEMÓRIA
DA SESSÃO
MEMÓRIA
CHAMADA
Figura 3. Tratamento de sessões Java.
um objeto que pode ser invocado como um procedi64,-%&,!-./%&46&'()*+(B&>&3!"!& #%34".64,-%&@!/!&
acionado por uma conexão no banco, o Oracle cria
automaticamente uma instância da máquina virtual dedicada para aquela conexão. Neste caso, cada
utilizador do banco possuirá individualmente um
coletor de lixo, uma área de memória estática e outra área de memória alocável. Isto garante que caso
ocorra um problema em alguma sessão as demais
,$%& 24#$%& !J4-!"!2& %#& 4/4,-9!:& J!:M!B& N4#.;3!#& ,!&
;D9#!&O&3%6%&%&I#!3:4&-#!-!&3!"!&2422$%&894&!3.%,!&
um código Java.
Implantação
Criando a classe Java
Este é o primeiro passo para a implantação da
rotina Java dentro do banco. Pode-se usar qualquer
ambiente de desenvolvimento como Eclipse ou
Netbeans.
Para a criação da classe será utilizada como base
uma regra de negócio simples. A ideia é ilustrar algumas das possibilidades que podem ser exploradas
e demonstrar o conteúdo técnico necessário. Serão
abordados a criação da classe, seu armazenamento
59 \
no banco de dados e a vinculação de seus métodos Listagem 1. !"#$%$ &'()*(&+, '-.(/#'+01+/#-*(2-)#$3
para que possam ser invocados como procedimentos
ou funções.
public static String registraAbastecimento(String cpf,
String placa, int tipo_combustivel,
A regra de negócio que será utilizada neste pe!"#$ quantidade) {
queno projeto é algo comum hoje em dia. Atualmente vários postos de combustível possuem programas
String sRetorno = null;
"4& ;&"4:."!"4B& *4#7& 9-.:.P!"%& 96& 3%,Q9,-%& "4& #4D#!2&
!"#$ fPreco = 0f;
para gravação dos dados de abastecimento de clien-42B& R4:42& ;&3!#$%& /.,39:!"%2& %& 3:.4,-40& %& /4539:%0& %&
44 5-'(6*+ /- $/ &+'72-#'$/ &+//+%$/ *$)/#+2
3%6G92-5/4:&9-.:.P!"%&4&29!&89!,-."!"40&%& #4<%&;&,!:&
// na base de dados
e a data da operação. Após o registro dos dados, será
sRetorno = buscaPessoa(cpf);
enviado um e-mail para o cliente informando sobre o
sRetorno = buscaVeiculo(placa);
fPreco = getPrecoCombustivel(tipo_combustivel);
abastecimento.
R!& ;&D9#!& S& 3%,2-!& 96& TAU& VT.!D#!6!& "4& A,-.1
if (sRetorno == null && fPreco > 0) {
dades e Relacionamentos) que demonstra a estrutura
em que os dados serão gravados.
try {
Connection conn = DriverManager.getConnection
(89%1*:%-;+<,#:*$))-*#($):=);
PreparedStatement stm =
conn.prepareStatement(
“INSERT INTO ABASTECIMENTOS(“
+”Sequencia, Cpf, Placa, Tipo_Combustivel,”
+”Quantidade, Valor, Data) VALUES (“ +
“SEQ_ABASTECIMENTO.NEXTVAL,?,?,?,?,?,?)”
);
stm.setString(1,cpf);
stm.setString(2,placa);
stm.setInt(3,tipo_combustivel);
stm.setFloat(4,quantidade);
stm.setFloat(5,quantidade * fPreco);
stm.setDate(6,new 9+5+3/>,3Date
(System.currentTimeMillis()));
Figura 4. Diagrama de entidades e relacionamentos.
>,-42&"4&/4#.;&3!#&%&3=".D%1J%,-4&F&.6 %#-!,-4&3.1
tar que ao efetuar a conexão com a base de dados não
é necessário informar o usuário e o servidor em que
ela está instalada. O próprio Oracle irá prover estas
informações, pois o código roda internamente como
um objeto de banco.
Serão demonstrados primeiro os métodos públicos que posteriormente serão invocados pelos procedimentos de banco de dados. Este é o objetivo. Criar
métodos que possam ser invocados como procedimentos ou funções nativos. Os métodos privados se#$%&"423#.-%2& %2-4#.%#64,-4B&R!&;&D9#!&W&F&"46%,21
trado o diagrama da classe que será criada.
stm.executeUpdate();
stm.close();
} catch (SQLException e) {
sRetorno = “Erro ao registrar o abastecimento”
+ “ no banco de dados.” + e.getMessage();
}
if (sRetorno == null) {
disparaEmail(cpf, quantidade * fPreco);
sRetorno = “O registro do abastecimento “
+ “foi concluído.”;
}
}
return(sRetorno);
Figura 5. Diagrama da classe que será criada.
O principal método da classe é o registraAbastecimento. Como pode ser visto no código contido na
Listagem 1 é a partir dele que é registrado o abastecimento. Antes de se gravar os dados de abasteci64,-%2&F&/4#.;&3!"!&!&4H.2-L,3.!&"%&3:.4,-4&"%&/4539:%B&
Após a conclusão do registro, executa-se o procedimento que envia o e-mail.
/ 60
}
O método getPrecoCombustivel serve para consultar o preço atual para um dos combustíveis cadastrados, caso o tipo informado não exista então é
retornado valor zero. O código do método pode ser
conferido na Listagem 2.
Listagem 2. !"#$%$ .-#?'-*$@$21</#(5-,3
public static "#$ getPrecoCombustivel(
int tipo_combustivel){
Listagem 4. !"#$%$ /-#?'-*$@$21</#(5-,3
public static void setPrecoCombustivel(
int tipoCombustivel, "#$ novoPreco) {
"#$ fResultado = 0;
String sConsulta = “SELECT SUM(Preco) FROM “
+ “ Combustiveis WHERE Tipo_Combustivel = ? “;
ResultSet rs = null;
try {
Connection conn =
DriverManager.getConnection
(89%1*:%-;+<,#:*$))-*#($):=);
PreparedStatement stm =
conn.prepareStatement(
“UPDATE Combustiveis “
+”SET Preco = ? “
+”WHERE Tipo_Combustivel = ?”);
stm.setFloat(1,novoPreco);
stm.setInt(2,tipoCombustivel);
try {
Connection conn = DriverManager.getConnection
(89%1*:%-;+<,#:*$))-*#($):=);
PreparedStatement stm =
conn.prepareStatement(sConsulta);
stm.setInt(1,tipo_combustivel);
rs = stm.executeQuery();
rs.next();
fResultado = rs.getFloat(1);
stm.executeQuery();
stm.close();
stm.close();
} catch(SQLException e) {}
return(fResultado);
} catch(SQLException e) {}
}
}
Os quatro métodos públicos descritos anteriorO método getAbastecimentosDia é uma simples con29:-!&894&/4#.;3!&89!,-%2&!G!2-43.64,-%2&J%#!6&#4- mente: registraAbastecimento, getPrecoCombustigistrados no dia. O código-fonte está disponível na vel, getAbastecimentosDia e setPrecoCombustivel
estarão disponíveis para serem disponibilizados
Listagem 3.
como funções e procedimentos de banco logo que a
Listagem 3. !"#$%$ .-#01+/#-*(2-)#$/A(+3
classe estiver compilada dentro da base de dados.
O banco de dados respeita o princípio da classe
public static int getAbastecimentosDiaB9+5+3/>,3A+#Java no que diz respeito à política de acesso às clasdata) {
ses. Sendo assim, não é possível criar funções ou
procedimentos para acessar os métodos privados diResultSet rs = null;
retamente. Estes métodos complementares da nossa
int iQtde = 0;
classe estão contidos na Listagem 5.
try {
Connection conn =
DriverManager.getConnection
(89%1*:%-;+<,#:*$))-*#($):=);
PreparedStatement stm =
conn.prepareStatement(
“SELECT COUNT(1) FROM Abastecimentos “
+”WHERE Data = ?”);
stm.setDate(1,data);
rs = stm.executeQuery();
rs.next();
iQtde = rs.getInt(1);
stm.close();
} catch(SQLException e) {}
return(iQtde);
}
R!&(.2-!D46&S& %"46%2&/4#.;3!#&%&6F-%"%&24-'#43%Combustivel. Este método, também muito simples,
não retorna valor algum e é responsável por ajustar o
preço de algum dos combustíveis cadastrados.
Listagem 5. !"#$%$/ &'(5+%$/3
private static String buscaPessoa(String sCpf) {
String sResultado = null;
String sConsulta = “SELECT COUNT(1) FROM Pessoas
WHERE Cpf = ? “;
ResultSet rs = null;
int iQtde = 0;
try {
Connection conn = DriverManager.getConnection(
89%1*:%-;+<,#:*$))-*#($):=);
PreparedStatement stm =
conn.prepareStatement(sConsulta);
stm.setString(1,sCpf);
rs = stm.executeQuery();
rs.next();
iQtde = rs.getInt(1);
stm.close();
61 \
} catch(SQLException e) {
sResultado = “Houve um erro na busca do CPF.” +
e.getMessage();
}
if (iQtde == 0) {
sResultado = “O CPF informado não está adastrado.”;
}
return(sResultado);
}
private static String buscaVeiculo(String sPlaca) {
String sResultado = null;
String sConsulta = “SELECT COUNT(1) FROM Veiculo
WHERE Placa = ? “;
ResultSet rs = null;
int iQtde = 0;
//busca o nome e o email da pessoa
ResultSet rs = null;
String sEmail = null;
String sNome = null;
try {
Connection conn = DriverManager.getConnection(
89%1*:%-;+<,#:*$))-*#($):=);
PreparedStatement stm = conn.prepareStatement(
“SELECT nome, email FROM Pessoas WHERE
Cpf = ?”);
stm.setString(1,cpf);
rs = stm.executeQuery();
rs.next();
sNome = rs.getString(1);
sEmail = rs.getString(2);
stm.close();
} catch(SQLException e) {}
try {
Connection conn = DriverManager.getConnection(
89%1*:%-;+<,#:*$))-*#($):=);
PreparedStatement stm =
conn.prepareStatement(sConsulta);
stm.setString(1,sPlaca);
rs = stm.executeQuery();
rs.next();
iQtde = rs.getInt(1);
stm.close();
} catch(SQLException e) {
sResultado = “Houve um erro na busca do veículo.” +
e.getMessage();
}
if (iQtde == 0) {
sResultado = “O veículo informado não está
cadastrado.”;
}
return(sResultado);
}
private static void disparaEmail(String cpf,
"#$ valor) {
/ 62
44 +9</#+ + *$)6.<'+CD$ %$ /-'5(%$' %// envio de emails
Properties propriedades = System.getProperties();
propriedades.put(“mail.smtp.host”,
“smtp.minhaempresa.com.br”);
Session session = Session.getDefaultInstance(
propriedades, null);
try {
// cria a mensagem de email
MimeMessage msg = new MimeMessage(session);
{ 44 +9</#+ $ -2+(, %$ '-2-#-)#InternetAddress[] enderecos=
InternetAddress.parse(
“[email protected]”);
msg.addFrom(enderecos);
}
{ 44 +9</#+ $ -2+(, %- %-/#()$
InternetAddress[] enderecos =InternetAddress.
parse(sEmail);
msg.addRecipients(Message.RecipientType.TO,
enderecos);
}
!"#$"%&'(!)*"!&$&%"+&!(!,(-"!'&!./&##"!.(-!(!
"#0&'(! .(-(! 12/3'(4! &##3-! .(-(! &$%"#",0&'(! ,&! 5gura 6.
// assunto do email
msg.setSubject(“Inclusão de registro.”);
// cria o corpo da mensagem de email com o texto
44 %-/-9+%$3
Multipart mp = new
MimeMultipart();
MimeBodyPart mbp = new MimeBodyPart();
mbp.setText(sNome +
“, foi efetuado um abastecimento em “
+ “seu nome no valor de: “
+ valor);
mp.addBodyPart(mbp);
msg.setContent(mp);
44 *$)6.<'+ + %+#+ %- -)5($
msg.setSentDate(new 9+5+3<#(,3Date());
// dispara o email para o servidor informado
Transport.send(msg);
}catch(Exception e){}
}
}
3.2. Compilando a classe para o banco de
dados
Uma vez que o código da classe foi criado agora é
necessário compilá-lo para dentro do banco de dados.
Para que seja possível efetuar a compilação é
necessário que, ao instalar os aplicativos da Oracle
na máquina cliente, os pacotes de desenvolvimento
tenham sido selecionados. Deve-se procurar por um
arquivo executável chamado loadjava no local do disco onde as aplicações estão instaladas. Geralmente o
caminho é: C:\Oracle\Dev10g\Bin
Ao encontrar o executável deve-se dispará-lo seguindo a seguinte sintaxe:
:%!"Q!/!&19&X9297#.%Y)X24,M!YZXG!,3%&"4&"!"%2Y&
-resolve Classe.java
'!#!&/4#.;3!#&24&%&%GQ4-%&J%.&3%##4-!64,-4&3#.!"%&
no banco de dados é só utilizar a consulta contida na
Listagem 6.
Listagem 6. !"#$%&'( )(*( +,*-./(* ($ /&($$,$ 0(+(
registradas na base de dados.
SELECT OBJECT_NAME, STATUS
FROM USER_OBJECTS
WHERE OBJECT_TYPE = ‘JAVA CLASS’;
Figura 6. Situação das classes Java compiladas dentro do banco
de dados.
3.3. Publicar a classe como procedimento
Depois de executar a consulta nos objetos Java
'&!6&#"!'"!'&'(#!"!1"%35.&%!)*"!&!./&##"!7(3!.(%%"0&mente compilada, será necessário criar as chamadas
de seus métodos. Para cada método público da classe
é necessário criar uma função para aqueles que retornam valor, ou então um procedimento, para aqueles
que não retornam valor. Assim será possível invocar
estes métodos como objetos nativos do banco. Estas
funções e procedimentos que serão criados vão servir
como uma espécie de intérprete. Constam na Listagem 7 os códigos necessários.
Listagem 7. Criação dos procedimentos e funções
)(*( -#+"/(* "$ 12'"3"$ 3( /&($$,4
CREATE OR REPLACE FUNCTION FUN_GRAVA_
ABASTECIMENTO
(CPF VARCHAR2, PLACA VARCHAR2, TIPO_COMBUSTIVEL
NUMBER, QUANTIDADE NUMBER)
RETURN VARCHAR2 AS LANGUAGE JAVA NAME
567($',/-1,#'"$!"#'*"&&,*4*,8-$'*(67($',/-1,#'"9:(+(4
lang.String,
:(+(4&(#84;'*-#8< -#'< ="('> *,'%*# :(+(4&(#84;'*-#8?@
CREATE OR REPLACE FUNCTION FUN_PRECO_
COMBUSTIVEL
(TIPO_COMBUSTIVEL NUMBER)
RETURN NUMBER AS LANGUAGE JAVA NAME
‘AbastecimentosController.getPrecoCombustivel(int)
*,'%*# ="('?@
CREATE OR REPLACE FUNCTION FUN_
ABASTECIMENTOS_DIA(DATA DATE)
RETURN NUMBER AS LANGUAGE JAVA NAME
567($',/-1,#'"$!"#'*"&&,*48,'67($',/-1,#'"$A-(9:(+(4
sql.Date)
return int’;
CREATE OR REPLACE PROCEDURE PRO_AJUSTA_PRECO_
COMBUSTIVEL
63 \
-&3#!*-&!.&-&'&!$&%&!5/0%&%!(#!&."##(#!&(!.D'3@(?
(TIPO_COMBUSTIVEL NUMBER, NOVO_VALOR NUMBER)
AS LANGUAGE JAVA NAME
567($',/-1,#'"$!"#'*"&&,*4$,'B*,/"!"17%$'-+,&9-#'< ="('>?@
Os comandos apresentados na Listagem 7 disponibilizam as chamadas para os métodos da classe.
A partir daí será possível acessá-los como os demais
$%(."'3-",0(#!"!7*,+8"#!9:;<=:!,&031(#!'(!>%&./"?!
Um exemplo destas chamadas é mostrado na Listagem 8.
Listagem 8. Exemplo de chamada de função no
Oracle.
DECLARE
RETURN_VALUE NUMBER := 0;
DATA DATE := TO_DATE(‘2011-01-31’,’ YYYY-MM-DD’);
BEGIN
RETURN_VALUE := FUN_ABASTECIMENTOS_
DIA(DATA);
DBMS_OUTPUT.PUT(‘RETURN_VALUE: ‘);
DBMS_OUTPUT.PUTLINE(RETURN_VALUE);
DBMS_OUTPUT.PUT(‘DATA: ‘);
DBMS_OUTPUT.PUTLINE(DATA);
END;
!"#$%&'()*&#+,"($#
A utilização de código Java dentro do Oracle vem
oferecer uma alternativa à programação em banco já
$%"13&-",0"! (7"%".3'&! .(-! (! "-$%"@(! '(! 9:;<=:4!
que até então era a única linguagem de desenvolvimento disponível nesta plataforma. Adiante serão
listadas as características positivas e negativas sobre
o uso de Java dentro do banco de dados.
Desvantagens
As vantagens em transferir parte do código da
aplicação para o banco de dados são muitas, entretanto é necessário também considerar os pontos fracos, dentre os quais podemos destacar:
Dependência em relação ao administrador do
banco de dados. Como o código Java precisará ser
compilado internamente e interpretado como procedimentos nativos, haverá a necessidade da intervenção do administrador.
E0",+A(!.(-!(#!%")*3#30(#!'"!#"%13'(%?! !,&0*%&/!
utilizar um banco de dados como um simples repositório. Ao mudar esta abordagem e agregar necessidade de processamento é necessário ter atenção com
os recursos do servidor disponibilizados para o SGBD.
Dependência em relação à arquitetura. Atualmente o Oracle é o único gerenciador de banco de
dados capaz de rodar nativamente código Java. Ao
empregar esta arquitetura será mais difícil trocar de
plataforma futuramente.
Conclusão
Toda tecnologia disponível tem seus pontos fortes e fracos. Neste caso abordado não seria diferente.
Em determinados ambientes, nos quais o servidor é
potente e diversos softwares compartilham uma mesma base de dados, empregar esta estratégia de desenvolvimento pode ser uma alternativa interessante.
Vantagens
Menor tráfego de dados na rede. Uma vez que
parte do processamento será realizada dentro do servidor que abriga o banco de dados, o software que o
usuário opera receberá os dados já processados, diminuindo assim o número de transações.
Melhor desempenho. As máquinas virtuais padrão que conhecemos rodam sobre sistemas operacionais, já a dos bancos de dados Oracle é um objeto
interno integrado ao núcleo do SGBD. Esta integra+A(!$"%-30"!*-!&."##(!-&3#!"5.3",0"!&(#!%".*%#(#!'"!
memória e processamento.
Facilidade de manutenção. Como uma base de
dados pode ser comum a diversos softwares, a centralização do código no banco de dados pode resultar em
ganho de tempo, pois não será necessário atualizar
cada um dos softwares e serviços que acessam.
Mais segurança. Como o acesso aos objetos em
B&1&!"#0&%2!.('35.&'(!.(-(!*-!(6C"0(!'",0%(!'&!6&#"!
de dados, o administrador poderá aplicar neles as diretivas de segurança do Oracle. Desta forma, criamos
/ 64
/referências
!"#$%&$'!()*+%$#,!-./%*#$!%&0!1%2%!345/$0!6/5*$07/$89,!
:;/)#!0$!<==>,!?)8@5&A2$#!$BC!D+44@CEEFFF,0$2$#5@$/,
*5BE0;E%/4)*#$,@+@EGGGH>IIE./%*#$J%&0J1%2%J345/$0J
Procedures.htm].
!K5$28L)'!M%0)B,!-1%2%C!NO4$&0)&P!4+$!@5F$/!5Q!6KE3RK9,!
<==S,!?)8@5&A2$#!$BC!D+44@CEEFFF,0)B#%/,*5BE@%@$/,@0QT,
!65/4Q5#)5'!U5B,!-1%2%!345/$0!6/5*$07/$8!?$2$#5@$/V8!
W7)0$'!X$#$%8$!<!YZ,<[9,!./%*#$'!B%/\5!0$!<==<,!?)8@5&A2$#!
$BC!D+44@CEE05F&#5%0,5/%*#$,*5BE05*8E*0E]I=S=I^=IE
_%2%,Z<=E%Z``SZ,@0QT,!
!ab$/'!M$&L%4%87;/%B%&)%B!c!(%/)&P'!3+$/b#!c!3%@)/'!
X)*L!c!d)$8$&;$/P'!()*+%$#,!-1%2%!?$2$#5@$/V8!W7)0$!I=P!
X$#$%8$!<!YI=,<[9,!./%*#$'!%P5845!0$!<==`,!?)8@5&A2$#!$BC!
D+44@CEE05F&#5%0,5/%*#$,*5BE05*8E*0E]IZG=`^=IE_%2%,I=<E
b14187.pdf].
Download