Mostrando postagens com marcador java. Mostrar todas as postagens
Mostrando postagens com marcador java. Mostrar todas as postagens

quinta-feira, 17 de julho de 2008

Usando o Spring para trabalhar com LDAP

Olá amiguinhos,

Estou construindo uma aplicação com GWT e Spring, para me ajudar na hora de administrar usuários do Squid, pelo LDAP. O LDAP é muito bom para centralizar contas de usuários e em breve minha aplicação funcionará também para PDC no Samba, contas de e-mails e tudo que usa o LDAP. Eu vou disponibilizar para a comunidade assim que terminado.

Bom, vamos lá. Não vou entrar em muitos detalhes para a configuração do LDAP, mas deixe a base principal como dc=teste,dc=com,dc=br e o usuário administrador como cn=admin,dc=teste,dc=com,dc=br e a senha como teste.

Crie um projeto no Eclipse, se você está usando Maven, melhor ainda, adicione as dependências do spring-ldap lá, senão, acesse o site www.springframework.org/ldap e faça o download da biblioteca e suas dependências, e adicione no projeto.

Crie um arquivo applicationContext.xml e adicione o seguinte:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="contextSource"
class="org.springframework.ldap.core.support.LdapContextSource">
<property name="url" value="ldap://localhost:389" />
<property name="base" value="dc=teste,dc=com,dc=br" />
<property name="userDn" value="cn=admin,dc=teste,dc=com,dc=br" />
<property name="password" value="teste" />
</bean>

<bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
<constructor-arg ref="contextSource" />
</bean>

<bean id="userDao" class="br.org.teste.server.UserLdapDao">
<property name="ldapTemplate" ref="ldapTemplate" />
</bean>

</beans>


Beleza, a parte do Spring já está configurada, agora só fazermos um DAO e um POJO para trabalharmos.

Bom, se você for trabalhar com o objeto do LDAP inetOrgPerson, você pode fazer a seguinte estrutura na classe POJO:

public class UserLdap {

private String fullName;
private String lastName;
private String uid;
private String givenName;
private String userPassword;
private String description;
private String mail;

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getUserId() {
return uid;
}

public void setUserId(String userId) {
this.uid = userId;
}

public void setGivenName(String givenName) {
this.givenName = givenName;
}

public String getGivenName() {
return givenName;
}

public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}

public Object getUserPassword() {
return userPassword;
}

public void setDescription(String description) {
this.description = description;
}

public String getDescription() {
return description;
}

public void setMail(String mail) {
this.mail = mail;
}

public String getMail() {
return mail;
}
}


Agora vamos fazer a classe DAO. Vou mostrar a classe que eu fiz para as minhas necessidades, se você quiser se aprofundar mais, acesse o site http://static.springframework.org/spring-ldap/docs/1.2.1/reference/

O difícil é entender o Spring, depois que você entende como funciona, a documentação no site se torna muito mais fácil.

Então vamos lá, crie a classe DAO com o seguinte conteúdo:

public class UserLdapDao {


private LdapTemplate ldapTemplate;


/**
* Define um LdapTemplate do Spring
* @param ldapTemplate Define um objeto LdapTemplate
* @author anderson
* @version 1.0
*/
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}


/**
* Metodo que compila um DN
* @param user Define um objeto UserLdap
* @return Retorna um objeto Name
*/
public Name buildDn(UserLdap user) {
DistinguishedName dn = new DistinguishedName();
dn.add("cn", user.getFullName());

return dn;
}


/**
* Metodo que insere uma nova entrada no Ldap
* @param user Define um objeto UserLdap
* @author anderson
* @version 1.0
*/
public void create(UserLdap user) {
Name dn = buildDn(user);
ldapTemplate.bind(dn, null, buildAttributes(user));
}


/**
* Metodo que remove uma entrada no Ldap
* @param user Define um objeto UserLdap
* @author anderson
* @version 1.0
*/
public void delete(UserLdap user) {
Name dn = buildDn(user);
ldapTemplate.unbind(dn);
}


