Compare commits

..

3 Commits

39 changed files with 714 additions and 119 deletions

7
.gitignore vendored
View File

@ -1,7 +0,0 @@
/pitiupi/build/
/pitiupi/dist/
/pitiupi.chat/build/
/pitiupi.chat/dist/
/pitiupi.chat/nbproject/private/
/pitiupi.screenCast/build/
/pitiupi.screenCast/dist/

5
.idea/misc.xml Normal file
View File

@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

105
.idea/workspace.xml Normal file
View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="62118a1b-608f-4144-b29c-cf46419ca676" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/pitiupi/Documentation.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pitiupi/PluginTutorial.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi.chat/build/built-jar.properties" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi.chat/build/built-jar.properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi.chat/dist/pitiupi.chat.jar" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi.chat/dist/pitiupi.chat.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi.screenCast/build/built-jar.properties" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi.screenCast/build/built-jar.properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi.screenCast/dist/pitiupi.screenCast.jar" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi.screenCast/dist/pitiupi.screenCast.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/.netbeans_automatic_build" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/.netbeans_update_resources" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/GUI/Login.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/GUI/Login.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/GUI/MainWindow.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/GUI/MainWindow.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/GUI/MenuBar.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/GUI/MenuBar.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/Main$1.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/Main$1.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/Main.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/Main.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/control/PluginLoader.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/control/PluginLoader.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/net/Socket.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/net/Socket.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/plugin/Plugin.class" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/build/classes/pitiupi/plugin/Plugin.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/dist/README.TXT" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/dist/pitiupi.jar" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/dist/pitiupi.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/plugins/pitiupi.chat.jar" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/plugins/pitiupi.chat.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/plugins/pitiupi.screenCast.jar" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/plugins/pitiupi.screenCast.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pitiupi/src/pitiupi/GUI/MenuBar.java" beforeDir="false" afterPath="$PROJECT_DIR$/pitiupi/src/pitiupi/GUI/MenuBar.java" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="3GbOdQKFlKqBjAjqjAv5TRPxr6Z" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Application.Main.executor": "Run",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "feature/help-and-documentation",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/extre/OneDrive/Documentos/Faculdade/2026_02/Monitoria/Pitiupi",
"settings.editor.selected.configurable": "preferences.keymap"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\extre\OneDrive\Documentos\Faculdade\2026_02\Monitoria\Pitiupi\pitiupi\plugins" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\extre\OneDrive\Documentos\Faculdade\2026_02\Monitoria\Pitiupi\pitiupi\plugins" />
</key>
</component>
<component name="RunManager">
<configuration name="Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="pitiupi.Main" />
<module name="pitiupi" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="pitiupi.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<recent_temporary>
<list>
<item itemvalue="Application.Main" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-bf35d07a577b-intellij.indexing.shared.core-IU-252.28539.54" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="62118a1b-608f-4144-b29c-cf46419ca676" name="Changes" comment="" />
<created>1784236888334</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1784236888334</updated>
</task>
<servers />
</component>
</project>

5
pitiupi.chat/.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="pitiupi.chat">
<CLASSES>
<root url="jar://$PROJECT_DIR$/dist/pitiupi.chat.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/pitiupi.chat.iml" filepath="$PROJECT_DIR$/pitiupi.chat.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -1,6 +1,6 @@
#Thu, 16 Jul 2026 15:21:36 -0300
#Sat, 18 Jul 2026 10:32:46 -0300
/home/flavio/NetBeansProjects/pitiupi.chat=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.chat=
/home/flavio/NetBeansProjects/pitiupi=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=

Binary file not shown.

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -94,7 +94,7 @@ public class ChatWindow extends javax.swing.JDialog {
if (jTextArea1.getText().trim().length() == 0) {
return;
}
ChatMessage message = new ChatMessage(mainWindow.getUserName(), jTextArea1.getText());
ChatMessage message = new ChatMessage(mainWindow.getName(), jTextArea1.getText());
this.sendChatMessage(message);
jTextArea1.setText("");
}

View File

