Author SHA1 Message Date
tony cb3731232b Atualizar criar-plugin.md 2026-09-25 10:54:12 -03:00
tony fa85b6ee0c Atualizar criar-plugin.md 2026-09-25 10:46:16 -03:00
tony 74153bcdf3 Atualizar criar-plugin.md 2026-09-25 10:44:38 -03:00
tony 70db48e645 Criação de Plugin 2026-09-24 06:03:42 -03:00
GustavoHMDS 81c37225fa atualizando o arquivo documentation.md 2026-08-11 14:35:51 -03:00
GustavoHMDS 5c69189293 atualizando documentacao depois do merge 2026-08-11 14:30:17 -03:00
GustavoHMDS 543073e84e resolvendo conflitos 2026-08-11 10:12:04 -03:00
tonyhcj 7f21e89a0a fix: salvando nome e porta 2026-07-29 15:00:44 -03:00
tonyhcj 239a10e79f fix: Corrige bug que exibia frame0 como nome do usuário 2026-07-28 22:11:37 -03:00
tonyhcj a7ba307daf fix: usar instanceId único em vez de userName para identificar heartbeats 2026-07-24 19:31:39 -03:00
tonyhcj 668b2d417b Corrige Preferences: name/port do Login agora atualizam MainWindow
- Login.commitEdit() no JSpinner para capturar porta digitada manualmente
- Main.java agora exibe o Login antes de criar o MainWindow
- MenuBar aplica os valores do Login no MainWindow via setUserName/setPort
- MainWindow: campos userName/port deixam de ser final; setPort() reconecta o socket
- Socket.java ganha close() para permitir reconexão limpa em nova porta
2026-07-22 18:53:11 -03:00
tonyhcj 5e835f4d75 Adiciona .gitignore 2026-07-22 15:58:13 -03:00
GustavoHMDS cf43f62793 Documentação de funções e arquivo Documentação.md 2026-07-21 20:33:28 -03:00
GustavoHMDS 5d650df8eb Merge branch 'feature/heartbeat-online-users' of https://git.alice.ufsj.edu.br/fls/pitiupi into feature/help-and-documentation 2026-07-21 11:09:23 -03:00
GustavoHMDS 3beadca0c1 comeco da documentacao 2026-07-21 11:06:23 -03:00
tonyhcj d2c155a2fa Adiciona painel de usuários online 2026-07-17 21:49:04 -03:00
tonyhcj eff5fd2f12 Integra recebimento de heartbeat no socket multicast 2026-07-17 21:48:48 -03:00
tonyhcj 076b4fcb43 Adiciona gerenciamento de heartbeat e peers 2026-07-17 21:48:26 -03:00
tonyhcj d22b95e74f Remove arquivos compilados do versionamento 2026-07-17 21:48:07 -03:00
tonyhcj c89061db2e Adiciona gitignore para arquivos gerados 2026-07-17 21:45:52 -03:00
72 changed files with 1192 additions and 150 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+83
View File
@@ -0,0 +1,83 @@
<?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="" />
<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">{
&quot;keyToString&quot;: {
&quot;Application.Main.executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;feature/help-and-documentation&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/extre/OneDrive/Documentos/Faculdade/2026_02/Monitoria/Pitiupi&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.keymap&quot;
}
}</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>
+258
View File
@@ -0,0 +1,258 @@
# Como criar um plugin para o pitiupi ?
O objetivo aqui, além de criar plugins para esse sistema, é entender **por que cada parte existe**. Isso é importante porque a ideia de **carregar código externo em tempo de execução** aparece em várias ferramentas nas áreas da computação.
---
## 1. Visão rápida
O repositório contém projetos Java **independentes**:
| Projeto | O que é |
|---|---|
| `pitiupi` | A ferramenta principal : P2P, janela, rede, e o "motor" que carrega plugins |
| `pitiupi.chat` | Um plugin que adiciona um chat |
| `pitiupi.screenCast` | Outro plugin que adiciona compartilhamento de tela |
Cada plugin é compilado separadamente com `ant` e o `.jar` gerado é colocado (ou linkado) dentro da pasta `pitiupi/plugins`.
Quando o pitiupi principal inicia, ele varre essa pasta e **carrega dinamicamente** cada plugin encontrado, sem que o código principal precise conhecer o plugin em tempo de compilação.
Isso é possível graças a três partes que trabalham juntas:
1. A **interface `Plugin`** : o contrato que todo plugin deve cumprir.
2. O **`PluginLoader`** : a classe do pitiupi que varre a pasta `plugins/` e carrega os `.jar`.
3. O **arquivo de registro do `ServiceLoader`** : um arquivo de texto dentro do `.jar` que diz "esta classe aqui implementa `Plugin`".
---
## 2. O contrato: a interface `Plugin`
Todo plugin **precisa** implementar esta interface (`pitiupi/src/pitiupi/plugin/Plugin.java`):
```java
public interface Plugin {
public String getName();
public String getAuthor();
public String getVersion();
public void createPlugin(MainWindow window);
public Message getMessage();
public void receiveMessage(byte[] message);
}
```
O que cada método faz:
- **`getName()` / `getAuthor()` / `getVersion()`** : indentificação de dados básicos.
- **`createPlugin(MainWindow window)`** : É aqui que o plugin cria sua interface gráfica, adiciona itens de menu na janela principal, etc.
- **`getMessage()`** : Serve como modelo para a camada de rede saber que tipo de mensagem esse plugin envia.
- **`receiveMessage(byte[] message)`** : recebe as mensagens que chegam pela rede e permite que o plugin interprete esses dados.
---
## 3. Como o pitiupi encontra e carrega os plugins
A classe `PluginLoader` (`pitiupi/src/pitiupi/control/PluginLoader.java`) faz o seguinte:
1. Olha dentro da pasta `plugins/` e lista todos os arquivos `.jar`.
2. Cria um `URLClassLoader` : um "carregador de classes" isolado, que sabe ler código de dentro desses `.jar`.
3. Usa `ServiceLoader.load(Plugin.class, pluginClassLoader)` para perguntar: *"dentre essas classes, quais implementam `Plugin`?"*
4. Para cada plugin encontrado, chama `createPlugin(mainWindow)` e guarda o plugin numa lista.
```java
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, pluginClassLoader);
for (Plugin plugin : serviceLoader) {
plugin.createPlugin(this.mainWindow);
this.mainWindow.getPlugins().add(plugin);
}
```
**Obs:** o `ServiceLoader` do Java **não escaneia o bytecode procurando quem implementa a interface**. Ele lê um arquivo de texto simples dentro do `.jar`. Se esse arquivo não existir ou estiver errado, o plugin é ignorado : mesmo que o código esteja perfeito.
---
## 4.`META-INF/services`
Dentro do projeto `pitiupi.chat` por exemplo, existe este arquivo:
```
pitiupi.chat/src/META-INF/services/pitiupi.plugin.Plugin
```
Repare em duas coisas:
- A **pasta** se chama `META-INF/services`.
- O **nome do arquivo** é o nome completo da interface: `pitiupi.plugin.Plugin`.
- O **conteúdo** do arquivo é uma linha com o nome completo da classe que implementa essa interface:
```
pitiupi.chat.plugin.ChatPlugin
```
- O`Plugin` diz **"eu sei fazer o trabalho"**.
- O arquivo em `META-INF/services` diz **"aqui está quem sabe fazer o trabalho"**.
- Sem o segundo, o pitiupi nunca fica sabendo que a classe existe.
---
## 5. Exemplo real: o `ChatPlugin`
Veja como o plugin de chat implementa o contrato : use isso como referência ao escrever o seu:
```java
public class ChatPlugin implements Plugin {
private ChatWindow chatWindow;
private MainWindow mainWindow;
@Override
public void createPlugin(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.chatWindow = new ChatWindow(mainWindow);
this.mainWindow.addMenu(new ChatMenu(chatWindow));
}
@Override
public void receiveMessage(byte[] message) {
// desserializa os bytes recebidos de volta em um objeto ChatMessage
ByteArrayInputStream bis = new ByteArrayInputStream(message);
try {
ObjectInputStream ois = new ObjectInputStream(bis);
if (ois.readObject() instanceof ChatMessage msg) {
this.chatWindow.receiveChatMessage(msg);
}
} catch (IOException | ClassNotFoundException ex) {
System.out.println(ex.toString());
}
}
@Override
public String getName() { return "Chat"; }
@Override
public String getAuthor() { return "Flávio, Tony e Gustavo "; }
@Override
public String getVersion() { return "0"; }
@Override
public Message getMessage() { return new ChatMessage(); }
}
```
Em `createPlugin` é onde o bum de "aparecer na tela" acontece: ele cria a janela do plugin e adiciona um menu na janela principal do pitiupi.
---
## 6. Passo a passo: criando um plugin novo do zero
Vamos chamar o plugin de exemplo de `SeuPlugin`.
### Passo 1 : Criar a estrutura do projeto
Se você quiser um jeito mais facil sem se aprofundar, você pode criar o projeto de duas formas:
- **Mais simples:** copie um projeto Ant existente, como `pitiupi.chat` por exemplo, e remova o código específico do Chat.
- **Outra opção:** crie um novo projeto no NetBeans usando **"Java Class Library"**, mantendo a mesma estrutura de pastas.
```
pitiupi.seuplugin/
├── build.xml
├── nbproject/
└── src/
├── META-INF/services/
└── pitiupi/seuplugin/plugin/
```
### Passo 2 : Adicionar o `pitiupi.jar` como dependência
O plugin precisa do arquivo **`pitiupi/dist/pitiupi.jar`** para acessar `Plugin` e `MainWindow`.
- **Se criou do zero:** configure o `pitiupi.jar` como dependência.
- **Se copiou um projeto existente:** essa configuração já está pronta.
- **Se criou pelo NetBeans:** **Properties → Libraries → Add JAR/Folder** → selecione `pitiupi/dist/pitiupi.jar`.
### Passo 3 : Implementar a interface `Plugin`
Crie `src/pitiupi/seuplugin/plugin/SeuPlugin.java`:
```java
package pitiupi.seuplugin.plugin;
import pitiupi.GUI.MainWindow;
import pitiupi.net.Message;
import pitiupi.plugin.Plugin;
public class SeuPlugin implements Plugin {
@Override
public void createPlugin(MainWindow window) {
System.out.println("SeuPlugin foi carregado!");
// aqui você criaria sua janela/menu, como o ChatPlugin faz
}
@Override
public void receiveMessage(byte[] message) {
// trate as mensagens recebidas da rede aqui
}
@Override
public String getName() { return "MeuPlugin"; }
@Override
public String getAuthor() { return "Seu Nome"; }
@Override
public String getVersion() { return "0.1"; }
@Override
public Message getMessage() { return null; } // troque por uma Message real, se tiver
}
```
### Passo 4 : Registrar o plugin no `ServiceLoader`
Crie o arquivo (sem extensão):
```
src/META-INF/services/pitiupi.plugin.Plugin
```
Com **uma única linha** dentro, o nome completo da sua classe:
```
pitiupi.seuplugin.plugin.MeuPlugin
```
### Passo 5 : Compilar com Ant
```bash
cd pitiupi.seuplugin
ant jar
```
Isso gera o `.jar` em `pitiupi.seuplugin/dist/`.
### Passo 6 : Linkar o `.jar` na pasta de plugins
```bash
cd pitiupi/plugins
ln -s ../../pitiupi.meuplugin/dist/*.jar .
```
Usar link simbólico (`ln -s`) é melhor durante o desenvolvimento: toda vez que você recompilar o plugin, não precisa copiar de novo.
## 7. Geral
```
Plugin.java → "o que todo plugin precisa saber fazer" (o contrato)
MeuPlugin.java → "como EU faço esse trabalho" (a implementação)
META-INF/services/pitiupi.plugin.Plugin → "aqui está quem faz o trabalho" (o registro)
PluginLoader.java → "eu procuro registros e chamo quem se registrou" (o carregador, já pronto no pitiupi)
```
Entendendo essas quatro peças, criar qualquer plugin novo : chat, screencast, ou algo totalmente diferente (arquivos, jogos, sensores) : é sempre o mesmo padrão: implementar a interface, registrar no `META-INF/services`, compilar e linkar.
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+9
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>
+5
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>
+8
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>
+6
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>
+3 -3
View File
@@ -1,6 +1,6 @@
#Fri, 17 Jul 2026 17:31:55 -0300
#Sat, 18 Jul 2026 10:32:46 -0300
/home/perolanegra/facul/pitiupi/pitiupi.chat=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.chat=
/home/perolanegra/facul/pitiupi/pitiupi=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=
Binary file not shown.
+2 -2
View File
@@ -47,8 +47,8 @@ javac.modulepath=
javac.processormodulepath=
javac.processorpath=\
${javac.classpath}
javac.source=21
javac.target=21
javac.source=25
javac.target=25
javac.test.classpath=\
${javac.classpath}:\
${build.classes.dir}
+11
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>
@@ -5,16 +5,18 @@
*/
package pitiupi.chat.GUI;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import pitiupi.chat.net.ChatMessage;
import pitiupi.plugin.PitiupiFacade;
import pitiupi.GUI.MainWindow;
import pitiupi.net.Message;
/**
*
* @author flavio
* @author tony
*/
public class ChatWindow extends javax.swing.JDialog {
@@ -27,10 +29,10 @@ public class ChatWindow extends javax.swing.JDialog {
private javax.swing.JSplitPane jSplitPane1;
private javax.swing.JTextArea jTextArea1;
private final PitiupiFacade pitiupi;
private final MainWindow mainWindow;
public ChatWindow(PitiupiFacade pitiupi) {
this.pitiupi = pitiupi;
public ChatWindow(MainWindow mainWindow) {
this.mainWindow = mainWindow;
initComponents();
}
@@ -92,13 +94,17 @@ public class ChatWindow extends javax.swing.JDialog {
if (jTextArea1.getText().trim().length() == 0) {
return;
}
ChatMessage message = new ChatMessage(pitiupi.getUserName(), jTextArea1.getText());
ChatMessage message = new ChatMessage(mainWindow.getUserName(), jTextArea1.getText());
this.sendChatMessage(message);
jTextArea1.setText("");
}
public void sendChatMessage(Message message) {
pitiupi.send(message);
try {
mainWindow.getSocket().send(message.toByteArray());
} catch (IOException ex) {
Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void receiveChatMessage(ChatMessage message) {
@@ -7,7 +7,7 @@ package pitiupi.chat.plugin;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import pitiupi.plugin.PitiupiFacade;
import pitiupi.GUI.MainWindow;
import pitiupi.chat.GUI.ChatMenu;
import pitiupi.chat.GUI.ChatWindow;
import pitiupi.chat.net.ChatMessage;
@@ -17,18 +17,20 @@ import pitiupi.plugin.Plugin;
/**
*
* @author flavio
* @author tony
*/
public class ChatPlugin implements Plugin {
private ChatWindow chatWindow;
private PitiupiFacade pitiupi;
private MainWindow mainWindow;
public ChatPlugin() {
}
@Override
public void createPlugin(PitiupiFacade pitiupi) {
this.pitiupi = pitiupi;
this.chatWindow = new ChatWindow(pitiupi);
this.pitiupi.addMenu(new ChatMenu(chatWindow));
public void createPlugin(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.chatWindow = new ChatWindow(mainWindow);
this.mainWindow.addMenu(new ChatMenu(chatWindow));
}
@Override
+61
View File
@@ -0,0 +1,61 @@
<?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="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/META-INF/services/pitiupi.plugin.Plugin" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/GUI/QuizWindow.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/control/Pergunta.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/control/Quiz.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/net/QuizMessage.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/plugin/Quiz.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pitiupi/screenCast/net/ScreenCastMessage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/pitiupi/screenCast/net/ScreenCastMessage.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"><![CDATA[{
"associatedIndex": 7
}]]></component>
<component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "QuizPlugin",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/extre/OneDrive/Documentos/Faculdade/2026_02/Monitoria/Pitiupi/pitiupi.screenCast",
"settings.editor.selected.configurable": "preferences.keymap"
}
}]]></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="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="" />
<created>1784745206049</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1784745206049</updated>
</task>
<servers />
</component>
</project>
@@ -1,6 +1,6 @@
#Thu, 16 Jul 2026 17:43:45 -0300
#Sat, 18 Jul 2026 10:33:07 -0300
/home/perolanegra/facul/2.26/fls/pitiupi/pitiupi.screenCast=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.screenCast=
/home/perolanegra/facul/2.26/fls/pitiupi/pitiupi=
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=
@@ -11,7 +11,7 @@ JAR files manifest file (MANIFEST.MF).
To run the project from the command line, go to the dist folder and
type the following:
java -jar "pitiupi.jar"
java -jar "pitiupi.screenCast.jar"
To distribute this project, zip up the dist folder (including the lib folder)
and distribute the ZIP file.
Binary file not shown.
Binary file not shown.
@@ -45,8 +45,8 @@ javac.modulepath=
javac.processormodulepath=
javac.processorpath=\
${javac.classpath}
javac.source=21
javac.target=21
javac.source=25
javac.target=25
javac.test.classpath=\
${javac.classpath}:\
${build.classes.dir}
+10
View File
@@ -0,0 +1,10 @@
# NetBeans
/build/
/dist/
/nbproject/private/
# Java compilado
*.class
# JARs gerados
*.jar
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+9
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>
+10
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
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>
+8
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
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>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+214
View File
@@ -0,0 +1,214 @@
# 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 peers 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, um identificador único da instância da aplicação
e um timestamp. Quando outra instância recebe essa mensagem, o `HeartbeatManager`
registra ou atualiza o peer correspondente, utilizando o identificador
da instância para diferenciá-lo das demais.
Um peer é considerado inativo quando permanece mais de 15 segundos
sem receber um novo heartbeat. O `HeartbeatManager` então remove o peer
da lista e notifica os listeners sobre a alteração.
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 peers 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);
+2
View File
@@ -0,0 +1,2 @@
# Como criar plugins para o Share my Sheet
+1 -1
View File
@@ -5,4 +5,4 @@ do.jlink=false
javac.debug=true
javadoc.preview=true
jlink.strip=false
user.properties.file=/home/perolanegra/snap/netbeans/145/build.properties
user.properties.file=/home/flavio/snap/netbeans/145/build.properties
+4 -1
View File
@@ -2,6 +2,9 @@
<project-private xmlns="http://www.netbeans.org/ns/project-private/1">
<editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/2" lastBookmarkId="0"/>
<open-files xmlns="http://www.netbeans.org/ns/projectui-open-files/2">
<group/>
<group>
<file>file:/home/flavio/NetBeansProjects/pitiupi/src/pitiupi/control/PluginManager.java</file>
<file>file:/home/flavio/NetBeansProjects/pitiupi/src/pitiupi/control/ImageTransformation.java</file>
</group>
</open-files>
</project-private>
+2 -2
View File
@@ -46,8 +46,8 @@ javac.modulepath=
javac.processormodulepath=
javac.processorpath=\
${javac.classpath}
javac.source=21
javac.target=21
javac.source=25
javac.target=25
javac.test.classpath=\
${javac.classpath}:\
${build.classes.dir}
+11
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.
Binary file not shown.
+72
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);
}
}
+25 -7
View File
@@ -1,8 +1,3 @@
/*
* 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;
@@ -11,6 +6,7 @@ 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;
@@ -20,11 +16,20 @@ 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: ");
@@ -43,8 +48,12 @@ public final class Login extends JDialog {
public void initComponents() {
this.setTitle("Login");
portField.setModel(new javax.swing.SpinnerNumberModel(8000, 8000, 9000, 1));
nameField.setText(System.getProperty("user.name"));
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);
JPanel topPanel = new JPanel(new GridBagLayout());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
@@ -83,6 +92,15 @@ 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);
});
+58 -5
View File
@@ -14,8 +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 {
@@ -24,18 +34,25 @@ public class MainWindow extends javax.swing.JFrame {
private javax.swing.JLabel status;
private javax.swing.JPanel statusPanel;
private javax.swing.JTabbedPane tabbedPane;
private HeartbeatManager heartbeatManager;
private OnlineUsersPanel onlineUsersPanel;
private Socket socket;
private final String userName;
private final int port;
private String userName;
private 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;
@@ -45,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");
@@ -63,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();
@@ -108,4 +137,28 @@ 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();
}
}
+36 -2
View File
@@ -9,18 +9,31 @@ 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 Gustavo
* @author tony
*
*/
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;
private javax.swing.JMenuItem exitMenu;
private final MainWindow mainWindow;
/**
* Cria a barra de menus principal da aplicação.
*
* @param mainWindow mainWindow janela principal da aplicação utilizada para executar
*/
public MenuBar(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.initComponents();
@@ -32,6 +45,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));
@@ -44,24 +58,44 @@ 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) {
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);
}
@@ -12,6 +12,12 @@ 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
*/
public class OnlineUsersPanel extends JPanel implements PeerListener {
private final DefaultListModel<String> listModel;
@@ -27,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(() -> {
+5 -4
View File
@@ -12,9 +12,9 @@ import java.util.logging.Logger;
/**
*
* @author flavio
* @author tony
*/
public class Main {
/**
* @param args the command line arguments
*/
@@ -40,14 +40,15 @@ 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(System.getProperty("user.name"), 8000);
main = new pitiupi.GUI.MainWindow(login.getName(), login.getPort());
main.setVisible(true);
}
});
@@ -14,7 +14,16 @@ 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
*/
public class HeartbeatManager {
private static final long HEARTBEAT_INTERVAL_MS = 5000;
@@ -24,17 +33,30 @@ 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();
/**
* Cria um gerenciador de heartbeat associado à janela principal da aplicação.
*
* @param mainWindow janela principal utilizada para obter o nome do usuário
* e o socket de comunicação.
*/
public HeartbeatManager(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
/**
* Inicia o mecanismo de heartbeat.
*/
public void start() {
scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::announce, 0, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
scheduler.scheduleAtFixedRate(this::checkTimeouts, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
}
/**
* Para o mecanismo de heartbeat e encerra as tarefas agendadas.
*/
public void stop() {
if (scheduler != null) {
scheduler.shutdownNow();
@@ -43,7 +65,7 @@ public class HeartbeatManager {
private void announce() {
try {
HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName());
HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName(), instanceId);
mainWindow.getSocket().send(msg.toByteArray());
} catch (IOException ex) {
Logger.getLogger(HeartbeatManager.class.getName()).log(Level.WARNING, "Falha ao enviar heartbeat", ex);
@@ -51,13 +73,25 @@ public class HeartbeatManager {
}
// Chamado pelo Socket quando um pacote é identificado como heartbeat
/**
* Processa uma mensagem de heartbeat recebida de outro usuário.
*
* <p>Atualiza o momento da última atividade do peer correspondente e notifica os listeners
* caso outra instância da aplicação seja identificada.</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 (multicast normalmente ecoa pro remetente)
if (msg.getUserName().equals(mainWindow.getUserName())) {
// ignora o próprio heartbeat
if (msg.getInstanceId().equals(instanceId) ) {
return;
}
String key = from.getHostAddress() + "#" + msg.getUserName();
String key = msg.getInstanceId();
boolean isNew = !peers.containsKey(key);
PeerInfo peer = peers.computeIfAbsent(key, k -> new PeerInfo(msg.getUserName(), from, msg.getTimestamp()));
@@ -83,15 +117,33 @@ 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()));
}
/**
* Remove um listener previamente registrado.
*
* @param l listener que deixará de receber atualizações.
*/
public void removePeerListener(PeerListener l) {
listeners.remove(l);
}
/**
* Retorna uma cópia da lista atual de peers conhecidos.
*
* @return lista contendo os peers atualmente conhecidos.
*/
public List<PeerInfo> getPeers() {
return new ArrayList<>(peers.values());
}
+14 -1
View File
@@ -1,7 +1,15 @@
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
*/
public class PeerInfo {
private final String userName;
@@ -26,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;
}
@@ -2,6 +2,20 @@ 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);
}
@@ -15,11 +15,15 @@ import java.util.Vector;
import pitiupi.GUI.MainWindow;
import pitiupi.Main;
import pitiupi.plugin.Plugin;
import pitiupi.plugin.PitiupiFacade;
/**
* 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
* @author tony
*/
public class PluginLoader {
@@ -56,12 +60,10 @@ public class PluginLoader {
// 4. Locate and instantiate all classes implementing the Plugin interface
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, pluginClassLoader);
PitiupiFacade facade = new PitiupiFacade(this.mainWindow);
System.out.println("\n--- Executing Discovered Plugins ---" + serviceLoader.findFirst());
for (Plugin plugin : serviceLoader) {
System.out.println("Loading: " + plugin.getName());
plugin.createPlugin(facade);
plugin.createPlugin(this.mainWindow);
this.mainWindow.getPlugins().add(plugin);
}
+15 -2
View File
@@ -1,12 +1,21 @@
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
*/
public class HeartbeatMessage extends Message {
private final String userName;
private final long timestamp;
private final String instanceId;
public HeartbeatMessage(String userName) {
public HeartbeatMessage(String userName, String instanceId) {
this.userName = userName;
this.instanceId = instanceId;
this.timestamp = System.currentTimeMillis();
}
@@ -17,4 +26,8 @@ public class HeartbeatMessage extends Message {
public long getTimestamp() {
return timestamp;
}
public String getInstanceId() {
return instanceId;
}
}
+14
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);
+63 -53
View File
@@ -17,10 +17,13 @@ import java.net.UnknownHostException;
import java.util.logging.Level;
import java.util.logging.Logger;
import pitiupi.plugin.Plugin;
import java.net.NetworkInterface;
import java.net.InetSocketAddress;
import pitiupi.net.HeartbeatMessage; // lib
import pitiupi.net.HeartbeatMessage;
/**
* "Responsável pela comunicação multicast da aplicação e pelo encaminhamento de mensagens aos 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
@@ -30,8 +33,14 @@ 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 {
@@ -44,75 +53,69 @@ public class Socket extends Thread {
multicastSocket.setSendBufferSize(256000);
multicastSocket.setReceiveBufferSize(256000);
multicastSocket.setReuseAddress(true);
System.out.println("Endereço local: " + multicastSocket.getLocalAddress());
System.out.println("Interface: " + multicastSocket.getNetworkInterface());
//InetAddress local = InetAddress.getLocalHost();
NetworkInterface nif = null;
for (NetworkInterface ni : java.util.Collections.list(
NetworkInterface.getNetworkInterfaces())) {
if (ni.isUp()
&& ni.supportsMulticast()
&& !ni.isLoopback()) {
nif = ni;
break;
}
}
if (nif != null) {
System.out.println("Interface escolhida:");
System.out.println("Nome: " + nif.getName());
System.out.println("Descrição: " + nif.getDisplayName());
System.out.println("Ativa: " + nif.isUp());
System.out.println("Multicast: " + nif.supportsMulticast());
System.out.println("Endereços IP:");
for (InetAddress ip : java.util.Collections.list(nif.getInetAddresses())) {
System.out.println(" " + ip.getHostAddress());
}
} else {
System.out.println("Nenhuma interface multicast encontrada");
}
multicastSocket.joinGroup(
new InetSocketAddress(address, main.getPort()),
nif
);
System.out.println("nif: " + nif);
// multicastSocket.joinGroup(address);
multicastSocket.joinGroup(address);
} catch (IOException ex) {
System.out.println("There is no socket connection. Sorry.");
System.out.println(ex.toString());
}
}
/**
* 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());
System.out.println("Enviando pacote");
System.out.println("Destino: " + address);
System.out.println("Porta: " + this.main.getPort());
System.out.println("Bytes: " + msg.length);
multicastSocket.send(msgPacket);
}
//-------------------------------------------------------------------------------------------------------
/**
* Encerra a comunicação multicast e interrompe a recepção de mensagens.
*/
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 (true) {
while (running) {
DatagramPacket msgPacket = new DatagramPacket(buf, buf.length);
try {
multicastSocket.receive(msgPacket);
} catch (IOException ex) {
if (!running) {
break;
}
throw ex;
}
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());
@@ -120,6 +123,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);
@@ -130,7 +140,7 @@ public class Socket extends Thread {
return null;
}
}
//------------------------------------------------------------------------------------------------------------------
@Override
public void run() {
try {
@@ -1,35 +0,0 @@
package pitiupi.plugin;
import java.io.IOException;
import javax.swing.JMenu;
import pitiupi.GUI.MainWindow;
import pitiupi.net.Message;
public class PitiupiFacade {
private final MainWindow mainWindow;
public PitiupiFacade(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
public void addMenu(JMenu menu) {
mainWindow.addMenu(menu);
}
public void send(Message message) {
try {
mainWindow.getSocket().send(message.toByteArray());
} catch (IOException ex) {
System.out.println("Falha ao enviar mensagem: " + ex);
}
}
public String getUserName() {
return mainWindow.getUserName();
}
public int getPort() {
return mainWindow.getPort();
}
}
+30 -2
View File
@@ -4,19 +4,47 @@
*/
package pitiupi.plugin;
import javax.swing.JMenu;
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
* @author tony
*/
public interface Plugin {
public String getName();
public String getAuthor();
public String getVersion();
public void createPlugin(PitiupiFacade pitiupi);
/**
* 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);
}