/* ****************************************************************************** * * Copyright 2008-2013 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.document; import java.awt.Dimension; import java.io.IOException; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Stack; import java.util.Vector; import javax.swing.text.BadLocationException; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import nl.dykema.jxmlnote.exceptions.BadDocumentException; import nl.dykema.jxmlnote.exceptions.BadMetaException; import nl.dykema.jxmlnote.exceptions.BadStyleException; import nl.dykema.jxmlnote.exceptions.DefaultXMLNoteErrorHandler; import nl.dykema.jxmlnote.exceptions.MarkExistsException; import nl.dykema.jxmlnote.exceptions.NoStyleException; import nl.dykema.jxmlnote.internationalization.DefaultXMLNoteTranslator; import nl.dykema.jxmlnote.internationalization.XMLNoteTranslator; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class XMLNoteXMLIn extends DefaultHandler { private class MXMLNoteMark extends XMLNoteMark { private int _end=-1; private int _offset=-1; public String content() { return null; } public Integer offset() { return _offset; } public Integer endOffset() { return _end; } public void endOffset(int e) { _end=e; } public MXMLNoteMark(String _id,int offset) { super(_id); _offset=offset; } } private class ImageData { private String url; private String id; private String description; private XMLNoteImageIconSize size; String getId() { return id; } URL getUrl() throws MalformedURLException { if (url==null) { return null; } else if (url.trim().equals("")) { return null; } else { return new URL(url); } } String getDescription() { return description; } XMLNoteImageIconSize getSize() { return size; } public ImageData(Attributes a) { url=a.getValue("url"); description=a.getValue("description"); id=a.getValue("id"); String width_in_pt=a.getValue("width_in_pt"); String height_in_pt=a.getValue("height_in_pt"); String width_in_px=a.getValue("width_in_px"); String height_in_px=a.getValue("height_in_px"); if (width_in_pt==null && height_in_pt==null) { int w=(width_in_px==null) ? -1 : Integer.parseInt(width_in_px); int h=(height_in_px==null) ? -1 : Integer.parseInt(height_in_px); if (h==0) { h=-1; } if (w==0) { w=-1; } size=new XMLNoteImageIconSize(w,h,XMLNoteImageIconSize.TYPE_PX); } else { int w=(width_in_pt==null) ? -1 : Integer.parseInt(width_in_pt); int h=(height_in_pt==null) ? -1 : Integer.parseInt(height_in_pt); if (h==0) { h=-1; } if (w==0) { w=-1; } size=new XMLNoteImageIconSize(w,h,XMLNoteImageIconSize.TYPE_PT); } } } private XMLNoteTranslator _translator=null; private XMLNoteDocument _doc=null; private static SAXParserFactory _factory=null; private Stack<DefaultHandler> _handlerStack=null; private Stack<String> _xpath=null; private Hashtable<String,MXMLNoteMark> _marks=null; private Hashtable<String,MXMLNoteMark> _incompleteMarks=null; private Integer getAlign(String align) { if (align==null) { return null; } else if (align.equals("left")) { return StyleConstants.ALIGN_LEFT; } else if (align.equals("right")) { return StyleConstants.ALIGN_RIGHT; } else if (align.equals("center")) { return StyleConstants.ALIGN_CENTER; } else if (align.equals("justify")) { return StyleConstants.ALIGN_JUSTIFIED; } else { return null; } } private Integer getIndent(String indent) { if (indent==null) { return null; } else { try { return Integer.parseInt(indent); } catch (NumberFormatException e) { return null; } } } private void unexpected(String qName,String expected) throws SAXException { String form=_translator.translate("E10010:xmlnote: unexpected element '%s', expected '%s'"); if (_xpath.isEmpty()) { String msg=String.format(form,"//"+qName,expected); throw new SAXException(msg); } else { String msg=String.format(form,_xpath.peek()+"/"+qName,expected); throw new SAXException(msg); } } private void wrongAttribute(String qName,String attr,String expected) throws SAXException { String form=_translator.translate("E10020:xmlnote: wrong attribute '%s', expected '%s'"); if (_xpath.isEmpty()) { throw new SAXException(String.format(form,"//"+qName+"@"+attr,expected)); } else { throw new SAXException(String.format(form,_xpath.peek()+"/"+qName+"@"+attr,expected)); } } private void pushNew(String qName) { if (_xpath.isEmpty()) { _xpath.push("//"+qName); } else { _xpath.push(_xpath.peek()+"/"+qName); } } private class UnexpectedHandler extends DefaultHandler { public void startElement(String uri,String localName,String qName,Attributes attributes) throws SAXException { unexpected(qName,"<nothing>"); } } private class ApplyAttributes { private int _start; private int _end; private SimpleAttributeSet _set; private ImageData _icon; public void setEnd(int endOffset) { _end=endOffset; } private XMLNoteImageIcon getIcon() throws SAXException { XMLNoteImageIcon.Provider prov=_doc.getXMLNoteImageIconProvider(); XMLNoteImageIcon icn=null; String description="no description"; XMLNoteImageIconSize size=new XMLNoteImageIconSize(-1,-1,XMLNoteImageIconSize.TYPE_PT); if (prov==null) { try { URL url=_icon.getUrl(); if (url==null) { String id=_icon.getId(); if (id!=null) { throw new SAXException("Image with id '"+id+"', but no XMLNoteImageIcon.Provider set on document"); } else { throw new SAXException("Image with no URL, can't load that image"); } } else { String d=_icon.getDescription(); if (d==null) { d=description; } XMLNoteImageIconSize s=_icon.getSize(); icn=new XMLNoteImageIcon(url,d,s); } } catch (MalformedURLException e) { throw new SAXException(e); } } else { String id=_icon.getId(); if (id!=null) { icn=prov.getIcon(id); } else { try { URL url=_icon.getUrl(); String d=_icon.getDescription(); if (d==null) { d=description; } XMLNoteImageIconSize s=_icon.getSize(); if (url==null) { throw new SAXException("Image with no URL, can't load that image"); } icn=prov.getIcon(url,d,s); } catch (MalformedURLException e) { throw new SAXException(e); } } } return icn; } public void applyAttributes(XMLNoteDocument doc,int parOffset) throws SAXException { if (_icon==null) { doc.setCharacterAttributes(parOffset+_start, _end-_start, _set, false); } else { SimpleAttributeSet set=new SimpleAttributeSet(); XMLNoteImageIcon icn=getIcon(); if (icn!=null) { StyleConstants.setIcon(set, icn); doc.setCharacterAttributes(parOffset+_start, _end-_start, set, true); } } } public ApplyAttributes(SimpleAttributeSet set,int startOffset) { _set=set; _start=startOffset; _icon=null; } public ApplyAttributes(ImageData icn,int startOffset) { _icon=icn; _start=startOffset; _set=null; } } private class ParHandler extends DefaultHandler { private String _name; private String _align; private String _indent; private String _txt; private Vector<ApplyAttributes> _attrs; private ApplyAttributes _bold; private ApplyAttributes _italic; private ApplyAttributes _underline; private ApplyAttributes _image; private ParsHandler _pars; private int _docOffset; public String getText() { return _txt; } public String styleId() { return _name; } public String align() { return _align; } public String indent() { return _indent; } public void applyMarkup(XMLNoteDocument d,int offset) throws SAXException { Iterator<ApplyAttributes> it=_attrs.iterator(); while (it.hasNext()) { it.next().applyAttributes(d,offset); } } public void startElement(String uri,String localName,String qName,Attributes attributes) throws SAXException { if (qName.equals("enter")) { _txt+="\n"; } else if (qName.equals("tab")) { _txt+="\t"; } else if (qName.equals("space")) { _txt+=" "; } else if (qName.equals("b")) { SimpleAttributeSet s=new SimpleAttributeSet(); s.addAttribute(StyleConstants.Bold, true); _bold=new ApplyAttributes(s,_txt.length()); } else if (qName.equals("i")) { SimpleAttributeSet s=new SimpleAttributeSet(); s.addAttribute(StyleConstants.Italic,true); _italic=new ApplyAttributes(s,_txt.length()); } else if (qName.equals("u")) { SimpleAttributeSet s=new SimpleAttributeSet(); s.addAttribute(StyleConstants.Underline,true); _underline=new ApplyAttributes(s,_txt.length()); } else if (qName.equals("mark")) { String _id=attributes.getValue("id"); String _class=attributes.getValue("class"); String _type=attributes.getValue("type"); if (_id==null) { String msg=_translator.translate("id attribute is required"); wrongAttribute(qName,"id",msg); } if (_type==null) { String msg=_translator.translate("type attribute is required"); wrongAttribute(qName,"type",msg); } if (!_type.equals("end") && !_type.equals("start")) { String form=_translator.translate("type attribute must be of value 'start' or 'end', got '%s'"); wrongAttribute(qName,"type",String.format(form,_type)); } if (_type.equals("start")) { //System.out.println("mark[str] = "+_id); MXMLNoteMark m=new MXMLNoteMark(_id,_docOffset+_txt.length()); m.markClass(_class); if (_marks.get(_id)==null) { MXMLNoteMark m1 = _incompleteMarks.get(_id); if (m1 != null) { // found an end mark before a start mark, resolve //System.out.println("mark[inc] = "+_id+", end offset = "+m1.endOffset()); m.endOffset(m1.endOffset()); _incompleteMarks.remove(_id); } _marks.put(_id, m); } else { String form=_translator.translate("E10001:Found a start of mark with id '%s'. A mark with this id already exists"); throw new SAXException(String.format(form,_id)); } } else if (_type.equals("end")) { //System.out.println("mark[end] = "+_id); MXMLNoteMark m=_marks.get(_id); if (m==null) { // Workaround for crash, where no start offset has been found. // Normally this is a result of a store of an empty start - end mark, // which can result in the storage of the end mark, followed by the start mark, // which isn't what you want. So we just ignore end marks without start marks // and ignore start marks that were already encountered as end marks. // Maybe we need to Garbage collect some tables for the marks. MXMLNoteMark m1 = new MXMLNoteMark(_id,_docOffset+_txt.length()); m1.endOffset(_docOffset+_txt.length()); m1.markClass(_class); _incompleteMarks.put(_id, m1); //String form=_translator.translate("E10002:Found an end of mark with id '%s' without a start"); //throw new SAXException(String.format(form,_id)); } else { m.endOffset(_docOffset+_txt.length()); } } } else if (qName.equals("image")) { _image=new ApplyAttributes(new ImageData(attributes),_txt.length()); //_txt+="i"; } else { unexpected(qName,"notes"); } } public void characters(char[] ch, int start, int length) throws SAXException { String s=new String(ch,start,length); s.replaceAll("\\s", ""); _txt+=s; } public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("enter")) { // nothing to do } else if (qName.equals("tab")) { // nothing to do } else if (qName.equals("space")) { // nothing to do } else if (qName.equals("mark")) { // nothing to do } else if (qName.equals("image")) { _image.setEnd(_txt.length()); _attrs.add(_image); _image=null; } else if (qName.equals("b")) { _bold.setEnd(_txt.length()); _attrs.add(_bold); _bold=null; } else if (qName.equals("i")) { _italic.setEnd(_txt.length()); _attrs.add(_italic); _italic=null; } else if (qName.equals("u")) { _underline.setEnd(_txt.length()); _attrs.add(_underline); _underline=null; } else { // unhandled situation, next of stack. _pars.endElement(uri, localName, qName); } } public ParHandler(String parName,String align,String indent,ParsHandler h,int currentTextOffset) { _pars=h; _name=parName; _align=align; _indent=indent; _txt=""; _attrs=new Vector<ApplyAttributes>(); _docOffset=currentTextOffset; } } private class ParsHandler extends DefaultHandler { public void startElement(String uri,String localName,String qName,Attributes attributes) throws SAXException { if (qName.equals("par") || qName.equals("h1") || qName.equals("h2") || qName.equals("h3") || qName.equals("h4") || qName.equals("sp")) { String styleId=qName; if (qName.equals("sp")) { styleId=attributes.getValue("style"); // be tolerant of a style less sp if (styleId==null) { styleId="par"; } } String align=attributes.getValue("align"); String indent=attributes.getValue("indent"); _handlerStack.push(new ParHandler(styleId,align,indent,this,_doc.getLength())); pushNew(qName); } else { unexpected(qName,"par|h1|h2|h3|h4|sp"); } } public void endElement(String uri, String localName, String qName) throws SAXException { DefaultHandler h=_handlerStack.pop(); _xpath.pop(); if (h instanceof ParHandler) { ParHandler p=(ParHandler) h; try { int offset=_doc.getLength(); try { String align=p.align(); String indent=p.indent(); //System.out.println(p.styleId()); _doc.addParagraph(p.getText(),p.styleId(),getAlign(align),getIndent(indent)); } catch (NoStyleException e2) { throw new SAXException(e2); } catch (BadStyleException e3) { throw new SAXException(e3); } p.applyMarkup(_doc,offset); } catch (BadLocationException e) { throw new SAXException(e); } } } } private class NotesHandler extends DefaultHandler { public void startElement(String uri,String localName,String qName,Attributes attributes) throws SAXException { if (qName.equals("notes")) { _handlerStack.push(new ParsHandler()); pushNew(qName); } else { unexpected(qName,"notes"); } } public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("notes")) { _handlerStack.pop(); _xpath.pop(); } } } private class MetaParamHandler extends DefaultHandler { public void startElement(String uri,String localName,String qName,Attributes attributes) throws SAXException { if (qName.equals("param")) { String name=attributes.getValue("name"); String type=attributes.getValue("type"); String value=attributes.getValue("value"); try { _doc.setMeta(name,type,value); } catch (BadMetaException e) { throw new SAXException(e); } } else { unexpected(qName,"param"); } } public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("meta")) { _handlerStack.pop(); _handlerStack.peek().endElement(uri, localName, qName); } } } private class MetaHandler extends DefaultHandler { public void startElement(String uri,String localName,String qName,Attributes attributes) throws SAXException { if (qName.equals("meta")) { _handlerStack.push(new MetaParamHandler()); pushNew(qName); } else { unexpected(qName,"meta"); } } public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("meta")) { _handlerStack.pop(); _xpath.pop(); } } } public void startElement(String uri,String localName,String qName,Attributes attributes) throws SAXException { if (_handlerStack.isEmpty()) { if (qName.equals("xmlnote")) { String version=attributes.getValue("version"); if (version==null) { String msg=_translator.translate("version attribute is required"); wrongAttribute(qName,"version",msg); } else if (!version.equals("2010.1")) { String form=_translator.translate("wrong version value. Got '%s', expected: 2010.1"); wrongAttribute(qName,"version",String.format(form,version)); } else { _handlerStack.push(new UnexpectedHandler()); _handlerStack.push(new NotesHandler());_xpath.push("//xmlnote"); _handlerStack.push(new MetaHandler());_xpath.push("//xmlnote"); } } else { unexpected(qName,"//xmlnote"); } } else { _handlerStack.peek().startElement(uri, localName, qName, attributes); } } public void endElement(String uri, String localName, String qName) throws SAXException { if (_handlerStack.isEmpty()) { if (!qName.equals("xmlnote")) { unexpected(qName,"xmlnote"); } } else { _handlerStack.peek().endElement(uri, localName, qName); } } public void characters(char[] ch, int start, int length) throws SAXException { if (_handlerStack.isEmpty()) { // do nothing } else { _handlerStack.peek().characters(ch,start,length); } } public void fromXML(String xml,XMLNoteImageIcon.Provider prov) throws BadDocumentException { //System.out.println(xml); InputSource source=new InputSource(new StringReader(xml)); if (prov!=null) { _doc.setXMLNoteImageIconProvider(prov); } boolean ignore=_doc.getUndoManager().setIgnore(true); try { SAXParser parser=_factory.newSAXParser(); try { parser.parse(source, this); Enumeration<MXMLNoteMark> en=_marks.elements(); while(en.hasMoreElements()) { MXMLNoteMark mark=en.nextElement(); if (mark.endOffset()==-1) { throw new BadDocumentException(String.format(_translator.translate("E10003:xmlnote Mark without end (mark id='%s')"), mark.id() ) ); } else { try { _doc.insertMark(mark.id(),mark.markClass(), mark.offset(), mark.endOffset()-mark.offset()); } catch (BadLocationException e) { throw new BadDocumentException(e); } catch (MarkExistsException e) { throw new BadDocumentException(e); } } } if (_incompleteMarks.size() > 0) { throw new BadDocumentException(String.format(_translator.translate("E10004:%d xmlnote End Mark(s) without start"), _incompleteMarks.size() ) ); } } catch (IOException e) { throw new BadDocumentException(e); } } catch (ParserConfigurationException e) { throw new BadDocumentException(e); } catch (SAXException e) { throw new BadDocumentException(e); } _doc.getUndoManager().setIgnore(ignore); _doc.getUndoManager().discardAllEdits(); } public XMLNoteXMLIn(XMLNoteDocument d) { _translator=new DefaultXMLNoteTranslator(); _doc=d; if (_factory==null) { _factory=SAXParserFactory.newInstance(); } _xpath=new Stack<String>(); _handlerStack=new Stack<DefaultHandler>(); _marks=new Hashtable<String,MXMLNoteMark>(); _incompleteMarks=new Hashtable<String,MXMLNoteMark>(); } }