/**
* Metodo que atualiza um registro no Ldap
* @param user Define um objeto UserLdap
* @author anderson
* @version 1.0
*/
public void update(UserLdap user) {
Name dn = buildDn(user);
ldapTemplate.rebind(dn, null, buildAttributes(user));
}


/**
* Metodo que compila os atributos para ser inserido, removido ou atualizado
* @param user Define um objeto UserLdap
* @return Retorna um objeto Attributes
* @author anderson
* @version 1.0
*/
private Attributes buildAttributes(UserLdap user) {
Attributes attrs = new BasicAttributes();
BasicAttribute ocattr = new BasicAttribute("objectclass");

ocattr.add("top");
ocattr.add("inetOrgPerson");

attrs.put(ocattr);
attrs.put("sn", user.getLastName());
attrs.put("givenName", user.getGivenName());
attrs.put("uid", user.getUserId());
attrs.put("userPassword", user.getUserPassword());
attrs.put("description", user.getDescription());
attrs.put("mail", user.getMail());

return attrs;
}


/**
* Pega todo os registros do Ldap da classe inetOrgPerson
* @return Retorna uma lista de resultados
* @author anderson
* @version 1.0
*/
@SuppressWarnings("unchecked")
public List getAll() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass","inetOrgPerson"));
filter.and(new EqualsFilter("objectclass","person"));

return ldapTemplate.search("",
filter.encode(), new PersonAttributesMapper());
}


/**
* Pesquisa injetando os resultados no objeto UserLdao
* @param uid Define um uid para a consulta (Ex.: uid=teste)
* @return Retorna um objeto UserLdap
* @author anderson
* @version 1.0
*/
@SuppressWarnings("unchecked")
public List find(String uid) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass","inetOrgPerson"));
filter.and(new WhitespaceWildcardsFilter("uid",uid));

return ldapTemplate.search("",
filter.encode(), new PersonAttributesMapper());
}


/**
* Classe interna para atribuir os valores para um pojo UserLdap
* @author anderson
* @version 1.0
*/
private class PersonAttributesMapper implements AttributesMapper {

public Object mapFromAttributes(Attributes attributes)
throws NamingException {

byte[] senha = (byte[])attributes.get("userPassword").get();

UserLdap user = new UserLdap();
user.setFullName((String)attributes.get("cn").get());
user.setLastName((String)attributes.get("sn").get());
user.setUserId((String)attributes.get("uid").get());
user.setGivenName((String)attributes.get("givenName").get());
user.setMail((String)attributes.get("mail").get());
user.setUserPassword(new String(senha));
user.setDescription((String)attributes.get("description").get());

return user;
}

}
}


O método buildAttributes e a classe interna PersonAttributesMapper são os mais importantes e que fazem o negócio funcionar. O método buildAttributes compila os atributos do objeto inetOrgPerson para a base LDAP, você pode definir o objectclass e seus atributos, quais você quiser. A classe interna PersonAttributesMapper retorna no seu POJO, os valores da pesquisa que você solicitou.

E para chamar o DAO, é simples. Na classe que você precisar, basta fazer o seguinte:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] { "applicationContext.xml" });
UserLdapDao userDao = (UserLdapDao) ctx.getBean("userDao");
List resultado = userDao.find("uid=teste");


O Spring é mil e uma utilidades, dá para trabalhar só com ele tranquilamente em seu projeto. Estou gostando de usar ele e a cada dia aprendo alguma coisa nova.

Enjoy...

sábado, 12 de julho de 2008

Problema ao usar o DialogBox no GWT 1.5-RC1

Olá amiguinhos,

Eu migrei a versão do GWT no meu sistema para 1.5-RC1, porque estava impossível trabalhar com o Hibernate corretamente, sem gambiarras, sem os recursos do Java 5, como Generics, Annotations, etc.

