/**
* 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;
}
}
}