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
105 changed files with 975 additions and 4173 deletions

No files matched your search

+7 -3
View File
@@ -1,3 +1,7 @@
build/ /pitiupi/build/
dist/ /pitiupi/dist/
*.class /pitiupi.chat/build/
/pitiupi.chat/dist/
/pitiupi.chat/nbproject/private/
/pitiupi.screenCast/build/
/pitiupi.screenCast/dist/
-10
View File
@@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-9
View File
@@ -1,9 +0,0 @@
<component name="libraryTable">
<library name="pitiupi">
<CLASSES>
<root url="jar://$PROJECT_DIR$/pitiupi.screenCast/dist/lib/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-9
View File
@@ -1,9 +0,0 @@
<component name="libraryTable">
<library name="pitiupi.chat">
<CLASSES>
<root url="jar://$PROJECT_DIR$/pitiupi.chat/dist/pitiupi.chat.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-9
View File
@@ -1,9 +0,0 @@
<component name="libraryTable">
<library name="pitiupi.screenCast">
<CLASSES>
<root url="jar://$PROJECT_DIR$/pitiupi.screenCast/dist/pitiupi.screenCast.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-10
View File
@@ -1,10 +0,0 @@
<component name="libraryTable">
<library name="plugins">
<CLASSES>
<root url="jar://$PROJECT_DIR$/pitiupi/plugins/pitiupi.screenCast.jar!/" />
<root url="jar://$PROJECT_DIR$/pitiupi/plugins/pitiupi.chat.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
+3 -2
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="openjdk-27" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project> </project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pitiupi.iml" filepath="$PROJECT_DIR$/.idea/pitiupi.iml" />
</modules>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?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$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</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>
+3 -23
View File
@@ -1,13 +1,10 @@
# pitiupi # pitiupi
Este repositório tem 4 projetos Java que são independentes: Ferramenta p2p para práticas em rede
### Sistema: Este repositório tem 3 projetos Java que são independentes:
- A ferramenta pitiupi (P2P) - A ferramenta pitiupi (P2P)
### Plugins:
- Um plugin de Chat - Um plugin de Chat
- Um plugin de Chat multiprotocolo (TCP/UDP)
- Um plugin de ScreenCast - Um plugin de ScreenCast
Cada um destes projetos deve ser compilado individualmente. Para isso, use a ferramenta ant no diretório do projeto. Cada um destes projetos deve ser compilado individualmente. Para isso, use a ferramenta ant no diretório do projeto.
@@ -24,26 +21,9 @@ Para criar links simbólicos no Linux, use o comando:
``` ```
cd pitiupi/plugins cd pitiupi/plugins
ln -s ../../pitiupi.chat/dist/*.jar . ln -s ../../pitiupi.chat/dist/*.jar .
ln -s ../../pitiupi.mpChat/dist/*.jar .
ln -s ../../pitiupi.screenCast/dist/*.jar . ln -s ../../pitiupi.screenCast/dist/*.jar .
``` ```
Isso deve resolver! Isso deve resolver!
## Como um plugin conversa com o pitiupi Para criar um novo plugin:
Um plugin nunca recebe acesso direto às classes internas do núcleo (ConnectionManager, GuiManager). Em vez disso, recebe uma facade, a classe Pitiupi, que expõe só o que um plugin precisa: enviar mensagem (multicast, unicast ou tcp), abrir conexão tcp privada, ler nome de usuário e porta, criar menu.
```java
public abstract void createPlugin(Pitiupi pitiupi);
```
## Para criar um novo plugin
1. Implemente `pitiupi.models.Plugin`, incluindo `createPlugin(Pitiupi pitiupi)`
2. Registre a classe em `src/META-INF/services/pitiupi.models.Plugin`, com o nome completo da classe numa linha
3. Compile com `ant jar`
4. Linke o `.jar` em `pitiupi/plugins`
Use `pitiupi.chat` como referência — é o exemplo mais simples e completo.
Plugins que ainda não foram atualizados para a Pitiupi (como uma versão mais antiga do screenCast) não compilam contra o núcleo atual e precisam de migração antes de serem usados.
+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.
+6
View File
@@ -0,0 +1,6 @@
#Sat, 18 Jul 2026 10:32:46 -0300
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.chat=
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.processormodulepath=
javac.processorpath=\ javac.processorpath=\
${javac.classpath} ${javac.classpath}
javac.source=17 javac.source=25
javac.target=17 javac.target=25
javac.test.classpath=\ javac.test.classpath=\
${javac.classpath}:\ ${javac.classpath}:\
${build.classes.dir} ${build.classes.dir}
-9
View File
@@ -7,14 +7,5 @@
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component> </component>
</module> </module>
@@ -0,0 +1 @@
pitiupi.chat.plugin.ChatPlugin
-20
View File
@@ -1,20 +0,0 @@
<?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$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+14 -15
View File
@@ -4,29 +4,28 @@
*/ */
package pitiupi.chat.GUI; package pitiupi.chat.GUI;
import pitiupi.models.Menus.MulticastMenus.MulticastMenu; import java.awt.event.ActionEvent;
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu; import javax.swing.JMenu;
import java.util.List;
/** /**
* *
* @author flavio * @author flavio
*/ */
public class ChatMenu extends MulticastSubMenu { public class ChatMenu extends JMenu {
ChatMulticastMenu chatMulticastMenu;
private final ChatWindow chatWindow;
private javax.swing.JMenuItem menuItem;
public ChatMenu(ChatWindow chatWindow) { public ChatMenu(ChatWindow chatWindow) {
this.chatMulticastMenu = new ChatMulticastMenu(chatWindow); this.chatWindow = chatWindow;
this.setText("Chat");
menuItem = new javax.swing.JMenuItem();
menuItem.setText("Show Chat Window");
menuItem.addActionListener(this::chatMenuActionPerformed);
this.add(menuItem);
} }
@Override private void chatMenuActionPerformed(ActionEvent ae) {
public String getText() { this.chatWindow.setVisible(true);
return "Chat";
}
@Override
public List<MulticastMenu> getMenus() {
return List.of(chatMulticastMenu);
} }
} }
@@ -1,21 +0,0 @@
package pitiupi.chat.GUI;
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;;
public class ChatMulticastMenu extends MulticastActionMenu {
ChatWindow chatWindow;
public ChatMulticastMenu(ChatWindow chatWindow) {
this.chatWindow = chatWindow;
}
@Override
public String getText() {
return "Show Chat Window";
}
@Override
public void onClick() {
chatWindow.setVisible(true);
}
}
@@ -5,35 +5,37 @@
*/ */
package pitiupi.chat.GUI; package pitiupi.chat.GUI;
import java.io.IOException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import pitiupi.chat.net.ChatMessage; import pitiupi.chat.net.ChatMessage;
import pitiupi.plugin.Pitiupi; import pitiupi.GUI.MainWindow;
import pitiupi.models.Messages.Message; import pitiupi.net.Message;
import javax.swing.*;
/** /**
* @author tony *
* @author flavio * @author flavio
*/ */
public class ChatWindow extends JDialog { public class ChatWindow extends javax.swing.JDialog {
private JButton jButton1; private javax.swing.JButton jButton1;
private JEditorPane jEditorPane1; private javax.swing.JEditorPane jEditorPane1;
private JPanel jPanel1; private javax.swing.JPanel jPanel1;
private JPanel jPanel2; private javax.swing.JPanel jPanel2;
private JScrollPane jScrollPane1; private javax.swing.JScrollPane jScrollPane1;
private JScrollPane jScrollPane2; private javax.swing.JScrollPane jScrollPane2;
private JSplitPane jSplitPane1; private javax.swing.JSplitPane jSplitPane1;
private JTextArea jTextArea1; private javax.swing.JTextArea jTextArea1;
private final Pitiupi pitiupi; private final MainWindow mainWindow;
public ChatWindow(Pitiupi pitiupi) { public ChatWindow(MainWindow mainWindow) {
this.pitiupi = pitiupi; this.mainWindow = mainWindow;
initComponents(); initComponents();
} }
/** /**
* This method is called from within the constructor to initialize the form. * This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always * WARNING: Do NOT modify this code. The content of this method is always
@@ -41,23 +43,23 @@ public class ChatWindow extends JDialog {
*/ */
private void initComponents() { private void initComponents() {
this.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); this.setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE);
this.setTitle("Chat Window"); this.setTitle("Chat Window");
this.setBounds(new java.awt.Rectangle(0, 0, 800, 600)); this.setBounds(new java.awt.Rectangle(0, 0, 800, 600));
this.getContentPane().setLayout(new java.awt.BorderLayout(5, 5)); this.getContentPane().setLayout(new java.awt.BorderLayout(5, 5));
this.setPreferredSize(new java.awt.Dimension(800, 600)); this.setPreferredSize(new java.awt.Dimension(800, 600));
jSplitPane1 = new JSplitPane(); jSplitPane1 = new javax.swing.JSplitPane();
jScrollPane1 = new JScrollPane(); jScrollPane1 = new javax.swing.JScrollPane();
jEditorPane1 = new JEditorPane(); jEditorPane1 = new javax.swing.JEditorPane();
jPanel1 = new JPanel(); jPanel1 = new javax.swing.JPanel();
jScrollPane2 = new JScrollPane(); jScrollPane2 = new javax.swing.JScrollPane();
jTextArea1 = new JTextArea(); jTextArea1 = new javax.swing.JTextArea();
jPanel2 = new JPanel(); jPanel2 = new javax.swing.JPanel();
jButton1 = new JButton(); jButton1 = new javax.swing.JButton();
jSplitPane1.setDividerLocation(500); jSplitPane1.setDividerLocation(500);
jSplitPane1.setOrientation(JSplitPane.VERTICAL_SPLIT); jSplitPane1.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT);
jEditorPane1.setContentType("text/html"); jEditorPane1.setContentType("text/html");
jPanel1.setLayout(new java.awt.BorderLayout()); jPanel1.setLayout(new java.awt.BorderLayout());
jTextArea1.setColumns(20); jTextArea1.setColumns(20);
@@ -89,16 +91,20 @@ public class ChatWindow extends JDialog {
} }
private void sendMessage() { private void sendMessage() {
if (jTextArea1.getText().trim().isEmpty()) { if (jTextArea1.getText().trim().length() == 0) {
return; return;
} }
ChatMessage message = new ChatMessage(pitiupi.getUserName(), jTextArea1.getText()); ChatMessage message = new ChatMessage(mainWindow.getUserName(), jTextArea1.getText());
this.sendChatMessage(message); this.sendChatMessage(message);
jTextArea1.setText(""); jTextArea1.setText("");
} }
public void sendChatMessage(Message message) { public void sendChatMessage(Message message) {
pitiupi.sendUdpMulticast(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) { public void receiveChatMessage(ChatMessage message) {
@@ -5,8 +5,8 @@
*/ */
package pitiupi.chat.net; package pitiupi.chat.net;
import pitiupi.net.Message;
import pitiupi.models.Messages.Message; import pitiupi.net.Message;
/** /**
* *
@@ -4,44 +4,49 @@
*/ */
package pitiupi.chat.plugin; package pitiupi.chat.plugin;
import java.util.ArrayList; import java.io.ByteArrayInputStream;
import java.util.Collection; import java.io.IOException;
import java.util.List; import java.io.ObjectInputStream;
import pitiupi.GUI.MainWindow;
import pitiupi.plugin.Pitiupi;
import pitiupi.chat.GUI.ChatMenu; import pitiupi.chat.GUI.ChatMenu;
import pitiupi.chat.GUI.ChatWindow; import pitiupi.chat.GUI.ChatWindow;
import pitiupi.chat.net.ChatMessage; import pitiupi.chat.net.ChatMessage;
import pitiupi.models.Menus.MenuCategory; import pitiupi.net.Message;
import pitiupi.models.Messages.Message; import pitiupi.plugin.Plugin;
import pitiupi.models.Plugin;
/** /**
*@author tony *
* @author flavio * @author flavio
*/ */
public class ChatPlugin extends Plugin { public class ChatPlugin implements Plugin {
private ChatWindow chatWindow; private ChatWindow chatWindow;
private MainWindow mainWindow;
@Override public ChatPlugin() {
public void createPlugin(Pitiupi pitiupi) {
this.chatWindow = new ChatWindow(pitiupi);
pitiupi.createMenu(MenuCategory.Social, new ChatMenu(chatWindow));
} }
@Override @Override
public List<Class<? extends Message>> getMessages() { public void createPlugin(MainWindow mainWindow) {
return List.of(ChatMessage.class); this.mainWindow = mainWindow;
this.chatWindow = new ChatWindow(mainWindow);
this.mainWindow.addMenu(new ChatMenu(chatWindow));
} }
@Override @Override
public void receiveMessage(Message message) { public void receiveMessage(byte[] message) {
if (message instanceof ChatMessage msg) { ByteArrayInputStream bis = new ByteArrayInputStream(message);
ObjectInputStream ois;
try {
ois = new ObjectInputStream(bis);
if (ois.readObject() instanceof ChatMessage msg) {
this.chatWindow.receiveChatMessage(msg); this.chatWindow.receiveChatMessage(msg);
} }
} catch (IOException ex) {
System.out.println(ex.toString());
} catch (ClassNotFoundException ex) {
System.out.println(ex.toString());
}
} }
@Override @Override
@@ -60,9 +65,7 @@ public class ChatPlugin extends Plugin {
} }
@Override @Override
public String getIdentifier() { public Message getMessage() {
return "Pitiupi.Chat.Example"; return new ChatMessage();
} }
} }
-5
View File
@@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
-5
View File
@@ -1,5 +0,0 @@
<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
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/pitiupi.mpChat.iml" filepath="$PROJECT_DIR$/pitiupi.mpChat.iml" />
</modules>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
-16
View File
@@ -1,16 +0,0 @@
JAR = ../pitiupi/dist/pitiupi.jar
OUT_DIR = out
META-INF_DIR = src/META-INF
DIST = dist/mpChat.jar
SOURCES = $(shell find -name "*.java")
all:
mkdir -p "$(OUT_DIR)"
mkdir -p "$(dir $(DIST))"
javac -cp "$(JAR)" -d "$(OUT_DIR)" $(SOURCES)
cp -r "$(META-INF_DIR)" "$(OUT_DIR)"
jar cf "$(DIST)" -C "$(OUT_DIR)" .
clean:
rm -rf "$(OUT_DIR)"/*
@@ -1 +0,0 @@
plugin.MultiProtocolChat
-20
View File
@@ -1,20 +0,0 @@
<?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" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>
@@ -1,22 +0,0 @@
package GUI;
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
public class MulticastMpChatMenu extends MulticastActionMenu {
PublicChatWindow publicChatWindow;
public MulticastMpChatMenu(PublicChatWindow publicChatWindow) {
this.publicChatWindow = publicChatWindow;
}
@Override
public void onClick() {
publicChatWindow.setVisible(true);
}
@Override
public String getText() {
return "MP-Chat";
}
}
@@ -1,122 +0,0 @@
package GUI;
import net.MpChatMessage;
import pitiupi.net.tcp.TcpConnection;
import pitiupi.plugin.Pitiupi;
import javax.swing.*;
import java.awt.*;
import java.net.InetAddress;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PrivateChatWindow extends JFrame {
private final Pitiupi pitiupi;
private final TcpConnection connection;
private final JTextArea receivedMessagesArea;
private final JTextField messageField;
public PrivateChatWindow(
Pitiupi pitiupi,
TcpConnection connection
) {
this.pitiupi = pitiupi;
this.connection = connection;
setTitle("Chat privado");
setSize(700, 500);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(
BorderFactory.createEmptyBorder(10, 10, 10, 10)
);
receivedMessagesArea = new JTextArea();
receivedMessagesArea.setEditable(false);
receivedMessagesArea.setLineWrap(true);
receivedMessagesArea.setWrapStyleWord(true);
JScrollPane messagesScrollPane =
new JScrollPane(receivedMessagesArea);
messagesScrollPane.setBorder(
BorderFactory.createTitledBorder("Mensagens")
);
mainPanel.add(
messagesScrollPane,
BorderLayout.CENTER
);
JPanel bottomPanel = new JPanel(
new BorderLayout(10, 10)
);
messageField = new JTextField();
JButton tcpButton = new JButton("TCP");
tcpButton.addActionListener(e -> sendTCP());
bottomPanel.add(
messageField,
BorderLayout.CENTER
);
bottomPanel.add(
tcpButton,
BorderLayout.EAST
);
mainPanel.add(
bottomPanel,
BorderLayout.SOUTH
);
setContentPane(mainPanel);
}
private void sendTCP() {
try {
String nome = pitiupi.getUserName();
MpChatMessage message = new MpChatMessage(
nome,
InetAddress.getLocalHost().getHostAddress(),
"TCP Private",
messageField.getText()
);
connection.send(message.toByteArray());
receiveMessage(message);
messageField.setText("");
} catch (Exception ex) {
Logger.getLogger(
PrivateChatWindow.class.getName()
).log(Level.SEVERE, null, ex);
}
}
public void receiveMessage(MpChatMessage message) {
SwingUtilities.invokeLater(() -> {
String texto = String.format(
"[%s] %s (%s): %s%n",
message.getTipoMensagem(),
message.getNome(),
message.getIp(),
message.getMessage()
);
receivedMessagesArea.append(texto);
receivedMessagesArea.setCaretPosition(
receivedMessagesArea.getDocument().getLength()
);
});
}
}
@@ -1,218 +0,0 @@
package GUI;
import net.MpChatMessage;
import pitiupi.control.ConnectionManager;
import pitiupi.plugin.Pitiupi;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.*;
import java.net.InetAddress;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PublicChatWindow extends JFrame {
private final Pitiupi pitiupi;
private JTextPane receivedMessagesArea;
private JTextField messageField;
private JTextField ipField;
public PublicChatWindow(Pitiupi pitiupi) {
this.pitiupi = pitiupi;
initComponents();
}
private void initComponents() {
setTitle("Public Chat");
setSize(700, 500);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(
BorderFactory.createEmptyBorder(10, 10, 10, 10)
);
initReceivedMessagesPanel(mainPanel);
initBottomPanel(mainPanel);
setContentPane(mainPanel);
}
private void initReceivedMessagesPanel(JPanel mainPanel) {
receivedMessagesArea = new JTextPane();
receivedMessagesArea.setEditable(false);
JScrollPane messagesScrollPane = new JScrollPane(receivedMessagesArea);
messagesScrollPane.setBorder(
BorderFactory.createTitledBorder("Mensagens recebidas")
);
mainPanel.add(messagesScrollPane, BorderLayout.CENTER);
}
private void initBottomPanel(JPanel mainPanel) {
messageField = new JTextField();
JPanel bottomPanel = new JPanel(
new BorderLayout(10, 10)
);
bottomPanel.add(messageField, BorderLayout.CENTER);
initIpField(bottomPanel);
initCommunicationButtons(bottomPanel);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
}
private void initIpField(JPanel bottomPanel) {
ipField = new JTextField();
ipField.setPreferredSize(new Dimension(130, 30));
JPanel ipPanel = new JPanel(
new BorderLayout(5, 5)
);
ipPanel.add(
new JLabel("IP destino:"),
BorderLayout.WEST
);
ipPanel.add(
ipField,
BorderLayout.CENTER
);
bottomPanel.add(ipPanel, BorderLayout.WEST);
}
private void initCommunicationButtons(JPanel bottomPanel) {
JButton tcpButton = new JButton("TCP");
JButton udpButton = new JButton("UDP");
JButton multicastButton = new JButton("Multicast");
tcpButton.addActionListener(e -> sendTCP());
udpButton.addActionListener(e -> sendUDP());
multicastButton.addActionListener(e -> sendMulticast());
JPanel buttonsPanel = new JPanel(
new FlowLayout(FlowLayout.RIGHT, 5, 0)
);
buttonsPanel.add(tcpButton);
buttonsPanel.add(udpButton);
buttonsPanel.add(multicastButton);
bottomPanel.add(
buttonsPanel,
BorderLayout.EAST
);
}
private MpChatMessage createMessage(String tipoMensagem) {
try {
String nome = pitiupi.getUserName();
String ip = InetAddress.getLocalHost().getHostAddress();
String message = messageField.getText();
return new MpChatMessage(
nome,
ip,
tipoMensagem,
message
);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public void receiveMessage(MpChatMessage msg) {
SwingUtilities.invokeLater(() -> {
String mensagem = String.format(
"[%s] %s (%s): %s%n",
msg.getTipoMensagem(),
msg.getNome(),
msg.getIp(),
msg.getMessage()
);
StyledDocument document = receivedMessagesArea.getStyledDocument();
SimpleAttributeSet attributes = new SimpleAttributeSet();
if (msg.getTipoMensagem().equals("TCP Unicast") || msg.getTipoMensagem().equals("UDP Unicast")) {
StyleConstants.setForeground(attributes, new Color(128, 0, 128));
}
try {
document.insertString(
document.getLength(),
mensagem,
attributes
);
receivedMessagesArea.setCaretPosition(
document.getLength()
);
} catch (BadLocationException ex) {
Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex);
}
});
}
private void sendTCP() {
try {
MpChatMessage msg = createMessage("TCP Unicast");
InetAddress ip = InetAddress.getByName(ipField.getText());
pitiupi.sendTcp(msg, ip);
receiveMessage(msg);
} catch (Exception ex) {
Logger.getLogger(
PublicChatWindow.class.getName()
).log(Level.SEVERE, null, ex);
}
}
private void sendUDP() {
try {
MpChatMessage msg = createMessage("UDP Unicast");
InetAddress ip = InetAddress.getByName(ipField.getText());
pitiupi.sendUdpUnicast(msg, ip);
receiveMessage(msg);
} catch (Exception ex) {
Logger.getLogger(
PublicChatWindow.class.getName()
).log(Level.SEVERE, null, ex);
}
}
private void sendMulticast() {
try {
MpChatMessage msg = createMessage("UDP Multicast");
pitiupi.sendUdpMulticast(msg);
} catch (Exception ex) {
Logger.getLogger(
PublicChatWindow.class.getName()
).log(Level.SEVERE, null, ex);
}
}
}
@@ -1,26 +0,0 @@
package GUI;
import control.PrivateChatController;
import pitiupi.models.Menus.UnicastMenus.UnicastActionMenu;
import pitiupi.models.User;
public class UnicastMpChatMenu extends UnicastActionMenu {
private final PrivateChatController controller;
public UnicastMpChatMenu(
PrivateChatController controller
) {
this.controller = controller;
}
@Override
public void onClick(User user) {
controller.openChat(user);
}
@Override
public String getText() {
return "Start Private Chat";
}
}
@@ -1 +0,0 @@
plugin.MultiProtocolChat
@@ -1,116 +0,0 @@
package control;
import GUI.PrivateChatWindow;
import net.MpChatMessage;
import pitiupi.models.User;
import pitiupi.net.tcp.TcpConnection;
import pitiupi.plugin.Pitiupi;
import plugin.MultiProtocolChat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class PrivateChatController {
private final Pitiupi pitiupi;
private final MultiProtocolChat multiProtocolChat;
/*
* Relaciona o endereço do usuário à janela de chat.
*/
private final Map<String, PrivateChatWindow> windows;
/*
* Endereços para os quais uma conexão já foi solicitada,
* mas ainda não chegou.
*/
private final Set<String> pendingConnections;
public PrivateChatController(
Pitiupi pitiupi,
MultiProtocolChat multiProtocolChat
) {
this.pitiupi = pitiupi;
this.multiProtocolChat = multiProtocolChat;
this.windows = new HashMap<>();
this.pendingConnections = new HashSet<>();
}
public void openChat(User user) {
String address = user.getAddress().getHostAddress();
PrivateChatWindow window = windows.get(address);
/*
* Já existe uma janela para esse usuário.
*/
if (window != null) {
window.setVisible(true);
window.toFront();
window.requestFocus();
return;
}
/*
* Já estamos esperando uma conexão para esse usuário.
*/
if (pendingConnections.contains(address)) {
return;
}
/*
* Ainda não existe conexão.
*/
pendingConnections.add(address);
multiProtocolChat.openConnection(address);
}
public void receiveMessage(TcpConnection connection, MpChatMessage message) {
String address = connection.getAddress().toString();
PrivateChatWindow window = windows.get(address);
if (window == null) {
return;
}
window.receiveMessage(message);
}
/**
* Deve ser chamado quando uma nova conexão privada chegar.
*/
public void connectionReceived(TcpConnection connection) {
String address = connection.getAddress().toString();
pendingConnections.remove(address);
PrivateChatWindow window = new PrivateChatWindow(
pitiupi,
connection
);
windows.put(address, window);
window.setVisible(true);
window.toFront();
window.requestFocus();
}
/**
* Fecha a janela e remove o relacionamento.
*/
public void closeChat(TcpConnection connection) {
String address = connection.getAddress().toString();
PrivateChatWindow window = windows.remove(address);
if (window != null) {
window.dispose();
}
}
}
-35
View File
@@ -1,35 +0,0 @@
package net;
import pitiupi.models.Messages.Message;
public class MpChatMessage extends Message {
private String nome;
private String ip;
private String tipoMensagem;
private String message;
public MpChatMessage(String nome, String ip, String tipoMensagem, String message) {
this.nome = nome;
this.ip = ip;
this.tipoMensagem = tipoMensagem;
this.message = message;
}
public MpChatMessage(){}
public String getNome() {
return nome;
}
public String getIp() {
return ip;
}
public String getTipoMensagem() {
return tipoMensagem;
}
public String getMessage() {
return message;
}
}
@@ -1,125 +0,0 @@
package plugin;
import GUI.MulticastMpChatMenu;
import GUI.PrivateChatWindow;
import GUI.PublicChatWindow;
import GUI.UnicastMpChatMenu;
import control.PrivateChatController;
import net.MpChatMessage;
import pitiupi.models.Menus.MenuCategory;
import pitiupi.models.Messages.Message;
import pitiupi.net.tcp.TcpConnection;
import pitiupi.models.Plugin;
import pitiupi.plugin.Pitiupi;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.InetAddress;
import java.util.List;
public class MultiProtocolChat extends Plugin {
Pitiupi pitiupi;
PrivateChatController privateChatController;
PublicChatWindow publicChatWindow;
@Override
public String getName() {
return "MultiProtocol chat";
}
@Override
public String getAuthor() {
return "Gustavo Henrique";
}
@Override
public String getVersion() {
return "0";
}
@Override
public String getIdentifier() {
return "Multi.Protocol.Chat.Pitiupi";
}
@Override
public void createPlugin(Pitiupi pitiupi) {
this.pitiupi = pitiupi;
privateChatController = new PrivateChatController(pitiupi, this);
publicChatWindow = new PublicChatWindow(pitiupi);
MulticastMpChatMenu multicastMpChatMenu = new MulticastMpChatMenu(publicChatWindow);
UnicastMpChatMenu unicastMpChatMenu = new UnicastMpChatMenu(privateChatController);
pitiupi.createMenu(MenuCategory.Social, multicastMpChatMenu);
pitiupi.createMenu(MenuCategory.Social, unicastMpChatMenu);
}
@Override
public List<Class<? extends Message>> getMessages() {
return List.of(MpChatMessage.class);
}
@Override
public void receiveMessage(Message message) {
if (message instanceof MpChatMessage mpChatMessage) {
publicChatWindow.receiveMessage(mpChatMessage);
}
}
@Override
public boolean receivePrivateConnection(TcpConnection connection) {
SwingUtilities.invokeLater(() -> {
privateChatController.connectionReceived(connection);
});
return true;
}
@Override
public boolean receiveBytes(TcpConnection connection, byte[] bytes) {
MpChatMessage mpChatMessage = parseMessage(bytes);
if (mpChatMessage == null) {
System.out.println("Invalid message received");
return true;
}
else System.out.println(mpChatMessage.getMessage());
privateChatController.receiveMessage(connection, mpChatMessage);
return true;
}
@Override
public boolean onSocketClosed(TcpConnection connection) {
privateChatController.closeChat(connection);
return true;
}
public void openConnection(String ip) {
try {
pitiupi.connectTcp(InetAddress.getByName(ip), getIdentifier(), getIdentifier());
}
catch (Exception e) {
System.out.println("Error opening connection: " + e.getMessage());
}
}
private MpChatMessage parseMessage(byte[] bytes) {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois;
try {
ois = new ObjectInputStream(bis);
if (ois.readObject() instanceof MpChatMessage msg) {
return msg;
}
} catch (IOException | ClassNotFoundException ex) {
System.out.println(ex.toString());
}
return null;
}
}
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AntConfiguration">
<buildFile url="file://$PROJECT_DIR$/build.xml" />
</component>
</project>
-3
View File
@@ -1,3 +0,0 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK" />
</project>
-9
View File
@@ -1,9 +0,0 @@
<?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$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+14 -68
View File
@@ -5,10 +5,14 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment=""> <list default="true" id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/../pitiupi.testSocket/Makefile" afterDir="false" /> <change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/META-INF/services/pitiupi.plugin.Plugin" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/GUI/QuizWindow.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/.idea/misc.xml" afterDir="false" /> <change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/control/Pergunta.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/src/pitiupi/net/tcp/ConnectionFactory.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> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -18,9 +22,9 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component> </component>
<component name="ProjectColorInfo">{ <component name="ProjectColorInfo"><![CDATA[{
&quot;associatedIndex&quot;: 7 "associatedIndex": 7
}</component> }]]></component>
<component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" /> <component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" />
<component name="ProjectViewState"> <component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
@@ -30,72 +34,17 @@
"keyToString": { "keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true", "ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true", "git-widget-placeholder": "QuizPlugin",
"codeWithMe.voiceChat.enabledByDefault": "false",
"git-widget-placeholder": "feature/separacao-gerenciadores",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true", "kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Gustavo/OneDrive/Documentos/Faculdade/2026_2/Monitoria/pitiupi/pitiupi.screenCast", "last_opened_file_path": "C:/Users/extre/OneDrive/Documentos/Faculdade/2026_02/Monitoria/Pitiupi/pitiupi.screenCast",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.15",
"project.structure.side.proportion": "0.2",
"settings.editor.selected.configurable": "preferences.keymap" "settings.editor.selected.configurable": "preferences.keymap"
} }
}]]></component> }]]></component>
<component name="RunManager">
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
<module name="pitiupi.screenCast" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration default="true" type="Tox" factoryName="Tox">
<module name="pitiupi.screenCast" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<method v="2" />
</configuration>
<configuration default="true" type="JetRunConfigurationType">
<module name="pitiupi.screenCast" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
<module name="pitiupi.screenCast" />
<option name="filePath" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-jdk-30f59d01ecdd-37e91769500f-intellij.indexing.shared.core-IU-261.26222.65" /> <option value="bundled-jdk-9823dce3aa75-bf35d07a577b-intellij.indexing.shared.core-IU-252.28539.54" />
<option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-IU-261.26222.65" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@@ -109,7 +58,4 @@
</task> </task>
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project> </project>
@@ -1,6 +1,6 @@
#Tue, 29 Sep 2026 11:04:58 -0300 #Sat, 18 Jul 2026 10:33:07 -0300
C\:\\Users\\Gustavo\\OneDrive\\Documentos\\Faculdade\\2026_2\\Monitoria\\pitiupi\\pitiupi.screenCast= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.screenCast=
C\:\\Users\\Gustavo\\OneDrive\\Documentos\\Faculdade\\2026_2\\Monitoria\\pitiupi\\pitiupi= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=
Binary file not shown.
Binary file not shown.
@@ -1,20 +0,0 @@
<?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$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -5,8 +5,6 @@
*/ */
package pitiupi.screenCast.GUI; package pitiupi.screenCast.GUI;
import pitiupi.control.ConnectionManager;
import pitiupi.control.GuiManager;
import pitiupi.screenCast.control.AlarmClock; import pitiupi.screenCast.control.AlarmClock;
import pitiupi.screenCast.control.ImageTransformation; import pitiupi.screenCast.control.ImageTransformation;
import pitiupi.screenCast.control.ScreenShot; import pitiupi.screenCast.control.ScreenShot;
@@ -40,13 +38,10 @@ public class ScreenCastWindow extends javax.swing.JFrame {
public final static int TIME = 1000; public final static int TIME = 1000;
private float compressRatio; private float compressRatio;
private final ConnectionManager connectionManager; private final MainWindow mainWindow;
private final GuiManager guiManager;
public ScreenCastWindow(ConnectionManager connectionManager, GuiManager guiManager) {
this.connectionManager = connectionManager;
this.guiManager = guiManager;
public ScreenCastWindow(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.compressRatio = 0.5f; this.compressRatio = 0.5f;
this.initComponents(); this.initComponents();
} }
@@ -96,12 +91,19 @@ public class ScreenCastWindow extends javax.swing.JFrame {
BufferedImage image = ScreenShot.takeAShot(); BufferedImage image = ScreenShot.takeAShot();
BufferedImage rescaled = ImageTransformation.resize(image, 1024, 768); BufferedImage rescaled = ImageTransformation.resize(image, 1024, 768);
byte[] byteImage = ImageTransformation.compress(rescaled, compressRatio); byte[] byteImage = ImageTransformation.compress(rescaled, compressRatio);
ScreenCastMessage message = new ScreenCastMessage(this.connectionManager.getUserName(), byteImage); ScreenCastMessage message = new ScreenCastMessage(this.mainWindow.getUserName(), byteImage);
msg = message.toByteArray();
if (!connectionManager.sendUdpMulticast(message)) this.compressRatio -= 0.1f;
} catch (AWTException | IOException ex) { } catch (AWTException | IOException ex) {
Logger.getLogger(AlarmClock.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(AlarmClock.class.getName()).log(Level.SEVERE, null, ex);
} }
if (msg != null) {
try {
this.mainWindow.getSocket().send(msg);
} catch (IOException ex) {
System.out.println(ex.toString());
this.compressRatio -= 0.1f;
}
}
} }
public void receiveScreenCastMessage(ScreenCastMessage message) { public void receiveScreenCastMessage(ScreenCastMessage message) {
@@ -8,8 +8,6 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import pitiupi.GUI.MainWindow; import pitiupi.GUI.MainWindow;
import pitiupi.control.ConnectionManager;
import pitiupi.control.GuiManager;
import pitiupi.net.Message; import pitiupi.net.Message;
import pitiupi.plugin.Plugin; import pitiupi.plugin.Plugin;
import pitiupi.screenCast.GUI.ScreenCastMenu; import pitiupi.screenCast.GUI.ScreenCastMenu;
@@ -23,6 +21,7 @@ import pitiupi.screenCast.net.ScreenCastMessage;
public class ScreenCastPlugin implements Plugin { public class ScreenCastPlugin implements Plugin {
private ScreenCastWindow screenCastWindow; private ScreenCastWindow screenCastWindow;
private MainWindow mainWindow;
@Override @Override
public String getName() { public String getName() {
@@ -40,14 +39,10 @@ public class ScreenCastPlugin implements Plugin {
} }
@Override @Override
public String getIdentifier() { public void createPlugin(MainWindow mainWindow) {
return "Pitiupi.ScreenCast.Example"; this.mainWindow = mainWindow;
} this.screenCastWindow = new ScreenCastWindow(mainWindow);
this.mainWindow.addMenu(new ScreenCastMenu(screenCastWindow));
@Override
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
this.screenCastWindow = new ScreenCastWindow(connectionManager, guiManager);
guiManager.addMenu(new ScreenCastMenu(screenCastWindow));
} }
@Override @Override
@@ -59,7 +54,9 @@ public class ScreenCastPlugin implements Plugin {
if (ois.readObject() instanceof ScreenCastMessage msg) { if (ois.readObject() instanceof ScreenCastMessage msg) {
this.screenCastWindow.receiveScreenCastMessage(msg); this.screenCastWindow.receiveScreenCastMessage(msg);
} }
} catch (IOException | ClassNotFoundException ex) { } catch (IOException ex) {
System.out.println(ex.toString());
} catch (ClassNotFoundException ex) {
System.out.println(ex.toString()); System.out.println(ex.toString());
} }
} }
-10
View File
@@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/pitiupi.testSocket.iml" filepath="$PROJECT_DIR$/pitiupi.testSocket.iml" />
</modules>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
-2
View File
@@ -8,5 +8,3 @@
# JARs gerados # JARs gerados
*.jar *.jar
.idea/
-6
View File
@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>
</component>
+1 -1
View File
@@ -1,5 +1,5 @@
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_26_PREVIEW" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK"> <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" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+75 -408
View File
@@ -1,23 +1,21 @@
# Visão geral # Arquitetura da aplicação
O `Pitiupi` é uma plataforma P2P baseada em uma arquitetura de plugins, ## 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 desenvolvida para permitir a criação de aplicações distribuídas sobre uma
infraestrutura comum de comunicação em rede. infraestrutura comum de comunicação em rede.
A aplicação fornece mecanismos de descoberta de usuários, comunicação entre A aplicação fornece mecanismos de descoberta de usuários, comunicação
peers, troca de mensagens serializadas e integração de extensões por meio de multicast, troca de mensagens serializadas e integração de extensões por meio
plugins. A comunicação de rede utiliza dois protocolos de transporte: UDP, de plugins. Dessa forma, novos comportamentos podem ser adicionados sem
empregado principalmente na descoberta e comunicação multicast entre as modificar o núcleo da aplicação.
instâncias, e TCP, utilizado para comunicação direta e persistente entre peers.
Dessa forma, novos comportamentos podem ser adicionados por meio de plugins
sem modificar o núcleo da aplicação.
O projeto foi desenvolvido como suporte às práticas da disciplina de Redes de 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 Computadores, servindo também como base para experimentação de aplicações
distribuídas e desenvolvimento de projetos acadêmicos. distribuídas e desenvolvimento de projetos acadêmicos.
# Arquitetura geral ## Arquitetura geral
A aplicação é dividida em três grupos principais: A aplicação é dividida em três grupos principais:
@@ -25,396 +23,103 @@ A aplicação é dividida em três grupos principais:
sendo responsável por inicializar e integrar esses componentes durante a sendo responsável por inicializar e integrar esses componentes durante a
execução. execução.
- **Sistema de plugins**: responsável por adicionar funcionalidades sem - **Sistema de plugins**: responsável por adicionar funcionalidades sem
alterar o núcleo da aplicação. alterar o núcleo.
- **Comunicação de rede**: responsável pela troca de mensagens entre - **Comunicação de rede**: responsável pela troca de mensagens entre
instâncias da aplicação. instâncias da aplicação.
## Inicialização da aplicação ## Inicialização da aplicação
1. A MainWindow é criada. 1. A MainWindow é criada.
2. O SocketUDP é inicializado, associa-se à porta configurada e ingressa no 2. O Socket é inicializado e inicia a comunicação multicast.
grupo multicast. 3. O HeartbeatManager é criado e inicia o envio periódico de heartbeats.
3. O SocketTCP é inicializado e cria um ServerSocket na porta da aplicação 4. O painel de usuários online é registrado como listener do HeartbeatManager.
para aceitar conexões TCP. 5. O PluginLoader procura e carrega os plugins disponíveis.
4. Os mecanismos de comunicação iniciam seus respectivos processos de recepção.
5. O HeartbeatManager é criado e inicia o envio periódico de heartbeats.
6. O painel de usuários online é registrado como listener do HeartbeatManager.
7. O PluginLoader procura e carrega os plugins disponíveis.
# Sistema de comunicação ## Sistema de comunicação
A comunicação entre as instâncias da aplicação é realizada por meio de dois ### Socket
mecanismos complementares:
- **UDP multicast/unicast**, implementado por `SocketUDP` A classe `Socket` é responsável pela comunicação de rede da aplicação.
- **TCP**, implementado por `SocketTCP` e `TcpConnection` Ela utiliza comunicação multicast UDP para permitir que diferentes
instâncias da aplicação troquem mensagens sem a necessidade de um servidor
central.
Ambos os mecanismos utilizam mensagens serializadas em `byte[]`, permitindo que A classe estende `Thread`, pois mantém um processo contínuo de recepção de
os plugins permaneçam independentes dos detalhes do protocolo de transporte. mensagens enquanto a aplicação continua executando suas demais atividades.
## Comunicação UDP 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.
A classe `SocketUDP` é responsável pela comunicação UDP da aplicação. #### Envio de mensagens
Ela utiliza um `MulticastSocket` associado à porta configurada pela aplicação e O método `send()` recebe uma mensagem já serializada em bytes e cria um
ingressa no grupo multicast `224.0.0.3`. Por ser baseada em UDP, a comunicação não `DatagramPacket`, enviando-o para o grupo multicast.
estabelece uma conexão permanente entre os peers.
A classe estende `Thread`, mantendo um processo contínuo de recepção de datagramas De forma geral, o socket atua apenas como mecanismo de transporte das mensagens.
enquanto a aplicação está em execução. A única exceção é o tratamento de HeartbeatMessage, utilizado pela infraestrutura
da aplicação para manter a lista de peers ativos.
O SocketUDP suporta dois modos de envio: #### Recepção de mensagens
- **multicast**, destinado ao grupo de peers O método `receive()` mantém um loop aguardando novas mensagens multicast.
- **unicast**, destinado diretamente ao endereço IP de um peer específico
### Inicialização 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()`.
Durante sua inicialização, o `SocketUDP`: Esse comportamento permite que a aplicação principal trate mensagens de
infraestrutura (como heartbeat), enquanto mensagens específicas de plugins
1. obtém o endereço do grupo multicast; são processadas pelas extensões correspondentes.
2. cria um `MulticastSocket` utilizando a porta configurada na `MainWindow`;
3. configura os buffers de envio e recepção;
4. habilita o reuso do endereço;
5. ingressa no grupo multicast.
O tamanho dos buffers de comunicação é configurado para `256000` bytes.
### Envio de mensagens UDP
O `SocketUDP` suporta dois modos de envio de mensagens: multicast e unicast, ambos
baseados no mesmo mecanismo interno de criação e envio de DatagramPacket.
O método `sendMulticast()` envia uma mensagem para o grupo multicast da aplicação.
Nesse caso, o `DatagramPacket` é direcionado ao endereço multicast configurado,
permitindo que todas as instâncias participantes do grupo recebam a mensagem
simultaneamente.
Já o método `sendUnicast()` permite enviar uma mensagem diretamente para um peer
específico. Nesse caso, o `DatagramPacket` é direcionado ao endereço IP informado pelo
chamador, mantendo a mesma porta utilizada pela aplicação.
Ambos os métodos são, na prática, abstrações de um único mecanismo interno de envio,
que utiliza o mesmo processo de criação e envio de `DatagramPacket` por baixo dos
panos. A diferença entre eles está apenas no endereço de destino: enquanto o multicast
utiliza o grupo compartilhado, o unicast direciona o pacote a um IP específico.
### Recepção de mensagens
O método `receive()` mantém um loop aguardando novos datagramas.
Ao receber uma mensagem, o `SocketUDP` inicialmente tenta identificar se os dados
correspondem a uma `HeartbeatMessage`.
O processamento segue o seguinte fluxo:
1. O datagrama é recebido pelo `MulticastSocket`.
2. Os dados recebidos são analisados pelo método `tryParseHeartbeat()`.
3. Caso seja identificada uma `HeartbeatMessage`, ela é encaminhada ao
`HeartbeatManager`, juntamente com o endereço IP de origem.
4. Caso não seja um heartbeat, a mensagem é encaminhada para todos os plugins
carregados.
5. Cada plugin decide se a mensagem recebida pertence à sua funcionalidade.
Essa separação mantém as mensagens de infraestrutura, como heartbeat, sob
responsabilidade do núcleo da aplicação, enquanto as mensagens específicas das
funcionalidades são processadas pelos plugins.
### Identificação de mensagens de heartbeat
O método `tryParseHeartbeat()` tenta desserializar os dados recebidos e verificar se o
objeto resultante é uma instância de `HeartbeatMessage`.
Caso a mensagem não possa ser desserializada ou não corresponda ao tipo esperado, o
método retorna `null`. Nesse caso, a mensagem é tratada como uma mensagem destinada aos
plugins.
### Encerramento
O método `close()` interrompe a execução do processo de recepção, remove a aplicação
do grupo multicast e fecha o `MulticastSocket`.
A variável `running` é utilizada para sinalizar que o processo de recepção deve ser
encerrado.
## Comunicação TCP
A comunicação TCP é implementada pela classe `SocketTCP`.
Diferentemente do UDP, o TCP estabelece conexões entre dois peers. Essas conexões
são mantidas enquanto forem necessárias e podem ser reutilizadas para o envio de
múltiplas mensagens.
A arquitetura utiliza uma conexão TCP persistente por endereço de peer.
O SocketTCP possui um mapa de conexões:
`InetAddress` -> `TcpConnection`
Esse mapa permite localizar rapidamente uma conexão existente para determinado peer e
reutilizá-la durante novos envios.
### SocketTCP
A classe `SocketTCP` estende `Thread` e implementa `PeerListener`.
Ao ser criada, ela inicializa um `ServerSocket` utilizando a porta configurada na
aplicação.
O servidor TCP permanece aguardando novas conexões enquanto a aplicação estiver
em execução.
### Envio de mensagens TCP
O método `send()` recebe uma mensagem serializada e o endereço IP do peer destinatário.
Antes de enviar a mensagem, o `SocketTCP` verifica se já existe uma conexão válida com
o peer.
O comportamento é:
1. Procurar uma conexão existente no mapa de conexões.
2. Verificar se a conexão está fechada.
3. Caso não exista uma conexão válida, criar um novo Socket TCP para o endereço do peer.
4. Encapsular o socket em uma `TcpConnection`.
5. Registrar a conexão no mapa.
6. Iniciar uma tarefa de recepção para essa conexão.
7. Enviar a mensagem pela conexão.
Assim, uma conexão TCP existente pode ser reutilizada para várias mensagens, evitando
a criação de um novo socket para cada envio.
### Recepção de conexões
O método `receive()` permanece aguardando novas conexões através de
`ServerSocket.accept()`.
Quando uma conexão é aceita:
O endereço IP do peer é obtido.
Um objeto `TcpConnection` é criado para encapsular o socket.
A conexão é registrada no mapa de conexões.
Caso já exista uma conexão associada ao mesmo endereço, a conexão anterior é encerrada.
Uma tarefa independente de recepção é criada para a nova conexão.
Esse modelo permite que o servidor aceite múltiplas conexões sem bloquear o
processamento das conexões já existentes.
### Recepção concorrente
A recepção das mensagens TCP é realizada de maneira concorrente por meio de um
`ExecutorService`.
Cada conexão possui uma tarefa própria responsável por receber continuamente as
mensagens.
O fluxo de recepção é:
```
SocketTCP
|
+-- Peer A → TcpConnection → tarefa de recepção
|
+-- Peer B → TcpConnection → tarefa de recepção
|
+-- Peer C → TcpConnection → tarefa de recepção
```
Dessa forma, uma conexão bloqueada ou aguardando dados não impede que outras conexões
continuem sendo processadas.
Quando uma mensagem é recebida por uma `TcpConnection`, ela é encaminhada para todos os
plugins registrados na `MainWindow`.
O plugin responsável pela funcionalidade deve identificar o tipo da mensagem e
realizar o processamento correspondente.
### TcpConnection
A classe TcpConnection encapsula uma conexão TCP individual entre dois peers.
Ela mantém:
o Socket utilizado na conexão;
um DataInputStream para recepção;
um DataOutputStream para envio.
Além de encapsular esses recursos, a classe é responsável pelo framing das mensagens.
### Framing das mensagens TCP
A classe `TcpConnection` encapsula uma conexão TCP individual entre dois peers.
Ela mantém:
1. o `Socket` utilizado na conexão;
2. um `DataInputStream` para recepção;
3. um `DataOutputStream` para envio.
Além de encapsular esses recursos, a classe é responsável pelo framing das mensagens.
Framing das mensagens TCP
O protocolo TCP fornece um fluxo contínuo de bytes e não preserva os limites das
mensagens enviadas pela aplicação.
Por esse motivo, é adicionado o tamanho da mensagem antes do conteúdo.
O formato utilizado é:
```
+----------------------+----------------------+
| tamanho da mensagem | dados da mensagem |
+----------------------+----------------------+
4 bytes N bytes
```
O tamanho é enviado como um `int`, seguido pelos bytes da mensagem.
No envio, o método `TcpConnection.send()`:
1. obtém o tamanho do vetor de bytes;
2. escreve o tamanho no `DataOutputStream`;
3. escreve os dados da mensagem;
4. realiza `flush()` no stream.
O acesso ao stream de saída é sincronizado para evitar que envios concorrentes
misturem seus dados.
Na recepção, o método `receive()`:
1. lê o tamanho da mensagem;
2. verifica se o tamanho é válido;
3. lê a quantidade correspondente de bytes;
4. retorna o vetor de bytes completo.
Esse mecanismo permite que várias mensagens sejam transmitidas pela mesma conexão
TCP sem que o receptor perca a delimitação entre elas.
### Integração entre TCP e gerenciamento de peers
O `SocketTCP` implementa a interface PeerListener para acompanhar as alterações na
lista de peers ativos.
O `HeartbeatManager` mantém a lista de peers atualmente detectados na rede. Quando
essa lista é alterada, o SocketTCP recebe a notificação por meio do método
`onPeersChanged()`.
O método obtém os endereços IP dos peers atualmente ativos e compara esses endereços
com as conexões TCP existentes.
Quando uma conexão pertence a um peer que não está mais ativo:
a conexão é removida do mapa;
a conexão TCP é encerrada;
os recursos associados são liberados.
Esse mecanismo impede que conexões TCP permaneçam abertas indefinidamente para peers
que já deixaram de participar da rede.
O fluxo de integração pode ser representado da seguinte forma:
```
HeartbeatManager
|
| peers ativos alterados
|
v
SocketTCP.onPeersChanged()
|
| compara peers ativos
|
v
+---- peer ativo ------> mantém conexão
|
+---- peer inativo ----> fecha conexão
```
### Message ### Message
A classe `Message` representa a estrutura base das mensagens trocadas pela aplicação. 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 Todas as mensagens utilizadas pelo sistema devem herdar dessa classe. Ela
`Serializable`, permitindo que objetos de mensagem sejam convertidos em vetores de implementa `Serializable`, permitindo que objetos de mensagem sejam
bytes para transmissão pela rede. convertidos em vetores de bytes para transmissão pela rede.
A serialização é realizada pelo método `toByteArray()`, que transforma uma instância A serialização é realizada pelo método `toByteArray()`, que transforma uma
da mensagem em uma representação binária enviada pelos mecanismos de comunicação. instância da mensagem em uma representação binária enviada pelo `Socket`.
As mensagens específicas devem estender essa classe, adicionando os atributos e Mensagens específicas devem estender essa classe adicionando os atributos e
comportamentos necessários para cada funcionalidade. comportamentos necessários para cada funcionalidade.
A camada de transporte não depende do conteúdo específico das mensagens. Tanto o
UDP quanto o TCP recebem mensagens serializadas como `byte[]`.
### Heartbeat ### Heartbeat
O mecanismo de heartbeat é utilizado para identificar quais usuários estão ativos O mecanismo de heartbeat é utilizado para identificar quais usuários estão
na rede. ativos na rede.
A aplicação envia periodicamente uma `HeartbeatMessage` contendo informações do usuário, A aplicação envia periodicamente uma `HeartbeatMessage` contendo
um identificador único da instância da aplicação e um timestamp. 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.
Essas mensagens são transmitidas utilizando a comunicação UDP multicast. 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.
Quando outra instância recebe uma mensagem de heartbeat, o `SocketUDP` identifica O fluxo de heartbeat é independente das mensagens dos plugins:
o tipo da mensagem e encaminha o objeto ao `HeartbeatManager`, juntamente com o endereço
IP de origem.
O `HeartbeatManager` registra ou atualiza o peer correspondente, utilizando o 1. `HeartbeatManager` cria uma `HeartbeatMessage`.
identificador da instância para diferenciá-lo das demais. 2. A mensagem é serializada utilizando `Message.toByteArray()`.
3. O `Socket` transmite a mensagem pela rede multicast.
Um peer é considerado inativo quando permanece mais de 15 segundos sem receber um novo 4. O `Socket` identifica mensagens de heartbeat recebidas e encaminha ao
heartbeat. O `HeartbeatManager` então remove o peer da lista e notifica todos os `HeartbeatManager`.
componentes registrados como listeners de mudanças de peers. 5. O `HeartbeatManager` atualiza a lista de peers ativos e notifica os
listeners.
Esses listeners podem incluir diferentes partes da aplicação, como componentes
da interface gráfica, mecanismos de comunicação (como o `SocketTCP`) e também plugins
que desejem reagir à presença ou ausência de usuários na rede.
Dessa forma, o sistema permite que tanto o núcleo da aplicação quanto extensões
externas sejam notificados sobre alterações no estado da rede, mantendo a arquitetura
flexível e extensível.
O fluxo completo é:
```
HeartbeatManager
|
| cria HeartbeatMessage
|
v
Message.toByteArray()
|
v
SocketUDP.sendMulticast()
|
v
UDP Multicast
|
v
SocketUDP.receive()
|
v
HeartbeatManager.receiveHeartbeat()
|
v
Lista de peers ativos
|
+----> Interface de usuários online
|
+----> SocketTCP (PeerListener)
|
+----> Plugins (PeerListener)
```
## Sistema de plugins ## Sistema de plugins
O sistema de plugins é responsável por permitir a extensão das funcionalidades da
aplicação de forma modular, sem a necessidade de alterações no núcleo do sistema.
Ele define um contrato comum que deve ser seguido por todas as extensões, garantindo
que possam ser carregadas dinamicamente e integradas à comunicação e à interface da
aplicação.
### Interface Plugin ### Interface Plugin
A interface `Plugin` define o contrato que deve ser implementado por qualquer A interface `Plugin` define o contrato que deve ser implementado por qualquer
@@ -468,10 +173,10 @@ MeusPlugins
| |
+---services +---services
| |
+---pitiupi.models.Plugin +---pitiupi.plugin.Plugin
``` ```
O arquivo `pitiupi.models.Plugin` deve conter o nome completo das classes que O arquivo `pitiupi.plugin.Plugin` deve conter o nome completo das classes que
implementam a interface Plugin: implementam a interface Plugin:
``` ```
@@ -481,7 +186,7 @@ MeusPlugins.plugins.NomeDaClasse
Para serem carregados, os plugins devem estar empacotados como arquivos JAR Para serem carregados, os plugins devem estar empacotados como arquivos JAR
e colocados no diretório plugins da aplicação. e colocados no diretório plugins da aplicação.
### Carregamento de plugins ### Carregamento
Ao iniciar a aplicação, o `PluginLoader` executa os seguintes passos: Ao iniciar a aplicação, o `PluginLoader` executa os seguintes passos:
@@ -504,44 +209,6 @@ Durante a inicialização, cada plugin recebe uma referência para a
Essa referência permite integrar componentes gráficos à aplicação, sendo a 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: principal forma de extensão a adição de menus à barra de menus:
``` ```java
JMenu menu = new JMenu("Meu Plugin"); JMenu menu = new JMenu("Meu Plugin");
window.addMenu(menu); window.addMenu(menu);
```
Visão geral da comunicação
A arquitetura de comunicação pode ser resumida da seguinte maneira:
```
+------------------+
| MainWindow |
+--------+---------+
|
+--------------------+--------------------+
| | |
v v v
HeartbeatManager SocketUDP SocketTCP
| | |
| | |
v v v
Heartbeat UDP Multicast TCP Connections
| |
| |
+---------+----------+
|
v
Plugins
|
v
Message
```
O UDP multicast fornece a infraestrutura de descoberta e manutenção da presença dos
peers, além de permitir comunicação multicast e unicast.
O TCP fornece canais diretos e persistentes entre peers, com uma conexão reutilizável
por endereço.
Os dois mecanismos encaminham as mensagens destinadas às funcionalidades da aplicação
para os plugins, mantendo separadas as responsabilidades de transporte, infraestrutura
e lógica específica de cada extensão.
+1 -511
View File
@@ -1,512 +1,2 @@
# Como criar plugins para o Pitiupi # Como criar plugins para o Share my Sheet
Aqui serão discutidos os métodos e classes que um desenvolvedor pode ou deve usar para criar um plugin para a plataforma Pitiupi.
Alguns deles estarão marcados com um <span style="color: red;">[REF]</span>, isso indica que eles estão sendo alterados para a versão 3.0 e deixarão de existir ou vão mudar consideravelmente.
## GUI
O Pitiupi é feito usando a biblioteca Swing do Java, por isso é altamente recomendado que os plugins também a utilizem na hora de criar sua interface gráfica, porém não é obrigatório.
## Como tornar o seu plugin visível para o Pitiupi
A busca por plugins é realizada em três etapas pelo pitiupi:
1. Encontra todos os jars em `plugins`.
2. Verifica o `META-INF` dos jars para saber onde estão as implementações da classe `Plugin`.
3. Carrega o plugin no sistema.
Em relação ao `META-INF`, ele deve conter um diretório `resources` e dentro dele um arquivo contendo o nome completamente qualificado da classe `Plugin` do pitiupi: `pitiupi.models.Plugin`, por fim dentro desse arquivo você deve colocar o nome completamente qualificado de todas as classes que implementam `Plugin` do seu projeto, um em cada linha.
```
build/
|---->META-INF/resources/pitiupi.models.Plugin
|---->MeusPlugins/...
|---->MinhasMensagens/...
|---->Restante dos .class
```
```
src/MeusPlugins/MeuPrimeiroPlugin
src/MeusPlugins/MeuSegundoPlugin
...
```
### Dicas
- Faça um makefile que automaticamente mova o META-INF para o build/ quando você compilar uma nova versão caso ele esteja sendo alterado com frequência.
Ex:
```
JAR = ../pitiupi/dist/pitiupi.jar
BUILD_DIR = build
META-INF_DIR = src/META-INF
DIST = dist/mpChat.jar
SOURCES = $(shell find -name "*.java")
all:
mkdir -p "$(BUILD_DIR)"
mkdir -p "$(dir $(DIST))"
javac -cp "$(JAR)" -d "$(BUILD_DIR)" $(SOURCES)
cp -r "$(META-INF_DIR)" "$(BUILD_DIR)"
jar cf "$(DIST)" -C "$(BUILD_DIR)" .
clean:
rm -rf "$(BUILD_DIR)"/*
```
- Para a versão 3.0 será necessário fazer o mesmo processo para as implementações da classe `Message`, então se quiser deixar preparado para a nova versão adicione `pitiupi.models.Messages.Message` em `resources` e coloque nele todas suas implementações de `Message`, assim como você fez para os de `Plugin`.
## Pitiupi
A classe `Pitiupi` é a interface que o seu plugin vai receber quando for criado, ela fornece os métodos necessários para interagir com a interface gráfica e comunicação para outros peers.
### boolean sendUdpMulticast(Message message)
Envia uma mensagem multicast, retorna `boolean` indicando se teve sucesso.
### boolean sendUdpUnicast(Message message, InetAddress address)
Envia uma mensagem unicast via udp para o endereço fornecido, retorna `boolean` indicando se teve sucesso.
### boolean sendTcp(Message message, InetAddress address)
Envia uma mensagem unicast via tcp para o endereço fornecido, retorna `boolean` indicando se teve sucesso.
### <span style="color: red;">[REF]</span> TcpConnection connectTcp(InetAddress address, String senderId, String receiverId)
Pede uma conexão tcp privada para o seu plugin com outro usuário, para utilizar essa funcionalidade é necessário implementar alguns métodos opcionais em Plugin. Maisinformações serão discutidas na seção TcpConnection.
### String getUserName()
Retorna o nome do usuário conectado
### int getPort()
Retorna a porta que o usuário está usando o Pitiupi
### void createMenu(MenuCategory category, Menu menu)
Adiciona um menu ao pitiupi, category define em qual das categoria de Menus ele será adicionado: Jogos, Social, Streaming.
Os `Menus` serão discutidos em uma seção posterior.
## Classe Plugin
Essa é a classe que o pitiupi vai procurar ao carregar o seu plugin, ela define um contrato de métodos que todo plugin deve possuir.
### getName()
Indica o nome público do plugin.
### getAuthor()
Indica o nome dos desenvolvedores do plugin.
### getVersion()
Indica a versão atual do plugin.
### getIdentifier()
Indica um id que o pitiupi vai utilizar para enontrar seu plugin, por isso é importante que ele seja bem especifico para evitar que dois plugins sejam confundidos durante a comunicação. Procure usar algo como: MeuPlugin.Autor.Números
### createPlugin(Pitiupi pitiupi)
Após o pitiupi identificar e carregar o seu plugin, ele vai chamar esse método para inicializá-lo, por isso pense nele como o construtor do seu plugin, é aqui que você inicializa atributos, adiciona os menus inciais, etc.
### getMessages()
Esse método indica ao pitiupi quais tipos de mensagem seu plugin quer receber, normalmente as que ele mesmo implementar.
Nesse caso, é normal que ele faça:
```
return List.of(
SuaMensagem1.class,
SuaMensagem2.class,
...
);
```
Caso você queira que seu plugin receba todo tipo de mensagem, existe um método `receiveAllTypes()` que ele pode sobreescrever para receber todas mensagens
que não sejam consideradas privadas.
### receiveMessage(Message message)
Esse é o método que o pitiupi vai chamar quando quiser te entregar uma mensagem pelo canal publico de mensagens. Note que ele entrega uma `Message`, então é necessário que
seu plugin confira o tipo real ele mesmo. Como em:
```
if (message instanceOf MinhaMensagem minhaMensagem) {...}
```
O pitiupi garante que é uma mensagem de um tipo que o seu plugin diz aceitar.
### <span style="color: red;">[REF]</span> métodos opcionais
Os métodos a seguir possuem uma implementação padrão da própria classe que retornam false. Os três ultimos são necessários caso seu plugin deseje criar e receber conexões privadas.
### <span style="color: red;">[REF]</span> receiveAllTypes()
Indica ao Pitiupi que seu plugin deseja receber todas as mensagens publicas da plataforma, independente do tipo.
### <span style="color: red;">[REF]</span> receivePrivateConnection(TcpConnection connection)
Esse método é chamado quando o Pitiupi precisa entregar uma `TcpConnection` ao seu plugin, seja um que você pediu ou recebeu de um peer.
### <span style="color: red;">[REF]</span> receiveBytes(TcpConnection connection, byte[] bytes)
Esse método é chamado para entregar as mensagens recebidas por um `TcpConnection`, o tcpConnection no parametro indica qual a fonte da mensagem.
### <span style="color: red;">[REF]</span> onSocketClosed(TcpConnection connection)
Esse é método é chamado quando o socket de um `TcpConnection` pertencente ao plugin é fechado.
## Message
Essa é a classe abstrata que o pitiupi usa para comunicação nos canais publicos.
<span style="color: red;">[REF]</span> Ela possui um atributo isPublic que por padrão é true, mas é possível defini-lo no construtor como false caso a mensagem deva ser privada.
Essa classe implementa `Serializable` e possui um método `toByteArray` que transforma a mensagem em `byte[]`. Porém é importante notar que caso ela possua um objeto de outra classe como atributo aquela classe também deve implementar `Serializable`.
## <span style="color: red;">[REF]</span> TcpConnection
Representa uma conexão Tcp ativa para algum outro peer, mensagens enviadas por essa conexão não chegam ao canal publico e em vez disso vão direto para destinatário.
Caso fique inativa (Sem enviar ou receber mensagens) por 5 minutos a conexão é fechada automaticamente e o dono é notificado.
### <span style="color: red;">[REF]</span> void send(byte[] message)
Envia um conjunto de bytes[] para o destinatario.
Caso queira enviar uma `Message` chame o método toByteArray para transforma-la em bytes primeiro.
### <span style="color: red;">[REF]</span> boolean isClosed()
Indica se a conexão esta fechada
### <span style="color: red;">[REF]</span> InetAddress getAddress()
Retorna o endereço do peer que ela esta conectada.
### <span style="color: red;">[REF]</span> String getReceiver()
Retorna o identificador do plugin que ela esta conectada.
### <span style="color: red;">[REF]</span> getReceiver()
Retorna o identifador do plugin que pediu pela conexão.
## Menu
Essa é a classe que indica ao pitiupi que um menu deve ser adicionado a janela principal da aplicação.
Menus podem ser de dois tipos `UnicastMenu` e `MulticastMenu` com a principal diferença entre eles sendo onde eles são adicionados na janela principal. Outras mudanças serão discutidas quando for relevante nas classes a seguir.
Atualmente Menus não podem ser alterados uma vez que são adicionados, então é importante que eles sejam feitos com isso em mente.
Tem um método `String getText()` que retorna o que deve ser escrito no Menu`.
### SubMenu
Permite que o `Menu` expanda em uma nova seção de menus.
`UnicastSubMenu` o método `getMenu()` retorna `List<UnicastMenu>`
`MulticastSubMenu` o método `getMenu()` retorna `List<MulticastMenu>`
Exemplo:
```
ChatCasualMulticastMenu chatCasualMulticastMenu;
ChatEstudoMutlticastMenu chatEstudoMutlticastMenu;
...
@Override
public List<MulticastMenu> getMenus() {
return List.of(chatCasualMulticastMenu, chatEstudoMutlticastMenu);
}
```
### ActionMenu
É um `Menu` que que realiza alguma ação quando selecionado pelo usuário.
`UnicastActionMenu` o método `onClick(User user)` é chamado quando selecionado pelo usuário, recebendo como parametro o peer escolhido pelo usuário.
`MulticastActionMenu` o método `onClick()` é chamado quando selecionado pelo usuário.
Exemplo:
```
@Override
public void onClick(User user) {
callManager.openCallWindow(user);
}
```
## Exemplo de plugin no pitiupi:
### Plugin
Implementa o contrato de plugin.
Inicializa a janela principal e cria os menus.
Também define quais tipos de mensagem ele quer receber.
```
public class ChatPlugin extends Plugin {
private ChatWindow chatWindow;
@Override
public void createPlugin(Pitiupi pitiupi) {
this.chatWindow = new ChatWindow(pitiupi);
pitiupi.createMenu(MenuCategory.Social, new ChatMenu(chatWindow));
}
@Override
public List<Class<? extends Message>> getMessages() {
return List.of(ChatMessage.class);
}
@Override
public void receiveMessage(Message message) {
if (message instanceof ChatMessage msg) {
this.chatWindow.receiveChatMessage(msg);
}
}
@Override
public String getName() {
return "Chat";
}
@Override
public String getAuthor() {
return "Flávio Schiavoni";
}
@Override
public String getVersion() {
return "0";
}
@Override
public String getIdentifier() {
return "Pitiupi.Chat.Example.123456";
}
}
```
### Message
Implementa uma mensagem customizada que o plugin vai utilizar para comunicação.
```
public class ChatMessage extends Message {
private String text;
private String name;
public ChatMessage(){
}
public ChatMessage(String name, String text){
this.name = name;
this.text = text;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the text
*/
public String getText() {
return text;
}
/**
* @param text the text to set
*/
public void setText(String text) {
this.text = text;
}
}
```
### Menus
Implementa os dois menus de Multicast utilizados pela aplicação.
#### ActionMenu
Esse menu apenas abre a janela principal do plugin.
```
public class ChatMulticastMenu extends MulticastActionMenu {
ChatWindow chatWindow;
public ChatMulticastMenu(ChatWindow chatWindow) {
this.chatWindow = chatWindow;
}
@Override
public String getText() {
return "Show Chat Window";
}
@Override
public void onClick() {
chatWindow.setVisible(true);
}
}
```
Esse menu abre a seção de menus do plugin Chat.
#### SubMenu
```
public class ChatMenu extends MulticastSubMenu {
ChatMulticastMenu chatMulticastMenu;
public ChatMenu(ChatWindow chatWindow) {
this.chatMulticastMenu = new ChatMulticastMenu(chatWindow);
}
@Override
public String getText() {
return "Chat";
}
@Override
public List<MulticastMenu> getMenus() {
return List.of(chatMulticastMenu);
}
}
```
### GUI
Essa é a janela principal do plugin de chat.
```
public class ChatWindow extends JDialog {
private JButton jButton1;
private JEditorPane jEditorPane1;
private JPanel jPanel1;
private JPanel jPanel2;
private JScrollPane jScrollPane1;
private JScrollPane jScrollPane2;
private JSplitPane jSplitPane1;
private JTextArea jTextArea1;
private final Pitiupi pitiupi;
public ChatWindow(Pitiupi pitiupi) {
this.pitiupi = pitiupi;
initComponents();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
private void initComponents() {
this.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
this.setTitle("Chat Window");
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));
jSplitPane1 = new JSplitPane();
jScrollPane1 = new JScrollPane();
jEditorPane1 = new JEditorPane();
jPanel1 = new JPanel();
jScrollPane2 = new JScrollPane();
jTextArea1 = new JTextArea();
jPanel2 = new JPanel();
jButton1 = new JButton();
jSplitPane1.setDividerLocation(500);
jSplitPane1.setOrientation(JSplitPane.VERTICAL_SPLIT);
jEditorPane1.setContentType("text/html");
jPanel1.setLayout(new java.awt.BorderLayout());
jTextArea1.setColumns(20);
jTextArea1.setRows(5);
jTextArea1.addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyTyped(java.awt.event.KeyEvent evt) {
if (evt.getKeyChar() == '\n') {
sendMessage();
}
}
});
jButton1.setText("Send");
jScrollPane1.setViewportView(jEditorPane1);
jScrollPane2.setViewportView(jTextArea1);
jSplitPane1.setTopComponent(jScrollPane1);
jPanel1.add(jScrollPane2, java.awt.BorderLayout.CENTER);
jPanel2.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT));
jButton1.addActionListener((java.awt.event.ActionEvent evt) -> {
sendMessage();
});
jPanel2.add(jButton1);
jPanel1.add(jPanel2, java.awt.BorderLayout.PAGE_END);
jSplitPane1.setRightComponent(jPanel1);
getContentPane().add(jSplitPane1, java.awt.BorderLayout.CENTER);
pack();
}
private void sendMessage() {
if (jTextArea1.getText().trim().isEmpty()) {
return;
}
ChatMessage message = new ChatMessage(pitiupi.getUserName(), jTextArea1.getText());
this.sendChatMessage(message);
jTextArea1.setText("");
}
public void sendChatMessage(Message message) {
pitiupi.sendUdpMulticast(message);
}
public void receiveChatMessage(ChatMessage message) {
SimpleDateFormat dt = new SimpleDateFormat("dd/MM/yyyyy hh:mm:ss");
Date date = new Date();
this.receiveChatMessage(dt.format(date) + " "
+ "<b>" + message.getName() + "</b>: "
+ message.getText() + "<br>");
}
public void receiveChatMessage(String message) {
jEditorPane1.setText(message + jEditorPane1.getText());
}
}
```
+2 -2
View File
@@ -46,8 +46,8 @@ javac.modulepath=
javac.processormodulepath= javac.processormodulepath=
javac.processorpath=\ javac.processorpath=\
${javac.classpath} ${javac.classpath}
javac.source=17 javac.source=25
javac.target=17 javac.target=25
javac.test.classpath=\ javac.test.classpath=\
${javac.classpath}:\ ${javac.classpath}:\
${build.classes.dir} ${build.classes.dir}
Binary file not shown.
Binary file not shown.
+1
View File
@@ -48,6 +48,7 @@ public final class Login extends JDialog {
public void initComponents() { public void initComponents() {
this.setTitle("Login"); this.setTitle("Login");
String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name")); String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name"));
int portaSalva = prefs.getInt(PREF_PORTA, 8000); int portaSalva = prefs.getInt(PREF_PORTA, 8000);
+101 -56
View File
@@ -5,13 +5,13 @@
*/ */
package pitiupi.GUI; package pitiupi.GUI;
import pitiupi.control.ConnectionManager; import pitiupi.net.Socket;
import pitiupi.control.GuiManager; import java.util.ArrayList;
import pitiupi.control.plugins.PluginManager; import java.util.List;
import javax.swing.JMenu;
import javax.swing.*; import pitiupi.control.PluginLoader;
import pitiupi.plugin.Plugin;
import java.awt.*; import pitiupi.control.HeartbeatManager;
/** /**
* Janela principal da aplicação. * Janela principal da aplicação.
@@ -27,15 +27,25 @@ import java.awt.*;
* @author tony * @author tony
* @author Gustavo * @author Gustavo
*/ */
public class MainWindow extends JFrame { public class MainWindow extends javax.swing.JFrame {
private MenuBar menuBar; private final MenuBar menuBar;
private JLabel status; private javax.swing.JScrollPane jScrollPane1;
OnlineUsersPanel onlineUsersPanel; private javax.swing.JLabel status;
private javax.swing.JPanel statusPanel;
private javax.swing.JTabbedPane tabbedPane;
private final ConnectionManager connectionManager; private HeartbeatManager heartbeatManager;
private final PluginManager pluginManager; private OnlineUsersPanel onlineUsersPanel;
private final GuiManager guiManager;
private Socket socket;
private String userName;
private int port;
private PluginLoader pluginManager;
private final List<Plugin> plugins;
/** /**
* Cria a janela principal da aplicação. * Cria a janela principal da aplicação.
@@ -44,53 +54,58 @@ public class MainWindow extends JFrame {
* @param port porta utilizada na comunicação multicast. * @param port porta utilizada na comunicação multicast.
*/ */
public MainWindow(String userName, int port) { public MainWindow(String userName, int port) {
initMenu(); this.userName = userName;
this.port = port;
pluginManager = new PluginManager(); this.menuBar = new MenuBar(this);
connectionManager = new ConnectionManager(this, userName, pluginManager, port); this.plugins = new ArrayList<>();
this.initComponents();
initUserPanel(); }
initStatusPanel();
guiManager = new GuiManager(this, menuBar, onlineUsersPanel);
loadPlugins();
this.pack();
private void initComponents() {
initGUI();
initSocket();
initHeartbeat();
initPlugins();
this.setSize(new java.awt.Dimension(1024, 768)); this.setSize(new java.awt.Dimension(1024, 768));
} }
private void initMenu() { private void initGUI() {
menuBar = new MenuBar(this);
this.setJMenuBar(menuBar); this.setJMenuBar(menuBar);
this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
this.setTitle("Pitiupi"); this.setTitle("Share My Sheet");
javax.swing.JScrollPane jScrollPane1 = new javax.swing.JScrollPane(); jScrollPane1 = new javax.swing.JScrollPane();
javax.swing.JTabbedPane tabbedPane = new javax.swing.JTabbedPane(); tabbedPane = new javax.swing.JTabbedPane();
statusPanel = new javax.swing.JPanel();
status = new javax.swing.JLabel();
jScrollPane1.setViewportView(tabbedPane); jScrollPane1.setViewportView(tabbedPane);
statusPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT));
this.getContentPane().add(jScrollPane1, java.awt.BorderLayout.CENTER); status.setText("Connected on port " + this.port + " name " + this.userName);
}
private void initUserPanel() {
onlineUsersPanel = new OnlineUsersPanel();
this.connectionManager.addPeerListener(onlineUsersPanel);
this.getContentPane().add(onlineUsersPanel, java.awt.BorderLayout.LINE_END);
}
private void initStatusPanel() {
JPanel statusPanel = new JPanel();
status = new JLabel();
statusPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
updateStatus();
statusPanel.add(status); statusPanel.add(status);
this.getContentPane().add(statusPanel, BorderLayout.PAGE_END); this.getContentPane().add(jScrollPane1, java.awt.BorderLayout.CENTER);
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();
}
private void initPlugins() {
this.pluginManager = new PluginLoader(this);
this.pluginManager.loadPlugins();
} }
public void exit() { public void exit() {
@@ -99,21 +114,51 @@ public class MainWindow extends JFrame {
System.exit(0); System.exit(0);
} }
public ConnectionManager getConnectionManager() { public HeartbeatManager getHeartbeatManager() {
return connectionManager; return heartbeatManager;
} }
public void loadPlugins() { public String getUserName(){
pluginManager.loadPlugins(connectionManager, guiManager); return this.userName;
}
public void addMenu(JMenu menu) {
this.menuBar.addPlugin(menu);
}
public int getPort() {
return this.port;
}
public Socket getSocket() {
return this.socket;
}
public List<Plugin> getPlugins() {
return plugins;
} }
public void setUserName(String userName) { public void setUserName(String userName) {
connectionManager.setUserName(userName); this.userName = userName;
updateStatus();
}
public void setPort(int port) {
this.port = port;
reconnect();
updateStatus(); updateStatus();
} }
private void updateStatus() { private void updateStatus() {
this.status.setText("Connected on port " + connectionManager.getPort() + " name " + connectionManager.getUserName()); 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 -50
View File
@@ -5,14 +5,8 @@
*/ */
package pitiupi.GUI; package pitiupi.GUI;
import pitiupi.models.Menus.MenuCategory; import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
/** /**
* Barra de menus principal da aplicação. * Barra de menus principal da aplicação.
@@ -27,14 +21,12 @@ import java.util.Map;
*/ */
public class MenuBar extends JMenuBar { public class MenuBar extends JMenuBar {
private JMenuItem preferencesMenu; private javax.swing.JMenuItem setupMenu;
private JMenuItem aboutMenu; private javax.swing.JMenuItem aboutMenu;
private JMenu fileMenu; private javax.swing.JMenu fileMenu;
private JMenu helpMenu; private javax.swing.JMenu helpMenu;
private JMenu pluginMenu; private javax.swing.JMenu pluginMenu;
private Map<MenuCategory, JMenu> categories; private javax.swing.JMenuItem exitMenu;
private JMenuItem exitMenu;
private final MainWindow mainWindow; private final MainWindow mainWindow;
/** /**
@@ -48,69 +40,63 @@ public class MenuBar extends JMenuBar {
} }
private void initComponents() { private void initComponents() {
initFileMenu(); fileMenu = new javax.swing.JMenu();
initPluginMenu(); exitMenu = new javax.swing.JMenuItem();
initHelpMenu(); helpMenu = new javax.swing.JMenu();
} pluginMenu = new javax.swing.JMenu();
setupMenu = new javax.swing.JMenuItem();
aboutMenu = new javax.swing.JMenuItem();
private void initFileMenu() { fileMenu.setText("File");
fileMenu = new JMenu("File"); exitMenu.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F4, java.awt.event.InputEvent.ALT_MASK));
exitMenu = new JMenuItem("Exit"); exitMenu.setText("Exit");
exitMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.ALT_DOWN_MASK));
exitMenu.addActionListener(this::exitMenuActionPerformed); exitMenu.addActionListener(this::exitMenuActionPerformed);
fileMenu.add(exitMenu); fileMenu.add(exitMenu);
this.add(fileMenu); this.add(fileMenu);
}
private void initPluginMenu() {
pluginMenu = new JMenu("Plugin");
categories = new HashMap<>();
for (MenuCategory category : MenuCategory.values()) {
JMenu newCategoryMenu = new JMenu(category.toString());
pluginMenu.add(newCategoryMenu);
categories.put(category, newCategoryMenu);
}
pluginMenu.setText("Plugin");
this.add(pluginMenu); this.add(pluginMenu);
}
private void initHelpMenu() { helpMenu.setText("Help");
helpMenu = new JMenu("Help");
preferencesMenu = new JMenuItem("Preferences");
aboutMenu = new JMenuItem("About");
preferencesMenu.addActionListener(this::setupMenuActionPerformed); setupMenu.setText("Preferences");
helpMenu.add(preferencesMenu); setupMenu.addActionListener(this::setupMenuActionPerformed);
helpMenu.add(setupMenu);
aboutMenu.setText("About");
aboutMenu.addActionListener(this::aboutMenuActionPerformed); aboutMenu.addActionListener(this::aboutMenuActionPerformed);
helpMenu.add(aboutMenu); helpMenu.add(aboutMenu);
this.add(helpMenu); this.add(helpMenu);
} }
private void exitMenuActionPerformed(ActionEvent evt) { private void exitMenuActionPerformed(java.awt.event.ActionEvent evt) {
mainWindow.exit(); mainWindow.exit();
} }
private void setupMenuActionPerformed(ActionEvent evt) {
private void setupMenuActionPerformed(java.awt.event.ActionEvent evt) {
Login login = new Login(); Login login = new Login();
login.setVisible(true); login.setVisible(true);
mainWindow.setUserName(login.getName()); mainWindow.setUserName(login.getName());
mainWindow.getConnectionManager().setPort(login.getPort()); mainWindow.setPort(login.getPort());
login.dispose(); login.dispose();
} }
private void aboutMenuActionPerformed(ActionEvent evt) { private void aboutMenuActionPerformed(java.awt.event.ActionEvent evt) {
About about = new About(); About about = new About();
about.setVisible(true); about.setVisible(true);
} }
public void addPluginMenu(MenuCategory category, JMenuItem menu){ /**
categories.get(category).add(menu); * 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 -62
View File
@@ -2,16 +2,14 @@ package pitiupi.GUI;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import javax.swing.BorderFactory;
import java.util.function.Consumer; import javax.swing.DefaultListModel;
import javax.swing.*; import javax.swing.JList;
import javax.swing.JPanel;
import pitiupi.models.Menus.MenuCategory; import javax.swing.JScrollPane;
import pitiupi.models.User; import javax.swing.SwingUtilities;
import pitiupi.control.PeerInfo;
import pitiupi.control.PeerListener; import pitiupi.control.PeerListener;
/** /**
@@ -22,16 +20,10 @@ import pitiupi.control.PeerListener;
*/ */
public class OnlineUsersPanel extends JPanel implements PeerListener { public class OnlineUsersPanel extends JPanel implements PeerListener {
private final DefaultListModel<User> listModel; private final DefaultListModel<String> listModel;
private final JList<User> userList; private final JList<String> userList;
JPopupMenu menu;
private Map<MenuCategory, JMenu> categories;
public OnlineUsersPanel() { public OnlineUsersPanel() {
menu = new JPopupMenu();
categories = new HashMap<>();
initMenu();
setLayout(new BorderLayout()); setLayout(new BorderLayout());
setBorder(BorderFactory.createTitledBorder("Online")); setBorder(BorderFactory.createTitledBorder("Online"));
setPreferredSize(new Dimension(160, 0)); setPreferredSize(new Dimension(160, 0));
@@ -39,48 +31,6 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
listModel = new DefaultListModel<>(); listModel = new DefaultListModel<>();
userList = new JList<>(listModel); userList = new JList<>(listModel);
add(new JScrollPane(userList), BorderLayout.CENTER); add(new JScrollPane(userList), BorderLayout.CENTER);
setupUserListActions();
}
private void initMenu() {
for (MenuCategory category : MenuCategory.values()) {
JMenu newCategory = new JMenu(category.toString());
menu.add(newCategory);
categories.put(category, newCategory);
}
}
private void setupUserListActions() {
userList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
int index = userList.locationToIndex(e.getPoint());
if (index == -1) {
return;
}
userList.setSelectedIndex(index);
menu.show(userList, e.getX(), e.getY());
}
});
}
public void initUnicastMenu(JMenuItem menuItem, Consumer<User> action) {
menuItem.addActionListener(e -> {
User user = userList.getSelectedValue();
if (user != null) {
action.accept(user);
}
});
}
public void addUnicastMenu(MenuCategory category, JMenuItem menuItem) {
categories.get(category).add(menuItem);
} }
/** /**
@@ -89,11 +39,11 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
* @param peers lista atualizada de usuários ativos na rede. * @param peers lista atualizada de usuários ativos na rede.
*/ */
@Override @Override
public void onPeersChanged(List<User> peers) { public void onPeersChanged(List<PeerInfo> peers) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
listModel.clear(); listModel.clear();
for (User peer : peers) { for (PeerInfo peer : peers) {
listModel.addElement(peer); listModel.addElement(peer.toString());
} }
}); });
} }
@@ -1,113 +0,0 @@
package pitiupi.control;
import pitiupi.GUI.MainWindow;
import pitiupi.control.plugins.PluginManager;
import pitiupi.models.Messages.HeartbeatMessage;
import pitiupi.models.Messages.Message;
import pitiupi.net.tcp.SocketTCP;
import pitiupi.net.tcp.TcpConnection;
import pitiupi.net.udp.SocketUDP;
import java.net.InetAddress;
public class ConnectionManager {
private final String pitiupiIdentifier;
private final PluginManager pluginManager;
private final HeartbeatManager heartbeatManager;
private SocketTCP socketTCP;
private SocketUDP socketUDP;
private String username;
private int port;
public ConnectionManager(MainWindow main, String username, PluginManager pluginManager, int port) {
this.pluginManager = pluginManager;
this.username = username;
this.port = port;
pitiupiIdentifier = "Pitiupi.Alice.2026";
reconnect();
heartbeatManager = new HeartbeatManager(this);
heartbeatManager.start();
}
public TcpConnection connectPluginTcp(InetAddress destinationAdress, String senderId, String receiverId) {
return socketTCP.createPrivateConnection(destinationAdress, senderId, receiverId);
}
public boolean sendTcp(Message message, InetAddress destinationAddress) {
return socketTCP.send(message, destinationAddress);
}
public boolean sendUdpUnicast(Message message, InetAddress destinationAddress) {
try {
socketUDP.sendUnicast(message, destinationAddress);
return true;
} catch (Exception ex) {
return false;
}
}
public boolean sendUdpMulticast(Message message) {
try {
socketUDP.sendMulticast(message);
return true;
} catch (Exception e) {
System.err.println("Error sending UDP Multicast");
}
return false;
}
public void broadcastLocal(byte[] message) {
pluginManager.handleMessage(message);
}
public void addPeerListener(PeerListener peerListener) {
heartbeatManager.addPeerListener(peerListener);
}
public void receiveHeartbeat(HeartbeatMessage message, InetAddress destinationAddress) {
heartbeatManager.receiveHeartbeat(message, destinationAddress);
}
public void setPort(int port) {
this.port = port;
reconnect();
}
public int getPort() {
return port;
}
public String getIdentifier(){
return this.pitiupiIdentifier;
}
private void reconnect() {
if (this.socketUDP != null) {
this.socketUDP.close();
}
if (this.socketTCP != null) {
this.socketTCP.close();
}
this.socketUDP = new SocketUDP(this, pluginManager);
this.socketTCP = new SocketTCP(this, pluginManager);
this.socketUDP.start();
this.socketTCP.start();
}
public String getUserName() {
return this.username;
}
public void setUserName(String userName) {
this.username = userName;
}
}
@@ -1,82 +0,0 @@
package pitiupi.control;
import pitiupi.GUI.MainWindow;
import pitiupi.GUI.MenuBar;
import pitiupi.GUI.OnlineUsersPanel;
import pitiupi.models.Menus.*;
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
import pitiupi.models.Menus.UnicastMenus.UnicastActionMenu;
import pitiupi.models.Menus.UnicastMenus.UnicastMenu;
import pitiupi.models.Menus.UnicastMenus.UnicastSubMenu;
import javax.swing.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class GuiManager {
private MainWindow mainWindow;
private MenuBar menuBar;
private OnlineUsersPanel onlineUsersPanel;
private Logger logger;
public GuiManager(MainWindow mainWindow, MenuBar menuBar, OnlineUsersPanel onlineUsersPanel) {
this.mainWindow = mainWindow;
this.menuBar = menuBar;
this.onlineUsersPanel = onlineUsersPanel;
this.logger = Logger.getLogger(GuiManager.class.getName());
}
public void createMenu(MenuCategory category, Menu menu) {
try {
JMenuItem newMenu = buildMenu(menu);
if (menu instanceof MulticastMenu) {
menuBar.addPluginMenu(category, newMenu);
}
if (menu instanceof UnicastMenu) {
onlineUsersPanel.addUnicastMenu(category, newMenu);
}
}
catch (IllegalArgumentException e) {
logger.log(Level.WARNING, e.getMessage());
}
}
private JMenuItem buildMenu(Menu menu) {
if (menu instanceof MulticastSubMenu multicastSubMenu) {
JMenu newMenu = new JMenu(menu.getText());
for (Menu child : multicastSubMenu.getMenus()) {
newMenu.add(buildMenu(child));
}
return newMenu;
}
if (menu instanceof UnicastSubMenu unicastSubMenu) {
JMenu newMenu = new JMenu(menu.getText());
for (Menu child : unicastSubMenu.getMenus()) {
newMenu.add(buildMenu(child));
}
return newMenu;
}
if (menu instanceof MulticastActionMenu multicastActionMenu) {
JMenuItem newMenu = new JMenuItem(menu.getText());
newMenu.addActionListener(e -> multicastActionMenu.onClick());
return newMenu;
}
if (menu instanceof UnicastActionMenu unicastActionMenu) {
JMenuItem newMenu = new JMenuItem(menu.getText());
onlineUsersPanel.initUnicastMenu(newMenu, unicastActionMenu::onClick);
return newMenu;
}
throw new IllegalArgumentException("Invalid menu type");
}
}
@@ -1,5 +1,6 @@
package pitiupi.control; package pitiupi.control;
import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -9,9 +10,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import pitiupi.models.User; import java.util.logging.Logger;
import pitiupi.models.Messages.HeartbeatMessage; import pitiupi.GUI.MainWindow;
import pitiupi.net.HeartbeatMessage;
import java.util.UUID; import java.util.UUID;
/** /**
* Gerencia o mecanismo de heartbeat da aplicação. * Gerencia o mecanismo de heartbeat da aplicação.
@@ -26,8 +28,9 @@ public class HeartbeatManager {
private static final long HEARTBEAT_INTERVAL_MS = 5000; private static final long HEARTBEAT_INTERVAL_MS = 5000;
private static final long PEER_TIMEOUT_MS = 15000; // ~3 batimentos perdidos private static final long PEER_TIMEOUT_MS = 15000; // ~3 batimentos perdidos
private final ConnectionManager connectionManager;
private final Map<String, User> peers = new ConcurrentHashMap<>(); private final MainWindow mainWindow;
private final Map<String, PeerInfo> peers = new ConcurrentHashMap<>();
private final List<PeerListener> listeners = new CopyOnWriteArrayList<>(); private final List<PeerListener> listeners = new CopyOnWriteArrayList<>();
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
private final String instanceId = UUID.randomUUID().toString(); private final String instanceId = UUID.randomUUID().toString();
@@ -35,9 +38,11 @@ public class HeartbeatManager {
/** /**
* Cria um gerenciador de heartbeat associado à janela principal da aplicação. * 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(ConnectionManager connectionManager) { public HeartbeatManager(MainWindow mainWindow) {
this.connectionManager = connectionManager; this.mainWindow = mainWindow;
} }
/** /**
@@ -59,8 +64,12 @@ public class HeartbeatManager {
} }
private void announce() { private void announce() {
HeartbeatMessage msg = new HeartbeatMessage(connectionManager.getUserName(), instanceId); try {
connectionManager.sendUdpMulticast(msg); 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);
}
} }
// Chamado pelo Socket quando um pacote é identificado como heartbeat // Chamado pelo Socket quando um pacote é identificado como heartbeat
@@ -85,7 +94,7 @@ public class HeartbeatManager {
String key = msg.getInstanceId(); String key = msg.getInstanceId();
boolean isNew = !peers.containsKey(key); boolean isNew = !peers.containsKey(key);
User peer = peers.computeIfAbsent(key, k -> new User(msg.getUserName(), from, msg.getTimestamp())); PeerInfo peer = peers.computeIfAbsent(key, k -> new PeerInfo(msg.getUserName(), from, msg.getTimestamp()));
peer.touch(System.currentTimeMillis()); peer.touch(System.currentTimeMillis());
if (isNew) { if (isNew) {
@@ -102,7 +111,7 @@ public class HeartbeatManager {
} }
private void notifyListeners() { private void notifyListeners() {
List<User> snapshot = new ArrayList<>(peers.values()); List<PeerInfo> snapshot = new ArrayList<>(peers.values());
for (PeerListener l : listeners) { for (PeerListener l : listeners) {
l.onPeersChanged(snapshot); l.onPeersChanged(snapshot);
} }
@@ -135,7 +144,7 @@ public class HeartbeatManager {
* *
* @return lista contendo os peers atualmente conhecidos. * @return lista contendo os peers atualmente conhecidos.
*/ */
public List<User> getPeers() { public List<PeerInfo> getPeers() {
return new ArrayList<>(peers.values()); return new ArrayList<>(peers.values());
} }
} }
@@ -1,4 +1,4 @@
package pitiupi.models; package pitiupi.control;
import java.net.InetAddress; import java.net.InetAddress;
/** /**
@@ -10,13 +10,13 @@ import java.net.InetAddress;
* *
* @author tony * @author tony
*/ */
public class User { public class PeerInfo {
private final String userName; private final String userName;
private final InetAddress address; private final InetAddress address;
private volatile long lastSeen; private volatile long lastSeen;
public User(String userName, InetAddress address, long lastSeen) { public PeerInfo(String userName, InetAddress address, long lastSeen) {
this.userName = userName; this.userName = userName;
this.address = address; this.address = address;
this.lastSeen = lastSeen; this.lastSeen = lastSeen;
@@ -1,7 +1,5 @@
package pitiupi.control; package pitiupi.control;
import pitiupi.models.User;
import java.util.List; import java.util.List;
/** /**
@@ -19,5 +17,5 @@ public interface PeerListener {
* *
* @param peers lista atualizada de usuários ativos. * @param peers lista atualizada de usuários ativos.
*/ */
void onPeersChanged(List<User> peers); void onPeersChanged(List<PeerInfo> peers);
} }
@@ -0,0 +1,74 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package pitiupi.control;
import java.awt.List;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.ServiceLoader;
import java.util.Vector;
import pitiupi.GUI.MainWindow;
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 {
private final pitiupi.GUI.MainWindow mainWindow;
public PluginLoader(MainWindow main) {
this.mainWindow = main;
}
public void loadPlugins() {
File pluginDir = new File("plugins");
File[] jarFiles = pluginDir.listFiles((dir, name) -> name.endsWith(".jar"));
if (jarFiles == null || jarFiles.length == 0) {
System.out.println("No plugins found in the " + pluginDir.getAbsolutePath() + " directory.");
return;
}
try {
// 2. Convert jar file paths into URLs
ArrayList urls = new ArrayList();
for (File file : jarFiles) {
urls.add(file.toURI().toURL());
System.out.println("Discovered plugin package: " + file.getName());
}
// 3. Create a class loader isolated from the application's default loader
URLClassLoader pluginClassLoader = new URLClassLoader(
(URL[])urls.toArray(new URL[0]),
Main.class.getClassLoader()
);
System.out.println(pluginClassLoader);
// 4. Locate and instantiate all classes implementing the Plugin interface
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, pluginClassLoader);
System.out.println("\n--- Executing Discovered Plugins ---" + serviceLoader.findFirst());
for (Plugin plugin : serviceLoader) {
System.out.println("Loading: " + plugin.getName());
plugin.createPlugin(this.mainWindow);
this.mainWindow.getPlugins().add(plugin);
}
} catch (MalformedURLException e) {
System.out.println(e.toString());
}
}
}
@@ -1,22 +0,0 @@
package pitiupi.control.plugins;
import pitiupi.models.Plugin;
class LoadedPlugin {
private final Plugin plugin;
private final ClassLoader classLoader;
public LoadedPlugin(Plugin plugin, ClassLoader classLoader) {
this.plugin = plugin;
this.classLoader = classLoader;
}
public Plugin getPlugin() {
return plugin;
}
public ClassLoader getClassLoader() {
return classLoader;
}
}
@@ -1,156 +0,0 @@
package pitiupi.control.plugins;
import pitiupi.plugin.Pitiupi;
import pitiupi.Main;
import pitiupi.control.ConnectionManager;
import pitiupi.control.GuiManager;
import pitiupi.models.Messages.Message;
import pitiupi.net.tcp.TcpConnection;
import pitiupi.models.Plugin;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PluginManager {
private final List<LoadedPlugin> loadedPlugins;
private final HashSet<String> loadedJars;
private final Logger logger;
public PluginManager() {
this.loadedPlugins = new ArrayList<>();
this.loadedJars = new HashSet<>();
logger = Logger.getLogger(getClass().getName());
}
public void handleMessage(byte[] msg) {
Message message = null;
for (LoadedPlugin loadedPlugin : loadedPlugins) {
message = deserialize(msg, loadedPlugin.getClassLoader());
if (message != null) break;
}
if (message == null) return;
for (LoadedPlugin loadedPlugin : loadedPlugins) {
Plugin plugin = loadedPlugin.getPlugin();
if (plugin.receiveAllTypes() && message.isPublic()) {
plugin.receiveMessage(message);
continue;
}
for (Class<? extends Message> messageType : plugin.getMessages()) {
if (messageType.isInstance(message)) {
plugin.receiveMessage(message);
break;
}
}
}
}
private Plugin getPlugin(String pluginId) {
for (LoadedPlugin loadedPlugin : loadedPlugins) {
if (loadedPlugin.getPlugin().getIdentifier().equals(pluginId)) {
return loadedPlugin.getPlugin();
}
}
return null;
}
public void handleMessageFromConnection(TcpConnection connection, byte[] msg) {
Plugin plugin = getPlugin(connection.getReceiver());
if (plugin == null) return;
plugin.receiveBytes(connection, msg);
}
public boolean handleConnection(TcpConnection connection, String receiverId) {
Plugin plugin = getPlugin(receiverId);
if (plugin == null) return false;
plugin.receivePrivateConnection(connection);
return true;
}
public void handleDisconnection(TcpConnection connection, String receiverId) {
Plugin plugin = getPlugin(receiverId);
if (plugin == null) return;
plugin.onSocketClosed(connection);
}
public void loadPlugins(ConnectionManager connectionManager, GuiManager guiManager) {
File pluginDir = new File("plugins");
File[] jarFiles = pluginDir.listFiles((dir, name) -> name.endsWith(".jar"));
Pitiupi pitiupi = new Pitiupi(connectionManager, guiManager);
if (jarFiles == null || jarFiles.length == 0) {
System.out.println("No plugins found in the " + pluginDir.getAbsolutePath() + " directory.");
return;
}
for (File file : jarFiles) {
if (loadedJars.contains(file.getName())) {
continue;
}
System.out.println("Discovered plugin package: " + file.getName());
try {
URLClassLoader classLoader = new URLClassLoader(
new URL[]{file.toURI().toURL()},
Main.class.getClassLoader()
);
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, classLoader);
for (Plugin plugin : serviceLoader) {
LoadedPlugin loadedPlugin = new LoadedPlugin(plugin, classLoader);
plugin.createPlugin(pitiupi);
loadedPlugins.add(loadedPlugin);
loadedJars.add(file.getName());
}
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to load plugin " + file.getName(), e);
}
}
}
private Message deserialize(byte[] data, ClassLoader classLoader) {
try {
ByteArrayInputStream input = new ByteArrayInputStream(data);
PluginObjectInputStream objectInput = new PluginObjectInputStream(input, classLoader);
Object object = objectInput.readObject();
if (!(object instanceof Message)) {
return null;
}
return (Message) object;
}
catch (ClassNotFoundException e) {
return null;
}
catch (IOException e) {
logger.log(Level.SEVERE, "Failed to load plugin package", e);
return null;
}
}
}
@@ -1,20 +0,0 @@
package pitiupi.control.plugins;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
public class PluginObjectInputStream extends ObjectInputStream {
private final ClassLoader classLoader;
public PluginObjectInputStream(InputStream input, ClassLoader classLoader) throws IOException {
super(input);
this.classLoader = classLoader;
}
@Override
protected Class<?> resolveClass(ObjectStreamClass description) throws ClassNotFoundException {
return Class.forName(description.getName(), false, classLoader);
}
}
@@ -1,5 +0,0 @@
package pitiupi.models.Menus;
public abstract class Menu {
public abstract String getText();
}
@@ -1,7 +0,0 @@
package pitiupi.models.Menus;
public enum MenuCategory {
Jogos,
Social,
Streaming
}
@@ -1,5 +0,0 @@
package pitiupi.models.Menus.MulticastMenus;
public abstract class MulticastActionMenu extends MulticastMenu{
public abstract void onClick();
}
@@ -1,9 +0,0 @@
package pitiupi.models.Menus.MulticastMenus;
import pitiupi.models.Menus.Menu;
public abstract class MulticastMenu extends Menu {
MulticastMenu() {
super();
}
}
@@ -1,7 +0,0 @@
package pitiupi.models.Menus.MulticastMenus;
import java.util.List;
public abstract class MulticastSubMenu extends MulticastMenu{
public abstract List<MulticastMenu> getMenus();
}
@@ -1,7 +0,0 @@
package pitiupi.models.Menus.UnicastMenus;
import pitiupi.models.User;
public abstract class UnicastActionMenu extends UnicastMenu{
public abstract void onClick(User user);
}
@@ -1,8 +0,0 @@
package pitiupi.models.Menus.UnicastMenus;
import pitiupi.models.Menus.Menu;
import pitiupi.models.User;
public abstract class UnicastMenu extends Menu {
UnicastMenu() {}
}
@@ -1,7 +0,0 @@
package pitiupi.models.Menus.UnicastMenus;
import java.util.List;
public abstract class UnicastSubMenu extends UnicastMenu{
public abstract List<UnicastMenu> getMenus();
}
@@ -1,50 +0,0 @@
package pitiupi.models.Messages;
import pitiupi.net.tcp.TcpConnection;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
public class InfoMessage extends Message {
private final InfoType type;
private final String message;
public InfoMessage(String message, InfoType type) {
this.message = message;
this.type = type;
}
public String getMessage() {
return message;
}
public InfoType getType() {
return type;
}
public static void sendSystemInfo(TcpConnection connection, InfoType type, String description) {
try {
InfoMessage error = new InfoMessage(description, type);
connection.send(error.toByteArray());
} catch (IOException ex) {
Logger.getLogger(InfoMessage.class.getName()).log(Level.WARNING, null, ex);
}
}
public static InfoMessage parseSystemInfo(byte[] bytes) {
try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis)) {
Object object = ois.readObject();
if (object instanceof InfoMessage) {
return (InfoMessage) object;
}
} catch (IOException | ClassNotFoundException ex) {
Logger.getLogger(InfoMessage.class.getName()).log(Level.WARNING, null, ex);
}
return null;
}
}
@@ -1,9 +0,0 @@
package pitiupi.models.Messages;
public enum InfoType {
CONNECTION_REJECTED,
RECEIVER_NOT_FOUND,
MESSAGE_REJECTED,
INTERNAL_ERROR,
CONNECTION_CLOSED,
}
@@ -1,5 +1,4 @@
package pitiupi.models.Messages; package pitiupi.net;
/** /**
* Mensagem utilizada para sinalizar a presença de um usuário na rede. * Mensagem utilizada para sinalizar a presença de um usuário na rede.
* *
@@ -3,7 +3,7 @@
* To change this template file, choose Tools | Templates * To change this template file, choose Tools | Templates
* and open the template in the editor. * and open the template in the editor.
*/ */
package pitiupi.models.Messages; package pitiupi.net;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -24,19 +24,6 @@ import java.io.Serializable;
* @author flavio * @author flavio
*/ */
public abstract class Message implements Serializable{ public abstract class Message implements Serializable{
private boolean isPublic;
protected Message(){
isPublic = true;
}
protected Message(boolean isPublic){
this.isPublic = isPublic;
}
public boolean isPublic(){
return isPublic;
}
/** /**
* Serializa a mensagem para um vetor de bytes. * Serializa a mensagem para um vetor de bytes.
+152
View File
@@ -0,0 +1,152 @@
/*
* 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.net;
import pitiupi.GUI.MainWindow;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.util.logging.Level;
import java.util.logging.Logger;
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
*/
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 {
this.address = InetAddress.getByName(Socket.INET_ADDR);
} catch (UnknownHostException ex) {
System.getLogger(Socket.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
try {
multicastSocket = new MulticastSocket(this.main.getPort());
multicastSocket.setSendBufferSize(256000);
multicastSocket.setReceiveBufferSize(256000);
multicastSocket.setReuseAddress(true);
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());
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 (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());
}
}
}
/**
* 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);
ObjectInput in = new ObjectInputStream(bais);
Object obj = in.readObject();
return (obj instanceof HeartbeatMessage) ? (HeartbeatMessage) obj : null;
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
return null;
}
}
@Override
public void run() {
try {
this.receive();
} catch (IOException | ClassNotFoundException ex) {
Logger.getLogger(Socket.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@@ -1,229 +0,0 @@
package pitiupi.net.tcp;
import pitiupi.models.Messages.Message;
import pitiupi.models.Messages.InfoMessage;
import pitiupi.models.Messages.InfoType;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Fábrica responsável pela criação e configuração de conexões TCP.
*
* <p>Esta classe centraliza o estabelecimento de conexões de saída e a
* configuração de conexões recebidas. Antes de uma conexão ser considerada
* válida, é realizado um handshake entre os peers para identificar o remetente,
* o destinatário e se a conexão é privada.</p>
*
* <p>Durante o handshake é utilizado um timeout menor, definido por
* {@link #HANDSHAKE_TIMEOUT}. Após a validação bem-sucedida, a conexão passa
* a utilizar o timeout padrão definido por {@link #STANDARD_TIMEOUT}.</p>
*/
public class ConnectionFactory {
private static final int HANDSHAKE_TIMEOUT = 1000;
private static final int STANDARD_TIMEOUT = 300_000;
/**
* Cria uma conexão TCP de saída com o peer de destino e realiza o handshake
* necessário para validá-la.
*
* <p>A conexão é configurada com os identificadores do remetente e do
* destinatário e com o tipo indicado pelo parâmetro {@code isPrivate}.
* Após o estabelecimento do socket, o método envia um
* {@link HandshakeMessage} e aguarda uma resposta válida.</p>
*
* <p>Se o handshake for validado com sucesso, o timeout da conexão é
* alterado de {@link #HANDSHAKE_TIMEOUT} para {@link #STANDARD_TIMEOUT}
* e a conexão é retornada. Caso contrário, {@code null} é retornado.</p>
*
* @param address endereço IP do peer de destino.
* @param port porta TCP na qual o peer de destino está aguardando conexões.
* @param sender identificador do remetente da conexão.
* @param receiver identificador do destinatário da conexão.
* @param isPrivate {@code true} se a conexão deve ser privada;
* {@code false} se deve ser pública.
* @return a conexão TCP estabelecida e validada ou {@code null} caso
* não seja possível estabelecer ou validar a conexão.
*/
public static TcpConnection createConnection(InetAddress address, int port, String sender, String receiver, boolean isPrivate) {
try {
TcpConnection connection;
Socket socket = new Socket(address, port);
connection = new TcpConnection(socket);
connection.setSender(sender);
connection.setReceiver(receiver);
connection.setPrivate(isPrivate);
HandshakeMessage handshake = new HandshakeMessage(isPrivate, sender, receiver);
connection.send(handshake.toByteArray());
HandshakeMessage handshakeResponse = validateConnection(connection);
if (handshakeResponse == null) {
return null;
}
connection.setTimeout(STANDARD_TIMEOUT);
return connection;
}
catch (IOException ex) {
Logger.getLogger(ConnectionFactory.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* Configura uma conexão TCP recebida e valida o handshake enviado pelo
* peer remoto.
*
* <p>O handshake recebido é utilizado para obter e configurar os
* identificadores do remetente e do destinatário, bem como o tipo da
* conexão. Após a validação bem-sucedida, o timeout da conexão é alterado
* para {@link #STANDARD_TIMEOUT}.</p>
*
* <p>O {@link Socket} recebido deve representar uma conexão TCP já aceita
* pelo servidor.</p>
*
* @param socket socket TCP correspondente à conexão recebida.
* @return a conexão TCP configurada e validada ou {@code null} caso
* o handshake seja inválido ou não possa ser processado.
*/
public static TcpConnection receiveConnection(Socket socket) {
try {
TcpConnection connection;
connection = new TcpConnection(socket);
HandshakeMessage handshake = validateConnection(connection);
if (handshake == null) return null;
connection.send(handshake.createResponse().toByteArray());
connection.setSender(handshake.getSender());
connection.setReceiver(handshake.getReceiver());
connection.setPrivate(handshake.isPrivate());
connection.setTimeout(STANDARD_TIMEOUT);
return connection;
}
catch (IOException ex) {
Logger.getLogger(ConnectionFactory.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* Aguarda e valida a mensagem de handshake de uma conexão TCP.
*
* <p>Durante a validação, a conexão utiliza {@link #HANDSHAKE_TIMEOUT}
* como limite para o recebimento do handshake. A mensagem recebida deve
* ser uma instância de {@link Message} e, especificamente, de
* {@link HandshakeMessage}. Mensagens de sistema ou mensagens de outros
* tipos são rejeitadas.</p>
*
* <p>Quando o handshake é recebido, seus campos de remetente e destinatário
* são validados por {@link #validateHandshake(HandshakeMessage, TcpConnection)}.
* Caso a validação seja bem-sucedida, o próprio handshake é retornado.</p>
*
* <p>Em caso de timeout, mensagem inválida ou erro durante o processamento,
* uma mensagem de sistema apropriada pode ser enviada ao peer e
* {@code null} é retornado.</p>
*
* @param connection conexão TCP da qual o handshake será recebido.
* @return o {@link HandshakeMessage} validado ou {@code null} caso o
* handshake não seja recebido ou seja inválido.
*/
private static HandshakeMessage validateConnection(TcpConnection connection) {
try {
connection.setTimeout(HANDSHAKE_TIMEOUT);
byte[] bytes = connection.receive();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
Object object = ois.readObject();
if (object instanceof Message message) {
InfoMessage sysInfo = InfoMessage.parseSystemInfo(bytes);
if (sysInfo != null) return null;
if (message instanceof HandshakeMessage handshake) {
return (validateHandshake(handshake, connection)) ? handshake : null;
}
}
InfoMessage.sendSystemInfo(
connection,
InfoType.CONNECTION_REJECTED,
"The message was not from an allowed type."
);
}
catch (SocketTimeoutException ex) {
InfoMessage.sendSystemInfo(
connection,
InfoType.CONNECTION_REJECTED,
"No handshake was received within the allowed time."
);
}
catch (ClassNotFoundException | IOException ex) {
Logger.getLogger(ConnectionFactory.class.getName()).log(Level.SEVERE, null, ex);
InfoMessage.sendSystemInfo(
connection,
InfoType.INTERNAL_ERROR,
"The server could not process the handshake."
);
}
return null;
}
/**
* Valida os identificadores presentes em uma mensagem de handshake.
*
* <p>O remetente e o destinatário devem estar presentes e não podem ser
* vazios ou conter apenas espaços em branco. Quando um dos identificadores
* é inválido, uma mensagem de rejeição é enviada ao peer e a validação
* falha.</p>
*
* @param handshake mensagem de handshake a ser validada.
* @param connection conexão associada ao handshake, utilizada para enviar
* uma mensagem de rejeição quando necessário.
* @return {@code true} se os identificadores do remetente e do destinatário
* forem válidos; {@code false} caso contrário.
*/
private static boolean validateHandshake(HandshakeMessage handshake, TcpConnection connection) {
String sender = handshake.getSender();
String receiver = handshake.getReceiver();
if (sender == null || sender.isBlank()) {
InfoMessage.sendSystemInfo(
connection,
InfoType.CONNECTION_REJECTED,
"Invalid Sender: " + sender + "."
);
return false;
}
if (receiver == null || receiver.isBlank()) {
InfoMessage.sendSystemInfo(
connection,
InfoType.CONNECTION_REJECTED,
"Invalid Receiver: " + receiver + "."
);
return false;
}
return true;
}
}
@@ -1,30 +0,0 @@
package pitiupi.net.tcp;
import pitiupi.models.Messages.Message;
class HandshakeMessage extends Message {
private final boolean isPrivate;
private final String sender;
private final String receiver;
public HandshakeMessage(boolean isPrivate, String sender, String receiver) {
this.isPrivate = isPrivate;
this.sender = sender;
this.receiver = receiver;
}
public boolean isPrivate() {
return isPrivate;
}
public String getSender() {
return sender;
}
public String getReceiver() {
return receiver;
}
public HandshakeMessage createResponse() {
return new HandshakeMessage(isPrivate, receiver, sender);
}
}
-457
View File
@@ -1,457 +0,0 @@
package pitiupi.net.tcp;
import pitiupi.control.ConnectionManager;
import pitiupi.control.plugins.PluginManager;
import pitiupi.models.Messages.Message;
import pitiupi.models.Messages.InfoMessage;
import pitiupi.models.Messages.InfoType;
import java.io.*;
import java.net.*;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Gerencia a comunicação TCP da aplicação.
*
* <p>Mantém conexões persistentes com os peers e reutiliza essas conexões
* para o envio de mensagens. As mensagens recebidas são encaminhadas aos
* plugins, que são responsáveis por interpretá-las.</p>
*
* <p>Novas conexões são aceitas pelo servidor e cada conexão possui uma
* tarefa própria para receber mensagens, permitindo que múltiplas conexões
* sejam mantidas simultaneamente.</p>
*
* <p>As conexões são associadas aos endereços dos peers e podem ser
* encerradas quando um peer deixa de estar ativo.</p>
*
* @author Gustavo
*/
public class SocketTCP extends Thread {
private final Logger logger;
private final ExecutorService connectionExecutor = Executors.newCachedThreadPool();
private final ConnectionManager connectionManager;
private final PluginManager pluginManager;
private final TcpConnectionManager tcpConnectionManager;
private ServerSocket serverSocket;
private InetAddress address;
private volatile boolean running = true;
/**
* Cria o servidor TCP da aplicação.
*
* <p>O servidor é criado na porta definida pela aplicação. As conexões
* com os peers são estabelecidas sob demanda durante o envio ou aceitas
* quando iniciadas por outros peers.</p>
*
* @param connectionManager interface de conexão da aplicação, usada para obter a porta,
* o identificador local e os plugins disponíveis.
*/
public SocketTCP(ConnectionManager connectionManager, PluginManager pluginManager) {
this.connectionManager = connectionManager;
this.pluginManager = pluginManager;
logger = Logger.getLogger(getClass().getName());
tcpConnectionManager = new TcpConnectionManager();
try {
serverSocket = new ServerSocket(connectionManager.getPort());
address = InetAddress.getByName(InetAddress.getLocalHost().getHostAddress());
} catch (IOException ex) {
System.out.println("There is no socket connection. Sorry.");
System.out.println(ex.toString());
}
}
/**
* Envia uma mensagem por meio de uma conexão TCP pública com o peer de destino.
*
* <p>A conexão pública existente para o endereço informado é reutilizada.
* Caso não exista uma conexão válida, uma nova conexão é estabelecida antes
* do envio. Falhas durante o estabelecimento ou envio são registradas na
* saída padrão e não são propagadas ao chamador.</p>
*
* @param msg mensagem que será enviada.
* @param destinationAddress endereço IP do peer que receberá a mensagem.
*/
public boolean send(Message msg, InetAddress destinationAddress) {
try {
TcpConnection connection = getPublicConnection(
destinationAddress,
connectionManager.getIdentifier(),
connectionManager.getIdentifier()
);
if (connection == null) {
System.out.println("Could not establish TCP connection.");
return false;
}
connection.send(msg.toByteArray());
} catch (Exception ex) {
System.out.println("Could not send TCP message.");
System.out.println(ex.toString());
return false;
}
return true;
}
/**
* Estabelece uma conexão TCP privada com um plugin específico do peer de destino.
*
* <p>A conexão é criada como privada e passa a ser gerenciada pelo
* {@link TcpConnectionManager}. O método também inicia a tarefa responsável
* por receber os dados dessa conexão.</p>
*
* @param destinationAddress endereço IP do peer de destino.
* @param senderId identificador do remetente da conexão.
* @param receiverId identificador do plugin que receberá os dados.
* @return a conexão privada estabelecida ou {@code null} quando a criação
* da conexão falhar.
* @throws IllegalArgumentException se o endereço de destino for o endereço
* local desta instância.
*/
public TcpConnection createPrivateConnection(InetAddress destinationAddress, String senderId, String receiverId) {
return establishOutgoingConnection(destinationAddress, senderId, receiverId, true);
}
/**
* Obtém uma conexão TCP pública existente com o peer de destino ou estabelece
* uma nova caso não exista uma conexão válida.
*
* <p>As conexões públicas são gerenciadas pelo {@link TcpConnectionManager}.
* O método primeiro verifica se já existe uma conexão pública registrada
* para o endereço de destino e, caso ela exista e não esteja fechada,
* reutiliza-a. Caso contrário, uma nova conexão pública é estabelecida,
* registrada no {@link TcpConnectionManager} e disponibilizada para uso.</p>
*
* <p>O acesso ao método é sincronizado para evitar que múltiplas threads
* estabeleçam simultaneamente conexões públicas para o mesmo peer.</p>
*
* @param destinationAddress endereço IP do peer de destino.
* @param senderId identificador usado como remetente no estabelecimento da conexão.
* @param receiverId identificador usado como destinatário no estabelecimento da conexão.
* @return uma conexão pública válida ou {@code null} caso não seja possível
* estabelecê-la.
*/
private synchronized TcpConnection getPublicConnection(InetAddress destinationAddress, String senderId, String receiverId) {
TcpConnection connection;
connection = tcpConnectionManager.getPublicConnection(destinationAddress);
if (connection != null && !connection.isClosed()) return connection;
connection = establishOutgoingConnection(destinationAddress, senderId, receiverId, false);
return connection;
}
/**
* Aguarda e aceita novas conexões TCP.
*
* <p>Cada socket aceito é encaminhado para processamento assíncrono,
* interpretado como uma {@link TcpConnection}, registrado pelo
* {@link TcpConnectionManager} e associado ao receptor adequado.</p>
*/
private void receive() {
while (running) {
try {
Socket socket = serverSocket.accept();
acceptIncomingConnection(socket);
} catch (IOException ex) {
if (!running) {
break;
}
System.out.println("Error accepting TCP connection.");
System.out.println(ex.toString());
}
}
}
/**
* Estabelece uma conexão TCP de saída com outro peer.
*
* <p>O método impede conexões com o próprio endereço local, cria a conexão
* por meio da {@link ConnectionFactory}, registra-a no gerenciador de
* conexões e inicia uma tarefa para receber dados continuamente.</p>
*
* @param destinationAddress endereço IP do peer de destino.
* @param sender identificador do remetente.
* @param receiver identificador do destinatário.
* @param isPrivate {@code true} para uma conexão destinada a um plugin
* específico; {@code false} para uma conexão pública.
* @return a conexão estabelecida ou {@code null} se a
* {@link ConnectionFactory} não conseguir criá-la.
* @throws IllegalArgumentException se o destino for o endereço desta máquina.
*/
private TcpConnection establishOutgoingConnection(InetAddress destinationAddress, String sender, String receiver, boolean isPrivate) {
if (address.equals(destinationAddress)) {
System.out.println("Failed to get socket.");
throw new IllegalArgumentException("Connections to the local host are forbidden.");
}
TcpConnection connection = ConnectionFactory.createConnection(destinationAddress, connectionManager.getPort(), sender, receiver, isPrivate);
if (connection == null) return null;
saveConnection(connection);
connectionExecutor.submit(() -> {
startReceiver(connection);
});
return connection;
}
/**
* Agenda o processamento de uma conexão TCP recebida.
*
* <p>A conexão é interpretada pela {@link ConnectionFactory}, registrada
* no gerenciador e associada a uma tarefa de recepção. O processamento é
* executado de forma assíncrona pelo executor compartilhado.</p>
*
* @param socket socket aceito pelo {@link ServerSocket}.
*/
private void acceptIncomingConnection(Socket socket) {
connectionExecutor.submit(() -> {
TcpConnection connection = ConnectionFactory.receiveConnection(socket);
if (connection == null) return;
saveConnection(connection);
startReceiver(connection);
});
}
/**
* Registra uma conexão no gerenciador de acordo com seu tipo.
*
* <p>Conexões privadas são armazenadas na coleção de conexões privadas;
* as demais são armazenadas na coleção de conexões públicas.</p>
*
* @param connection conexão que será registrada.
*/
private void saveConnection(TcpConnection connection) {
if (connection.isPrivate()) {
tcpConnectionManager.addPrivateConnection(connection);
}
else {
tcpConnectionManager.addPublicConnection(connection);
}
}
/**
* Seleciona e inicia o receptor adequado para uma conexão.
*
* <p>Conexões públicas são tratadas pelo receptor de mensagens públicas.
* Conexões privadas destinadas ao próprio sistema são tratadas
* separadamente; as demais são encaminhadas ao plugin identificado como
* destinatário. Quando solicitado, o plugin também é notificado sobre o
* estabelecimento da conexão.</p>
*
* @param connection conexão cujo fluxo de entrada será processado.
*/
private void startReceiver(TcpConnection connection) {
if (!connection.isPrivate()) {
publicConnectionReceiver(connection);
return;
}
if (connection.getReceiver().equals(connectionManager.getIdentifier())) {
systemConnectionReceiver(connection);
return;
}
if (pluginManager.handleConnection(connection, connection.getReceiver())) {
privateConnectionReceiver(connection);
}
else {
InfoMessage.sendSystemInfo(
connection,
InfoType.RECEIVER_NOT_FOUND,
"This peer does not have the plugin: " + connection.getReceiver()
);
}
}
/**
* Processa continuamente as mensagens recebidas em uma conexão pública.
*
* <p>As mensagens são desserializadas e, quando são instâncias de
* {@link Message}, encaminhadas a todos os plugins da aplicação. Mensagens
* de controle do sistema podem encerrar a conexão ou registrar uma
* condição de erro.</p>
*
* <p>O método também trata timeouts e erros de I/O, removendo a conexão
* do {@link TcpConnectionManager} quando ela deixa de ser utilizável.</p>
*
* @param connection conexão pública da qual as mensagens serão recebidas.
*/
private void publicConnectionReceiver(TcpConnection connection) {
while (running) {
try {
byte[] bytes = connection.receive();
InfoMessage sysMessage = InfoMessage.parseSystemInfo(bytes);
if (sysMessage == null) {
pluginManager.handleMessage(bytes);
continue;
}
switch (sysMessage.getType()) {
case CONNECTION_CLOSED, INTERNAL_ERROR -> {
tcpConnectionManager.closeConnection(connection);
return;
}
case MESSAGE_REJECTED -> {
logger.log(Level.SEVERE, "Somehow sent something that was not an Message");
}
default -> {}
}
}
catch (SocketTimeoutException ex) {
if (connection.hasTimedOut()) {
tcpConnectionManager.closeConnection(connection, "Connection timeout");
return;
}
}
catch (IOException ex) {
tcpConnectionManager.closeConnection(connection);
}
}
}
/**
* Processa os dados recebidos em uma conexão privada e os encaminha
* ao plugin destinatário.
*
* <p>Dados que não representam mensagens de controle são entregues
* diretamente ao {@link pitiupi.models.Plugin}. Mensagens de controle
* relacionadas ao encerramento ou a erros internos provocam a notificação
* do plugin e o fechamento da conexão.</p>
*
* @param connection conexão privada da qual os dados serão recebidos.
*/
private void privateConnectionReceiver(TcpConnection connection) {
while (running) {
try {
byte[] message = connection.receive();
InfoMessage sysMessage = InfoMessage.parseSystemInfo(message);
if (sysMessage == null) {
pluginManager.handleMessageFromConnection(connection, message);
continue;
}
switch (sysMessage.getType()) {
case CONNECTION_CLOSED, INTERNAL_ERROR -> {
pluginManager.handleDisconnection(connection, connection.getReceiver());
tcpConnectionManager.closeConnection(connection);
}
default -> {
continue;
}
}
}
catch (SocketTimeoutException ex) {
if (connection.hasTimedOut()) {
tcpConnectionManager.closeConnection(connection, "Connection timeout");
return;
}
}
catch (IOException ex) {
tcpConnectionManager.closeConnection(connection);
}
}
}
/**
* Processa mensagens recebidas em uma conexão destinada ao próprio sistema.
*
* <p>Atualmente, mensagens destinadas ao sistema são apenas verificadas
* quanto ao seu formato de controle. Não há processamento adicional de
* mensagens de sistema neste método.</p>
*
* @param connection conexão destinada ao identificador da própria aplicação.
*/
private void systemConnectionReceiver(TcpConnection connection) {
while (running) {
try {
byte[] message = connection.receive();
try (ByteArrayInputStream bis = new ByteArrayInputStream(message);
ObjectInputStream ois = new ObjectInputStream(bis)) {
InfoMessage sysMessage = InfoMessage.parseSystemInfo(message);
if (sysMessage != null) continue;
//TODO: Ainda não existem mensagens trocadas entre o sistema.
}
}
catch (SocketTimeoutException ex) {
if (connection.hasTimedOut()) return;
}
catch (IOException ex) {
tcpConnectionManager.closeConnection(connection);
}
}
}
/**
* Encerra uma conexão TCP e registra o motivo informado.
*
* <p>Quando nenhuma descrição é fornecida, é utilizado um motivo padrão.
* O fechamento efetivo da conexão é delegado ao {@link TcpConnectionManager}.</p>
*
* @param connection conexão que será encerrada.
* @param description motivo do encerramento; pode ser {@code null}.
*/
public void disconnect(TcpConnection connection, String description) {
tcpConnectionManager.closeConnection(
connection,
Objects.requireNonNullElse(description, "No reason provided")
);
}
@Override
public void run() {
receive();
}
/**
* Encerra o servidor TCP e libera os recursos associados.
*
* <p>Interrompe o laço de aceitação de conexões, fecha o
* {@link ServerSocket}, encerra todas as conexões gerenciadas e interrompe
* as tarefas do executor.</p>
*
* <p>Após a chamada, esta instância não deve ser reutilizada para aceitar
* novas conexões.</p>
*/
public void close() {
running = false;
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
tcpConnectionManager.closeAllConnections("The peer must have closed the application or changed ports");
connectionExecutor.shutdownNow();
}
}
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more