Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb3731232b | ||
|
|
fa85b6ee0c | ||
|
|
74153bcdf3 | ||
|
|
70db48e645 | ||
|
|
81c37225fa | ||
|
|
5c69189293 | ||
|
|
543073e84e | ||
|
|
7f21e89a0a | ||
|
|
239a10e79f | ||
|
|
a7ba307daf | ||
|
|
668b2d417b | ||
|
|
5e835f4d75 | ||
|
|
cf43f62793 | ||
|
|
5d650df8eb | ||
|
|
3beadca0c1 |
No files matched your search
@@ -0,0 +1,7 @@
|
||||
/pitiupi/build/
|
||||
/pitiupi/dist/
|
||||
/pitiupi.chat/build/
|
||||
/pitiupi.chat/dist/
|
||||
/pitiupi.chat/nbproject/private/
|
||||
/pitiupi.screenCast/build/
|
||||
/pitiupi.screenCast/dist/
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+83
@@ -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">{
|
||||
"associatedIndex": 0
|
||||
}</component>
|
||||
<component name="ProjectId" id="3GbOdQKFlKqBjAjqjAv5TRPxr6Z" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"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>
|
||||
+258
@@ -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.
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
+9
@@ -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>
|
||||
Generated
+5
@@ -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>
|
||||
Generated
+8
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
@@ -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=
|
||||
Vendored
BIN
Binary file not shown.
@@ -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>
|
||||
@@ -94,7 +94,7 @@ public class ChatWindow extends javax.swing.JDialog {
|
||||
if (jTextArea1.getText().trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
ChatMessage message = new ChatMessage(mainWindow.getName(), jTextArea1.getText());
|
||||
ChatMessage message = new ChatMessage(mainWindow.getUserName(), jTextArea1.getText());
|
||||
this.sendChatMessage(message);
|
||||
jTextArea1.setText("");
|
||||
}
|
||||
|
||||
Generated
+61
@@ -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 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=
|
||||
BIN
Binary file not shown.
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
Generated
+9
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="pitiupi">
|
||||
<CLASSES>
|
||||
<root url="jar://$PROJECT_DIR$/dist/pitiupi.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
Generated
+10
@@ -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>
|
||||
Generated
+5
@@ -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>
|
||||
Generated
+8
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
@@ -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);
|
||||
@@ -0,0 +1,2 @@
|
||||
# Como criar plugins para o Share my Sheet
|
||||
|
||||
@@ -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.
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 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;
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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(() -> {
|
||||
|
||||
@@ -12,9 +12,9 @@ import java.util.logging.Logger;
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
* @author tony
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
/**
|
||||
* @param args the command line arguments
|
||||
*/
|
||||
@@ -40,16 +40,17 @@ 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,13 @@ 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
|
||||
*/
|
||||
@@ -27,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();
|
||||
@@ -46,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);
|
||||
@@ -54,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
|
||||
if (msg.getUserName().equals(mainWindow.getUserName())) {
|
||||
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()));
|
||||
@@ -86,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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,9 +11,11 @@ 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();
|
||||
}
|
||||
|
||||
@@ -20,4 +26,8 @@ public class HeartbeatMessage extends Message {
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getInstanceId() {
|
||||
return instanceId;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -20,6 +20,10 @@ import pitiupi.plugin.Plugin;
|
||||
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
|
||||
@@ -29,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 {
|
||||
@@ -50,31 +60,76 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
multicastSocket.receive(msgPacket);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user