From 16f5f552d14499075d460c694a41901776d56bb9 Mon Sep 17 00:00:00 2001 From: Danny van Heumen Date: Sun, 9 Feb 2014 21:24:36 +0100 Subject: [PATCH] Support for IRC control codes. The following IRC control codes are now supported: * Bold * Italics * Underline * Normal (clears all outstanding codes) * Color (foreground and background) * 16 colors (00 up to and including 15) --- .../communicator/impl/protocol/irc/Color.java | 56 ++++++ .../impl/protocol/irc/ControlChar.java | 96 +++++++++ .../impl/protocol/irc/IrcStack.java | 4 +- .../communicator/impl/protocol/irc/Utils.java | 182 ++++++++++++++++-- .../impl/protocol/irc/ColorTest.java | 31 +++ .../impl/protocol/irc/ControlCharTest.java | 52 +++++ .../impl/protocol/irc/UtilsTest.java | 115 +++++++++-- 7 files changed, 499 insertions(+), 37 deletions(-) create mode 100644 src/net/java/sip/communicator/impl/protocol/irc/Color.java create mode 100644 src/net/java/sip/communicator/impl/protocol/irc/ControlChar.java create mode 100644 test/net/java/sip/communicator/impl/protocol/irc/ColorTest.java create mode 100644 test/net/java/sip/communicator/impl/protocol/irc/ControlCharTest.java diff --git a/src/net/java/sip/communicator/impl/protocol/irc/Color.java b/src/net/java/sip/communicator/impl/protocol/irc/Color.java new file mode 100644 index 000000000..3e5b5307a --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/Color.java @@ -0,0 +1,56 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.irc; + +/** + * IRC color codes that can be specified in the color control code. + * + * @author Danny van Heumen + */ +public enum Color +{ + WHITE("White"), + BLACK("Black"), + BLUE("Navy"), + GREEN("Green"), + RED("Red"), + BROWN("Maroon"), + PURPLE("Purple"), + ORANGE("Orange"), + YELLOW("Yellow"), + LIGHT_GREEN("Lime"), + TEAL("Teal"), + LIGHT_CYAN("Cyan"), + LIGHT_BLUE("RoyalBlue"), + PINK("Fuchsia"), + GREY("Grey"), + LIGHT_GREY("Silver"); + + /** + * Instance containing the html representation of this color. + */ + private String html; + + /** + * Constructor for enum entries. + * + * @param html HTML representation for color + */ + private Color(String html) + { + this.html = html; + } + + /** + * Get the HTML representation of this color. + * + * @return returns html representation or null if none exist + */ + public String getHtml() + { + return this.html; + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/irc/ControlChar.java b/src/net/java/sip/communicator/impl/protocol/irc/ControlChar.java new file mode 100644 index 000000000..46bb61687 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/ControlChar.java @@ -0,0 +1,96 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.irc; + +/** + * Enum with available IRC control characters. + * + * @author Danny van Heumen + */ +public enum ControlChar +{ + BOLD('\u0002', "b"), + COLOR('\u0003', "font"), + NORMAL('\u000F', null), + ITALICS('\u001D', "i"), + UNDERLINE('\u001F', "u"); + + /** + * The IRC control code. + */ + private char code; + + /** + * HTML tag that expresses the specific formatting requirement. + */ + private String tag; + + /** + * Constructor. + * + * @param code the control code + */ + private ControlChar(char code, String htmlTag) + { + this.code = code; + this.tag = htmlTag; + } + + /** + * Find enum instance by IRC control code. + * + * @param code IRC control code + * @return returns enum instance or null if no instance was found + */ + public static ControlChar byCode(char code) + { + for (ControlChar controlChar : values()) + { + if (controlChar.getCode() == code) + return controlChar; + } + return null; + } + + /** + * Get the IRC control code. + * + * @return returns the IRC control code + */ + public char getCode() + { + return this.code; + } + + /** + * Get the HTML start tag, optionally including extra parameters. + * + * @param addition optional addition to be included before closing the start + * tag + * @return returns HTML start tag. + */ + public String getHtmlStart(String... addition) + { + StringBuilder tag = new StringBuilder("<" + this.tag); + for (String add : addition) + { + tag.append(" "); + tag.append(add); + } + tag.append('>'); + return tag.toString(); + } + + /** + * Get the HTML end tag. + * + * @return returns the HTML end tag + */ + public String getHtmlEnd() + { + return ""; + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java b/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java index 1475e8269..a3f854903 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java @@ -833,7 +833,7 @@ private void deliverReceivedMessageToPrivateChat( { ChatRoomMember member = chatroom.getChatRoomMember(user); MessageIrcImpl message = - new MessageIrcImpl(text, "text/plain", "UTF-8", null); + new MessageIrcImpl(text, "text/html", "UTF-8", null); chatroom.fireMessageReceivedEvent(message, member, new Date(), ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED); } @@ -1071,7 +1071,7 @@ public void onChannelMessage(ChannelPrivMsg msg) String text = Utils.parse(msg.getText()); MessageIrcImpl message = - new MessageIrcImpl(text, "text/plain", "UTF-8", null); + new MessageIrcImpl(text, "text/html", "UTF-8", null); ChatRoomMemberIrcImpl member = new ChatRoomMemberIrcImpl(IrcStack.this.provider, this.chatroom, msg.getSource().getNick(), diff --git a/src/net/java/sip/communicator/impl/protocol/irc/Utils.java b/src/net/java/sip/communicator/impl/protocol/irc/Utils.java index 1e921c203..e946cdbf1 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/Utils.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/Utils.java @@ -5,6 +5,10 @@ */ package net.java.sip.communicator.impl.protocol.irc; +import java.util.*; + +import net.java.sip.communicator.util.*; + /** * Some IRC-related utility methods. * @@ -13,11 +17,9 @@ public final class Utils { /** - * Starting value of HTML entities. - * - * The first 32 chars cannot be converted into HTML entities. + * Logger */ - private static final int START_OF_HTML_ENTITIES = 32; + private static final Logger LOGGER = Logger.getLogger(Utils.class); /** * Private constructor since we do not need to construct anything. @@ -29,40 +31,178 @@ private Utils() /** * Parse IRC text message and process possible control codes. * - * 1. Remove chars which have a value < 32, since there are no equivalent - * HTML entities available when storing them in the history log. - * - * TODO Support bold (0x02) control code. - * TODO Support italics (0x1D) control code. - * TODO Support underline (0x1F) control code. * TODO Support reverse (0x16) control code? - * TODO Support color coding: 0x03<00-15>[,00-15] * - * @param text message + * @param text the message * @return returns the processed message or null if text message was null, - * since there is nothing to modify there + * since there is nothing to modify there */ public static String parse(String text) { if (text == null) return null; - - StringBuilder builder = new StringBuilder(text); - // TODO support IRC control codes for formatting (now only removes them) + final Stack formatting = new Stack(); - for (int i = 0; i < builder.length(); ) + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { - if (builder.charAt(i) < START_OF_HTML_ENTITIES) + char val = text.charAt(i); + ControlChar control = ControlChar.byCode(val); + if (control != null) { - builder.deleteCharAt(i); + // value is a control character, so do something special + switch (control) + { + case ITALICS: + case UNDERLINE: + case BOLD: + if (formatting.size() > 0 && formatting.peek() == control) + { + formatting.pop(); + builder.append(control.getHtmlEnd()); + } + else + { + formatting.push(control); + builder.append(control.getHtmlStart()); + } + break; + case NORMAL: + while (formatting.size() > 0) + { + ControlChar c = formatting.pop(); + builder.append(c.getHtmlEnd()); + } + break; + case COLOR: + if (formatting.size() > 0 && formatting.peek() == control) + { + formatting.pop(); + builder.append(control.getHtmlEnd()); + } + else + { + final List adds = new LinkedList(); + try + { + // parse foreground color code + final Color foreground = + parseForegroundColor(text.substring(i + 1)); + adds.add("color=\"" + foreground.getHtml() + "\""); + i += 2; + + // parse background color code + final Color background = + parseBackgroundColor(text.substring(i + 1)); + adds.add("bgcolor=\"" + background.getHtml() + "\""); + i += 3; + } + catch (IllegalArgumentException e) + { + LOGGER.debug("Invalid color code.", e); + } + formatting.push(control); + String htmlTag = + control.getHtmlStart(adds.toArray(new String[adds + .size()])); + builder.append(htmlTag); + } + break; + default: + LOGGER.warn("Unsupported IRC control code encountered: " + + control); + break; + } } else { - // nothing to do here, go to next char - i++; + // value is a normal character, just append + builder.append(val); } } + + // Finish any outstanding formatting. + while (formatting.size() > 0) + { + builder.append(formatting.pop().getHtmlEnd()); + } + return builder.toString(); } + + /** + * Parse background color code starting with the separator. + * + * @param text the text starting with the background color (separator) + * @return returns the background color + */ + private static Color parseBackgroundColor(String text) + { + try + { + if (text.charAt(0) == ',') + { + // if available, also parse background color + int color = + Integer.parseInt("" + text.charAt(1) + text.charAt(2)); + return Color.values()[color]; + } + throw new IllegalArgumentException( + "no color separator present, hence no background color present"); + } + catch (StringIndexOutOfBoundsException e) + { + // Abort parsing background color. Assume only + // foreground color available. + throw new IllegalArgumentException( + "text stopped before the background color code was finished"); + } + catch (NumberFormatException e) + { + // No background color defined, ignoring ... + throw new IllegalArgumentException( + "value isn't a background color code"); + } + catch (ArrayIndexOutOfBoundsException e) + { + LOGGER.info("Specified IRC color is not a known color code.", e); + throw new IllegalArgumentException( + "background color value is not a known color"); + } + } + + /** + * Parse foreground color and return corresponding Color instance. + * + * @param text the text to parse, starting with color code + * @return returns Color instance + */ + private static Color parseForegroundColor(String text) + { + try + { + int color = Integer.parseInt("" + text.charAt(0) + text.charAt(1)); + return Color.values()[color]; + } + catch (StringIndexOutOfBoundsException e) + { + // Invalid control code, since text has ended. + LOGGER + .trace("ArrayIndexOutOfBounds during foreground color control code parsing."); + throw new IllegalArgumentException("missing foreground color code"); + } + catch (NumberFormatException e) + { + // Invalid text color value + LOGGER.trace("Invalid foreground color code encountered."); + throw new IllegalArgumentException("invalid foreground color code"); + } + catch (ArrayIndexOutOfBoundsException e) + { + LOGGER.info("Specified IRC color is not a known color code.", e); + throw new IllegalArgumentException( + "foreground color value is not a known color"); + } + } } diff --git a/test/net/java/sip/communicator/impl/protocol/irc/ColorTest.java b/test/net/java/sip/communicator/impl/protocol/irc/ColorTest.java new file mode 100644 index 000000000..aaa2572f7 --- /dev/null +++ b/test/net/java/sip/communicator/impl/protocol/irc/ColorTest.java @@ -0,0 +1,31 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.irc; + +import junit.framework.*; + +/** + * @author Danny van Heumen + */ +public class ColorTest + extends TestCase +{ + + protected void setUp() throws Exception + { + super.setUp(); + } + + public void testHtmlRepresentation() + { + Assert.assertEquals("White", Color.WHITE.getHtml()); + } + + protected void tearDown() throws Exception + { + super.tearDown(); + } +} diff --git a/test/net/java/sip/communicator/impl/protocol/irc/ControlCharTest.java b/test/net/java/sip/communicator/impl/protocol/irc/ControlCharTest.java new file mode 100644 index 000000000..475b7054e --- /dev/null +++ b/test/net/java/sip/communicator/impl/protocol/irc/ControlCharTest.java @@ -0,0 +1,52 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.irc; + +import junit.framework.*; + +/** + * @author Danny van Heumen + */ +public class ControlCharTest + extends TestCase +{ + + protected void setUp() throws Exception + { + super.setUp(); + } + + protected void tearDown() throws Exception + { + super.tearDown(); + } + + public void testFindByControlCharUnknown() + { + Assert.assertNull(ControlChar.byCode(' ')); + } + + public void testFindByControlCharBold() + { + Assert.assertSame(ControlChar.BOLD, ControlChar.byCode('\u0002')); + } + + public void testGetHtmlStartSimple() + { + Assert.assertEquals("", ControlChar.BOLD.getHtmlStart()); + } + + public void testGetHtmlStartAdvanced() + { + Assert.assertEquals("", + ControlChar.BOLD.getHtmlStart("bla=\"foo\"")); + } + + public void testGetHtmlEnd() + { + Assert.assertEquals("", ControlChar.BOLD.getHtmlEnd()); + } +} diff --git a/test/net/java/sip/communicator/impl/protocol/irc/UtilsTest.java b/test/net/java/sip/communicator/impl/protocol/irc/UtilsTest.java index 0d61313d3..283891e0f 100644 --- a/test/net/java/sip/communicator/impl/protocol/irc/UtilsTest.java +++ b/test/net/java/sip/communicator/impl/protocol/irc/UtilsTest.java @@ -1,7 +1,15 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. See terms of license at gnu.org. + */ package net.java.sip.communicator.impl.protocol.irc; import junit.framework.*; +/** + * @author Danny van Heumen + */ public class UtilsTest extends TestCase { @@ -10,7 +18,7 @@ protected void setUp() throws Exception { super.setUp(); } - + protected void tearDown() throws Exception { super.tearDown(); @@ -20,50 +28,129 @@ public void testNullText() { Assert.assertEquals(null, Utils.parse(null)); } - + public void testParseEmptyString() { Assert.assertEquals("", Utils.parse("")); } - + public void testParseStringWithoutControlCodes() { final String message = "My normal message without any control codes."; Assert.assertEquals(message, Utils.parse(message)); } - + public void testParseStringWithBoldCode() { - final String ircMessage = "My \u0002bold\u0002 message."; - final String htmlMessage = "My bold message."; + final String ircMessage = + "My \u0002bold\u0002 message \u0002BOLD!\u0002."; + final String htmlMessage = "My bold message BOLD!."; Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); } - + public void testParseStringWithItalicsCode() { - final String ircMessage = "My \u001Ditalics\u001D message."; - final String htmlMessage = "My italics message."; + final String ircMessage = + "My \u001Ditalics\u001D message \u001DITALICS!\u001D."; + final String htmlMessage = "My italics message ITALICS!."; Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); } public void testParseStringWithUnderlineCode() { - final String ircMessage = "My \u001Funderlined\u001F message."; - final String htmlMessage = "My italics message."; + final String ircMessage = + "My \u001Funderlined\u001F message \u001FUNDERLINED!!!\u001F."; + final String htmlMessage = + "My underlined message UNDERLINED!!!."; Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); } public void testParseStringWithForegroundColorCode() { final String ircMessage = "My \u000304RED\u0003 message."; - final String htmlMessage = "My RED message."; + final String htmlMessage = "My RED message."; Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); } public void testParseStringWithForegroundAndBackgroundColorCode() { - final String ircMessage = "My \u000304,12RED on Light Blue\u0003 message."; - final String htmlMessage = "My RED on Light Blue message."; + final String ircMessage = + "My \u000304,12RED on Light Blue\u0003 message."; + final String htmlMessage = + "My RED on Light Blue message."; + Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); + } + + public void testParseStringWithInvalidBgColorCode() + { + final String ircMessage = + "My \u000304,BRIGHT RED on Light Blue\u0003 message."; + final String htmlMessage = + "My ,BRIGHT RED on Light Blue message."; + Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); + } + + public void testParseStringWithInvalidColorControlCode() + { + final String ircMessage = + "My \u0003BRIGHT RED on Light Blue\u0003 message."; + final String htmlMessage = + "My BRIGHT RED on Light Blue message."; + Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); + } + + public void testParseStringWithInvalidSecondControlCode() + { + final String ircMessage = + "My \u000304,12RED on Light Blue\u000304,12 message."; + final String htmlMessage = + "My RED on Light Blue04,12 message."; + Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); + } + + public void testParseStringWithIncompleteForegroundColorControlCode() + { + final String ircMessage = "My \u0003"; + final String htmlMessage = "My "; + Assert.assertTrue(Utils.parse(ircMessage).startsWith(htmlMessage)); + } + + public void testParseStringWithIncompleteBackgroundColorControlCode() + { + final String ircMessage = "My \u000310,"; + final String htmlMessage = "My ,"; + Assert.assertTrue(Utils.parse(ircMessage).startsWith(htmlMessage)); + } + + public void testParseSringAndNeutralizeWithNormalControlCode() + { + final String ircMessage = + "My \u0002\u001D\u001F\u000304,12RED on Light Blue\u000F message."; + final String htmlMessage = + "My RED on Light Blue message."; + Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); + } + + public void testParseStringWithUnclosedFormattingI() + { + final String ircMessage = + "My \u0002\u001D\u001F\u000304,12RED on Light Blue message."; + final String htmlMessage = + "My RED on Light Blue message."; + Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); + } + + public void testParseUnknownForegroundColor() + { + final String ircMessage = "\u000399TEST"; + final String htmlMessage = "99TEST"; + Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); + } + + public void testParseUnknownBackgroundCOlor() + { + final String ircMessage = "\u000300,99TEST"; + final String htmlMessage = ",99TEST"; Assert.assertEquals(htmlMessage, Utils.parse(ircMessage)); } }