Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

W3CMultiSchemaFactory class needs extended to allow for custom resolving of XSD's locations #175

Open
Orbisman opened this issue Jul 2, 2023 · 2 comments

Comments

@Orbisman
Copy link
Contributor

Orbisman commented Jul 2, 2023

Projects requiring external multiple XSDs with the same schemaLocation in the import, require a means to resolve the local XSD within the applications file jar or file structure. This becomes an issue when multiple versions of the same XSD is maintained and the XSD filename or path is changed to reflect the version, ie. custom_v1.xsd, custom_v2.xsd or schema/v1/custom.xsd, schema/v2/custom.xsd-> [ <xsd:import namespace="com.org/custom_feature" schemaLocation="custom.xsd"/> ]

Example JUnit code snippet:

  @Test
   public void test() throws XMLStreamException, SAXException, IOException 
   {	   
         File basePath = new File(System.getProperty("user.dir") + "/schema");

	   // Source xml file
	   File xmlFile = new File(basePath, "refxml/mydata.xml");
		
	   // XSD fies
	   File xsdMyFile = new File(basePath, "schema/example.xsd");
	   File xsdCustomFileV1 = new File(basePath, "schema/custom_v1.xsd");
		
	   // Validation first		
	   Map<String, Source> schemas = new HashMap<>();
	   schemas.put("com.myorg/main", new StreamSource(xsdMyFile));
	   schemas.put("com.org/custom_feature", new StreamSource(xsdCustomFileV1));
					
	    try 
	    {    	
	    	W3CMultiSchemaFactory sf = new W3CMultiSchemaFactory();	
	    	sf.setEntityResolver(new EntityResolver() {
				@Override
				public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException 
				{							
		        	System.out.println("ResolveEntity() - Before->Public id: [" + publicId + "], System id: [" + systemId + "]");
		        	
		        	InputSource is = null;
		        	
		        	File tmp = new File(URI.create(systemId).getPath());
		        	
		        	String fileName = tmp.getName();
		        	if ("custom.xsd".equals(fileName))
		        	{
		        		tmp = xsdCustomFileV1;
		        	}
		        			    		        	
		        	try
		        	{
		        		is = new InputSource(new FileReader(tmp));
		        		is.setSystemId(tmp.toURI().toString());
		        	}
		        	catch (IOException ex)
	    			{
	    				ex.printStackTrace();
	    			}
		        	
		        	if (is != null)
		        	{
                        System.out.println("ResolveEntity() - Before->Public id: [" + is.getPublicId() + "], System id: [" + is.getSystemId() + "]");
		        	}
					return is;
				}				
			});
	    	
			schema = sf.createSchema(basePath.toURI().toString(), schemas);
			
            // Parse
            final WstxInputFactory xf = new WstxInputFactory();

            xf.setProperty(XMLInputFactory2.P_PRESERVE_LOCATION, Boolean.TRUE );
            xf.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
            xf.setProperty(XMLInputFactory.IS_COALESCING, true);
						
            XMLStreamReader2 reader = (XMLStreamReader2) xf.createXMLStreamReader(new BufferedInputStream(new FileInputStream(xmlFile)));
            reader.setValidationProblemHandler( new ValidationProblemHandler() {

			@Override
			public void reportProblem(XMLValidationProblem arg0) throws XMLValidationException 
			{
				if (arg0.getSeverity() == XMLValidationProblem.SEVERITY_WARNING )
				{
					System.out.println("Validation Warning: " + arg0.getMessage());
				}
				else
				{
					System.out.println("Validation Error: " + arg0.getMessage());	
					throw arg0.toException();
				}
			}			
		});		
		reader.validateAgainst(schema);
						    
	    while (reader.hasNext()) 
	    {
	    	int code = reader.next();
	    	
        }

Suggested changes:

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;

import org.codehaus.stax2.validation.XMLValidationSchema;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

import com.ctc.wstx.msv.W3CSchema;
import com.ctc.wstx.shaded.msv_core.grammar.ExpressionPool;
import com.ctc.wstx.shaded.msv_core.grammar.xmlschema.XMLSchemaGrammar;
import com.ctc.wstx.shaded.msv_core.grammar.xmlschema.XMLSchemaSchema;
import com.ctc.wstx.shaded.msv_core.reader.GrammarReaderController;
import com.ctc.wstx.shaded.msv_core.reader.GrammarReaderController2;
import com.ctc.wstx.shaded.msv_core.reader.State;
import com.ctc.wstx.shaded.msv_core.reader.xmlschema.EmbeddedSchema;
import com.ctc.wstx.shaded.msv_core.reader.xmlschema.MultiSchemaReader;
import com.ctc.wstx.shaded.msv_core.reader.xmlschema.SchemaState;
import com.ctc.wstx.shaded.msv_core.reader.xmlschema.WSDLGrammarReaderController;
import com.ctc.wstx.shaded.msv_core.reader.xmlschema.XMLSchemaReader;

public class W3CMultiSchemaFactory
{
    private final SAXParserFactory parserFactory;
    private EntityResolver entityResolver;

    public W3CMultiSchemaFactory() {
        parserFactory = SAXParserFactory.newInstance();
        parserFactory.setNamespaceAware(true); 
    }

    static class RecursiveAllowedXMLSchemaReader extends XMLSchemaReader 
    {
        Set<String> sysIds = new TreeSet<String>();
        
        public RecursiveAllowedXMLSchemaReader(GrammarReaderController controller, SAXParserFactory parserFactory) 
        {
            super(controller, parserFactory, new StateFactory() {            	
                @Override
                public State schemaHead(String expectedNamespace) {
                    return new SchemaState(expectedNamespace) {
                        private XMLSchemaSchema old;

                        @Override
                        protected void endSelf() {
                            super.endSelf();
                            RecursiveAllowedXMLSchemaReader r = (RecursiveAllowedXMLSchemaReader)reader;
                            r.currentSchema = old;
                        }

                        @Override
                        protected void onTargetNamespaceResolved(String targetNs, boolean ignoreContents) 
                        {
                            RecursiveAllowedXMLSchemaReader r = (RecursiveAllowedXMLSchemaReader)reader;
                            // sets new XMLSchemaGrammar object.
                            old = r.currentSchema;
                            r.currentSchema = r.getOrCreateSchema(targetNs);
                            if (ignoreContents) {
                                return;
                            }
                            if (!r.isSchemaDefined(r.currentSchema)) {
                                r.markSchemaAsDefined(r.currentSchema);
                            }
                        }
                    };
                }
            }, new ExpressionPool());
        }

		@Override
        public void setLocator(Locator locator) {
            if (locator == null && getLocator() != null && getLocator().getSystemId() != null) 
            {
                sysIds.add(getLocator().getSystemId());
            }
            super.setLocator(locator);            
        }

        @Override
        public void switchSource(Source source, State newState) 
        {   
            String url = source.getSystemId();
            if (url != null && sysIds.contains(url)) {
                return;
            }
            
            super.switchSource(source, newState);
        }
    }

    static class FixedWSDLGrammarReaderController extends WSDLGrammarReaderController
    {
    	private EntityResolver entityResolver;
    	
    	public FixedWSDLGrammarReaderController(GrammarReaderController2 nextController,
									   String baseURI, Map<String, EmbeddedSchema> sources)
    	{
    		super(nextController, baseURI, sources);    		
    	}
    	
    	public void setEntityResolver(EntityResolver entityResolver)
    	{
    		this.entityResolver = entityResolver;
    	}
    	    	
    	@Override
		public InputSource resolveEntity(String publicId, String systemId)
    	{
    		InputSource is = null;
    		if (entityResolver != null)
    		{
    			try
    			{
    				is = entityResolver.resolveEntity(publicId, systemId);
    			}
    			catch (SAXException | IOException ex)
    			{
    				ex.printStackTrace();
    			}
    		}
    		else {
    			is = super.resolveEntity(publicId, systemId);
    		}
    		
			return is;
		}
    }
    
    /**
     * Creates an XMLValidateSchema that can be used to validate XML instances against
     * any of the schemas defined in the Map of schemaSources.
     *
     * @param baseURI Base URI for resolving dependant schemas
     * @param schemaSources Map of schemas, namespace to Source
     */
    public XMLValidationSchema createSchema(String baseURI,
            Map<String, Source> schemaSources) throws XMLStreamException
    {
        Map<String, EmbeddedSchema> embeddedSources = new HashMap<String, EmbeddedSchema>();
        for (Map.Entry<String, Source> source : schemaSources.entrySet()) {
            if (source.getValue() instanceof DOMSource) {
                Node nd = ((DOMSource)source.getValue()).getNode();
                Element el = null;
                if (nd instanceof Element) {
                    el = (Element)nd;
                } else if (nd instanceof Document) {
                    el = ((Document)nd).getDocumentElement();
                }
                embeddedSources.put(source.getKey(), new EmbeddedSchema(source.getValue().getSystemId(), el));
            }
        }
        
        final FixedWSDLGrammarReaderController ctrl = new FixedWSDLGrammarReaderController(null, baseURI, embeddedSources);
        ctrl.setEntityResolver(entityResolver);
        
        final RecursiveAllowedXMLSchemaReader xmlSchemaReader = new RecursiveAllowedXMLSchemaReader(ctrl, parserFactory);
        
        final MultiSchemaReader multiSchemaReader = new MultiSchemaReader(xmlSchemaReader);
        for (Source source : schemaSources.values()) {
            multiSchemaReader.parse(source);
        }

        XMLSchemaGrammar grammar = multiSchemaReader.getResult();
        if (grammar == null) {
            throw new XMLStreamException("Failed to load schemas");
        }
        return new W3CSchema(grammar);
    }
    
    public W3CMultiSchemaFactory setEntityResolver(EntityResolver entityResolver)
    {
    	this.entityResolver = entityResolver;
    	
    	return this;
    }
}
@cowtowncoder
Copy link
Member

Sounds like useful/necessary thing to add. If this could be done in form of PR I'd be happy to review, get merged.

@Orbisman
Copy link
Contributor Author

Orbisman commented Jul 10, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants