diff --git a/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/EventManager.java b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/EventManager.java new file mode 100644 index 0000000..2ee3c89 --- /dev/null +++ b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/EventManager.java @@ -0,0 +1,48 @@ +package com.jfrog.ide.eclipse.ui.webview.events; + +import org.cef.browser.CefBrowser; +import com.jfrog.ide.common.webview.events.WebviewEvent; + +/** + * The EventManager is responsible for managing events between the IDE and the Webview. + * It handles the creation of a receiver and sender, allowing communication between the components. + */ +public class EventManager { + private final static String IDE_SEND_FUNC_NAME = "sendMessageToIdeFunc"; + private final Receiver receiver; + private final Sender sender; + + /** + * Constructs a new EventManager with the provided CefBrowser. + * Note: The eventManager must be created before the webview is initialized. + * + * @param cefBrowser The JBCefBrowser associated with the webview. + * @param project The Project associated with the IDE. + */ + public EventManager(CefBrowser cefBrowser) { + this.receiver = new Receiver(cefBrowser); + this.sender = new Sender(cefBrowser); + } + + /** + * Invoked when the webview finishes loading. + * Creates the IDE send function body and sends it to the webview. + * Finally, it runs onLoadEvent, if provided. + * + * @param onLoadEnd A {@link Runnable} to run when the webview finishes loading. + */ + public void onWebviewLoadEnd() { + String ideSendFuncBody = this.receiver.createIdeSendFuncBody(IDE_SEND_FUNC_NAME); + this.sender.sendIdeSendFunc(IDE_SEND_FUNC_NAME, ideSendFuncBody); + } + + /** + * Sends an event of the specified type and data to the webview. + * + * @param type The type of the webview event. + * @param data The data associated with the event. + */ + public void send(WebviewEvent.Type type, Object data) { + this.sender.sendEvent(type, data); + } +} diff --git a/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/Receiver.java b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/Receiver.java new file mode 100644 index 0000000..27a1840 --- /dev/null +++ b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/Receiver.java @@ -0,0 +1,64 @@ +package com.jfrog.ide.eclipse.ui.webview.events; + +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.browser.CefMessageRouter; +import org.cef.callback.CefQueryCallback; +import org.cef.handler.CefMessageRouterHandlerAdapter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jfrog.ide.common.webview.events.IdeEvent; +import com.jfrog.ide.eclipse.log.Logger; + +import static com.jfrog.ide.common.utils.Utils.createMapper; + + +/** + * The Receiver class is responsible for handling events received from the webview in the IDE. + * It sets up the necessary query handling and provides a mechanism to process the received events. + */ +public class Receiver { + private final CefMessageRouter messageRouter; + + public Receiver(CefBrowser cefBrowser) { + this.messageRouter = CefMessageRouter.create(); + this.messageRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request, boolean persistent, CefQueryCallback callback) { + try { + IdeEvent event = unpack(request); + handler(event); + callback.success("Request processed successfully."); + } catch (JsonProcessingException e) { + Logger.getInstance().error(String.format("Failed to parse event from the Webview: %s", e.getMessage())); + callback.failure(500,String.format("Invalid request format: %s", e.getMessage())); + return false; + } + return true; + } + }, true); + cefBrowser.getClient().addMessageRouter(messageRouter); + } + + public static IdeEvent unpack(String raw) throws JsonProcessingException { + ObjectMapper ow = createMapper(); + return ow.readValue(raw, IdeEvent.class); + } + + public String createIdeSendFuncBody(String ideSendFunctionName) { + // This JS function sends a message to the Java side using the message router. + return String.format("window['%s'] = obj => { let raw = JSON.stringify(obj); cefQuery({request: raw}); };",ideSendFunctionName); + } + + /** + * Handles the received IdeEvent. + * + * @param event The received IdeEvent to handle. + */ + private void handler(IdeEvent event) { + // TODO: add logic for handling Webview events such as: JUMP_TO_CODE + Logger.getInstance().info(String.format("Received event from the webview: %s", event.getType())); + } + +} \ No newline at end of file diff --git a/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/Sender.java b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/Sender.java new file mode 100644 index 0000000..85e9575 --- /dev/null +++ b/bundle/src/main/java/com/jfrog/ide/eclipse/ui/webview/events/Sender.java @@ -0,0 +1,81 @@ +package com.jfrog.ide.eclipse.ui.webview.events; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jfrog.ide.common.webview.events.WebviewEvent; +import com.jfrog.ide.eclipse.log.Logger; + +import org.cef.browser.CefBrowser; + +import static com.jfrog.ide.common.utils.Utils.createMapper; + +import javax.swing.SwingUtilities; + +/** + * The Sender class is responsible for sending events from the IDE to the webview. + * It utilizes a CefBrowser instance to execute JavaScript code in the webview. + */ +public class Sender { + CefBrowser browser; + + /** + * @param browser The CefBrowser instance associated with the webview. + */ + public Sender(CefBrowser browser) { + this.browser = browser; + } + + /** + * Packs the webview event into a JSON string representation. + * + * @param type The type of the webview event. + * @param data The data associated with the webview event. + * @return The JSON string representation of the packed webview event. + * @throws JsonProcessingException If an error occurs during JSON processing. + */ + public static String pack(WebviewEvent.Type type, Object data) throws JsonProcessingException { + ObjectMapper ow = createMapper(); + return ow.writeValueAsString(new WebviewEvent(type, data)); + } + + /** + * Sends the IDE send function to the webview. This function allows sending data back from the webview to the IDE. + * + * @param ideSendFuncName The name of the IDE send function. + * @param ideSendFuncBody The body of the IDE send function. + */ + public void sendIdeSendFunc(String ideSendFuncName, String ideSendFuncBody) { + // Send the function to jcef, this must be first before updating the webview. + // Otherwise, the webview will not find any methods to use and will drop the request. + this.send(ideSendFuncBody); + // Update JFrog webview with the function to be used in order to send data back to the IDE. + this.sendEvent(WebviewEvent.Type.SET_EMITTER, "return " + ideSendFuncName); + } + + /** + * Sends a webview event with the specified type and data to the webview. + * + * @param type The type of the webview event. + * @param data The data associated with the webview event. + */ + public void sendEvent(WebviewEvent.Type type, Object data) { + try { + String raw = pack(type, data); + Logger.getInstance().debug(String.format("Sending data to jfrog webview: %s", raw)); + this.send(String.format("window.postMessage( %s )", raw)); + } catch (JsonProcessingException e) { + Logger.getInstance().error(String.format("Failed to pack the Webview event: %s", e.getMessage())); + } + } + + /** + * Sends the specified event to the webview by executing the JavaScript code. + * this action must be done on the SWT thread + * @param event The JavaScript code to be executed in the webview. + */ + public void send(String event) { + SwingUtilities.invokeLater(() -> { + browser.executeJavaScript(event, "", 0); + }); + } +} \ No newline at end of file diff --git a/tests/src/main/java/com/jfrog/ide/eclipse/webview/CefBrowserStub.java b/tests/src/main/java/com/jfrog/ide/eclipse/webview/CefBrowserStub.java new file mode 100644 index 0000000..c9b82c9 --- /dev/null +++ b/tests/src/main/java/com/jfrog/ide/eclipse/webview/CefBrowserStub.java @@ -0,0 +1,76 @@ +package com.jfrog.ide.eclipse.webview; + +import java.awt.Component; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.util.Vector; +import java.util.concurrent.CompletableFuture; + +import org.cef.CefClient; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefDevToolsClient; +import org.cef.browser.CefFrame; +import org.cef.callback.CefPdfPrintCallback; +import org.cef.callback.CefRunFileDialogCallback; +import org.cef.callback.CefStringVisitor; +import org.cef.handler.CefDialogHandler.FileDialogMode; +import org.cef.handler.CefRenderHandler; +import org.cef.handler.CefWindowHandler; +import org.cef.misc.CefPdfPrintSettings; +import org.cef.network.CefRequest; + +class CefBrowserStub implements CefBrowser { + public String lastJs; public int jsCallCount = 0; + @Override public void executeJavaScript(String code, String url, int line) { lastJs = code; jsCallCount++; } + @Override public CefClient getClient() { throw new UnsupportedOperationException(); } + @Override public String getURL() { throw new UnsupportedOperationException(); } + @Override public void loadURL(String url) { throw new UnsupportedOperationException(); } + @Override public void reload() { throw new UnsupportedOperationException(); } + @Override public void stopLoad() { throw new UnsupportedOperationException(); } + @Override public void goBack() { throw new UnsupportedOperationException(); } + @Override public void goForward() { throw new UnsupportedOperationException(); } + @Override public boolean isLoading() { throw new UnsupportedOperationException(); } + @Override public void close(boolean force) { throw new UnsupportedOperationException(); } + @Override public void setFocus(boolean enable) { throw new UnsupportedOperationException(); } + @Override public void setZoomLevel(double zoomLevel) { throw new UnsupportedOperationException(); } + @Override public double getZoomLevel() { throw new UnsupportedOperationException(); } + @Override public void startDownload(String url) { throw new UnsupportedOperationException(); } + @Override public void print() { throw new UnsupportedOperationException(); } + @Override public void stopFinding(boolean clearSelection) { throw new UnsupportedOperationException(); } + @Override public boolean canGoBack() { return false; } + @Override public boolean canGoForward() { return false; } + @Override public void closeDevTools() { } + @Override public void createImmediately() { } + @Override public CompletableFuture createScreenshot(boolean arg0) { return null; } + @Override public boolean doClose() { return false; } + @Override public void find(String arg0, boolean arg1, boolean arg2, boolean arg3) { } + @Override public CefDevToolsClient getDevToolsClient() { return null; } + @Override public CefFrame getFocusedFrame() { return null; } + @Override public CefFrame getFrameByIdentifier(String arg0) { return null; } + @Override public CefFrame getFrameByName(String arg0) { return null; } + @Override public int getFrameCount() { return 0; } + @Override public Vector getFrameIdentifiers() { return null; } + @Override public Vector getFrameNames() { return null; } + @Override public int getIdentifier() { return 0; } + @Override public CefFrame getMainFrame() { return null; } + @Override public CefRenderHandler getRenderHandler() { return null; } + @Override public void getSource(CefStringVisitor arg0) { } + @Override public void getText(CefStringVisitor arg0) { } + @Override public Component getUIComponent() { return null; } + @Override public CefWindowHandler getWindowHandler() { return null; } + @Override public CompletableFuture getWindowlessFrameRate() { return null; } + @Override public boolean hasDocument() { return false; } + @Override public boolean isPopup() { return false; } + @Override public void loadRequest(CefRequest arg0) { } + @Override public void onBeforeClose() { } + @Override public void openDevTools() { } + @Override public void openDevTools(Point arg0) { } + @Override public void printToPDF(String arg0, CefPdfPrintSettings arg1, CefPdfPrintCallback arg2) { } + @Override public void reloadIgnoreCache() { } + @Override public void replaceMisspelling(String arg0) { } + @Override public void runFileDialog(FileDialogMode arg0, String arg1, String arg2, Vector arg3, int arg4, CefRunFileDialogCallback arg5) { } + @Override public void setCloseAllowed() { } + @Override public void setWindowVisibility(boolean arg0) { } + @Override public void setWindowlessFrameRate(int arg0) { } + @Override public void viewSource() { } +} \ No newline at end of file diff --git a/tests/src/main/java/com/jfrog/ide/eclipse/webview/ReceiverTest.java b/tests/src/main/java/com/jfrog/ide/eclipse/webview/ReceiverTest.java new file mode 100644 index 0000000..275aea5 --- /dev/null +++ b/tests/src/main/java/com/jfrog/ide/eclipse/webview/ReceiverTest.java @@ -0,0 +1,64 @@ +package com.jfrog.ide.eclipse.webview; + +import com.jfrog.ide.eclipse.ui.webview.events.Receiver; +import com.jfrog.ide.common.webview.events.IdeEvent; +import junit.framework.TestCase; +import com.fasterxml.jackson.core.JsonProcessingException; + +public class ReceiverTest extends TestCase { + public void testUnpack() throws JsonProcessingException { + String json = "{\"type\":\"JUMP_TO_CODE\",\"data\":{\"filePath\":\"/path/to/file.java\",\"lineNumber\":42}}"; + IdeEvent event = Receiver.unpack(json); + assertNotNull(event); + assertEquals("JUMP_TO_CODE", event.getType().toString()); + assertNotNull(event.getData()); + assertTrue((event.getData().toString()).contains("filePath")); + } + + public void testUnpackWithNullData() throws JsonProcessingException { + String json = "{\"type\":\"JUMP_TO_CODE\",\"data\":null}"; + IdeEvent event = Receiver.unpack(json); + assertNotNull(event); + assertEquals("JUMP_TO_CODE", event.getType().toString()); + assertNull(event.getData()); + } + + public void testUnpackWithExtraFields() throws JsonProcessingException { + String json = "{\"type\":\"JUMP_TO_CODE\",\"data\":{\"filePath\":\"path\"},\"extra\":\"data\"}"; + IdeEvent event = Receiver.unpack(json); + assertNotNull(event); + assertEquals("JUMP_TO_CODE", event.getType().toString()); + assertNotNull(event.getData()); + assertTrue(event.getData().toString().contains("filePath")); + } + + public void testUnpackWithInvalidJson() { + String json = "{type:BAD_JSON}"; + try { + Receiver.unpack(json); + fail("Expected JsonProcessingException"); + } catch (JsonProcessingException e) { + // Expected + } + } + + public void testUnpackWithMissingType() throws JsonProcessingException { + String json = "{\"data\":{}}"; + IdeEvent event = Receiver.unpack(json); + assertNotNull(event); + assertTrue(json.contains("data")); + assertNull(event.getType()); + + } + + public void testUnpackWithDifferentEventType() throws JsonProcessingException { + String json = "{\"type\":\"SOME_EVENT\",\"data\":{\"value\":42}}"; + try + { + Receiver.unpack(json); + fail("Expected JsonProcessingException"); + } catch (JsonProcessingException e) { + // Expected + } + } +} \ No newline at end of file diff --git a/tests/src/main/java/com/jfrog/ide/eclipse/webview/SenderTest.java b/tests/src/main/java/com/jfrog/ide/eclipse/webview/SenderTest.java new file mode 100644 index 0000000..9e23d9d --- /dev/null +++ b/tests/src/main/java/com/jfrog/ide/eclipse/webview/SenderTest.java @@ -0,0 +1,80 @@ +package com.jfrog.ide.eclipse.webview; + +import com.jfrog.ide.eclipse.ui.webview.events.Sender; +import com.jfrog.ide.common.webview.events.WebviewEvent; +import junit.framework.TestCase; +import com.fasterxml.jackson.core.JsonProcessingException; + +import javax.swing.SwingUtilities; + +public class SenderTest extends TestCase { + public void testPack() throws JsonProcessingException { + String type = "SET_EMITTER"; + String data = "testData"; + // Use reflection or a mock WebviewEvent if needed, here we just check JSON structure + String json = Sender.pack(WebviewEvent.Type.SET_EMITTER, data); + assertTrue(json.contains(type)); + assertTrue(json.contains(data)); + } + + public void testSend() throws Exception { + CefBrowserStub browser = new CefBrowserStub(); + Sender sender = new Sender(browser); + String alert = "alert('message')"; + // SwingUtilities.invokeLater is async, so wait for it + sender.send(alert); + SwingUtilities.invokeAndWait(() -> {}); // Wait for EDT + assertEquals(alert, browser.lastJs); + assertEquals(1, browser.jsCallCount); + } + + public void testPackThrowsException() { + try { + // Jackson cannot serialize objects with circular references + Object circular = new Object() { + public Object self = this; + }; + Sender.pack(WebviewEvent.Type.SET_EMITTER, circular); + fail("Expected JsonProcessingException"); + } catch (JsonProcessingException e) { + // Expected + } + } + + public void testPackWithNullData() throws Exception { + String json = Sender.pack(WebviewEvent.Type.SET_EMITTER, null); + assertTrue(json.contains("SET_EMITTER")); + assertFalse(json.contains("data")); + } + + public void testPackWithPrimitiveData() throws Exception { + String json = Sender.pack(WebviewEvent.Type.SET_EMITTER, 123); + assertTrue(json.contains("123")); + } + + public void testPackWithComplexData() throws Exception { + class Data { public String data = "message"; } + String json = Sender.pack(WebviewEvent.Type.SET_EMITTER, new Data()); + assertTrue(json.contains("data")); + assertTrue(json.contains("message")); + } + + public void testSendWithEmptyString() throws Exception { + CefBrowserStub browser = new CefBrowserStub(); + Sender sender = new Sender(browser); + sender.send(""); + SwingUtilities.invokeAndWait(() -> {}); + assertEquals("", browser.lastJs); + assertEquals(1, browser.jsCallCount); + } + + public void testSendWithNullString() throws Exception { + CefBrowserStub browser = new CefBrowserStub(); + Sender sender = new Sender(browser); + sender.send(null); + SwingUtilities.invokeAndWait(() -> {}); + assertNull(browser.lastJs); + assertEquals(1, browser.jsCallCount); + } + +} \ No newline at end of file