/* ******************************************************************************
*
* Copyright 2008-2010 Hans Dijkema
*
* JRichTextEditor is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* JRichTextEditor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with JRichTextEditor. If not, see <http://www.gnu.org/licenses/>.
*
* ******************************************************************************/
package nl.dykema.jxmlnote.clipboard;
import java.awt.Component;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.InputEvent;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import java.util.Vector;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.TransferHandler;
import javax.swing.text.BadLocationException;
import nl.dykema.jxmlnote.clipboard.XMLNoteClipboardListener.MarkHandling;
import nl.dykema.jxmlnote.clipboard.XMLNoteClipboardListener.Moment;
import nl.dykema.jxmlnote.clipboard.XMLNoteClipboardListener.Type;
import nl.dykema.jxmlnote.document.XMLNoteDocument;
import nl.dykema.jxmlnote.document.XMLNoteMark;
import nl.dykema.jxmlnote.exceptions.DefaultXMLNoteErrorHandler;
import nl.dykema.jxmlnote.html.HtmlToXHtml;
import nl.dykema.jxmlnote.html.XHtmlToXMLNote;
import nl.dykema.jxmlnote.html.XMLNoteToHtml;
import nl.dykema.jxmlnote.widgets.JXMLNotePane;
public class XMLNoteTransferHandler extends TransferHandler implements ClipboardOwner {
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Transferable for XMLNote
///////////////////////////////////////////////////////////////////////////////////////////////////////
class Range {
private int start,end;
XMLNoteDocument doc;
public int getStart() {
return start;
}
public int getEnd() {
return end;
}
public XMLNoteDocument getDoc() {
return doc;
}
public Range(XMLNoteDocument d,int s,int e) {
doc=d;
start=s;
end=e;
}
}
class XMLNoteTransferable implements Transferable {
private DataFlavor [] _flavors=null;
private Object[] _data=null;
private Range _selectionRange;
public void removeText() {
if (_selectionRange!=null) {
try {
_selectionRange.getDoc().remove(_selectionRange.getStart(), _selectionRange.getEnd()-_selectionRange.getStart());
} catch (BadLocationException e) {
// unexpected
DefaultXMLNoteErrorHandler.exception(e);
}
}
}
private ByteBuffer makeByteBuffer(String in) {
try {
byte[] bytes=in.getBytes("UTF-8");
return ByteBuffer.wrap(bytes);
} catch (UnsupportedEncodingException e) {
// this is unexpected
DefaultXMLNoteErrorHandler.exception(e);
return null;
}
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
if (!isDataFlavorSupported(flavor)) {
throw new UnsupportedFlavorException(flavor);
} else {
int i,n;
for(i=0,n=_flavors.length;i<n && !_flavors[i].equals(flavor);i++);
if (i==n) {
throw new IOException("No flavor supported");
} else {
return _data[i];
}
}
}
public DataFlavor[] getTransferDataFlavors() {
return _flavors;
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
int i;
for(i=0;i<_flavors.length && !flavor.equals(_flavors[i]);i++);
return (i<_flavors.length);
}
public XMLNoteTransferable(int action) {
Vector<DataFlavor> v=new Vector<DataFlavor>();
Vector<Object> d=new Vector<Object>();
Object o;
_selectionRange=getSelectionRange();
o=getXMLNoteData(action);
if (o!=null) {
v.add(new XMLNoteDataFlavor("String"));
d.add(o);
}
o=getHtmlData();
if (o!=null) {
v.add(new DataFlavor("text/html;charset=UTF-8;class=java.lang.String","HTML DataFlavor"));
d.add(o);
}
o=getTextData();
if (o!=null) {
v.add(DataFlavor.stringFlavor);
d.add(o);
}
if (v.size()==0) {
_flavors=null;
_data=null;
} else {
_flavors=new DataFlavor[v.size()];
_data=new Object[v.size()];
v.toArray(_flavors);
d.toArray(_data);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Private variables
///////////////////////////////////////////////////////////////////////////////////////////////////////
private static final long serialVersionUID = 1L;
private TransferHandler _defaultHandler;
private JXMLNotePane _editPane;
private Hashtable<String,Set<String>> _acceptedClasses;
private Hashtable<String,Set<String>> _acceptedCharsets;
private int _action; // briefly used by exportToClipboard.
// used by createTransferable()!
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Clipboard Ownership
///////////////////////////////////////////////////////////////////////////////////////////////////////
public void lostOwnership(Clipboard clip, Transferable t) {
// Doesn't do anything (yet?)
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Support functions
///////////////////////////////////////////////////////////////////////////////////////////////////////
private void throwFatal(JComponent c) {
try {
String msg=String.format("Type '%s' is not supported by XMLNoteTransferHandler", c.getClass().getName());
throw new Exception(msg);
} catch (Exception E) {
DefaultXMLNoteErrorHandler.fatal(E,-1,E.getMessage());
}
}
private boolean validCharset(String charset,String cl) {
Set<String> charset_set=_acceptedCharsets.get(cl);
if (charset_set==null) {
return false;
} else {
return charset_set.contains(charset);
}
}
private boolean validClass(String baseMimeType,String cl) {
Set<String> mimes=_acceptedClasses.get(cl);
if (mimes==null) {
return false;
} else {
return mimes.contains(baseMimeType);
}
}
private String getCharset(MimeType mt) {
return mt.getParameter("charset");
}
private String getTypeClass(MimeType mt) {
String cl=mt.getParameter("class");
if (cl==null) {
cl=mt.getParameter("representationclass");
return cl;
} else {
return cl;
}
}
private DataFlavor flavorToImport(DataFlavor[] flavors) {
DataFlavor canUse=null;
int priority=3;
for (DataFlavor f : flavors) {
try {
MimeType mt=new MimeType(f.getMimeType());
//System.out.println(mt);
String baset=mt.getBaseType();
if (baset.equals("text/html") && priority>1) {
String charset=getCharset(mt);
String cl=getTypeClass(mt);
if (validCharset(charset,cl) && validClass(baset,cl)) {
canUse=f;
priority=1;
}
} else if (baset.equals("text/plain") && priority>2) {
String charset=getCharset(mt);
String cl=getTypeClass(mt);
if (validCharset(charset,cl) && validClass(baset,cl)) {
canUse=f;
priority=1;
}
} else if (baset.equals(XMLNoteDataFlavor.mimetype()) && priority>0) {
String charset=getCharset(mt);
String cl=getTypeClass(mt);
if (validCharset(charset,cl) && validClass(baset,cl)) {
canUse=f;
priority=0;
}
}
} catch (MimeTypeParseException e) {
// does nothing
}
}
return canUse;
}
private ByteBuffer correctFireFoxBahaviourIfNecessary(ByteBuffer in) {
// If the byte buffer starts with Unicode Replacement characters for UTF-8 (two times)
// 0xEF 0xBF 0xBD and the sequence of bytes that follows is of pattern <number> 0 <number> 0 ...
// This must be firefox clipboard data and we convert it to a new buffer.
// first of all, the byte buffer must contain at least 3+3+2 bytes, i.e. 8.
//CharBuffer b=in.asCharBuffer();
if (in.limit()>=8) { // we must assume that an exact buffer size has been allocated
//System.out.println(String.format("%02x %02x %02x",in.get(0),in.get(1),in.get(2)));
//System.out.println(String.format("%02x %02x %02x",in.get(3),in.get(4),in.get(5)));
//System.out.println(String.format("%02x %02x %02x",in.get(6),in.get(7),in.get(8)));
int ef=(byte) 0xef;
int bf=(byte) 0xbf;
int bd=(byte) 0xbd;
if (in.get(0)==ef && in.get(1)==bf && in.get(2)==bd &&
in.get(3)==ef && in.get(4)==bf && in.get(5)==bd && in.get(7)==0) {
// OK, this must be something from firefox, convert it.
// Filter out all ef bf bd 00 sequences too
int i,n,k;
// Next get all bytes
for(i=6,k=0,n=in.limit();i<n;i+=2,k++) {
if (in.get(i)==ef && in.get(i+1)==bf && in.get(i+2)==bd && in.get(i+3)==0) {
i+=2;
} else {
in.put(k,in.get(i));
}
}
in.limit(k);
return in;
} else {
return in;
}
} else {
return in;
}
}
private String getDataAsString(Transferable t,DataFlavor f) throws UnsupportedFlavorException, IOException, MimeTypeParseException {
Object o=t.getTransferData(f);
if (o instanceof java.nio.ByteBuffer) {
MimeType mt=new MimeType(f.getMimeType());
java.nio.ByteBuffer buf=(java.nio.ByteBuffer) o;
String charset=getCharset(mt);
// Firefox has strange behaviour with java under Linux.
// Correct that if at all possible.
buf=correctFireFoxBahaviourIfNecessary(buf);
// Now convert the stuff
Charset cset=Charset.forName(charset);
CharBuffer cbuf=cset.decode(buf);
StringBuffer sbuf=new StringBuffer();
sbuf.append(cbuf.subSequence(0, cbuf.length()));
return sbuf.toString();
} else if (o instanceof java.lang.String) {
return (String) o;
} else {
return null;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Data retrievers for the transferable
///////////////////////////////////////////////////////////////////////////////////////////////////////
private Object getXMLNoteData(int action) {
XMLNoteDocument doc=_editPane.getXMLNoteDoc();
int start,end;
start=_editPane.getSelectionStart();
end=_editPane.getSelectionEnd();
try {
XMLNoteDocument copyDoc=doc.getPart(start,end,doc.getClipboardIncludeMarks());
Iterator<XMLNoteClipboardListener> it=doc.clipboardListenerIterator();
while (it.hasNext()) {
Type t=Type.COPY;
if (action==MOVE) { t=Type.MOVE; }
it.next().exportToClipboard(_editPane, copyDoc, Moment.BEFORE,t);
}
String xml=copyDoc.toXML();
while (it.hasNext()) {
Type t=Type.COPY;
if (action==MOVE) { t=Type.MOVE; }
it.next().exportToClipboard(_editPane, copyDoc, Moment.AFTER,t);
}
return xml;
} catch (Exception e) {
return null;
}
}
private Object getHtmlData() {
XMLNoteDocument doc=_editPane.getXMLNoteDoc();
int start,end;
start=_editPane.getSelectionStart();
end=_editPane.getSelectionEnd();
try {
XMLNoteDocument copyDoc=doc.getPart(start,end,XMLNoteDocument.FULL);
return XMLNoteToHtml.toString(copyDoc,_editPane.getMarkMarkupProviderMaker());
} catch (Exception e) {
return null;
}
}
private Object getTextData() {
XMLNoteDocument doc=_editPane.getXMLNoteDoc();
int start,end;
start=_editPane.getSelectionStart();
end=_editPane.getSelectionEnd();
try {
if ((end-start)<=0) {
return null;
} else {
return doc.getText(start, end-start);
}
} catch (Exception e) {
return null;
}
}
private Range getSelectionRange() {
return new Range(_editPane.getXMLNoteDoc(),_editPane.getSelectionStart(),_editPane.getSelectionEnd());
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Overridden TransferHandler
///////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Calls the previous handler of the JXMLNotePane.
* Drag is not supported yet
*/
public void exportAsDrag(JComponent comp,InputEvent e,int action) {
_defaultHandler.exportAsDrag(comp,e,action);
}
/**
* Exports the JXMLNoteDocument to the clipboard for the currently selected text.
*/
public void exportToClipboard(JComponent comp,Clipboard clip,int action) throws IllegalStateException {
//System.out.println(_defaultHandler.getClass());
_action=action;
if ((action == COPY || action == MOVE)
&& (getSourceActions(comp) & action) != 0) {
Transferable t = createTransferable(comp);
if (t != null) {
try {
clip.setContents(t, null); // clip.setContents(createTransferable(comp),this);
exportDone(comp, t, action);
return;
} catch (IllegalStateException ise) {
exportDone(comp, t, NONE);
throw ise;
}
}
}
exportDone(comp, null, NONE);
}
/**
* Imports the data for XMLNote documents (application/xmlnote+xml, text/html, or text/plain) are
* supported. If XMLNote is imported, the XMLNote Document that is imported may contain XMLNoteMarks.
* These XMLNoteMarks will be reassigned new ids(), if XMLNoteDocument.getMarkIdReallocator() on the
* document that the clipboard data is pasted into, is not null.
*/
public boolean importData(TransferHandler.TransferSupport support) {
return importData((JComponent) support.getComponent(),support.getTransferable(),support);
}
/**
* Imports the data for XMLNote documents (application/xmlnote+xml, text/html, or text/plain) are
* supported.
*/
public boolean importData(JComponent comp,Transferable t) {
return importData(comp,t,null);
}
private boolean importData(JComponent comp,Transferable t,TransferSupport support) {
DataFlavor flavor=flavorToImport(t.getTransferDataFlavors());
if (flavor!=null) {
try {
MimeType mt=new MimeType(flavor.getMimeType());
String baset=mt.getBaseType();
if (baset.equals(XMLNoteDataFlavor.mimetype())) {
String data=getDataAsString(t,flavor);
if (importXMLNoteData(data)) {
return true;
} else {
return importText(comp,t,support);
}
} else if (baset.equals("text/html")) {
String data=getDataAsString(t,flavor);
if (importHtmlData(data)) {
return true;
} else {
return importText(comp,t,support);
}
} else {
return importText(comp,t,support);
}
} catch (Exception E) {
DefaultXMLNoteErrorHandler.exception(E);
return false;
}
} else {
return false;
}
}
private boolean importText(JComponent comp,Transferable t,TransferSupport support) {
// TODO: Change this to something else.
if (support==null) {
return _defaultHandler.importData(comp,t);
} else {
return _defaultHandler.importData(support);
}
}
private boolean importXMLNoteData(String data) {
XMLNoteDocument doc=_editPane.getXMLNoteDoc();
boolean fl=doc.setLongEdit(true);
try {
XMLNoteDocument pasteDoc=new XMLNoteDocument(data,doc.getXMLNoteImageIconProvider(),doc.getStyles());
// Correct behaviour for pasting. Maybe due to a problem with copyInto.
// TODO: Check problem with copyInto()
pasteDoc.setMeta("startsWithParagraph", false);
pasteDoc.setMeta("endsWithParagraph", false);
if (doc.getMarkIdReassigner()!=null) {
pasteDoc.reassignMarkIds(doc.getMarkIdReassigner());
}
int start=_editPane.getSelectionStart();
int end=_editPane.getSelectionEnd();
if (start<end) { doc.remove(start, end-start); }
if (!doc.operationVetoed()) {
boolean pasteMarks=doc.getAllowMarksPasted();
boolean donotpaste=false;
Iterator<XMLNoteClipboardListener> it=doc.clipboardListenerIterator();
while (it.hasNext()) {
MarkHandling m=it.next().importData(pasteDoc, _editPane, start, Moment.BEFORE);
if (m==MarkHandling.DO_NOT_PASTE) { donotpaste=true; }
else if (m==MarkHandling.FORCE_PASTE) { pasteMarks=true; }
}
Vector<XMLNoteMark> adjustedMarks=doc.copyInto(pasteDoc, start, (donotpaste) ? false : pasteMarks);
it=doc.clipboardListenerIterator();
while (it.hasNext()) {
it.next().importMarks(pasteDoc,adjustedMarks,_editPane);
}
it=doc.clipboardListenerIterator();
while (it.hasNext()) {
it.next().importData(pasteDoc, _editPane, start, Moment.AFTER);
}
}
doc.setLongEdit(fl);
return true;
} catch (Exception E) {
doc.setLongEdit(fl);
DefaultXMLNoteErrorHandler.exception(E);
return false;
}
}
private boolean importHtmlData(String data) {
XMLNoteDocument doc=_editPane.getXMLNoteDoc();
boolean fl=doc.setLongEdit(true);
try {
String xhtml=HtmlToXHtml.fromHtml(data);
XMLNoteDocument pasteDoc=XHtmlToXMLNote.convert(xhtml,doc.getXMLNoteImageIconProvider(),doc.getStyles());
// Correct behaviour for pasting. Maybe due to a problem with copyInto.
// TODO: Check problem with copyInto()
pasteDoc.setMeta("startsWithParagraph", false);
pasteDoc.setMeta("endsWithParagraph", false);
int start=_editPane.getSelectionStart();
int end=_editPane.getSelectionEnd();
if (start<end) { doc.remove(start, end-start); }
doc.copyInto(pasteDoc, start, doc.getAllowMarksPasted());
doc.setLongEdit(fl);
return true;
} catch (Exception e) {
doc.setLongEdit(fl);
return false;
}
}
/**
* Returns <code>canImport((JComponent) support.component(),supprt.getDataFlavors());</code>
*/
public boolean canImport(TransferHandler.TransferSupport support) {
Component c=support.getComponent();
if (c instanceof JComponent) {
return canImport((JComponent) c,support.getDataFlavors());
} else {
return _defaultHandler.canImport(support);
}
}
/**
* Looks if one of the dataflavors for text/html, XMLNote or text/plain can be used.
*/
public boolean canImport(JComponent comp,DataFlavor[] transferFlavors) {
return flavorToImport(transferFlavors)!=null;
}
/**`
* @return COPY_OR_MOVE for an instance of JXMLNotePane
*/
public int getSourceActions(JComponent c) {
return _defaultHandler.getSourceActions(c);
}
/**
* @return a transferable for XMLNoteDocument. Supports three dataflavors:
* <code>text/plain</code>, <code>text/html</code> and <code>text/xml</code> (xmlnote xml).
*/
protected Transferable createTransferable(JComponent c) {
return new XMLNoteTransferable(_action);
}
/**
* Calls the previous handler of the JXMLNotePane.
*/
public Icon getVisualRepresentation(Transferable t) {
return _defaultHandler.getVisualRepresentation(t);
}
/**
* Removes text after copied to clipboard.
*/
protected void exportDone(JComponent source,Transferable data,int action) {
if (action == MOVE) {
XMLNoteTransferable t = (XMLNoteTransferable) data;
t.removeText();
}
}
/**
* Creates a new XMLNoteTransferHandler on a JXMLNotePane. This transferhandler is specific for
* JXMLNotePanes, not for any other type of JComponent.
*
*
* @param _edit
*/
public XMLNoteTransferHandler(JXMLNotePane editPane) {
_defaultHandler=editPane.getTransferHandler();
_editPane=editPane;
editPane.setTransferHandler(this);
// initialize accepted mimetypes per representation class
_acceptedClasses=new Hashtable<String,Set<String>>();
Set<String> htmlset=new HashSet<String>();
htmlset.add("text/html");
_acceptedClasses.put("java.nio.ByteBuffer",htmlset);
Set<String> otherset=new HashSet<String>();
otherset.add("text/plain");
otherset.add("application/xmlnote+xml");
_acceptedClasses.put("java.lang.String",otherset);
// initialize accepted charsets per representation class
_acceptedCharsets=new Hashtable<String,Set<String>>();
htmlset=new HashSet<String>();
htmlset.add("UTF-8");
_acceptedCharsets.put("java.nio.ByteBuffer",htmlset);
otherset=new HashSet<String>();
otherset.add("Unicode");
otherset.add("UTF-8");
_acceptedCharsets.put("java.lang.String",otherset);
}
}