blob: d6ae6ffbde51902f9ace14b5a7c725d60da49bc9 [file] [log] [blame]
/* Copyright 2004 The Apache Software Foundation
*
* 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.apache.xmlbeans.impl.tool;
import org.apache.xmlbeans.impl.xb.xsdownload.DownloadedSchemasDocument;
import org.apache.xmlbeans.impl.xb.xsdownload.DownloadedSchemaEntry;
import org.apache.xmlbeans.impl.xb.xsdownload.DownloadedSchemasDocument.DownloadedSchemas;
import org.apache.xmlbeans.impl.util.HexBin;
import org.apache.xmlbeans.impl.common.IOUtil;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.XmlBeans;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument.Schema;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument;
public abstract class BaseSchemaResourceManager extends SchemaImportResolver
{
private static final String USER_AGENT = "XMLBeans/" + XmlBeans.getVersion() + " (" + XmlBeans.getTitle() + ")";
private String _defaultCopyDirectory;
private DownloadedSchemasDocument _importsDoc;
private Map _resourceForFilename = new HashMap();
private Map _resourceForURL = new HashMap();
private Map _resourceForNamespace = new HashMap();
private Map _resourceForDigest = new HashMap();
private Map _resourceForCacheEntry = new HashMap();
private Set _redownloadSet = new HashSet();
protected BaseSchemaResourceManager()
{
// concrete subclasses should call init in their constructors
}
protected final void init()
{
if (fileExists(getIndexFilename()))
{
try
{
_importsDoc = DownloadedSchemasDocument.Factory.parse( inputStreamForFile( getIndexFilename() ) );
}
catch (IOException e)
{
_importsDoc = null;
}
catch (Exception e)
{
throw (IllegalStateException)(new IllegalStateException("Problem reading xsdownload.xml: please fix or delete this file")).initCause(e);
}
}
if (_importsDoc == null)
{
try
{
_importsDoc = DownloadedSchemasDocument.Factory.parse(
"<dls:downloaded-schemas xmlns:dls='http://www.bea.com/2003/01/xmlbean/xsdownload' defaultDirectory='" + getDefaultSchemaDir() + "'/>"
);
}
catch (Exception e)
{
throw (IllegalStateException)(new IllegalStateException()).initCause(e);
}
}
String defaultDir = _importsDoc.getDownloadedSchemas().getDefaultDirectory();
if (defaultDir == null)
defaultDir = getDefaultSchemaDir();;
_defaultCopyDirectory = defaultDir;
// now initialize data structures
DownloadedSchemaEntry[] entries = _importsDoc.getDownloadedSchemas().getEntryArray();
for (int i = 0; i < entries.length; i++)
{
updateResource(entries[i]);
}
}
public final void writeCache() throws IOException
{
InputStream input = _importsDoc.newInputStream(new XmlOptions().setSavePrettyPrint());
writeInputStreamToFile(input, getIndexFilename());
}
public final void processAll(boolean sync, boolean refresh, boolean imports)
{
if (refresh)
{
_redownloadSet = new HashSet();
}
else
{
_redownloadSet = null;
}
String[] allFilenames = getAllXSDFilenames();
if (sync)
syncCacheWithLocalXsdFiles(allFilenames, false);
SchemaResource[] starters = (SchemaResource[])
_resourceForFilename.values().toArray(new SchemaResource[0]);
if (refresh)
redownloadEntries(starters);
if (imports)
resolveImports(starters);
_redownloadSet = null;
}
public final void process(String[] uris, String[] filenames, boolean sync, boolean refresh, boolean imports)
{
if (refresh)
{
_redownloadSet = new HashSet();
}
else
{
_redownloadSet = null;
}
if (filenames.length > 0)
syncCacheWithLocalXsdFiles(filenames, true);
else if (sync)
syncCacheWithLocalXsdFiles(getAllXSDFilenames(), false);
Set starterset = new HashSet();
for (int i = 0; i < uris.length; i++)
{
SchemaResource resource = (SchemaResource)lookupResource(null, uris[i]);
if (resource != null)
starterset.add(resource);
}
for (int i = 0; i < filenames.length; i++)
{
SchemaResource resource = (SchemaResource)_resourceForFilename.get(filenames);
if (resource != null)
starterset.add(resource);
}
SchemaResource[] starters = (SchemaResource[])
starterset.toArray(new SchemaResource[0]);
if (refresh)
redownloadEntries(starters);
if (imports)
resolveImports(starters);
_redownloadSet = null;
}
/**
* Adds items to the cache that point to new files that aren't
* described in the cache, and optionally deletes old entries.
*
* If an old file is gone and a new file is
* found with exactly the same contents, the cache entry is moved
* to point to the new file.
*/
public final void syncCacheWithLocalXsdFiles(String[] filenames, boolean deleteOnlyMentioned)
{
Set seenResources = new HashSet();
Set vanishedResources = new HashSet();
for (int i = 0; i < filenames.length; i++)
{
String filename = filenames[i];
// first, if the filename matches exactly, trust the filename
SchemaResource resource = (SchemaResource)_resourceForFilename.get(filename);
if (resource != null)
{
if (fileExists(filename))
seenResources.add(resource);
else
vanishedResources.add(resource);
continue;
}
// new file that is not in the index?
// not if the digest is known to the index and the original file is gone - that's a rename!
String digest = null;
try
{
digest = shaDigestForFile(filename);
resource = (SchemaResource)_resourceForDigest.get(digest);
if (resource != null)
{
String oldFilename = resource.getFilename();
if (!fileExists(oldFilename))
{
warning("File " + filename + " is a rename of " + oldFilename);
resource.setFilename(filename);
seenResources.add(resource);
if (_resourceForFilename.get(oldFilename) == resource)
_resourceForFilename.remove(oldFilename);
if (_resourceForFilename.containsKey(filename))
_resourceForFilename.put(filename, resource);
continue;
}
}
}
catch (IOException e)
{
// unable to read digest... no problem, ignore then
}
// ok, this really is a new XSD file then, of unknown URL origin
DownloadedSchemaEntry newEntry = addNewEntry();
newEntry.setFilename(filename);
warning("Caching information on new local file " + filename);
if (digest != null)
newEntry.setSha1(digest);
seenResources.add(updateResource(newEntry));
}
if (deleteOnlyMentioned)
deleteResourcesInSet(vanishedResources, true);
else
deleteResourcesInSet(seenResources, false);
}
/**
* Iterates through every entry and refetches it from its primary URL,
* if known. Replaces the contents of the file if the data is different.
*/
private void redownloadEntries(SchemaResource[] resources)
{
for (int i = 0; i < resources.length; i++)
{
redownloadResource(resources[i]);
}
}
private void deleteResourcesInSet(Set seenResources, boolean setToDelete)
{
Set seenCacheEntries = new HashSet();
for (Iterator i = seenResources.iterator(); i.hasNext(); )
{
SchemaResource resource = (SchemaResource)i.next();
seenCacheEntries.add(resource._cacheEntry);
}
DownloadedSchemas downloadedSchemas = _importsDoc.getDownloadedSchemas();
for (int i = 0; i < downloadedSchemas.sizeOfEntryArray(); i++)
{
DownloadedSchemaEntry cacheEntry = downloadedSchemas.getEntryArray(i);
if (seenCacheEntries.contains(cacheEntry) == setToDelete)
{
SchemaResource resource = (SchemaResource)_resourceForCacheEntry.get(cacheEntry);
warning("Removing obsolete cache entry for " + resource.getFilename());
if (resource != null)
{
_resourceForCacheEntry.remove(cacheEntry);
if (resource == _resourceForFilename.get(resource.getFilename()))
_resourceForFilename.remove(resource.getFilename());
if (resource == _resourceForDigest.get(resource.getSha1()))
_resourceForDigest.remove(resource.getSha1());
if (resource == _resourceForNamespace.get(resource.getNamespace()))
_resourceForNamespace.remove(resource.getNamespace());
// Finally, any or all URIs
String[] urls = resource.getSchemaLocationArray();
for (int j = 0; j < urls.length; j++)
{
if (resource == _resourceForURL.get(urls[j]))
_resourceForURL.remove(urls[j]);
}
}
downloadedSchemas.removeEntry(i);
i -= 1;
}
}
}
private SchemaResource updateResource(DownloadedSchemaEntry entry)
{
// The file
String filename = entry.getFilename();
if (filename == null)
return null;
SchemaResource resource = new SchemaResource(entry);
_resourceForCacheEntry.put(entry, resource);
if (!_resourceForFilename.containsKey(filename))
_resourceForFilename.put(filename, resource);
// The digest
String digest = resource.getSha1();
if (digest != null)
{
if (!_resourceForDigest.containsKey(digest))
_resourceForDigest.put(digest, resource);
}
// Next, the namespace
String namespace = resource.getNamespace();
if (namespace != null)
{
if (!_resourceForNamespace.containsKey(namespace))
_resourceForNamespace.put(namespace, resource);
}
// Finally, any or all URIs
String[] urls = resource.getSchemaLocationArray();
for (int j = 0; j < urls.length; j++)
{
if (!_resourceForURL.containsKey(urls[j]))
_resourceForURL.put(urls[j], resource);
}
return resource;
}
private static DigestInputStream digestInputStream(InputStream input)
{
MessageDigest sha;
try
{
sha = MessageDigest.getInstance("SHA");
}
catch (NoSuchAlgorithmException e)
{
throw (IllegalStateException)(new IllegalStateException().initCause(e));
}
DigestInputStream str = new DigestInputStream(input, sha);
return str;
}
private DownloadedSchemaEntry addNewEntry()
{
return _importsDoc.getDownloadedSchemas().addNewEntry();
}
private class SchemaResource implements SchemaImportResolver.SchemaResource
{
SchemaResource(DownloadedSchemaEntry entry)
{
_cacheEntry = entry;
}
DownloadedSchemaEntry _cacheEntry;
public void setFilename(String filename)
{
_cacheEntry.setFilename(filename);
}
public String getFilename()
{
return _cacheEntry.getFilename();
}
public Schema getSchema()
{
if (!fileExists(getFilename()))
redownloadResource(this);
try
{
return SchemaDocument.Factory.parse(inputStreamForFile(getFilename())).getSchema();
}
catch (Exception e)
{
return null; // return null if _any_ problems reading schema file
}
}
public String getSha1()
{
return _cacheEntry.getSha1();
}
public String getNamespace()
{
return _cacheEntry.getNamespace();
}
public void setNamespace(String namespace)
{
_cacheEntry.setNamespace(namespace);
}
public String getSchemaLocation()
{
if (_cacheEntry.sizeOfSchemaLocationArray() > 0)
return _cacheEntry.getSchemaLocationArray(0);
return null;
}
public String[] getSchemaLocationArray()
{
return _cacheEntry.getSchemaLocationArray();
}
public int hashCode()
{
return getFilename().hashCode();
}
public boolean equals(Object obj)
{
return this == obj || getFilename().equals(((SchemaResource)obj).getFilename());
}
public void addSchemaLocation(String schemaLocation)
{
_cacheEntry.addSchemaLocation(schemaLocation);
}
}
/**
* Called when the ImportLoader wishes to resolve the
* given import. Should return a SchemaResource whose
* "equals" relationship reveals when a SchemaResource is
* duplicated and shouldn't be examined again.
*
* Returns null if the resource reference should be ignored.
*/
public SchemaImportResolver.SchemaResource lookupResource(String nsURI, String schemaLocation)
{
SchemaResource result = fetchFromCache(nsURI, schemaLocation);
if (result != null)
{
if (_redownloadSet != null)
{
redownloadResource(result);
}
return result;
}
if (schemaLocation == null)
{
warning("No cached schema for namespace '" + nsURI + "', and no url specified");
return null;
}
result = copyOrIdentifyDuplicateURL(schemaLocation, nsURI);
if (_redownloadSet != null)
_redownloadSet.add(result);
return result;
}
private SchemaResource fetchFromCache(String nsURI, String schemaLocation)
{
SchemaResource result;
if (schemaLocation != null)
{
result = (SchemaResource)_resourceForURL.get(schemaLocation);
if (result != null)
return result;
}
if (nsURI != null)
{
result = (SchemaResource)_resourceForNamespace.get(nsURI);
if (result != null)
return result;
}
return null;
}
private String uniqueFilenameForURI(String schemaLocation) throws IOException, URISyntaxException
{
String localFilename = new URI( schemaLocation ).getRawPath();
int i = localFilename.lastIndexOf('/');
if (i >= 0)
localFilename = localFilename.substring(i + 1);
if (localFilename.endsWith(".xsd"))
localFilename = localFilename.substring(0, localFilename.length() - 4);
if (localFilename.length() == 0)
localFilename = "schema";
// TODO: remove other unsafe characters for filenames?
String candidateFilename = localFilename;
int suffix = 1;
while (suffix < 1000)
{
String candidate = _defaultCopyDirectory + "/" + candidateFilename + ".xsd";
if (!fileExists(candidate))
return candidate;
suffix += 1;
candidateFilename = localFilename + suffix;
}
throw new IOException("Problem with filename " + localFilename + ".xsd");
}
private void redownloadResource(SchemaResource resource)
{
if (_redownloadSet != null)
{
if (_redownloadSet.contains(resource))
return;
_redownloadSet.add(resource);
}
String filename = resource.getFilename();
String schemaLocation = resource.getSchemaLocation();
String digest = null;
// nothing to do?
if (schemaLocation == null || filename == null)
return;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try
{
URL url = new URL( schemaLocation );
URLConnection conn = url.openConnection();
conn.addRequestProperty("User-Agent", USER_AGENT);
conn.addRequestProperty("Accept", "application/xml, text/xml, */*");
DigestInputStream input = digestInputStream(conn.getInputStream());
IOUtil.copyCompletely(input, buffer);
digest = HexBin.bytesToString(input.getMessageDigest().digest());
}
catch (Exception e)
{
warning("Could not copy remote resource " + schemaLocation + ":" + e.getMessage());
return;
}
if (digest.equals(resource.getSha1()) && fileExists(filename))
{
warning("Resource " + filename + " is unchanged from " + schemaLocation + ".");
return;
}
try
{
InputStream source = new ByteArrayInputStream(buffer.toByteArray());
writeInputStreamToFile(source, filename);
}
catch (IOException e)
{
warning("Could not write to file " + filename + " for " + schemaLocation + ":" + e.getMessage());
return;
}
warning("Refreshed " + filename + " from " + schemaLocation);
}
private SchemaResource copyOrIdentifyDuplicateURL(String schemaLocation, String namespace)
{
String targetFilename;
String digest;
SchemaResource result;
try
{
targetFilename = uniqueFilenameForURI(schemaLocation);
}
catch (URISyntaxException e)
{
warning("Invalid URI '" + schemaLocation + "':" + e.getMessage());
return null;
}
catch (IOException e)
{
warning("Could not create local file for " + schemaLocation + ":" + e.getMessage());
return null;
}
try
{
URL url = new URL( schemaLocation );
DigestInputStream input = digestInputStream(url.openStream());
writeInputStreamToFile(input, targetFilename);
digest = HexBin.bytesToString(input.getMessageDigest().digest());
}
catch (Exception e)
{
warning("Could not copy remote resource " + schemaLocation + ":" + e.getMessage());
return null;
}
result = (SchemaResource)_resourceForDigest.get(digest);
if (result != null)
{
deleteFile(targetFilename);
result.addSchemaLocation(schemaLocation);
if (!_resourceForURL.containsKey(schemaLocation))
_resourceForURL.put(schemaLocation, result);
return result;
}
warning("Downloaded " + schemaLocation + " to " + targetFilename);
DownloadedSchemaEntry newEntry = addNewEntry();
newEntry.setFilename(targetFilename);
newEntry.setSha1(digest);
if (namespace != null)
newEntry.setNamespace(namespace);
newEntry.addSchemaLocation(schemaLocation);
return updateResource(newEntry);
}
/**
* Updates actual namespace in the table.
*/
public void reportActualNamespace(SchemaImportResolver.SchemaResource rresource, String actualNamespace)
{
SchemaResource resource = (SchemaResource)rresource;
String oldNamespace = resource.getNamespace();
if (oldNamespace != null && _resourceForNamespace.get(oldNamespace) == resource)
_resourceForNamespace.remove(oldNamespace);
if (!_resourceForNamespace.containsKey(actualNamespace))
_resourceForNamespace.put(actualNamespace, resource);
resource.setNamespace(actualNamespace);
}
private String shaDigestForFile(String filename) throws IOException
{
DigestInputStream str = digestInputStream(inputStreamForFile(filename));
byte[] dummy = new byte[4096];
for (int i = 1; i > 0; i = str.read(dummy));
str.close();
return HexBin.bytesToString(str.getMessageDigest().digest());
}
// SOME METHODS TO OVERRIDE ============================
protected String getIndexFilename()
{
return "./xsdownload.xml";
}
protected String getDefaultSchemaDir()
{
return "./schema";
}
/**
* Produces diagnostic messages such as "downloading X to file Y".
*/
abstract protected void warning(String msg);
/**
* Returns true if the given filename exists. The filenames
* are of the form "/foo/bar/zee.xsd" and should be construed
* as rooted at the root of the project.
*/
abstract protected boolean fileExists(String filename);
/**
* Gets the data in the given filename as an InputStream.
*/
abstract protected InputStream inputStreamForFile(String filename) throws IOException;
/**
* Writes an entire file in one step. An InputStream is passed and
* copied to the file.
*/
abstract protected void writeInputStreamToFile(InputStream input, String filename) throws IOException;
/**
* Deletes a file. Sometimes immediately after writing a new file
* we notice that it's exactly the same as an existing file and
* we delete it. We never delete a file that was given to us
* by the user.
*/
abstract protected void deleteFile(String filename);
/**
* Returns a list of all the XSD filesnames in the project.
*/
abstract protected String[] getAllXSDFilenames();
}