@ -1,6 +1,6 @@
#Thu, 16 Jul 2026 15:21:39 -0300
#Sat, 18 Jul 2026 10:33:07 -0300
/home/flavio/NetBeansProjects/pitiupi.screenCast=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.screenCast=
/home/flavio/NetBeansProjects/pitiupi=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=

Binary file not shown.

5
pitiupi/.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="pitiupi">
<CLASSES>
<root url="jar://$PROJECT_DIR$/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -0,0 +1,10 @@
<component name="libraryTable">
<library name="plugins">
<CLASSES>
<root url="jar://$PROJECT_DIR$/plugins/pitiupi.chat.jar!/" />
<root url="jar://$PROJECT_DIR$/plugins/pitiupi.screenCast.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

5
pitiupi/.idea/misc.xml Normal file
View File

@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/pitiupi.iml" filepath="$PROJECT_DIR$/pitiupi.iml" />
</modules>
</component>
</project>

6
pitiupi/.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,208 @@
# Arquitetura da aplicação
## Visão geral
O Share My Sheet é uma plataforma P2P baseada em uma arquitetura de plugins,
desenvolvida para permitir a criação de aplicações distribuídas sobre uma
infraestrutura comum de comunicação em rede.
A aplicação fornece mecanismos de descoberta de usuários, comunicação
multicast, troca de mensagens serializadas e integração de extensões por meio
de plugins. Dessa forma, novos comportamentos podem ser adicionados sem
modificar o núcleo da aplicação.
O projeto foi desenvolvido como suporte às práticas da disciplina de Redes de
Computadores, servindo também como base para experimentação de aplicações
distribuídas e desenvolvimento de projetos acadêmicos.
## Arquitetura geral
A aplicação é dividida em três grupos principais:
- **Núcleo da aplicação**: A `MainWindow` atua como ponto central da aplicação,
sendo responsável por inicializar e integrar esses componentes durante a
execução.
- **Sistema de plugins**: responsável por adicionar funcionalidades sem
alterar o núcleo.
- **Comunicação de rede**: responsável pela troca de mensagens entre
instâncias da aplicação.
## Inicialização da aplicação
1. A MainWindow é criada.
2. O Socket é inicializado e inicia a comunicação multicast.
3. O HeartbeatManager é criado e inicia o envio periódico de heartbeats.
4. O painel de usuários online é registrado como listener do HeartbeatManager.
5. O PluginLoader procura e carrega os plugins disponíveis.
## Sistema de comunicação
### Socket
A classe `Socket` é responsável pela comunicação de rede da aplicação.
Ela utiliza comunicação multicast UDP para permitir que diferentes
instâncias da aplicação troquem mensagens sem a necessidade de um servidor
central.
A classe estende `Thread`, pois mantém um processo contínuo de recepção de
mensagens enquanto a aplicação continua executando suas demais atividades.
Durante sua inicialização, o socket:
- obtém o endereço do grupo multicast;
- cria um `MulticastSocket` utilizando a porta configurada;
- configura os buffers de comunicação;
- entra no grupo multicast.
#### Envio de mensagens
O método `send()` recebe uma mensagem já serializada em bytes e cria um
`DatagramPacket`, enviando-o para o grupo multicast.
De forma geral, o socket atua apenas como mecanismo de transporte das mensagens.
A única exceção é o tratamento de HeartbeatMessage, utilizado pela infraestrutura
da aplicação para manter a lista de usuários ativos.
#### Recepção de mensagens
O método `receive()` mantém um loop aguardando novas mensagens multicast.
Ao receber uma mensagem:
1. Os dados são analisados para verificar se correspondem a um
`HeartbeatMessage`.
2. Caso seja um heartbeat, a mensagem é encaminhada ao `HeartbeatManager`
para atualização dos usuários ativos.
3. Caso contrário, a mensagem é enviada para todos os plugins carregados
através do método `receiveMessage()`.
Esse comportamento permite que a aplicação principal trate mensagens de
infraestrutura (como heartbeat), enquanto mensagens específicas de plugins
são processadas pelas extensões correspondentes.
### Message
A classe `Message` representa a estrutura base das mensagens trocadas pela
aplicação.
Todas as mensagens utilizadas pelo sistema devem herdar dessa classe. Ela
implementa `Serializable`, permitindo que objetos de mensagem sejam
convertidos em vetores de bytes para transmissão pela rede.
A serialização é realizada pelo método `toByteArray()`, que transforma uma
instância da mensagem em uma representação binária enviada pelo `Socket`.
Mensagens específicas devem estender essa classe adicionando os atributos e
comportamentos necessários para cada funcionalidade.
### Heartbeat
O mecanismo de heartbeat é utilizado para identificar quais usuários estão
ativos na rede.
A aplicação envia periodicamente uma `HeartbeatMessage` contendo informações
do usuário e um timestamp. Quando outra instância recebe essa mensagem, o
`HeartbeatManager` atualiza o registro do usuário correspondente.
O fluxo de heartbeat é independente das mensagens dos plugins:
1. `HeartbeatManager` cria uma `HeartbeatMessage`.
2. A mensagem é serializada utilizando `Message.toByteArray()`.
3. O `Socket` transmite a mensagem pela rede multicast.
4. O `Socket` identifica mensagens de heartbeat recebidas e encaminha ao
`HeartbeatManager`.
5. O `HeartbeatManager` atualiza a lista de usuários ativos e notifica os
listeners.
## Sistema de plugins
### Interface Plugin
A interface `Plugin` define o contrato que deve ser implementado por qualquer
extensão da aplicação.
Um plugin deve fornecer informações básicas de identificação (`name`,
`author` e `version`), inicializar seu estado através de `createPlugin()` e
participar do sistema de comunicação utilizando mensagens derivadas de
`Message`.
Os principais métodos são:
#### Identificação
Os métodos:
- `getName()`
- `getAuthor()`
- `getVersion()`
fornecem informações utilizadas pela aplicação para identificar o plugin.
#### Inicialização
`createPlugin(MainWindow window)`
É chamado após o carregamento do plugin e permite que ele registre seus
componentes na aplicação, como menus ou elementos de interface.
#### Comunicação
`getMessage()`
Deve retornar uma mensagem utilizada pelo plugin para comunicação com outras
instâncias da aplicação.
`receiveMessage(byte[] message)`
É chamado pela aplicação sempre que uma mensagem é recebida. Cada plugin deve
verificar se a mensagem pertence ao seu tipo e realizar o processamento
correspondente.
### Descoberta de plugins
Para serem reconhecidos, os plugins devem possuir os arquivos de configuração
do Java Service Provider Interface (SPI):
```
MeusPlugins
|
+---META-INF
|
+---services
|
+---pitiupi.plugin.Plugin
```
O arquivo `pitiupi.plugin.Plugin` deve conter o nome completo das classes que
implementam a interface Plugin:
```
MeusPlugins.plugins.NomeDaClasse
```
Para serem carregados, os plugins devem estar empacotados como arquivos JAR
e colocados no diretório plugins da aplicação.
### Carregamento
Ao iniciar a aplicação, o `PluginLoader` executa os seguintes passos:
1. Procura arquivos `.jar` dentro do diretório `plugins`.
2. Cria um `URLClassLoader` contendo os JARs encontrados.
3. Utiliza `ServiceLoader` para localizar classes que implementam
`Plugin`.
4. Instancia cada plugin encontrado.
5. Chama o método `createPlugin(MainWindow)`.
6. Registra o plugin na lista de plugins ativos da aplicação.
## Interface gráfica
A interface gráfica da aplicação é baseada em Swing e é centralizada na
classe `MainWindow`.
Durante a inicialização, cada plugin recebe uma referência para a
`MainWindow` através do método `createPlugin(MainWindow)`.
Essa referência permite integrar componentes gráficos à aplicação, sendo a
principal forma de extensão a adição de menus à barra de menus:
```java
JMenu menu = new JMenu("Meu Plugin");
window.addMenu(menu);

View File

@ -0,0 +1,2 @@
# Como criar plugins para o Share my Sheet

11
pitiupi/pitiupi.iml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

Binary file not shown.

View File

@ -0,0 +1,72 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package pitiupi.GUI;
import javax.swing.*;
/**
* Janela que exibe informações sobre a aplicação, incluindo versão,
* descrição e autores.
*
* @author gustavo
*/
public class About extends javax.swing.JFrame {
public About () {
this.setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE);
initComponents();
}
private void initComponents() {
this.setTitle("About");
this.setBounds(new java.awt.Rectangle(0, 0, 800, 600));
this.getContentPane().setLayout(new java.awt.BorderLayout(5, 5));
this.setPreferredSize(new java.awt.Dimension(800, 600));
JScrollPane scroll = getJScrollPane();
this.add(scroll);
}
private static JScrollPane getJScrollPane() {
JEditorPane texto = new JEditorPane();
texto.setContentType("text/html");
texto.setEditable(false);
texto.setText("""
<html>
<body>
<h1>Share My Sheet</h1>
\s
<p>
<b>Versão:</b> 1.0
</p>
\s
<p>
Plataforma P2P baseada em plugins para o desenvolvimento de aplicações
distribuídas. Criada para apoiar as práticas da disciplina de Redes de
Computadores e servir como base para projetos acadêmicos e TCCs.
\s
</p>
\s
<p>
<b>Autores:</b><br>
Flávio<br>
Tony<br>
Gustavo Henrique
</p>
\s
<p>
Universidade Federal de São João del-Rei (UFSJ)
</p>
\s
</body>
</html>
\s""");
return new JScrollPane(texto);
}
}

View File

@ -1,3 +1,8 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package pitiupi.GUI;
import java.awt.BorderLayout;
@ -6,7 +11,6 @@ import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.util.prefs.Preferences;
import javax.swing.JButton;
import javax.swing.JDialog;
@ -16,16 +20,15 @@ import javax.swing.JSpinner;
import javax.swing.JTextField;
/**
* Janela de preferencias da aplicação.
*
* <p>Permite ao usuário informar o nome utilizado na rede e a porta
* multicast que será utilizada pela comunicação.</p>
*
* @author flavio
* @author tony
*/
public final class Login extends JDialog {
private static final Preferences prefs = Preferences.userNodeForPackage(Login.class);
private static final String PREF_NOME = "lastUserName";
private static final String PREF_PORTA = "lastPort";
JLabel nameLabel = new JLabel("Name: ");
JLabel portLabel = new JLabel("Port: ");
@ -44,12 +47,8 @@ public final class Login extends JDialog {
public void initComponents() {
this.setTitle("Login");
String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name"));
int portaSalva = prefs.getInt(PREF_PORTA, 8000);
portField.setModel(new javax.swing.SpinnerNumberModel(portaSalva, 8000, 9000, 1));
nameField.setText(nomeSalvo);
portField.setModel(new javax.swing.SpinnerNumberModel(8000, 8000, 9000, 1));
nameField.setText(System.getProperty("user.name"));
JPanel topPanel = new JPanel(new GridBagLayout());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
@ -88,15 +87,6 @@ public final class Login extends JDialog {
this.add(buttonPanel, BorderLayout.SOUTH);
okButton.addActionListener((ActionEvent e) -> {
try {
portField.commitEdit();
} catch (java.text.ParseException ex) {
return;
}
prefs.put(PREF_NOME, nameField.getText());
prefs.putInt(PREF_PORTA, (Integer) portField.getValue());
Login.this.setVisible(false);
});

View File

@ -14,9 +14,18 @@ import pitiupi.plugin.Plugin;
import pitiupi.control.HeartbeatManager;
/**
* Janela principal da aplicação.
*
* <p>Responsável por inicializar a interface gráfica e coordenar os
* principais componentes da aplicação, incluindo comunicação de rede,
* gerenciamento de heartbeat e carregamento de plugins.</p>
*
* <p>A instância desta classe é compartilhada com os plugins para permitir
* integração com a aplicação principal.</p>
*
* @author flavio
* @author tony
* @author Gustavo
*/
public class MainWindow extends javax.swing.JFrame {
@ -32,13 +41,18 @@ public class MainWindow extends javax.swing.JFrame {
private Socket socket;
private String userName;
private int port;
private final String userName;
private final int port;
private PluginLoader pluginManager;
private final List<Plugin> plugins;
/**
* Cria a janela principal da aplicação.
*
* @param userName nome utilizado para identificação na rede.
* @param port porta utilizada na comunicação multicast.
*/
public MainWindow(String userName, int port) {
this.userName = userName;
this.port = port;
@ -48,6 +62,14 @@ public class MainWindow extends javax.swing.JFrame {
}
private void initComponents() {
initGUI();
initSocket();
initHeartbeat();
initPlugins();
this.setSize(new java.awt.Dimension(1024, 768));
}
private void initGUI() {
this.setJMenuBar(menuBar);
this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
this.setTitle("Share My Sheet");
@ -66,22 +88,26 @@ public class MainWindow extends javax.swing.JFrame {
this.getContentPane().add(statusPanel, java.awt.BorderLayout.PAGE_END);
this.pack();
}
private void initSocket() {
this.socket = new Socket(this);
this.socket.start();
}
private void initHeartbeat() {
this.heartbeatManager = new HeartbeatManager(this);
this.onlineUsersPanel = new OnlineUsersPanel();
this.heartbeatManager.addPeerListener(this.onlineUsersPanel);
this.getContentPane().add(this.onlineUsersPanel, java.awt.BorderLayout.LINE_END);
this.heartbeatManager.start();
}
this.setSize(new java.awt.Dimension(1024, 768));
private void initPlugins() {
this.pluginManager = new PluginLoader(this);
this.pluginManager.loadPlugins();
}
public void exit() {
this.setVisible(false);
this.dispose();
@ -111,28 +137,4 @@ public class MainWindow extends javax.swing.JFrame {
public List<Plugin> getPlugins() {
return plugins;
}
public void setUserName(String userName) {
this.userName = userName;
updateStatus();
}
public void setPort(int port) {
this.port = port;
reconnect();
updateStatus();
}
private void updateStatus() {
this.status.setText("Connected on port " + this.port + " name " + this.userName);
}
private void reconnect() {
if (this.socket != null) {
this.socket.close();
}
this.socket = new Socket(this);
this.socket.start();
}
}

View File

@ -9,14 +9,18 @@ import javax.swing.JMenu;
import javax.swing.JMenuBar;
/**
* Barra de menus principal da aplicação.
*
* <p>Gerencia os menus padrões da interface e disponibiliza um espaço
* para que plugins adicionem seus próprios menus.</p>
*
* @author flavio
* @author tony
*
* @author Gustavo
*/
public class MenuBar extends JMenuBar {
private javax.swing.JMenuItem setupMenu;
private javax.swing.JMenuItem aboutMenu;
private javax.swing.JMenu fileMenu;
private javax.swing.JMenu helpMenu;
private javax.swing.JMenu pluginMenu;
@ -34,6 +38,7 @@ public class MenuBar extends JMenuBar {
helpMenu = new javax.swing.JMenu();
pluginMenu = new javax.swing.JMenu();
setupMenu = new javax.swing.JMenuItem();
aboutMenu = new javax.swing.JMenuItem();
fileMenu.setText("File");
exitMenu.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F4, java.awt.event.InputEvent.ALT_MASK));
@ -46,29 +51,39 @@ public class MenuBar extends JMenuBar {
this.add(pluginMenu);
helpMenu.setText("Help");
setupMenu.setText("Preferences");
setupMenu.addActionListener(this::setupMenuActionPerformed);
helpMenu.add(setupMenu);
aboutMenu.setText("About");
aboutMenu.addActionListener(this::aboutMenuActionPerformed);
helpMenu.add(aboutMenu);
this.add(helpMenu);
}
private void exitMenuActionPerformed(java.awt.event.ActionEvent evt) {
mainWindow.exit();
}
private void setupMenuActionPerformed(java.awt.event.ActionEvent evt) {
private void setupMenuActionPerformed(java.awt.event.ActionEvent evt) {
Login login = new Login();
login.setVisible(true);
mainWindow.setUserName(login.getName());
mainWindow.setPort(login.getPort());
login.dispose();
}
private void aboutMenuActionPerformed(java.awt.event.ActionEvent evt) {
About about = new About();
about.setVisible(true);
}
/**
* Adiciona um menu fornecido por um plugin à seção de plugins da barra
* de menus.
*
* @param menu menu criado pelo plugin.
*/
public void addPlugin(JMenu menu){
this.pluginMenu.add(menu);
}

View File

@ -11,7 +11,10 @@ import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import pitiupi.control.PeerInfo;
import pitiupi.control.PeerListener;
/**
* Painel da interface gráfica responsável por exibir os usuários ativos
* na rede.
*
* @author tony
*/
@ -30,6 +33,11 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
add(new JScrollPane(userList), BorderLayout.CENTER);
}
/**
* Atualiza a lista de usuários exibida no painel.
*
* @param peers lista atualizada de usuários ativos na rede.
*/
@Override
public void onPeersChanged(List<PeerInfo> peers) {
SwingUtilities.invokeLater(() -> {

View File

@ -12,10 +12,8 @@ import java.util.logging.Logger;
/**
*
* @author flavio
* @author tony
*/
public class Main {
/**
* @param args the command line arguments
*/
@ -41,17 +39,16 @@ public class Main {
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(pitiupi.GUI.MainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
pitiupi.GUI.Login login = new pitiupi.GUI.Login();
login.setVisible(true);
pitiupi.GUI.MainWindow main;
main = new pitiupi.GUI.MainWindow(login.getName(), login.getPort());
main = new pitiupi.GUI.MainWindow(System.getProperty("user.name"), 8000);
main.setVisible(true);
}
}
});
}

View File

@ -14,8 +14,12 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import pitiupi.GUI.MainWindow;
import pitiupi.net.HeartbeatMessage;
import java.util.UUID;
/**
* Gerencia o mecanismo de heartbeat da aplicação.
*
* <p>Responsável por anunciar a presença da aplicação na rede, receber
* heartbeats de outros usuários, controlar o tempo desde a última atividade
* dos peers e notificar listeners quando a lista de usuários ativos muda.</p>
*
* @author tony
*/
@ -28,7 +32,6 @@ public class HeartbeatManager {
private final Map<String, PeerInfo> peers = new ConcurrentHashMap<>();
private final List<PeerListener> listeners = new CopyOnWriteArrayList<>();
private ScheduledExecutorService scheduler;
private final String instanceId = UUID.randomUUID().toString();
public HeartbeatManager(MainWindow mainWindow) {
this.mainWindow = mainWindow;
@ -48,21 +51,32 @@ public class HeartbeatManager {
private void announce() {
try {
HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName(), instanceId);
HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName());
mainWindow.getSocket().send(msg.toByteArray());
} catch (IOException ex) {
Logger.getLogger(HeartbeatManager.class.getName()).log(Level.WARNING, "Falha ao enviar heartbeat", ex);
}
}
// Chamado pelo Socket quando um pacote é identificado como heartbeat
/**
* Processa uma mensagem de heartbeat recebida de outro usuário.
*
* <p>Atualiza a informação do peer correspondente e notifica os listeners
* caso um novo usuário seja identificado.</p>
*
* <p>Mensagens de heartbeat enviadas pela própria aplicação são ignoradas.</p>
*
* @param msg mensagem de heartbeat recebida.
* @param from endereço de rede do usuário que enviou a mensagem.
*/
public void receiveHeartbeat(HeartbeatMessage msg, InetAddress from) {
// ignora o próprio heartbeat
if (msg.getInstanceId().equals(instanceId) ) {
if (msg.getUserName().equals(mainWindow.getUserName())) {
return;
}
String key = msg.getInstanceId();
String key = from.getHostAddress() + "#" + msg.getUserName();
boolean isNew = !peers.containsKey(key);
PeerInfo peer = peers.computeIfAbsent(key, k -> new PeerInfo(msg.getUserName(), from, msg.getTimestamp()));
@ -88,6 +102,14 @@ public class HeartbeatManager {
}
}
/**
* Adiciona um listener para receber atualizações sobre os peers conhecidos.
*
* <p>O listener recebe imediatamente o estado atual dos peers ao ser
* registrado.</p>
*
* @param l listener que será notificado sobre alterações.
*/
public void addPeerListener(PeerListener l) {
listeners.add(l);
l.onPeersChanged(new ArrayList<>(peers.values()));

View File

@ -2,6 +2,11 @@ package pitiupi.control;
import java.net.InetAddress;
/**
* Representa um usuário identificado na rede e as informações necessárias
* para acompanhar sua presença através de mensagens de heartbeat.
*
* <p>Armazena o nome do usuário, seu endereço de rede e o instante da
* última mensagem recebida.</p>
*
* @author tony
*/
@ -29,6 +34,11 @@ public class PeerInfo {
return lastSeen;
}
/**
* Atualiza o instante da última atividade do usuário.
*
* @param timestamp novo instante de atividade.
*/
public void touch(long timestamp) {
this.lastSeen = timestamp;
}

View File

@ -1,10 +1,21 @@
package pitiupi.control;
import java.util.List;
/**
* Interface para receber notificações sobre alterações nos usuários
* conhecidos na rede.
*
* <p>Implementações desta interface são notificadas sempre que a lista
* de usuários ativos é modificada.</p>
*
* @author tony
*/
public interface PeerListener {
/**
* Chamado quando a lista de usuários conhecidos na rede é alterada.
*
* @param peers lista atualizada de usuários ativos.
*/
void onPeersChanged(List<PeerInfo> peers);
}

View File

@ -17,7 +17,12 @@ import pitiupi.Main;
import pitiupi.plugin.Plugin;
/**
* Responsável por localizar, carregar e registrar os plugins
* presentes no diretório {@code plugins}.
*
* <p>Os plugins são descobertos utilizando {@link java.util.ServiceLoader}
* e devem estar empacotados como arquivos JAR contendo a configuração
* {@code META-INF/services} correspondente.</p>
* @author flavio
*/
public class PluginLoader {

View File

@ -1,5 +1,9 @@
package pitiupi.net;
/**
* Mensagem utilizada para sinalizar a presença de um usuário na rede.
*
* <p>É enviada periodicamente para permitir que a aplicação identifique
* usuários ativos e mantenha o controle dos participantes conectados.</p>
*
* @author tony
*/
@ -7,11 +11,9 @@ public class HeartbeatMessage extends Message {
private final String userName;
private final long timestamp;
private final String instanceId;
public HeartbeatMessage(String userName, String instanceId) {
public HeartbeatMessage(String userName) {
this.userName = userName;
this.instanceId = instanceId;
this.timestamp = System.currentTimeMillis();
}
@ -22,8 +24,4 @@ public class HeartbeatMessage extends Message {
public long getTimestamp() {
return timestamp;
}
public String getInstanceId() {
return instanceId;
}
}

View File

@ -12,11 +12,25 @@ import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* Classe base para as mensagens trocadas entre a aplicação e os plugins.
*
* <p>As mensagens devem estender esta classe para utilizar o sistema de
* comunicação da aplicação. Subclasses podem adicionar os dados e a lógica
* necessários para representar diferentes tipos de mensagens.</p>
*
* <p>Por implementar {@link Serializable}, objetos derivados desta classe
* podem ser convertidos em bytes para transmissão pela rede.</p>
*
* @author flavio
*/
public abstract class Message implements Serializable{
/**
* Serializa a mensagem para um vetor de bytes.
*
* @return representação serializada da mensagem.
* @throws IOException caso ocorra um erro durante a serialização.
*/
public byte[] toByteArray() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutput out = new ObjectOutputStream(baos);

View File

@ -20,6 +20,10 @@ import pitiupi.plugin.Plugin;
import pitiupi.net.HeartbeatMessage;
/**
* Responsável pela comunicação multicast entre a aplicação e os plugins.
*
* <p>Esta classe gerencia o envio e o recebimento de mensagens, além de
* manter a conexão com o grupo multicast utilizado pela aplicação.</p>
*
* @author flavio
* @author tony
@ -29,9 +33,13 @@ public class Socket extends Thread {
private MulticastSocket multicastSocket;
private InetAddress address;
private MainWindow main;
private volatile boolean running = true;
public final static String INET_ADDR = "224.0.0.3";
/**
* Cria e inicializa a conexão multicast utilizada pela aplicação.
*
* @param main janela principal da aplicação.
*/
public Socket(MainWindow main) {
this.main = main;
try {
@ -51,41 +59,41 @@ public class Socket extends Thread {
}
}
/**
* Envia uma mensagem para o grupo multicast da aplicação.
*
* @param msg mensagem serializada a ser enviada.
* @throws IOException caso ocorra um erro durante o envio da mensagem.
*/
public void send(byte[] msg) throws IOException {
DatagramPacket msgPacket;
msgPacket = new DatagramPacket(msg, msg.length, this.address, this.main.getPort());
multicastSocket.send(msgPacket);
}
public void close() {
running = false;
if (multicastSocket != null) {
try {
multicastSocket.leaveGroup(address);
} catch (IOException ex) {
}
multicastSocket.close();
}
}
/**
* Inicia o loop de recepção de mensagens da aplicação.
*
* <p>As mensagens recebidas são verificadas inicialmente para identificar
* mensagens de heartbeat. Caso não sejam heartbeats, seu conteúdo é
* encaminhado para todos os plugins carregados, que decidem se devem ou
* não processá-lo.</p>
*
* @throws IOException caso ocorra um erro durante a recepção da mensagem.
* @throws ClassNotFoundException caso a desserialização de uma mensagem falhe.
*/
public void receive() throws UnknownHostException, IOException, ClassNotFoundException {
byte[] buf = new byte[256000];
while (running) {
while (true) {
DatagramPacket msgPacket = new DatagramPacket(buf, buf.length);
try {
multicastSocket.receive(msgPacket);
} catch (IOException ex) {
if (!running) {
break;
}
throw ex;
}
multicastSocket.receive(msgPacket);
HeartbeatMessage heartbeat = tryParseHeartbeat(msgPacket.getData());
if (heartbeat != null) {
main.getHeartbeatManager().receiveHeartbeat(heartbeat, msgPacket.getAddress());
continue;
}
for (Plugin plugin : main.getPlugins()) {
System.out.println(msgPacket.getAddress().toString());
plugin.receiveMessage(msgPacket.getData());
@ -93,8 +101,13 @@ public class Socket extends Thread {
}
}
/**
* Tenta desserializar os dados recebidos como uma {@code HeartbeatMessage}.
*
* @param data dados serializados da mensagem.
* @return a mensagem desserializada caso os dados representem uma
* {@code HeartbeatMessage}; caso contrário, {@code null}.
*/
private HeartbeatMessage tryParseHeartbeat(byte[] data) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(data);

View File

@ -9,6 +9,10 @@ import pitiupi.GUI.MainWindow;
import pitiupi.net.Message;
/**
* Interface base para plugins da aplicação.
*
* <p>Cada plugin deve implementar esta interface para ser carregado
* pela aplicação e participar do sistema de comunicação por mensagens.</p>
*
* @author flavio
*/
@ -17,7 +21,30 @@ public interface Plugin {
public String getName();
public String getAuthor();
public String getVersion();
/**
* Inicializa o plugin.
*
* <p>Este método é chamado pela aplicação após o carregamento do plugin.</p>
*
* @param window janela principal da aplicação.
*/
public void createPlugin(MainWindow window);
/**
* Retorna uma mensagem utilizada pelo plugin.
*
* @return mensagem criada pelo plugin.
*/
public Message getMessage();
/**
* Recebe uma mensagem enviada pela aplicação.
*
* <p>A mensagem é recebida em formato serializado. O plugin deve verificar
* se a mensagem pertence ao seu tipo e realizar o processamento necessário.</p>
*
* @param message mensagem serializada recebida.
*/
public void receiveMessage(byte[] message);
}