Chat.CHAT_BUFFER_SIZE)
{
int msgElementCount = 0;
Element firstMsgElement = null;
int firstMsgIndex = 0;
Element rootElement = this.document.getDefaultRootElement();
// Count how many messages we have in the document.
for (int i = 0; i < rootElement.getElementCount(); i++)
{
String idAttr = (String) rootElement.getElement(i)
.getAttributes().getAttribute("identifier");
if(idAttr != null
&& (idAttr.equals("message")
|| idAttr.equals("statusMessage")
|| idAttr.equals("systemMessage")))
{
if(firstMsgElement == null)
{
firstMsgElement = rootElement.getElement(i);
firstMsgIndex = i;
}
msgElementCount++;
}
}
// If we doesn't have any known elements in the document or if we
// have only one long message we don't want to remove it.
if(firstMsgElement == null || msgElementCount < 2)
return;
try
{
// Remove the header of the message if such exists.
if(firstMsgIndex > 0)
{
Element headerElement = rootElement.getElement(firstMsgIndex - 1);
String idAttr = (String) headerElement
.getAttributes().getAttribute("identifier");
if(idAttr != null && idAttr.equals("messageHeader"))
{
this.document.remove(headerElement.getStartOffset(),
headerElement.getEndOffset()
- headerElement.getStartOffset());
}
}
// Remove the message itself.
this.document.remove(firstMsgElement.getStartOffset(),
firstMsgElement.getEndOffset()
- firstMsgElement.getStartOffset());
}
catch (BadLocationException e)
{
logger.error("Error removing messages from chat: ", e);
}
}
}
/**
* Highlights keywords searched in the history.
*
* @param message the source message
* @param contentType the content type
* @param keyword the searched keyword
* @return the formatted message
*/
private String processKeyword( String message,
String contentType,
String keyword)
{
String startPlainTextTag;
String endPlainTextTag;
if (HTML_CONTENT_TYPE.equals(contentType))
{
startPlainTextTag = "";
endPlainTextTag = "";
}
else
{
startPlainTextTag = START_PLAINTEXT_TAG;
endPlainTextTag = END_PLAINTEXT_TAG;
}
Matcher m
= Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE)
.matcher(message);
StringBuffer msgBuffer = new StringBuffer();
int prevEnd = 0;
while (m.find())
{
msgBuffer.append(message.substring(prevEnd, m.start()));
prevEnd = m.end();
String keywordMatch = m.group().trim();
msgBuffer.append(endPlainTextTag);
msgBuffer.append("
");
msgBuffer.append(keywordMatch);
msgBuffer.append("");
msgBuffer.append(startPlainTextTag);
}
/*
* If the keyword didn't match, let the outside world be able to
* discover it.
*/
if (prevEnd == 0)
return message;
msgBuffer.append(message.substring(prevEnd));
return msgBuffer.toString();
}
/**
* Formats the given message. Processes all smiley chars, new lines and
* links.
*
* @param message the message to be formatted
* @param contentType the content type of the message to be formatted
* @param keyword the word to be highlighted
* @return the formatted message
*/
private String formatMessage(String message,
String contentType,
String keyword)
{
// If the message content type is HTML we won't process links and
// new lines, but only the smileys.
if (!HTML_CONTENT_TYPE.equals(contentType))
{
/*
* We disallow HTML in plain-text messages. But processKeyword
* introduces HTML. So we'll allow HTML if processKeyword has
* introduced it in order to not break highlighting.
*/
boolean processHTMLChars;
if ((keyword != null) && (keyword.length() != 0))
{
String messageWithProcessedKeyword
= processKeyword(message, contentType, keyword);
/*
* The same String instance will be returned if there was no
* keyword match. Calling #equals() is expensive so == is
* intentional.
*/
processHTMLChars = (messageWithProcessedKeyword == message);
message = messageWithProcessedKeyword;
}
else
processHTMLChars = true;
message
= processNewLines(
processLinksAndHTMLChars(message, processHTMLChars));
}
// If the message content is HTML, we process br and img tags.
else
{
if ((keyword != null) && (keyword.length() != 0))
message = processKeyword(message, contentType, keyword);
message = processImgTags(processBrTags(message));
}
return message;
}
/**
* Formats all links in a given message and optionally escapes special HTML
* characters such as <, >, & and " in order to prevent HTML
* injection in plain-text messages such as writing
*
</PLAINTEXT>
, HTML which is going to be rendered as
* such and
<PLAINTEXT>
. The two procedures are carried
* out in one call in order to not break URLs which contain special HTML
* characters such as &.
*
* @param message The source message string.
* @param processHTMLChars
true to escape the special HTML chars;
* otherwise,
false
* @return The message string with properly formatted links.
*/
private String processLinksAndHTMLChars(String message,
boolean processHTMLChars)
{
Matcher m = URL_PATTERN.matcher(message);
StringBuffer msgBuffer = new StringBuffer();
int prevEnd = 0;
while (m.find())
{
String fromPrevEndToStart = message.substring(prevEnd, m.start());
if (processHTMLChars)
fromPrevEndToStart = processHTMLChars(fromPrevEndToStart);
msgBuffer.append(fromPrevEndToStart);
prevEnd = m.end();
String url = m.group().trim();
msgBuffer.append(END_PLAINTEXT_TAG);
msgBuffer.append("
");
msgBuffer.append(url);
msgBuffer.append("");
msgBuffer.append(START_PLAINTEXT_TAG);
}
String fromPrevEndToEnd = message.substring(prevEnd);
if (processHTMLChars)
fromPrevEndToEnd = processHTMLChars(fromPrevEndToEnd);
msgBuffer.append(fromPrevEndToEnd);
return msgBuffer.toString();
}
/**
* Escapes special HTML characters such as <, >, & and " in
* the specified message.
*
* @param message the message to be processed
* @return the processed message with escaped special HTML characters
*/
private String processHTMLChars(String message)
{
return
message
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """);
}
/**
* Formats message new lines.
*
* @param message The source message string.
* @return The message string with properly formatted new lines.
*/
private String processNewLines(String message)
{
/*
*
tags are needed to visualize a new line in the html format, but
* when copied to the clipboard they are exported to the plain text
* format as ' ' and not as '\n'.
*
* See bug N4988885:
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4988885
*
* To fix this we need "
" - the HTML-Code for ASCII-Character No.10
* (Line feed).
*/
return
message
.replaceAll(
"\n",
END_PLAINTEXT_TAG + "
" + START_PLAINTEXT_TAG);
}
/**
* Opens a link in the default browser when clicked and shows link url in a
* popup on mouseover.
*
* @param e The HyperlinkEvent.
*/
public void hyperlinkUpdate(HyperlinkEvent e)
{
if (e.getEventType() == HyperlinkEvent.EventType.ENTERED)
{
String href = e.getDescription();
this.currentHref = href;
}
else if (e.getEventType() == HyperlinkEvent.EventType.EXITED)
{
this.currentHref = "";
}
}
/**
* Returns the text pane of this conversation panel.
*
* @return The text pane of this conversation panel.
*/
public JTextPane getChatTextPane()
{
return chatTextPane;
}
/**
* Returns the time of the last received message.
*
* @return The time of the last received message.
*/
public long getLastIncomingMsgTimestamp()
{
return lastIncomingMsgTimestamp;
}
/**
* When a right button click is performed in the editor pane, a popup menu
* is opened.
* In case of the Scheme being internal, it won't open the Browser but
* instead it will trigger the forwarded action.
*
* @param e The MouseEvent.
*/
public void mouseClicked(MouseEvent e)
{
Point p = e.getPoint();
SwingUtilities.convertPointToScreen(p, e.getComponent());
if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0
|| (e.isControlDown() && !e.isMetaDown()))
{
openContextMenu(p);
}
else if ((e.getModifiers() & InputEvent.BUTTON1_MASK) != 0
&& currentHref != null && currentHref.length() != 0)
{
URI uri;
try
{
uri = new URI(currentHref);
}
catch (URISyntaxException e1)
{
logger.error("Invalid URL", e1);
return;
}
if(uri.getScheme().equals("jitsi"))
{
for(ChatLinkClickedListener l:chatLinkClickedListeners)
{
l.chatLinkClicked(uri);
}
}
else
GuiActivator.getBrowserLauncher().openURL(currentHref);
// after opening the link remove the currentHref to avoid
// clicking on the window to gain focus to open the link again
this.currentHref = "";
}
}
/**
* Opens this panel context menu at the given point.
*
* @param p the point where to position the left-top cornet of the context
* menu
*/
private void openContextMenu(Point p)
{
if (currentHref != null && currentHref.length() != 0
&& !currentHref.startsWith("jitsi://"))
{
rightButtonMenu.insert(openLinkItem, 0);
rightButtonMenu.insert(copyLinkItem, 1);
rightButtonMenu.insert(copyLinkSeparator, 2);
}
else
{
rightButtonMenu.remove(openLinkItem);
rightButtonMenu.remove(copyLinkItem);
rightButtonMenu.remove(copyLinkSeparator);
}
if (chatTextPane.getSelectedText() != null)
{
rightButtonMenu.enableCopy();
}
else
{
rightButtonMenu.disableCopy();
}
rightButtonMenu.setInvoker(chatTextPane);
rightButtonMenu.setLocation(p.x, p.y);
rightButtonMenu.setVisible(true);
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public void lostOwnership(Clipboard clipboard, Transferable contents) {}
/**
* Returns the chat container.
*
* @return the chat container
*/
public ChatConversationContainer getChatContainer()
{
return chatContainer;
}
/**
* Copies the selected conversation panel content to the clipboard.
*/
public void copyConversation()
{
this.chatTextPane.copy();
}
/**
* Creates new document and all the messages that will be processed in the
* future will be appended in it.
*/
public void clear()
{
this.document = (HTMLDocument) editorKit.createDefaultDocument();
Constants.loadSimpleStyle(
document.getStyleSheet(), chatTextPane.getFont());
}
/**
* Sets the given document to the editor pane in this panel.
*
* @param document the document to set
*/
public void setContent(HTMLDocument document)
{
synchronized (scrollToBottomRunnable)
{
scrollToBottomIsPending = true;
this.document = document;
chatTextPane.setDocument(this.document);
}
}
/**
* Sets the default document contained in this panel, created on init or
* when clear is invoked.
*/
public void setDefaultContent()
{
setContent(document);
}
/**
* Returns the document contained in this panel.
*
* @return the document contained in this panel
*/
public HTMLDocument getContent()
{
return (HTMLDocument) this.chatTextPane.getDocument();
}
/**
* Returns the right button popup menu.
*
* @return the right button popup menu
*/
public ChatRightButtonMenu getRightButtonMenu()
{
return rightButtonMenu;
}
/**
* Returns the date of the first message in the current page.
*
* @return the date of the first message in the current page
*/
public Date getPageFirstMsgTimestamp()
{
Element rootElement = this.document.getDefaultRootElement();
Element firstMessageElement = null;
for(int i = 0; i < rootElement.getElementCount(); i ++)
{
String idAttr = (String) rootElement.getElement(i)
.getAttributes().getAttribute("identifier");
if (idAttr != null && idAttr.equals("messageHeader"))
{
firstMessageElement = rootElement.getElement(i);
break;
}
}
if(firstMessageElement == null)
return new Date(Long.MAX_VALUE);
String dateObject = firstMessageElement
.getAttributes().getAttribute("date").toString();
return new Date(Long.parseLong(dateObject));
}
/**
* Returns the date of the last message in the current page.
*
* @return the date of the last message in the current page
*/
public Date getPageLastMsgTimestamp()
{
Element rootElement = this.document.getDefaultRootElement();
Element lastMessageElement = null;
for(int i = rootElement.getElementCount() - 1; i >= 0; i --)
{
String idAttr = (String) rootElement.getElement(i)
.getAttributes().getAttribute("identifier");
if (idAttr != null && idAttr.equals("messageHeader"))
{
lastMessageElement = rootElement.getElement(i);
break;
}
}
if(lastMessageElement == null)
return new Date(0);
String dateObject = lastMessageElement
.getAttributes().getAttribute("date").toString();
return new Date(Long.parseLong(dateObject));
}
/**
* Formats HTML tags <br/> to <br> or <BR/> to <BR>.
* The reason of this function is that the ChatPanel does not support
* <br /> closing tags (XHTML syntax), thus we have to remove every
* slash from each <br /> tags.
* @param message The source message string.
* @return The message string with properly formatted <br> tags.
*/
private String processBrTags(String message)
{
// The resulting message after being processed by this function.
StringBuffer processedMessage = new StringBuffer();
// Compile the regex to match something like
or
.
// This regex is case sensitive and keeps the style or other
// attributes of the
tag.
Matcher m
= Pattern.compile("<\\s*[bB][rR](.*?)(/\\s*>)").matcher(message);
int start = 0;
// while we find some
closing tags with a slash inside.
while(m.find())
{
// First, we have to copy all the message preceding the
tag.
processedMessage.append(message.substring(start, m.start()));
// Then, we find the position of the slash inside the tag.
int slash_index = m.group().lastIndexOf("/");
// We copy the
tag till the slash exclude.
processedMessage.append(m.group().substring(0, slash_index));
// We copy all the end of the tag following the slash exclude.
processedMessage.append(m.group().substring(slash_index+1));
start = m.end();
}
// Finally, we have to add the end of the message following the last
//
tag, or the whole message if there is no
tag.
processedMessage.append(message.substring(start));
return processedMessage.toString();
}
/**
* Formats HTML tags <img ... /> to < img ... ></img> or
* <IMG ... /> to <IMG></IMG>.
* The reason of this function is that the ChatPanel does not support
* <img /> tags (XHTML syntax).
* Thus, we remove every slash from each <img /> and close it with a
* separate closing tag.
* @param message The source message string.
* @return The message string with properly formatted <img> tags.
*/
private String processImgTags(String message)
{
// The resulting message after being processed by this function.
StringBuffer processedMessage = new StringBuffer();
// Compile the regex to match something like
or
//
. This regex is case sensitive and keeps the style,
// src or other attributes of the
tag.
Pattern p = Pattern.compile("<\\s*[iI][mM][gG](.*?)(/\\s*>)");
Matcher m = p.matcher(message);
int slash_index;
int start = 0;
// while we find some
self-closing tags with a slash inside.
while(m.find()){
// First, we have to copy all the message preceding the
tag.
processedMessage.append(message.substring(start, m.start()));
// Then, we find the position of the slash inside the tag.
slash_index = m.group().lastIndexOf("/");
// We copy the
tag till the slash exclude.
processedMessage.append(m.group().substring(0, slash_index));
// We copy all the end of the tag following the slash exclude.
processedMessage.append(m.group().substring(slash_index+1));
// We close the tag with a separate closing tag.
processedMessage.append("");
start = m.end();
}
// Finally, we have to add the end of the message following the last
//
tag, or the whole message if there is no
tag.
processedMessage.append(message.substring(start));
return processedMessage.toString();
}
/**
* Extend Editor pane to add URL tooltips.
*/
private class MyTextPane
extends JTextPane
{
/**
* Returns the string to be used as the tooltip for
event.
*
* @param event the
MouseEvent
* @return the string to be used as the tooltip for
event.
*/
@Override
public String getToolTipText(MouseEvent event)
{
return
((currentHref != null) && (currentHref.length() != 0))
? currentHref
: null;
}
}
/**
* Adds a custom component at the end of the conversation.
*
* @param component the component to add at the end of the conversation.
*/
public void addComponent(ChatConversationComponent component)
{
synchronized (scrollToBottomRunnable)
{
StyleSheet styleSheet = document.getStyleSheet();
Style style
= styleSheet
.addStyle(
StyleConstants.ComponentElementName,
styleSheet.getStyle("body"));
// The image must first be wrapped in a style
style
.addAttribute(
AbstractDocument.ElementNameAttribute,
StyleConstants.ComponentElementName);
TransparentPanel wrapPanel
= new TransparentPanel(new BorderLayout());
wrapPanel.add(component, BorderLayout.NORTH);
style
.addAttribute(StyleConstants.ComponentAttribute, wrapPanel);
style.addAttribute("identifier", "messageHeader");
style.addAttribute("date", component.getDate().getTime());
scrollToBottomIsPending = true;
// Insert the component style at the end of the text
try
{
document
.insertString(document.getLength(), "ignored text", style);
}
catch (BadLocationException e)
{
logger.error("Insert in the HTMLDocument failed.", e);
}
}
}
/**
* Registers a new link click listener.
*
* @param listener the object that should be notified when an internal
* link was clicked.
*/
public void addChatLinkClickedListener(ChatLinkClickedListener listener)
{
if(!chatLinkClickedListeners.contains(listener))
chatLinkClickedListeners.add(listener);
}
/**
* Remove a registered link click listener.
*
* @param listener a registered click listener to remove
*/
public void removeChatLinkClickedListener(ChatLinkClickedListener listener)
{
chatLinkClickedListeners.remove(listener);
}
/**
* Returns the date string to show for the given date.
*
* @param date the date to format
* @return the date string to show for the given date
*/
public static String getDateString(long date)
{
if (GuiUtils.compareDatesOnly(date, System.currentTimeMillis()) < 0)
{
StringBuffer dateStrBuf = new StringBuffer();
GuiUtils.formatDate(date, dateStrBuf);
dateStrBuf.append(" ");
return dateStrBuf.toString();
}
return "";
}
/**
* Reloads images.
*/
public void loadSkin()
{
openLinkItem.setIcon(
new ImageIcon(ImageLoader.getImage(ImageLoader.BROWSER_ICON)));
copyLinkItem.setIcon(
new ImageIcon(ImageLoader.getImage(ImageLoader.COPY_ICON)));
getRightButtonMenu().loadSkin();
}
/**
* Highlights the string in multi user chat.
*
* @param message the message to process
* @param contentType the content type of the message
* @param keyWord the keyword to highlight
* @return the message string with the keyword highlighted
*/
public String processChatRoomHighlight(String message, String contentType,
String keyWord)
{
return processKeyword(message, contentType, keyWord);
}
public String processMeCommand(ChatMessage chatMessage)
{
String contentType = chatMessage.getContentType();
String message = chatMessage.getMessage();
String msgID = "message";
String chatString = "";
String endHeaderTag = "";
String startDivTag = "
";
String endDivTag = "
";
String startPlainTextTag;
String endPlainTextTag;
if (HTML_CONTENT_TYPE.equals(contentType))
{
startPlainTextTag = "";
endPlainTextTag = "";
}
else
{
startPlainTextTag = START_PLAINTEXT_TAG;
endPlainTextTag = END_PLAINTEXT_TAG;
}
if (message.length() > 4 && message.substring(0, 4).equals("/me "))
{
chatString = startDivTag + "
";
endHeaderTag = "" + endDivTag;
chatString +=
processHTMLChars("*** " + chatMessage.getContactName() + " "
+ message.substring(4))
+ endHeaderTag;
Map
listSources =
GuiActivator.getReplacementSources();
Iterator> entrySetIter =
listSources.entrySet().iterator();
StringBuffer msgStore = new StringBuffer(chatString);
for (int i = 0; i < listSources.size(); i++)
{
Map.Entry entry =
entrySetIter.next();
ReplacementService source = entry.getValue();
boolean isSmiley = source instanceof SmiliesReplacementService;
if (isSmiley)
{
String sourcePattern = source.getPattern();
Pattern p =
Pattern.compile(sourcePattern, Pattern.CASE_INSENSITIVE
| Pattern.DOTALL);
Matcher m = p.matcher(msgStore);
StringBuffer msgTemp = new StringBuffer(chatString);
while (m.find())
{
msgTemp.insert(m.start(), startPlainTextTag);
msgTemp.insert(m.end() + startPlainTextTag.length(),
endPlainTextTag);
}
if (msgTemp.length() != msgStore.length())
msgStore = msgTemp;
}
}
return msgStore.toString();
}
else
return "";
}
private static String createIncomingMessageTag(
String messageID,
String incomingMessageHeader,
String incomingMessageParagraph)
{
StringBuffer messageBuff = new StringBuffer();
//
messageBuff.append("");
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append(incomingMessageHeader);
messageBuff.append(incomingMessageParagraph);
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append("
");
messageBuff.append("
");
return messageBuff.toString();
}
private static String createOutgoingMessageStyle()
{
StringBuffer styleBuff = new StringBuffer();
styleBuff.append("background-image:");
styleBuff.append("url('bundle://30.0:1/resources/images/impl/gui/lookandfeel/selectedTabMiddle.png');");
styleBuff.append("background-repeat:");
styleBuff.append("repeat-x;");
return styleBuff.toString();
}
private static String createSmsMessageStyle()
{
StringBuffer styleBuff = new StringBuffer();
styleBuff.append("background-image:");
styleBuff.append("url('bundle://30.0:1/resources/images/impl/gui/lookandfeel/tabRight.png');");
styleBuff.append("background-repeat:");
styleBuff.append("repeat-x;");
return styleBuff.toString();
}
// .box {
// width: 100%;
// margin: 0px auto;
// }
private static String createBoxStyle()
{
return "style=\"width: 100%;"
+ " margin-top: 0px;"
+ " margin-bottom: 0px;"
+ " margin-left: auto;"
+ " margin-right: auto;\"";
}
// .box div.topleft {
// display: block;
// background: url("i/box-bg.png") top left no-repeat white;
// padding: 0em 0em 0em 1.0em;
// }
private static String createTopLeftStyle()
{
return "style=\"display: block;"
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
+ " background-repeat: no-repeat;"
+ " background-position: top left;"
+ " background-color: #FFFFFF;"
+ " padding-top: 0em;"
+ " padding-right: 0em;"
+ " padding-bottom: 0em;"
+ " padding-left: 0em;"
+ "\"";
}
// .box div.topright {
// display: block;
// background: url("i/box-bg.png") top right no-repeat white;
// padding: 1.0em;
// margin: -1.0em 0 0 1.0em;
// }
private static String createTopRightStyle()
{
return "style=\"display: block;"
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
+ " background-repeat: no-repeat;"
+ " background-position: top right;"
+ " background-color: #FFFFFF;"
+ " padding-top: 1em;"
+ " padding-right: 1em;"
+ " padding-bottom: 1em;"
+ " padding-left: 1em;"
+ " margin-top: -1.0em;"
+ " margin-right: 0em;"
+ " margin-bottom: 0em;"
+ " margin-left: 1.0em;"
+ "\"";
}
// .box div.bottomleft {
// display: block;
// height: 55px;
// margin-top: -1.0em;
// background: url("i/box-bg.png") bottom left no-repeat white;
// }
private static String createBottomLeftStyle()
{
return "style=\"display: block;"
+ " height: 25px;"
+ " margin-top: -1.0em;"
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
+ " background-repeat: no-repeat;"
+ " background-position: bottom left;"
+ " background-color: #FFFFFF;"
+ "\"";
}
// .box div.bottomright {
// display: block;
// background: url("i/box-bg.png") bottom right no-repeat white;
// height: 55px;
// margin-left: 3.0em;
// }
private static String createBottomRightStyle()
{
return "style=\"display: block;"
+ " height: 25px;"
+ " margin-left: 3.0em;"
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
+ " background-repeat: no-repeat;"
+ " background-position: bottom right;"
+ " background-color: #FFFFFF;"
+ "\"";
}
// .box div.topright div {
// margin-right: 1.5em;
// }
private static String createMessageDivStyle()
{
return "style=\"margin-right: 1.5em;\"";
}
// .box h4 {
// margin-bottom: 0.4em;
// background-image: none;
// background-repeat: no-repeat;
// margin:0;
// padding:0;
// text-align:center;
// padding-bottom:15px;
// }
}