blob: 4cdbcf11e2a3ff4d8979cc4128432e65025ec726 [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.XmlBeans;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.impl.common.IOUtil;
import org.apache.xmlbeans.impl.util.HexBin;
import org.apache.xmlbeans.impl.xb.xsdownload.DownloadedSchemaEntry;
import org.apache.xmlbeans.impl.xb.xsdownload.DownloadedSchemasDocument;
import org.apache.xmlbeans.impl.xb.xsdownload.DownloadedSchemasDocument.DownloadedSchemas;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument.Schema;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
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 java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public abstract class BaseSchemaResourceManager extends SchemaImportResolver {
private static final String USER_AGENT = "XMLBeans/" + XmlBeans.getVersion() + " (" + XmlBeans.getTitle() + ")";
private String _defaultCopyDirectory;
private DownloadedSchemasDocument _importsDoc;
private final Map<String, SchemaResource> _resourceForFilename = new HashMap<>();
private final Map<String, SchemaResource> _resourceForURL = new HashMap<>();
private final Map<String, SchemaResource> _resourceForNamespace = new HashMap<>();
private final Map<String, SchemaResource> _resourceForDigest = new HashMap<>();
private final Map<DownloadedSchemaEntry, SchemaResource> _resourceForCacheEntry = new HashMap<>();
private Set<SchemaResource> _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 new IllegalStateException("Problem reading xsdownload.xml: please fix or delete this file", 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 new IllegalStateException(e);
}
}
String defaultDir = _importsDoc.getDownloadedSchemas().getDefaultDirectory();
if (defaultDir == null) {
defaultDir = getDefaultSchemaDir();
}
_defaultCopyDirectory = defaultDir;
// now initialize data structures
DownloadedSchemaEntry[] entries = _importsDoc.getDownloadedSchemas().getEntryArray();
for (DownloadedSchemaEntry entry : entries) {
updateResource(entry);
}
}
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) {
_redownloadSet = refresh ? new HashSet<>() : null;
String[] allFilenames = getAllXSDFilenames();
if (sync) {
syncCacheWithLocalXsdFiles(allFilenames, false);
}
SchemaResource[] starters = _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) {
_redownloadSet = refresh ? new HashSet<>() : null;
if (filenames.length > 0) {
syncCacheWithLocalXsdFiles(filenames, true);
} else if (sync) {
syncCacheWithLocalXsdFiles(getAllXSDFilenames(), false);
}
Set<SchemaResource> starterset = new HashSet<>();
for (String s : uris) {
SchemaResource resource = (SchemaResource) lookupResource(null, s);
if (resource != null) {
starterset.add(resource);
}
}
for (String filename : filenames) {
SchemaResource resource = _resourceForFilename.get(filename);
if (resource != null) {
starterset.add(resource);
}
}
SchemaResource[] starters = 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.
* <p>
* 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<SchemaResource> seenResources = new HashSet<>();
Set<SchemaResource> vanishedResources = new HashSet<>();
for (String filename : filenames) {
// first, if the filename matches exactly, trust the filename
SchemaResource resource = _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 = _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 (SchemaResource resource : resources) {
redownloadResource(resource);
}
}
private void deleteResourcesInSet(Set<SchemaResource> seenResources, boolean setToDelete) {
Set<DownloadedSchemaEntry> seenCacheEntries = new HashSet<>();
for (SchemaResource resource : seenResources) {
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 = _resourceForCacheEntry.get(cacheEntry);
if (resource != null) {
warning("Removing obsolete cache entry for " + resource.getFilename());
_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 (String url : urls) {
if (resource == _resourceForURL.get(url)) {
_resourceForURL.remove(url);
}
}
}
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 (String url : urls) {
if (!_resourceForURL.containsKey(url)) {
_resourceForURL.put(url, resource);
}
}
return resource;
}
private static DigestInputStream digestInputStream(InputStream input) {
MessageDigest sha;
try {
sha = MessageDigest.getInstance("SHA");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
return new DigestInputStream(input, sha);
}
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) {
if (this == obj) {
return true;
}
if (!(obj instanceof SchemaResource)) {
return false;
}
SchemaResource sr = (SchemaResource) obj;
return getFilename().equals(sr.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.
* <p>
* 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 = _resourceForURL.get(schemaLocation);
if (result != null) {
return result;
}
}
if (nsURI != null) {
result = _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;
// 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 = _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];
int i = 1;
while (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();
}