Na migração, percebi que as funções das janelas feitas pelo DialogBox não mais funcionava. Fiquei louco e fiz vários testes e descobri o problema. Agora eu não sei se isso é um bug ou de agora em diante vai ser assim. Eu criava o dialog box assim:

public class Teste extends DialogBox {

public Teste() {
setText("Teste");
setSize("200","200");
center();

final Button btFechar = new Button("Fechar", new ClickListener() {
public void onClick(Widget sender) {
hide();
}

});
}
}


Depois ao instanciar a classe, basta chamar o método show(). Isso funciona no GWT 1.4, inclusive no Swing e AWT também. Agora para funcionar tem que ser feito assim:

public class Teste {

public Teste() {
DialogBox d = createDialog();
d.setSize("200","200");
d.center();
d.show();
}


public DialogBox createDialog() {
final DialogBox dialogBox = new DialogBox();
dialogBox.ensureDebugId("cwDialogBox");
dialogBox.setText("Teste");

Button btFechar = new Button("Fechar", new ClickListener() {
public void onClick(Widget sender) {
hide();
}

});

dialogBox.setWidget(btFechar);

return dialogBox;
}
}


Vamos torcer para ser apenas um bug da versão release candidate.

Enjoy...

quinta-feira, 10 de julho de 2008

VistaFei - Ferramenta RAD para GWT

Olá amiguinhos,

Hoje estava dando umas voltas pelo Orkut e achei uma comunidade relacionada ao GWT. E um cara falou do tal VistaFei. Entrei no site e já vi de cara que era free e tinha uma versão enterprise. Fiz o download dele e logo me surpriendi com a facilidade do programa. Eles usaram uma versão do Eclipse e trabalharam o plugin deles em cima do Eclipse, então se você só quer uma ferramenta para desenhar telas, está mais que bom.

http://www.wirelexsoft.com/VistaFei.html

Enjoy...

terça-feira, 8 de julho de 2008

Usando o Spring para enviar e-mails

Olá amiguinhos,

Em uma de minhas pesquisas, percebi que não tem um material bem explicativo sobre o Spring quando se trata de usar outras bibliotecas ou frameworks para executar tal serviço. Um amigo me explicou a filosofia do Spring, que por sinal é bem simples. A partir daí, eu entendi como funciona e agora da pra por pra funcionar qualquer coisa.

O Spring é interessante quando você quer criar uma instancia única para a aplicação inteira. O Hibernate por exemplo, o objeto SessionFactory é gigantesco e ficar instanciando isso o tempo todo, ou mais de uma vez, vai causar uma lentidão no sistema. Eu migrei o meu sistema Open Source, usando Spring junto com o Hibernate, o tempo de resposta de uma transação, mudou surpreendentemente. Também estou usando o Spring com o JavaMail. Não notei muita diferença de velocidade, mas pode ser muito útil mais pra frente quando o sistema ter que enviar e-mails em massa automaticamente para os administradores. Quem quiser conferir meu sistema Open Source, acesse http://articulus.sourceforge.net

