package org.cagrid.gme.service.impl; import gov.nih.nci.cagrid.common.Utils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xerces.impl.xs.SchemaGrammar; import org.apache.xerces.xni.XNIException; import org.apache.xerces.xni.parser.XMLInputSource; import org.apache.xerces.xs.StringList; import org.cagrid.core.common.FaultHelper; import org.cagrid.gme.model.XMLSchema; import org.cagrid.gme.model.XMLSchemaBundle; import org.cagrid.gme.model.XMLSchemaDocument; import org.cagrid.gme.model.XMLSchemaImportInformation; import org.cagrid.gme.model.XMLSchemaNamespace; import org.cagrid.gme.service.exception.InvalidSchemaSubmissionException; import org.cagrid.gme.service.exception.NoSuchNamespaceExistsException; import org.cagrid.gme.service.exception.UnableToDeleteSchemaException; import org.cagrid.gme.service.impl.dao.XMLSchemaInformationDao; import org.cagrid.gme.service.impl.domain.XMLSchemaInformation; import org.cagrid.gme.service.impl.persistence.SchemaPersistenceGeneralException; import org.cagrid.gme.service.impl.sax.GMEErrorHandler; import org.cagrid.gme.service.impl.sax.GMEXMLSchemaLoader; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.locks.ReentrantReadWriteLock; @Transactional public class GME { protected static Log LOG = LogFactory.getLog(GME.class.getName()); protected XMLSchemaInformationDao schemaDao; // provides coarse grain persistence layer locking, used to ensure integrity // of protected ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void setXMLSchemaInformationDao(XMLSchemaInformationDao schemaDao) { if (schemaDao == null) { throw new IllegalArgumentException("Cannot use a null XMLSchemaInformationDao!"); } this.schemaDao = schemaDao; } public void deleteSchemas(Collection<URI> schemaNamespaces) throws NoSuchNamespaceExistsException, UnableToDeleteSchemaException { if (schemaNamespaces == null || schemaNamespaces.size() == 0) { String description = "null or empty set is not a valid collection of namespaces to delete."; LOG.debug("Refusing to delete schema: " + description); throw FaultHelper.createFaultException(NoSuchNamespaceExistsException.class, description); } // need to get a "lock" on the database here this.lock.writeLock().lock(); try { Map<URI, XMLSchemaInformation> schemaMap = new HashMap<URI, XMLSchemaInformation>(); for (URI ns : schemaNamespaces) { XMLSchemaInformation schema = this.schemaDao.getByTargetNamespace(ns); if (schema == null) { String description = "No schema is published with given targetNamespace (" + ns + ")"; LOG.debug("Refusing to delete schema: " + description); throw FaultHelper.createFaultException(NoSuchNamespaceExistsException.class, description); } else { schemaMap.put(ns, schema); } // TODO: permission check // 1. Check permissions on each schema being deleted; fail if // don't have permissions // 2. Check that all schemas being deleted are in a state where // the // contents can be deleted ; fail otherwise } for (URI ns : schemaNamespaces) { Collection<XMLSchema> dependingXMLSchemas = this.schemaDao.getDependingXMLSchemas(ns); // get the list of depending schemas for each of the namespace // for each, make sure the list is 0, or every depending schema // will // also be deleted (i.e. the namespace is in the provided list // to // delete) for (XMLSchema dependingSchema : dependingXMLSchemas) { URI dependingTargetNamespace = dependingSchema.getTargetNamespace(); if (!schemaMap.containsKey(dependingTargetNamespace)) { String description = "Cannot delete XMLSchema (" + ns + ") as it is imported by XMLSchema (" + dependingTargetNamespace + "). You must also delete the XMLSchema (" + dependingTargetNamespace + ") or udpate it to not depend on XMLSchema (" + ns + ")"; // REVIST: should i keep going and build up a list of // these, // or just failfast? LOG.debug("Refusing to delete schema: " + description); throw FaultHelper.createFaultException(UnableToDeleteSchemaException.class, description); } else { LOG.debug("So far ok to delete XMLSchema (" + ns + ") even though it is imported by XMLSchema (" + dependingTargetNamespace + "), as it is being requested to be deleted as well."); } } } // ok to delete all these schemas now this.schemaDao.delete(schemaMap.values()); } finally { // release database lock this.lock.writeLock().unlock(); } } public void publishSchemas(List<XMLSchema> schemas) throws InvalidSchemaSubmissionException { // 0. sanity check submission if (schemas == null || schemas.size() == 0) { String message = "No schemas were found in the submission."; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } // need to get a "lock" on the database here this.lock.writeLock().lock(); try { // 3. Create a list of "processed schemas" Map<URI, SchemaGrammar> processedSchemas = verifySubmissionAndInitializeProcessedSchemasMap(schemas); // 4. Create a model with error and entity resolver (on // callback // to imports/includes/redefines, entity resolver needs to first // load schema from // submission if present, if not in submission load from DB, if not // in // DB error out) GMEXMLSchemaLoader schemaLoader = new GMEXMLSchemaLoader(schemas, this.schemaDao); // 5. Call processSchema() for each schema being uploaded for (XMLSchema submittedSchema : schemas) { try { processSchema(schemaLoader, processedSchemas, submittedSchema); } catch (Exception e) { String message = "Problem processing schema submissions; the schema [" + submittedSchema.getTargetNamespace() + "] was not valid:" + Utils.getExceptionMessage(e); LOG.error(message, e); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } } // 8. Commit new/modified schemas to database, populating dependency // schema information (gathered from imports) commitSchemas(schemas, processedSchemas); } finally { // release database lock this.lock.writeLock().unlock(); } } protected Map<URI, SchemaGrammar> verifySubmissionAndInitializeProcessedSchemasMap(List<XMLSchema> schemas) throws InvalidSchemaSubmissionException { Map<URI, SchemaGrammar> processedSchemas = new HashMap<URI, SchemaGrammar>(); for (XMLSchema submittedSchema : schemas) { // verify the schema's have unique and valid URIs URI namespace = submittedSchema.getTargetNamespace(); if (namespace == null) { String message = "The schema submission contained a schema with a null URI."; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } if (processedSchemas.containsKey(namespace)) { String message = "The schema submission contained multiple schemas of the same URI (" + namespace + "). If you intend to submit schemas with includes, you need to package them as a single Schema with multiple SchemaDocuments."; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } else { if (submittedSchema.getRootDocument() == null) { String message = "The schema submission contained a schema [" + submittedSchema.getTargetNamespace() + "] without a root schema document."; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } // REVISIT: should probably check SchemaDocument rules here: // unique systemIDs for all [this is accomplished by using // Set now] // need to actually check that the schema rules are true // (i.e include must have no ns, or same ns), but basically // need the parsed model to know this... so i can check at // resolution time, but if something is never referenced, it // wont be loaded and so may be invalid... probably need to // check it after we have the grammar (can we ask for // includes and check for correspondence in the submission?) // [this is checked by combination of parser (checking rules // of what it reads) and matching // the loaded doc size matches the submitted doc // size (meaning they were all read)] // preload the submission schemas with "null" models (which // will be replaced by the actual models once they are // processed) so they don't get inappropriately processed // (as could be the case during the loading of "depending // schemas" ) processedSchemas.put(namespace, null); } // TODO: permission check // 1. Check permissions on each schema being published; fail if // don't have permissions // 2. Check that all schemas being published are either not yet // published, or are in a state where the contents can be // updated ; // fail otherwise XMLSchema storedSchema = this.schemaDao.getXMLSchemaByTargetNamespace(namespace); if (storedSchema != null) { // TODO: check that it can be modified before doing this // (right now it is never allowed) // if(storeSchema.getState... != ){ String message = "The schema [" + namespace + "] already exists and cannot be modified."; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } } return processedSchemas; } protected void commitSchemas(List<XMLSchema> schemas, Map<URI, SchemaGrammar> processedSchemas) throws InvalidSchemaSubmissionException { // if got here with no error, schemas are ok to persist // build up DB objects to commit Map<XMLSchema, List<URI>> toCommit = new HashMap<XMLSchema, List<URI>>(); for (XMLSchema submittedSchema : schemas) { // extract the schema model SchemaGrammar schemaGrammar = processedSchemas.get(submittedSchema.getTargetNamespace()); assert schemaGrammar != null; assert !toCommit.containsKey(submittedSchema); // this has all the expanded locations which built up the schema // for us (using the namespace as the basesystemID, this should // be the ns+systemID) // REVISIT: is there a better way to figure out the // included/redefined documents? StringList documentLocations = schemaGrammar.getDocumentLocations(); for (XMLSchemaDocument schemaDocument : submittedSchema.getAdditionalDocuments()) { URI expandedURI; try { // REVISIT: is this the right way to construct this. // NOTE: this must match the behavior of the // GMEEntityResolver when it create the baseSystemID for // the XMLInputSource it loads expandedURI = new URI(submittedSchema.getTargetNamespace().toString() + "/" + schemaDocument.getSystemID()); } catch (Exception e) { String message = "Problem processing schema submissions; the schema [" + submittedSchema.getTargetNamespace() + "] included a SchemaDocument [" + schemaDocument.getSystemID() + "] whose expanded URI was not valid:" + Utils.getExceptionMessage(e); LOG.error(message, e); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } if (!documentLocations.contains(expandedURI.toString())) { String message = "Problem processing schema submissions; the schema [" + submittedSchema.getTargetNamespace() + "] included a SchemaDocument [" + schemaDocument.getSystemID() + "] which was not used by the parsed grammar"; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } } // we must have a document for the root and each additional // schema document if (documentLocations.getLength() != submittedSchema.getAdditionalDocuments().size() + 1) { String message = "Problem processing schema submissions; the schema [" + submittedSchema.getTargetNamespace() + "] contained [" + submittedSchema.getAdditionalDocuments().size() + "] SchemaDocuments but the parsed grammar contained [" + documentLocations.getLength() + "]. All SchemaDocuments must be used by the Schema."; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } // build an import list List<URI> importList = new ArrayList<URI>(); Vector importedGrammars = schemaGrammar.getImportedGrammars(); if (importedGrammars != null) { for (int i = 0; i < importedGrammars.size(); i++) { SchemaGrammar importedSchema = (SchemaGrammar) importedGrammars.get(i); String importedTargetNS = importedSchema.getTargetNamespace(); LOG.info("Schema [" + schemaGrammar.getTargetNamespace() + "] imports schema [" + importedTargetNS + "]"); try { importList.add(new URI(importedTargetNS)); } catch (URISyntaxException e) { String message = "Problem processing schema submissions; the schema [" + submittedSchema.getTargetNamespace() + "] imported a schema [" + importedTargetNS + "] whose URI was not valid:" + Utils.getExceptionMessage(e); LOG.error(message, e); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } } } // add the schema and its import list to the Map to commit toCommit.put(submittedSchema, importList); } // TODO: replace this call by embedding its logic above // commit to database this.storeSchemas(toCommit); } // TODO: rewrite this with the caller above to directly make use of the DAO // instead of building up a map and calling this (this is refactor cruft // leftover from removing the SchemaPersitence layer) // TODO: need to add rollBackFor=... to specify exceptions which should // cause rollbacks (does that rollbackfor subclasses of the specified // exception?) private void storeSchemas(Map<XMLSchema, List<URI>> schemasToStore) { // REVISIT: is there a simpler way to do this // this is a list of newly persistent XMLSchemaInformation (for those // which are being saved), and already persistent XMLSchemaInformation // (for those that are being imported and not updated) Map<URI, XMLSchemaInformation> persistedInfos = new HashMap<URI, XMLSchemaInformation>(); // foreach XMLSchema for (XMLSchema s : schemasToStore.keySet()) { // find PersistableXMLSchema (by URI), create if null, save XMLSchemaInformation info = this.schemaDao.getByTargetNamespace(s.getTargetNamespace()); if (info == null) { info = new XMLSchemaInformation(); } // -setSchema XMLSchema on XMLSchemaInformation info.setSchema(s); this.schemaDao.save(info); // -put in hash of URI->XMLSchemaInformation persistedInfos.put(s.getTargetNamespace(), info); } // all new/updated schemas are now in the hash and persistent // foreach XMLSchema (make the changes) for (XMLSchema s : schemasToStore.keySet()) { // -get PersistableXMLSchema from hash XMLSchemaInformation info = persistedInfos.get(s.getTargetNamespace()); Set<XMLSchemaInformation> importSet = new HashSet<XMLSchemaInformation>(); List<URI> importList = schemasToStore.get(s); // -foreach URI in import List for (URI importedURI : importList) { // --if not in hash XMLSchemaInformation importedInfo = persistedInfos.get(importedURI); if (importedInfo == null) { // --- getReference to PersistableXMLSchema, put in hash importedInfo = this.schemaDao.getByTargetNamespace(importedURI); // this must either be new and already in the hash (the // containing if), or existing and therefore in the db // already assert importedInfo != null; persistedInfos.put(s.getTargetNamespace(), importedInfo); } // --add toimportSet importSet.add(importedInfo); } // -set importSet on PersistableXMLSchema info.setImports(importSet); // TODO: I don't think I should have to do this... shouldn't the // DAO-returned objects still be persistent and notice the changes? this.schemaDao.save(info); } } protected void processSchema(GMEXMLSchemaLoader schemaLoader, Map<URI, SchemaGrammar> processedSchemas, XMLSchema schemaToProcess) throws XNIException, IOException, SchemaPersistenceGeneralException, InvalidSchemaSubmissionException { LOG.debug("About to process schema [" + schemaToProcess.getTargetNamespace() + "]."); // 6.2. Add the schema to the model which will recursively fire // callbacks to the entity resolver for all imports (loading all // dependency schemas, and their dependency schemas, etc) String ns = schemaToProcess.getTargetNamespace().toString(); XMLSchemaDocument rootSD = schemaToProcess.getRootDocument(); // REVISIT: what to set for the baseSystemID? it's used to convert // includes, etc into full URIs and so is relevant later when examining // documentLocations of the SchemaGrammar XMLInputSource xis = new XMLInputSource(ns, rootSD.getSystemID(), ns, new StringReader(rootSD.getSchemaText()), "UTF-16"); SchemaGrammar model = (SchemaGrammar) schemaLoader.loadGrammar(xis); if (model == null) { GMEErrorHandler errorHandler = schemaLoader.getErrorHandler(); throw errorHandler.createXMLParseException(); } // we should not have processed this schema before, and if the URI // is in the list it should have a null model (indicating it was // preloaded from the submission schemas set) assert !processedSchemas.containsKey(schemaToProcess.getTargetNamespace()) || processedSchemas.get(schemaToProcess.getTargetNamespace()) == null; // store the resultant schema model (this needs to happen before // recursion) processedSchemas.put(schemaToProcess.getTargetNamespace(), model); String targetURI = model.getTargetNamespace(); if (!ns.equals(targetURI)) { String message = "Problem processing schema submissions; the schema[" + schemaToProcess.getTargetNamespace() + "] was not valid as its acutal targetURI [" + targetURI + "] did not match."; LOG.error(message); throw FaultHelper.createFaultException(InvalidSchemaSubmissionException.class, message); } // 6.3. Look in the DB for depending schemas (will only be present if // schema was already published and is being updated) Collection<XMLSchema> dependingSchemas = this.schemaDao.getDependingXMLSchemas(schemaToProcess .getTargetNamespace()); // 6.4. For each depending schema not in the list of "processed schemas" // call processSchema() for (XMLSchema dependingSchema : dependingSchemas) { if (processedSchemas.containsKey(dependingSchema.getTargetNamespace())) { LOG.debug("Depending schema [" + dependingSchema.getTargetNamespace() + "] was already processed (or will be processed)."); } else { LOG.debug("Processing depending schema [" + dependingSchema.getTargetNamespace() + "] which is not in the submission package."); processSchema(schemaLoader, processedSchemas, dependingSchema); } } } /** * Returns the targetNamespaces (represented by URIs) of all published * XMLSchemas * * @return the targetNamespaces (represented by URIs) of all published * XMLSchemas */ @Transactional(readOnly = true) public Collection<URI> getNamespaces() { this.lock.readLock().lock(); try { return this.schemaDao.getAllNamespaces(); } finally { this.lock.readLock().unlock(); } } /** * Returns a published XMLSchema with a targetNamespace equal to the given * URI * * @param uri * the targetNamespace of the desired XMLSchema * @return a published XMLSchema with a targetNamespace equal to the given * URI * @throws NoSuchNamespaceExistsException * if there is no published Schema with a targetNamespace equal * to the given URI */ @Transactional(readOnly = true) public XMLSchema getSchema(URI uri) throws NoSuchNamespaceExistsException { this.lock.readLock().lock(); try { XMLSchema result = this.schemaDao.getMaterializedXMLSchemaByTargetNamespace(uri); if (result == null) { String description = "No schema is published with given targetNamespace (" + uri + ")"; LOG.debug("Cannot retrieve requested schema: " + description); throw FaultHelper.createFaultException(NoSuchNamespaceExistsException.class, description); } else { return result; } } finally { this.lock.readLock().unlock(); } } /** * Return a Collection of URIs representing the targetNamespaces of the * XMLSchemas which are imported by the XMLSchema identified by the given * targetNamespace * * @param targetNamespace * the targetNamespace of the desired XMLSchema * @return a Collection of URIs representing the targetNamespaces of the * XMLSchemas which are imported by the XMLSchema identified by the * given targetNamespace * @throws NoSuchNamespaceExistsException * if there is no published Schema with a targetNamespace equal * to the given URI */ @Transactional(readOnly = true) public Collection<URI> getImportedNamespaces(URI targetNamespace) throws NoSuchNamespaceExistsException { XMLSchemaInformation info = this.schemaDao.getByTargetNamespace(targetNamespace); if (info == null) { String description = "No schema is published with given targetNamespace (" + targetNamespace + ")"; LOG.debug("Cannot retrieve imported namespaces of schema: " + description); throw FaultHelper.createFaultException(NoSuchNamespaceExistsException.class, description); } List<URI> result = new ArrayList<URI>(); Set<XMLSchemaInformation> imports = info.getImports(); for (XMLSchemaInformation importedInfo : imports) { result.add(importedInfo.getSchema().getTargetNamespace()); } return result; } /** * Return a Collection of URIs representing the targetNamespaces of the * XMLSchemas which import the XMLSchema identified by the given * targetNamespace * * @param targetNamespace * the targetNamespace of the desired XMLSchema * @return a Collection of URIs representing the targetNamespaces of the * XMLSchemas which import the XMLSchema identified by the given * targetNamespace * @throws NoSuchNamespaceExistsException * if there is no published Schema with a targetNamespace equal * to the given URI */ @Transactional(readOnly = true) public Collection<URI> getImportingNamespaces(URI targetNamespace) throws NoSuchNamespaceExistsException { XMLSchema schema = this.schemaDao.getXMLSchemaByTargetNamespace(targetNamespace); if (schema == null) { String description = "No schema is published with given targetNamespace (" + targetNamespace + ")"; LOG.debug("Cannot retrieve importing namespaces of schema: " + description); throw FaultHelper.createFaultException(NoSuchNamespaceExistsException.class, description); } Collection<XMLSchema> dependingXMLSchemas = this.schemaDao.getDependingXMLSchemas(targetNamespace); List<URI> result = new ArrayList<URI>(); for (XMLSchema importingSchema : dependingXMLSchemas) { result.add(importingSchema.getTargetNamespace()); } return result; } @Transactional(readOnly = true) public XMLSchemaBundle getSchemBundle(URI targetNamespace) throws NoSuchNamespaceExistsException { XMLSchemaInformation info = this.schemaDao.getByTargetNamespace(targetNamespace); if (info == null) { String description = "No schema is published with given targetNamespace (" + targetNamespace + ")"; LOG.debug("Cannot retrieve schema and dependencies: " + description); throw FaultHelper.createFaultException(NoSuchNamespaceExistsException.class, description); } XMLSchemaBundle bundle = new XMLSchemaBundle(); collectSchemasForBundle(bundle, info); return bundle; } private void collectSchemasForBundle(XMLSchemaBundle bundle, XMLSchemaInformation info) { // make sure we haven't already processed these schema, such as from // another schemas's import Set<XMLSchema> schemas = bundle.getXmlSchemaCollection().getXMLSchema(); if (schemas.contains(info.getSchema())) { // we've already processed this, so return return; } // add this to the bucket and make sure it is populated this.schemaDao.materializeXMLSchemaInformation(info); schemas.add(info.getSchema()); // create importinfo for each imported schema (if any) if (info.getImports().size() > 0) { Set<XMLSchemaImportInformation> importInfoSet = bundle.getImportInformationCollection().getXMLSchemaImportInformation(); // make a new importinfo for this schema XMLSchemaImportInformation importInfo = new XMLSchemaImportInformation(); importInfo.setXMLSchemaNamespace(new XMLSchemaNamespace(info.getSchema().getTargetNamespace())); assert !importInfoSet.contains(importInfo) : "The bundle should not contain import information about XMLSchema (" + info.getSchema().getTargetNamespace() + ") as it did not contain the schema itself."; // add the collected import set importInfoSet.add(importInfo); // recursively process each of the schemas this schema imports for (XMLSchemaInformation importedInfo : info.getImports()) { importInfo.getImports().getXMLSchemaNamespace().add(new XMLSchemaNamespace(importedInfo.getSchema().getTargetNamespace())); collectSchemasForBundle(bundle, importedInfo); } } } }