/** * Copyright 1999-2009 The Pegadi Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.pegadi.artis; //UI imports import org.pegadi.util.XMLUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.swing.*; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.Style; import javax.swing.text.StyleContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.awt.datatransfer.ClipboardOwner; import java.awt.event.ActionEvent; import java.io.InputStream; import java.util.*; /** * An editor for editing the body of an {@link org.pegadi.model.Article Article}. * This is usually the main part of the article. * * This is a validating editor, mainly aimed at editing the new article structures defined in pegadi.xsd * * @version $Id$ */ public class ValidatingTextEditor extends AbstractTextEditor implements ClipboardOwner{ /** The document {@link #text} is editing. */ private ValidatingTextDocument textDoc; public AbstractTextDocument getTextDoc() { return textDoc; } public void setTextDoc(AbstractTextDocument textDoc) { this.textDoc = (ValidatingTextDocument) textDoc; } protected ResourceBundle styleTranslations; public ValidatingTextEditor(Element xml) { this(xml, null); } public ValidatingTextEditor(Element xml, Artis artis) { super(xml, artis); } /** * Sets the element to edit. * * @param xml The new element for the editor to use. */ public void setXML(Element xml) { xml.normalize(); Document doc = xml.getOwnerDocument(); String nodeName = xml.getNodeName(); String docName = doc.getDocumentElement().getTagName(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = null; try { factory.setIgnoringElementContentWhitespace(false); builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException pce) { log.error("Parser configuration exception", pce); } String schemaLocationAttrValue = doc.getDocumentElement().getAttribute("xsi:schemaLocation"); String[] locations = schemaLocationAttrValue.split(" ++"); String schemaLocation = locations[1]; //according to spec, always name-value pairs: namespace - location log.debug("Value of the schemaLocation attribute:" + schemaLocationAttrValue); for(int i = 0; i < locations.length; i++ ) { log.debug("location array number: " + i + ":" + locations[i]); } log.debug("About to fetch and parse schemaLocation: " + schemaLocation); try { schema = builder.parse(schemaLocation).getDocumentElement(); } catch (SAXException se) { log.error("couldn't parse text", se); } catch (java.io.IOException ioe) { // If fetching schema location failed, we try with filename only as a last resort. schemaLocation = doc.getDocumentElement().getAttribute("xsi:schemaLocation").substring(schemaLocation.lastIndexOf('/') + 1, schemaLocation.length()); InputStream stream = getClass().getClassLoader().getResourceAsStream(schemaLocation); log.info("io-exc: Trying once more without 'http://': ("+schemaLocation+")"); try { schema = builder.parse(stream).getDocumentElement(); } catch (SAXException se) { log.error("couldn't parse text the second time. Giving up.", se); return; } catch (java.io.IOException ioe2) { log.error("io-exc: Last try failed", ioe2); JOptionPane.showMessageDialog(this, textStr.getString("schemafetch_failed"), "", JOptionPane.ERROR_MESSAGE); return; } } Element schemaRoot = XMLUtil.getSchemaElement(docName,schema); Element schemaStart = XMLUtil.getSchemaReference(nodeName, schemaRoot); Hashtable allowed = createElementList(schemaStart); StyleContext styles = loadStyles(allowed); ValidatingTextDocument tmpDoc = new ValidatingTextDocument(xml, styles, xml.getOwnerDocument().getDoctype()); text.setStyledDocument(tmpDoc); textDoc = tmpDoc; textDoc.addEasterListener(new EasterListener() { public void easterEventHappened() { fireEaster(); } }); mElement = xml; text.addCaretListener(new CaretListener() { public void caretUpdate(CaretEvent e) { caretMoved(e); fireCaretUpdated(e); } }); textDoc.addDocumentListener(new DocumentListener() { public void insertUpdate(DocumentEvent e) { fireTextChanged(); } public void removeUpdate(DocumentEvent e) { fireTextChanged(); } public void changedUpdate(DocumentEvent e) {} }); textDoc.addUpdatePerformedListener(new UpdatePerformedListener() { public void updatePerformed(int offset) { fireUpdatePerformed(offset); } }); loadParagraphStyles(styleCombo, styles); } protected void fireUpdatePerformed(int offset) { updateStyleCombo(offset); } /** * This method is called when the paragraph style combo box * is changed. * * @param e The <code>ActionEvent</code> from the selection. */ protected void styleComboAction(ActionEvent e) { log.debug("styleComboAction fired."); if (styleChange) { javax.swing.text.Element para; log.debug("Selected style is: " + styleCombo.getSelectedItem()); Style s = textDoc.getStyle( ((StyleProperty) styleCombo.getSelectedItem()).getStyle()); if (s != null) { int start = Math.min(dot, mark), end = Math.max(dot, mark); for (int here = end; here >= start;here = para.getStartOffset()-1) { para = textDoc.getParagraphElement(here); textDoc.setParagraphAttributes(para.getStartOffset(), para.getEndOffset() - para.getStartOffset(), s, true); } fireTextChanged(); } } text.requestFocus(); } /** * Loads all the paragraph styles from the current document into * the <code>JComboBox</code>. * <br>A paragraph style is a style that * has a certain value for the <code>display</code> property. At * the moment the only valid value for this propery is <code>block</code>. * * @param list The <code>JComboBox</code> to fill. * @param styles The source of styles. */ protected void loadParagraphStyles(JComboBox list, StyleContext styles) { Enumeration e = styles.getStyleNames(); ResourceBundle t = loadStyleTranslations(); Style s; Object display; while (e.hasMoreElements()) { s = styles.getStyle(e.nextElement().toString()); display = s.getAttribute("display"); if (display != null && display.equals("block")) { // We have to make sure that styles are not added more // than once to ComboBox. int items = list.getItemCount(); boolean flag = false; for(int i=0;i<items;++i){ if( ((StyleProperty) list.getItemAt(i)).toString().equals(s.getName())){ flag = true; break; } } // FIXME: Here we should include shortkey description // in combobox in same style as the pulldown // menus. Description in plain text is horribly // ugly... (handegar) if(!flag) { StyleProperty sp = new StyleProperty(); sp.setDescription(t.getString(s.getName())); sp.setStyle(s.getName()); list.addItem(sp); } } } } /** * Loads style translations for the editor, as defined in * pegadi.artis.TextStyleTranslations.properties.<br> * These translations defines that the schema element 'p' shall be displayed as 'Paragraph' in the * style combo box. * * @return The styles defined in the file. If the loading fails * (likely source is an IOException) this function will return an empty hashtable. */ protected ResourceBundle loadStyleTranslations() { ResourceBundle translations; try { translations = ResourceBundle.getBundle("org.pegadi.artis.TextStyleTranslations"); Enumeration names = translations.getKeys(); while (names.hasMoreElements()) { String n = names.nextElement().toString(); String v = translations.getString(n); log.debug("Text style translation: " + n + "=" + v); } setStyleTranslations(translations); return translations; } catch (NullPointerException npe) { log.error("Unable to get URL for TextStyles.properties", npe); return null; } } protected void setStyleTranslations(ResourceBundle rb) { this.styleTranslations = rb; } /* (non-Javadoc) * @see org.pegadi.artis.TextEditor#createUI(java.util.Locale) */ protected void createUI(Locale loc) { super.createUI(loc); // add a listener to react on any caret (cursor) movement text.addCaretListener( new CaretListener() { public void caretUpdate(CaretEvent e) { log.debug("CARET MOVED...to posistion: " + e.getDot()); } }); } /** * Create a list of all allowed elements with regards to the schema. * * @param root The root node for the element which this editor is editing. * @return A list of elements, with the element name as key. */ protected Hashtable createElementList(Element root) { log.debug("Creating allowed element list from schema for root:" + root.getTagName()); Hashtable list = new Hashtable(10); Stack elements = new Stack(); elements.push(root); while (!elements.empty()) { Object o = elements.pop(); log.debug("Analysing : " + o.toString() + " which is: " + o.getClass().toString()); if (o instanceof Node) { NodeList nl = ((Node)o).getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node in = nl.item(i); if (in instanceof Element && ((Element)in).getTagName().equals("xsd:element")) { String name = ((Element)in).getAttribute("name"); log.debug("Node is an Element with name: " + name); if (name == null || name.equals("")) { name = ((Element)in).getAttribute("ref"); log.debug(" No name or null element. ref is :" + name); Element refElement = XMLUtil.getSchemaElement(name, schema); elements.push(refElement); } if (!"".equals(name)) { if (!list.containsKey(name)) { log.debug(" Name will be put in 'list': " + name); list.put(name, in); } } } else { log.debug(" XX: Node will be pushed to 'elements':" + in.getClass().toString()+", "+ in.toString()); elements.push(in); } } } else { if(o != null) log.warn("Object on stack is not a Node: " + o); } // Check if this node has a type reference if (o instanceof Element && ((Element)o).getTagName().equals("xsd:element")) { // If this is a type, add the (element) children of the type String type = ((Element)o).getAttribute("type"); // FIXME: This will also add native types, e.g. 'string' and 'integer' if (type != null) { Element typeElem = XMLUtil.getSchemaElement(type, schema); log.debug("Pushing type-element: " + type); elements.push(typeElem); } // if the element has a type reference } // Check if this node is a group construct if (o instanceof Element && ((Element)o).getTagName().equals("xsd:group")) { String ref = ((Element)o).getAttribute("ref"); log.debug("This is a <xsd:group> construct with reference: " + ref); if (ref != null) { Element refElem = XMLUtil.getSchemaElement(ref, schema); if (refElem != null) { log.debug("Pushing ref-element: " + ref); elements.push(refElem); } } } } // While the stack has elements log.debug("Elements found:" ); for (Object element : elements) { Element e = (Element) element; log.debug(", " + e.getTagName()); } log.debug("Hashtable:" ); Enumeration lk = list.keys(); while (lk.hasMoreElements()) { String key = (String) lk.nextElement(); log.debug(", " + key); } return list; } /** * Called when the caret is moved or the selection in the document is changed.<p> * Remember that the dot is always the position of the cursor, while mark is * the other end of the selection. Dragging the cursor from 2 to 5 will * give dot=5 and mark=2. * * @param e The event that triggered this method. */ protected void caretMoved(CaretEvent e) { log.debug("CaretMoved"); if (!cursorChange) { dot = e.getDot(); mark = e.getMark(); /** String name = getParagraphStyle(dot); log.debug("Current paragraph style is: " + name); if (name != null) { String[] childs = getAllowedElementsTest(name); if (childs != null || childs.length > 0) { log.debug("Number of allowed paragraph styles is: " + childs.length); for (int x=0;x < childs.length; x++) { log.debug("Child " + x + " is " + childs[x]); } styleChange = false; styleCombo.removeAllItems(); for (int i=0;i < childs.length; i++) { String n = childs[i]; log.debug("Allowed paragraph style nr. " + i + " is: " + n); StyleProperty sp = new StyleProperty(n); sp.setDescription(this.styleTranslations.getProperty(n)); styleCombo.addItem(sp); } } else log.debug("No allowable paragraph styles at caret location."); styleCombo.setSelectedItem(new StyleProperty(name)); styleCombo.revalidate(); styleChange = true; } */ updateStyleCombo(dot); updateCharacterButtons(); updateEditActions(); } } protected void updateStyleCombo(int offset) { log.debug("updateStyleCombo() with offset: " + offset); int paranum = textDoc.getParagraphNumber(offset); String name = getParagraphStyle(offset); String prevName = textDoc.getPreviousParagraphName(offset); log.debug("Current paragraph number is: "+ paranum); log.debug("Current paragraph style is : " + name); log.debug("Previous paragraph style is: " + prevName); if (name != null) { Vector elements = ValidationRules.getAllowedElements(paranum, name, prevName); // Vector elements = ValidationRules.getAllowedElements(name); if (elements != null && elements.size() > 0) { log.debug("Number of allowed paragraph styles is: " + elements.size() ); Iterator i = elements.iterator(); styleChange = false; styleCombo.removeAllItems(); while (i.hasNext()) { String n = (String) i.next(); log.debug("Allowed paragraph style is: " + n); StyleProperty sp = new StyleProperty(n); sp.setDescription(this.styleTranslations.getString(n)); styleCombo.addItem(sp); } } else log.debug("No allowable paragraph styles at caret location."); styleCombo.setSelectedItem(new StyleProperty(name)); styleCombo.revalidate(); styleChange = true; } } protected String[] getAllowedElementsTest(String currElement) { Vector v = new Vector(); if (currElement.equals("headline")) v.add("headline"); else if (currElement.equals("lead")) v.add("lead"); else if (currElement.equals("p")) { v.add("p"); v.add("subhead"); v.add("quote"); } else if (currElement.equals("subhead")) { v.add("subhead"); v.add("p"); v.add("quote"); } else if (currElement.equals("kicker")) v.add("kicker"); String[] a = {"dummy"}; return (String[]) v.toArray(a); /** if (currElement == "headline") tempElements[0] = "headline"; else if (currElement == "lead") tempElements[0] = "lead"; else if (currElement == "p") { tempElements[0] = "p"; tempElements[1]="subhead"; tempElements[2]= "quote"; } else if (currElement == "subhead") { tempElements[0] = "subhead"; tempElements[1]="p"; tempElements[2] ="quote"; } else if (currElement == "kicker") tempElements[0] = "kicker"; return tempElements; */ } protected String[] getAllowedElements(String currElement) { log.debug("getAllowedElements(): Finding allowed elements when current element is: " + currElement); Node elm = XMLUtil.getSchemaElement(currElement, schema); if (elm != null) { if (elm instanceof Element && ((Element)elm).getTagName().equals("xsd:element")) { String name = ((Element)elm).getAttribute("name"); int noChild = elm.getChildNodes().getLength(); log.debug("Element has " + noChild +" children"); log.debug("Node is an Element with name: " + name); if (name == null || name.equals("")) { name = ((Element)elm).getAttribute("ref"); log.debug(" No name or null element. ref is :" + name); Element refElement = XMLUtil.getSchemaElement(name, schema); } if (name != null && !name.equals("")) { } } } return null; } /** * Simple class to use for populating combo boxes. * */ protected class StyleProperty { protected String style; protected String description; public StyleProperty () { } public StyleProperty(String style) { setStyle(style); } /** * @return Returns the name. */ public String getStyle() { return style; } /** * @param name The name to set. */ public void setStyle(String name) { this.style = name; } /** * @return Returns the value. */ public String getDescription() { return description; } /** * @param value The value to set. */ public void setDescription(String value) { this.description = value; } /** * Returns the description of the StyleProperty. */ public String toString() { return this.getDescription(); } public boolean equals(Object o) { if (o instanceof StyleProperty) { return ((StyleProperty) o).getStyle().equals(this.getStyle()); } else return false; } } }