Bom, vamos ao que interessa. Já expliquei a filosofia do Spring, ele cria uma instancia única para trabalhar no sistema inteiro. Mas é claro que ele não faz só isso, mas é sua principal caracteristica. Crie um projeto qualquer e adicione as bibliotecas do Spring e do JavaMail. Primeiro de tudo, edite seu web.xml da seguinte forma:

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:WEB-INF/classes/*.*</param-value>
</context-param>

<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>


Fazendo isso, o Spring vai procurar no contexto dele, os beans que serão instanciados. Próximo passo é criar um arquivo chamado applicationContext.xml. Se você criou um projeto pelo Maven, salve no diretório resources, senão salve em WEB-INF, junto com o web.xml.

Vamos criar os beans que serão instanciados da seguinte forma:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="mailSender" autowire="byName"
class="org.springframework.mail.javamail.JavaMailSenderImpl">

<property name="host" value="mail.seuhost.com.br" />

</bean>


<bean id="mailMessage" autowire="byName"
class="org.springframework.mail.SimpleMailMessage">

<property name="from"
value="teste@teste.com.br" />
<property name="to"
value="outroteste@teste.com.br" />
<property name="subject"
value="Teste" />
<property name="text"
value="Teste do Spring com o JavaMail" />

</bean>


<bean id="sendMail" autowire="byName"
class="br.org.teste.SendMail">

<property name="mailSender" ref="mailSender" />

</bean>

</beans>


Agora o mais importante, você vai ter que criar as classes e seus atributos, exatamente como criou nos beans, assim o Spring vai fazer a injeção nos objetos conforme você declarou.

Crie uma classe chamada SendMail e um pacote chamado br.org.teste e salve a classe lá dentro. Crie a seguinte estrutura:

public class SendMail {

private JavaMailSender mailSender;
private SimpleMailMessage mailMessage;

public void setMailSender(JavaMailSender mailSender) {
this.mailSender = mailSender;
}

public void setMailMessage(SimpleMailMessage mailMessage) {
this.mailMessage = mailMessage;
}

public void send() {
mailSender.send(mailMessage);
}
}


Reparam nas propriedades da classe? Possui os mesmos nomes dos beans, assim quando você for chamar o bean, ele vai fazer a injeção dos objetos pelo nome.

Agora crie uma classe que vai chamar o SendMail, pode ser um Servlet mesmo. Para chamar faça o seguinte:

ClassPathXmlApplicationContext ctx = 
new ClassPathXmlApplicationContext(
new String[] {"applicationContext.xml"});

SendMail send = (SendMail)ctx.getBean("sendMail");
send.send();


Simples né? Muita gente se embaralha totalmente no Spring, mas ele é mais simples do que parece. Eu também penei muito pra entender. Dizem por ae que não precisa invocar o contexto usando a classe ClassPathXmlApplicationContext, que o Spring faz a injeção automaticamente, mas nos meus testes ele só dava NullPointer dae eu desisti. Vou pesquisar mais. Amanhã ou depois eu posto como se faz para trabalhar com o Hibernate e Spring.

Enjoy...

domingo, 22 de junho de 2008

Eliminando o XML de configuração do Hibernate

Olá amiguinhos,

Você ainda usa o arquivo hibernate.cfg.xml? Pois é, eu também usava, até uns dias atrás. Vou explicar como é fácil usar a classe Configuration do Hibernate.

Suponho que você esteja usando um construtor para iniciar as configurações do Hibernate. Veja só como ficaria o construtor.

 public HibernateInitialize() {
Configuration cfg = new AnnotationConfiguration()
.addAnnotatedClass(SuaClasseAnotada.class)
.setProperty(Environment.HBM2DDL_AUTO, "update")
.setProperty(Environment.SHOW_SQL, "false")
.setProperty(Environment.DIALECT,
"org.hibernate.dialect.MySQLInnoDBDialect")
.setProperty("hibernate.connection.driver_class",
"com.mysql.jdbc.Driver")
.setProperty("hibernate.connection.url",
"jdbc:mysql://localhost:3306/test")
.setProperty("hibernate.connection.username", "test")
.setProperty("hibernate.connection.password", "test")
.setProperty("hibernate.connection.pool_size", "10");

SessionFactory sf = cfg.buildSessionFactory();
session = sf.openSession();
tx = session.beginTransaction();
}


Dispensa comentários né? Você acaba de montar a mesma estrutura do XML mas dentro da classe que faz a inicialização do Hibernate.

Nesse link você encontra todas as propriedades para JDBC do Hibernate.

Enjoy...

sexta-feira, 20 de junho de 2008

Configurando cluster com JBoss

Olá amiguinhos,

Tive que fazer uma tarefa aqui no trabalho e não encontrava a solução. Precisava fazer um cluster com o Tomcat, sendo que, se eu desse deploy em um servidor Tomcat, o outro também deveria receber esse deploy. E não estava acontecendo isso e eu estava ficando desesperado já. Resolvi perguntar para meu professor de Protocolos, Cléber, se ele já havia feito cluster com o Tomcat. Ele me deu a dica de usar o JBoss.

O JBoss é um servidor de aplicações J2EE, assim como o Tomcat, mas com recursos bem melhores. Se quiser saber mais sobre o JBoss, clique aqui.

O JBoss já vem pronto para ser cluster. Você não precisa fazer nenhuma configuração se você não quiser, se for algo bem simples é claro. Vou ensinar como fazer um cluster conforme o meu cenário.

Supondo que você instalou o JBoss em /opt/jboss-4.2.2.GA. Eu criei um link simbólico chamado jboss para facilitar. Acesse o diretório /opt/jboss/bin e execute o seguinte comando:

# ./run.sh -c all -b 172.16.5.30


Onde -c é para informar qual configuração você quer, que é all, e -b é o seu IP. Agora acesse o diretório /opt/jboss/server e execute um ls para você ver. Ali fica os diretórios all, default e minimal. Como indicamos que seria o all, vamos trabalhar dentro do diretório all. É um pouco diferente o esquema do JBoss mas logo você acostuma.

Por padrão, o JBoss trabalha a comunicação entre os clusters, usando o protocolo UDP. Fica a seu critério. Se sua rede for um pouco instável, é recomendado você usar o protocolo TCP, pois é orientado a conexão.

Se você quiser usar o TCP, você precisa ter que fazer um trabalho muito árduo. Foi uma das configurações mais complicadas que eu já fiz e sinceramente, espero não precisar fazer mais. Hehehehehe.

Abra o seguinte arquivo:

# vim /opt/jboss/server/all/deploy/cluster-service.xml


Procure a tag <Config> e comente ela da seguinte forma:

<!--<Config>
....
....
</Config>-->


Um pouco mais para baixo, tem outra tag <Config> mas ela já está comentada. É a configuração para TCP. Descomente-a. E dentro da tag <Config> procure a tag <TCPPING> e altere o atributo initial_hosts da seguinte forma:

<TCPPING 
initial_hosts="${jboss.bind.address}[7800],172.16.5.163[7800]"
port_range="3"
timeout="3000"
down_thread="false" up_thread="false"
num_initial_members="3"/>


Onde ${jboss.bind.address}[7800] pega seu IP local e o outro IP seria o outro nó do seu cluster. Você pode indicar quantos você quiser. O TCPPING amarra os principais nós ao seu cluster. Assim ele não fica procurando e já vai direto ao nó.

Dificil, não? É eu sei, dá vontade de formatar o PC e começar tudo de novo de tão complicado hehehehehe.

Farm War Deployment


Bom, se você já deu uma brincada no JBoss para ver como ele funciona, já percebeu que para fazer deploy nele, basta apenas copiar o .war para o diretório /opt/jboss/server/all/deploy. Mas no nosso caso, precisamos replicar a aplicação para todos os nós, assim, você faz 1 deploy e ele se encarrega de mandar para os outros nós automaticamente. Meus olhos enxeram de lágrimas quando eu vi a facilidade de fazer isso. Basta copiar o .war para o diretório /opt/jboss/server/all/farm.

Fique olhando no console, onde você executou o comando run.sh, o que ele fez.

Simples, não? Ah, um detalhe muito importante. Se você configurou 1 nó como TCP, todos os outros precisam ser TCP também. Configure os outros nós conforme a configuração acima e execute o comando run.sh da mesma forma, apenas alterando o IP.

Espero ter ajudado alguém com esse post.

Enjoy...

terça-feira, 17 de junho de 2008

Trabalhando com Threads e evitando congelamento

Olá amiguinhos,

Vou ensinar um esquema que me ajudou por bastante tempo. Controle por iterações no sistema. Um objeto do tipo observador, irá acompanhar as mudanças do sistema.

Vamos lá, crie a seguinte classe, que será nossa interface gráfica principal:

package br.org.testethread;

import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Principal extends JFrame {

private static final long serialVersionUID = 1L;

public Principal() {
super("Teste Thread");
setSize(150, 150);

Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
int w = getSize().width;
int h = getSize().height;
int x = (dim.width-w) / 2;
int y = (dim.height-h) / 2;
setLocation(x, y);


JPanel p = new JPanel();

final JLabel lb = new JLabel("0");
final JButton bt = new JButton("Clique aqui!");

bt.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {
for (int i=0; i < 5000000; i++) {
lb.setText(String.valueOf(i));
}
}

});

p.add(lb);
p.add(bt);

getContentPane().add(p);
}


public static void main(String[] args) {
new Principal().setVisible(true);
}

}




Rode o programa e clique no botão para você ver o que acontece. A interface vai congelar, dependendo do seu processador é claro. Se sua máquina for muito potente, aumente a quantidade de laços. Aqui no meu, tive que deixar em 5 milhões para ficar congelada a tela.

Para resolver esse problema, crie uma classe chamada Observador com a seguinte estrutura:

package br.org.testethread;

import java.util.Observable;
import java.util.Observer;

public class Observador extends Observable implements Runnable {

public Observador(Observer observador) {
// Adiciona o objeto observador a lista de observadores
addObserver(observador);
}

public void run() {
int i;
for (i = 0; i <= 10; i++) {
// Notifica o processamento a cada 10 iterações
if ((i % 10 == 0)) {
notifyObservers(new Integer(i));
setChanged();
}
}

// Notifica fim do processo
notifyObservers(new Boolean(true));
setChanged();
}
}


Já temos a nossa classe do tipo Observer, ela vai observar as alterações dos objetos a cada 10 iterações e vai notificar os observadores. Agora precisamos fazer algumas alterações na nossa classe Principal, ela deverá ficar da seguinte forma:

package br.org.testethread;

import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

// Adiciona a interface Observer
public class Principal extends JFrame implements Observer {

private static final long serialVersionUID = 1L;

// Cria um objeto do tipo Thread
private Thread processo;

// Declara o label para uso global
private JLabel lb;

public Principal() {
super("Teste Thread");
setSize(150, 150);

Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
int w = getSize().width;
int h = getSize().height;
int x = (dim.width - w) / 2;
int y = (dim.height - h) / 2;
setLocation(x, y);

JPanel p = new JPanel();

lb = new JLabel("0");
final JButton bt = new JButton("Clique aqui!");

bt.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {
// metodo que vai criar uma nova thread
executaProcesso();
}

});

p.add(lb);
p.add(bt);

getContentPane().add(p);
}

public static void main(String[] args) {
new Principal().setVisible(true);
}


private void executaProcesso() {
if (processo == null) {
processo = new Thread(new Observador(this));
processo.start();
} else {
JOptionPane.showMessageDialog(this,
"O processo ainda está em execução!");
}
}

public void update(Observable o, Object arg) {
if (arg instanceof Integer) {
// O laco foi transportado para essa parte
for (int i = 0; i < 5000000; i++) {
lb.setText(String.valueOf(i));
}
} else if (arg instanceof Boolean) {
if (((Boolean) arg).booleanValue()) {
JOptionPane.showMessageDialog(this,
"Processo terminado!");
}
}
}
}


Agora clique no botão para você ver. Você vai acompanhar a incrementação sem que a tela congele. Enquanto ele continua incrementando, experimente clicar no botão novamente para ver o que acontece. Ele te alerta que o processo ainda não terminou. A interface Observer vai "observar" o processo de acordo com a classe Observador. Não é muito difícil de entender, mas você precisa ter uma noção básica de Thread para entender.

Não testei esse esquema para Web, quem quiser testar e comentar aqui, seria legal. Mas acredito que para Web o esquema é meio diferente.

Enjoy...