package tk.eclipse.plugin.xmleditor.editors; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import jp.aonir.fuzzyxml.FuzzyXMLDocType; import jp.aonir.fuzzyxml.FuzzyXMLDocument; import jp.aonir.fuzzyxml.FuzzyXMLElement; import jp.aonir.fuzzyxml.FuzzyXMLNode; import jp.aonir.fuzzyxml.FuzzyXMLParser; import jp.aonir.fuzzyxml.internal.RenderContext; import jp.aonir.fuzzyxml.internal.WOHTMLRenderDelegate; import org.apache.xerces.parsers.SAXParser; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.resource.StringConverter; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IFileEditorInput; import org.objectstyle.wolips.wodclipse.core.Activator; import org.objectstyle.wolips.wodclipse.core.preferences.PreferenceConstants; import org.xml.sax.InputSource; import tk.eclipse.plugin.htmleditor.HTMLPlugin; import tk.eclipse.plugin.htmleditor.HTMLProjectParams; import tk.eclipse.plugin.htmleditor.HTMLUtil; import tk.eclipse.plugin.htmleditor.editors.HTMLConfiguration; import tk.eclipse.plugin.htmleditor.editors.HTMLSourceEditor; import tk.eclipse.plugin.htmleditor.editors.IHTMLOutlinePage; /** * The XML editor. * * @author Naoki Takezoe */ public class XMLEditor extends HTMLSourceEditor { private ArrayList<IDTDResolver> resolvers = new ArrayList<IDTDResolver>(); public static final String GROUP_XML = "_xml"; public static final String ACTION_GEN_DTD = "_generate_dtd"; public static final String ACTION_GEN_XSD = "_generate_xsd"; public static final String ACTION_ESCAPE_XML = "_escape_xml"; public static final String ACTION_FORMAT_XML = "_format_xml"; private String[] _classNameAttributes = null; private List<ElementSchemaMapping> _schemaMappings = null; /** * The constructor. */ public XMLEditor() { this(new XMLConfiguration(HTMLPlugin.getDefault().getColorProvider())); } /** * The constructor for customize this editor. * <p> * This editor is initialized with the given <code>XMLConfiguration</code>. * * @param config the editor configuration for this editor */ public XMLEditor(XMLConfiguration config){ super(config); setAction(ACTION_GEN_DTD,new GenerateDTDAction()); setAction(ACTION_GEN_XSD,new GenerateXSDAction()); setAction(ACTION_ESCAPE_XML, new EscapeXMLAction()); } @Override protected void createActions() { super.createActions(); // add content content format action setAction(ACTION_FORMAT_XML, new FormatXMLAction()); } /** This method is called when configuration is changed. */ @Override protected void handlePreferenceStoreChanged(PropertyChangeEvent event){ super.handlePreferenceStoreChanged(event); _classNameAttributes = null; _schemaMappings = null; } public List<ElementSchemaMapping> getSchemaMappings(){ if(_schemaMappings==null){ _schemaMappings = ElementSchemaMapping.loadFromPreference(); } return _schemaMappings; } public String[] getClassNameAttributes(){ if(_classNameAttributes==null){ // Load classname attrs from the preference store IPreferenceStore store = getPreferenceStore(); if(store.getBoolean(HTMLPlugin.PREF_ENABLE_CLASSNAME)){ _classNameAttributes = StringConverter.asArray( store.getString(HTMLPlugin.PREF_CLASSNAME_ATTRS)); } else { _classNameAttributes = new String[0]; } } return _classNameAttributes; } /** * Returns the <code>XMLOutlinePage</code>. * * @see XMLOutlinePage */ @Override protected IHTMLOutlinePage createOutlinePage(){ return new XMLOutlinePage(this); } /** * Adds <code>IDTDResolver</code>. * * @param resolver IDTDResolver */ public void addDTDResolver(IDTDResolver resolver){ resolvers.add(resolver); } /** * Returns an array of <code>IDTDResolver</code> * that was added by <code>addEntityResolver()</code>. * * @return an array of <code>IDTDResolver</code> */ public IDTDResolver[] getDTDResolvers(){ return resolvers.toArray(new IDTDResolver[resolvers.size()]); } /** * Validates the XML document. * <p> * If <code>getValidation()</code> returns <code>false</code>, * this method do nothing. */ @Override protected void doValidate(){ try { ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { try { IFileEditorInput input = (IFileEditorInput)getEditorInput(); String xml = getDocumentProvider().getDocument(input).get(); IFile resource = input.getFile(); //String charset = resource.getCharset(); //charset = "Shift_JIS"; resource.deleteMarkers(IMarker.PROBLEM,false,0); HTMLProjectParams params = new HTMLProjectParams(resource.getProject()); if(!params.getValidateXML()){ return; } if(params.getUseDTD()==false){ // remove DOCTYPE decl Matcher matcher = patternDoctypePublic.matcher(xml); if(matcher.find()){ xml = removeMatched(xml,matcher.start(),matcher.end()); } matcher = patternDoctypeSystem.matcher(xml); if(matcher.find()){ xml = removeMatched(xml,matcher.start(),matcher.end()); } } SAXParser parser = new SAXParser(); String dtd = getDTD(xml, false); String[] xsd = getXSD(xml, false); // Validation configuration if((dtd==null && xsd==null) || !params.getUseDTD()){ parser.setFeature("http://xml.org/sax/features/validation", false); } else { parser.setFeature("http://xml.org/sax/features/validation", true); parser.setFeature("http://apache.org/xml/features/continue-after-fatal-error", true); } if(xsd!=null && params.getUseDTD()){ // parser.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema"); // parser.setProperty("http://java.sun.com/xml/jaxp/properties/schemaSource", xsd); parser.setFeature("http://apache.org/xml/features/validation/schema", true); parser.setFeature("http://xml.org/sax/features/namespaces", true); } parser.setFeature("http://xml.org/sax/features/use-entity-resolver2", true); parser.setEntityResolver(new DTDResolver(getDTDResolvers(), input.getFile().getLocation().makeAbsolute().toFile().getParentFile())); parser.setErrorHandler(new XMLValidationHandler(resource)); parser.parse(new InputSource(new StringReader(xml))); //new ByteArrayInputStream(xml.getBytes(charset)))); } catch(Exception ex){ // ignore //HTMLPlugin.logException(ex); } } },null); } catch(Exception ex){ HTMLPlugin.logException(ex); } } /** replace to whitespaces */ private String removeMatched(String source,int start,int end){ StringBuffer sb = new StringBuffer(); sb.append(source.substring(0,start)); for(int i=start;i<end + 1;i++){ char c = source.charAt(i); if(c=='\r' || c=='\n'){ sb.append(c); } else { sb.append(" "); } } sb.append(source.substring(end+1,source.length())); return sb.toString(); } /** * Returns URI of DTD (SystemID) which is used in the document. * If any DTD isn't used, this method returns <code>null</code>. * * @param xml XML * @return URL of DTD */ public String getDTD(String xml, boolean useElementMapping){ // PUBLIC Identifier Matcher matcher = patternDoctypePublic.matcher(xml); if(matcher.find()){ return matcher.group(2); } // SYSTEM Identifier matcher = patternDoctypeSystem.matcher(xml); if(matcher.find()){ return matcher.group(1); } // Root element mappings if(useElementMapping){ String firstTag = getFirstTag(xml); if(firstTag!=null){ List<ElementSchemaMapping> schemaMappings = getSchemaMappings(); for(int i=0;i<schemaMappings.size();i++){ ElementSchemaMapping mapping = schemaMappings.get(i); if(mapping.getRootElement().equals(firstTag) && mapping.getFilePath().endsWith(".dtd")){ return "file:" + mapping.getFilePath(); } } } } return null; } /** * Returns URI (schema location) of XML schema which is used in the document. * If any XML schema isn't used, this method returns <code>null</code>. * * @param xml XML * @return URL of XML schema */ public String[] getXSD(String xml, boolean useElementMapping){ // PUBLIC Identifier Matcher matcher = patternNsXSD.matcher(xml); if(matcher.find()){ String matched = matcher.group(1).trim(); matched.replaceAll("\r\n","\n"); matched.replaceAll("\r","\n"); String[] xsd = matched.split("\n| |\t"); for(int i=0;i<xsd.length;i++){ xsd[i] = xsd[i].trim(); } return xsd; } matcher = patternNoNsXSD.matcher(xml); if(matcher.find()){ return new String[]{matcher.group(3).trim()}; } // Root element mappings if(useElementMapping){ String firstTag = getFirstTag(xml); if(firstTag!=null){ List schemaMappings = getSchemaMappings(); for(int i=0;i<schemaMappings.size();i++){ ElementSchemaMapping mapping = (ElementSchemaMapping)schemaMappings.get(i); if(mapping.getRootElement().equals(firstTag) && mapping.getFilePath().endsWith(".xsd")){ return new String[]{ "file:" + mapping.getFilePath() }; } } } } return null; } /** * Extracts the first element name in the given xml source. */ private static String getFirstTag(String xml){ FuzzyXMLDocument doc = new FuzzyXMLParser(false).parse(xml); FuzzyXMLNode[] nodes = doc.getDocumentElement().getChildren(); for(int i=0;i<nodes.length;i++){ if(nodes[i] instanceof FuzzyXMLElement){ return ((FuzzyXMLElement)nodes[i]).getName(); } } return null; } /** Reular expressions to get DOCTYPE declaration */ private Pattern patternDoctypePublic = Pattern.compile("<!DOCTYPE[\\s\r\n]+?[^<]+?[\\s\r\n]+?PUBLIC[\\s\r\n]*?\"(.+?)\"[\\s\r\n]*?\"(.+?)\".*?>",Pattern.DOTALL); private Pattern patternDoctypeSystem = Pattern.compile("<!DOCTYPE[\\s\r\n]+?[^<]+?[\\s\r\n]+?SYSTEM[\\s\r\n]*?\"(.+?)\".*?>",Pattern.DOTALL); /** Reular expressions to get schema location of XMLschema */ private Pattern patternNsXSD = Pattern.compile("schemaLocation[\\s\r\n]*?=[\\s\r\n]*?\"(.+?)\"",Pattern.DOTALL); private Pattern patternNoNsXSD = Pattern.compile("noNamespaceSchemaLocation[\\s\r\n]*?=[\\s\r\n]*?\"(.+?)\"",Pattern.DOTALL); /** * Update informations about code-completion. */ @Override protected void updateAssist(){ try { XMLConfiguration config = (XMLConfiguration)getSourceViewerConfiguration(); config.getClassNameHyperlinkProvider().setEditor(this); if(!isFileEditorInput()){ return; } super.updateAssist(); IFileEditorInput input = (IFileEditorInput)getEditorInput(); HTMLProjectParams params = new HTMLProjectParams(input.getFile().getProject()); if(params.getUseDTD()==false){ return; } String xml = getDocumentProvider().getDocument(input).get(); // Update DTD based completion information. String dtd = getDTD(xml, true); if(dtd!=null){ DTDResolver resolver = new DTDResolver(getDTDResolvers(), input.getFile().getLocation().makeAbsolute().toFile().getParentFile()); InputStream in = resolver.getInputStream(dtd); if(in!=null){ Reader reader = new InputStreamReader(in); // update AssistProcessor XMLAssistProcessor assistProcessor = (XMLAssistProcessor)((HTMLConfiguration)getSourceViewerConfiguration()).getAssistProcessor(); assistProcessor.updateDTDInfo(reader); reader.close(); } } // Update XML Schema based completion information. String[] xsd = getXSD(xml, true); if(xsd!=null){ DTDResolver resolver = new DTDResolver(getDTDResolvers(), input.getFile().getLocation().makeAbsolute().toFile().getParentFile()); for(int i=0;i<xsd.length;i++){ InputStream in = resolver.getInputStream(xsd[i]); if(in!=null){ Reader reader = new InputStreamReader(in); // update AssistProcessor XMLAssistProcessor assistProcessor = (XMLAssistProcessor)((HTMLConfiguration)getSourceViewerConfiguration()).getAssistProcessor(); assistProcessor.updateXSDInfo(xsd[i],reader); reader.close(); } } } } catch(Exception ex){ HTMLPlugin.logException(ex); } } @Override protected void addContextMenuActions(IMenuManager menu){ menu.add(new Separator(GROUP_HTML)); //addAction(menu,GROUP_HTML,ACTION_OPEN_PALETTE); addAction(menu,GROUP_HTML,ACTION_ESCAPE_XML); addAction(menu,GROUP_HTML,ACTION_COMMENT); addAction(menu,GROUP_HTML,ACTION_FORMAT_XML); menu.add(new Separator(GROUP_XML)); addAction(menu,GROUP_XML,ACTION_GEN_DTD); addAction(menu,GROUP_XML,ACTION_GEN_XSD); } @Override protected void updateSelectionDependentActions() { super.updateSelectionDependentActions(); ITextSelection sel = (ITextSelection)getSelectionProvider().getSelection(); if(sel.getText().equals("")){ getAction(ACTION_ESCAPE_XML).setEnabled(false); } else { getAction(ACTION_ESCAPE_XML).setEnabled(true); } } //////////////////////////////////////////////////////////////////////////// // actions //////////////////////////////////////////////////////////////////////////// /** * The action to escape XML special chars in the selection. */ private class EscapeXMLAction extends Action { public EscapeXMLAction(){ super(HTMLPlugin.getResourceString("HTMLEditor.EscapeAction")); setEnabled(false); setAccelerator(SWT.CTRL | '\\'); } @Override public void run() { ITextSelection sel = (ITextSelection)getSelectionProvider().getSelection(); IDocument doc = getDocumentProvider().getDocument(getEditorInput()); try { doc.replace(sel.getOffset(),sel.getLength(),HTMLUtil.escapeXML(sel.getText())); } catch (BadLocationException e) { HTMLPlugin.logException(e); } } } /** * The action to generate DTD from XML. */ private class GenerateDTDAction extends Action { public GenerateDTDAction(){ super(HTMLPlugin.getResourceString("XMLEditor.GenerateDTD"), HTMLPlugin.getDefault().getImageRegistry().getDescriptor(HTMLPlugin.ICON_DTD)); } @Override public void run() { FileDialog dialog = new FileDialog(getViewer().getTextWidget().getShell(),SWT.SAVE); dialog.setFilterExtensions(new String[]{"*.dtd"}); String file = dialog.open(); if(file!=null){ try { SchemaGenerator.generateDTDFromXML(getFile(), new File(file)); } catch(Exception ex){ HTMLPlugin.openAlertDialog(ex.toString()); } } } } /** * The action to generate XML schema from XML. */ private class GenerateXSDAction extends Action { public GenerateXSDAction(){ super(HTMLPlugin.getResourceString("XMLEditor.GenerateXSD"), HTMLPlugin.getDefault().getImageRegistry().getDescriptor(HTMLPlugin.ICON_XSD)); } @Override public void run() { FileDialog dialog = new FileDialog(getViewer().getTextWidget().getShell(),SWT.SAVE); dialog.setFilterExtensions(new String[]{"*.xsd"}); String file = dialog.open(); if(file!=null){ try { SchemaGenerator.generateXSDFromXML(getFile(), new File(file)); } catch(Exception ex){ HTMLPlugin.openAlertDialog(ex.toString()); } } } } /** * The action to format (correct indentation) XML. */ private class FormatXMLAction extends Action { public FormatXMLAction(){ super(HTMLPlugin.getResourceString("XMLEditor.FormatXML")); setActionDefinitionId("tk.eclipse.plugin.xmleditor.format"); } @Override public void run(){ try { IDocument xmlDocument = getDocumentProvider().getDocument(getEditorInput()); String xmlString = xmlDocument.get(); FuzzyXMLDocument htmlModel = new FuzzyXMLParser(false, false).parse(xmlString); FuzzyXMLElement documentElement = htmlModel.getDocumentElement(); IPreferenceStore prefs = Activator.getDefault().getPreferenceStore(); RenderContext renderContext = new RenderContext(true); renderContext.setShowNewlines(true); renderContext.setIndentSize(prefs.getInt(PreferenceConstants.INDENT_SIZE)); renderContext.setIndentTabs(prefs.getBoolean(PreferenceConstants.INDENT_TABS)); renderContext.setTrim(true); renderContext.setLowercaseAttributes(prefs.getBoolean(PreferenceConstants.LOWERCASE_ATTRIBUTES)); renderContext.setLowercaseTags(prefs.getBoolean(PreferenceConstants.LOWERCASE_TAGS)); renderContext.setSpacesAroundEquals(prefs.getBoolean(PreferenceConstants.SPACES_AROUND_EQUALS)); renderContext.setSpaceInEmptyTags(true); renderContext.setAddMissingQuotes(true); renderContext.setDelegate(new WOHTMLRenderDelegate(prefs.getBoolean(PreferenceConstants.STICKY_WOTAGS))); StringBuffer xmlBuffer = new StringBuffer(); FuzzyXMLDocType docType = htmlModel.getDocumentType(); if (docType != null) { docType.toXMLString(renderContext, xmlBuffer); } for (FuzzyXMLNode node : documentElement.getChildren()) { node.toXMLString(renderContext, xmlBuffer); //htmlBuffer.append("\n"); } xmlDocument.set(xmlBuffer.toString().trim()); // // Format XML using XSL // IEditorInput input = getEditorInput(); // String charset = System.getProperty("file.encoding"); // if(input instanceof IFileEditorInput){ // charset = ((IFileEditorInput)input).getFile().getCharset(); // } // // TransformerFactory transFactory = TransformerFactory.newInstance(); // // String xsl = new String(HTMLUtil.readStream( // XMLEditor.class.getResourceAsStream("format.xsl"))); // xsl = xsl.replaceAll("\\$\\{charset\\}", charset); // // Source xslSource = new StreamSource(new StringReader(xsl)); // Transformer transformer = transFactory.newTransformer(xslSource); // // IDocument doc = getDocumentProvider().getDocument(getEditorInput()); // ByteArrayOutputStream out = new ByteArrayOutputStream(); // // // Source source = new StreamSource(new StringReader(doc.get())); // Result result = new StreamResult(out); // // transformer.transform(source, result); //doc.set(new String(out.toByteArray(), charset)); } catch(Exception ex){ HTMLPlugin.openAlertDialog(ex.toString()); } } } }