/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.internal.xml.xsd.typeprovider;
import gw.config.CommonServices;
import gw.fs.IDirectory;
import gw.fs.IFile;
import gw.internal.xml.xsd.ResourceFileXmlSchemaSource;
import gw.internal.xml.xsd.typeprovider.schema.XmlSchema;
import gw.lang.ir.builder.IRClassBuilder;
import gw.lang.ir.builder.IRElementBuilder;
import gw.lang.ir.builder.IRMethodBuilder;
import gw.lang.reflect.*;
import gw.lang.reflect.java.IJavaClassInfo;
import gw.lang.reflect.module.IModule;
import gw.util.GosuClassUtil;
import gw.util.GosuExceptionUtil;
import gw.util.Pair;
import gw.util.concurrent.LockingLazyVar;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import static gw.lang.ir.builder.IRBuilderMethods.*;
public abstract class XmlSchemaResourceTypeLoaderBase<T> extends TypeLoaderBase implements IUninitializableTypeLoader, IResourceVerifier {
private static IdentityHashMap<Class<? extends IXmlTypeData>,Class<?>> _typeProxies = new IdentityHashMap<Class<? extends IXmlTypeData>, Class<?>>();
protected static Set<IXmlSchemaExceptionListener> _exceptionListeners = new HashSet<IXmlSchemaExceptionListener>();
private boolean _processingTypeData;
public static final String IGNORE_JAVA_CLASSES_PROPERTY = "gw.internal.xsd.useJavaClassesIfAvailable";
private final String _fileExtension;
protected static final String WSC_EXTENSION = "wsc";
protected final Map<String, XmlSchemaIndex<T>> _schemasByNamespaceCache = new HashMap<String, XmlSchemaIndex<T>>();
protected final LockingLazyVar<Map<String,IFile>> _schemaNamespaces = new LockingLazyVar<Map<String,IFile>>( TypeSystem.getGlobalLock() ) {
@Override
protected Map<String,IFile> init() {
return loadAllSchemaNamespaces();
}
};
protected final LockingLazyVar<Set<String>> _allNamespaces = new LockingLazyVar<Set<String>>( TypeSystem.getGlobalLock() ) {
@Override
protected Set<String> init() {
return XmlSchemaResourceTypeLoaderBase.getAllNamespaces( true, XmlSchemaResourceTypeLoaderBase.this, getAdditionalSubPackages() );
}
};
protected final LockingLazyVar<Set<String>> _allSchemaNamespacesAndSubNamespaces = new LockingLazyVar<Set<String>>( TypeSystem.getGlobalLock() ) {
@Override
protected Set<String> init() {
return XmlSchemaResourceTypeLoaderBase.getAllNamespaces( false, XmlSchemaResourceTypeLoaderBase.this, getAdditionalSubPackages() );
}
};
public XmlSchemaResourceTypeLoaderBase( String fileExtension, IModule module ) {
super( module );
_fileExtension = fileExtension;
}
public static Set<String> getAllNamespaces( boolean includeSuperNamespaces, XmlSchemaResourceTypeLoaderBase<?> typeLoader, String... additional ) {
Set<String> namespaces = new HashSet<String>();
for ( String namespace : typeLoader.getAllSchemaNamespaces() ) {
addAllNamespacesForSchema( includeSuperNamespaces, typeLoader, namespaces, namespace, additional );
}
return namespaces;
}
public static void addAllNamespacesForSchema( boolean includeSuperNamespaces, XmlSchemaResourceTypeLoaderBase<?> typeLoader, Set<String> namespaces, String namespace, String... additional ) {
if ( includeSuperNamespaces ) {
addAllSubNamespaces( namespaces, namespace + typeLoader.getElementsNamespacePrefix() );
addAllSubNamespaces( namespaces, namespace + typeLoader.getTypesNamespacePrefix() );
addAllSubNamespaces( namespaces, namespace + typeLoader.getEnumerationsNamespacePrefix() );
addAllSubNamespaces( namespaces, namespace + ".attributes" );
}
else {
namespaces.add( namespace );
namespaces.add( namespace + typeLoader.getElementsNamespacePrefix() );
namespaces.add( namespace + typeLoader.getTypesNamespacePrefix() );
namespaces.add( namespace + typeLoader.getEnumerationsNamespacePrefix() );
namespaces.add( namespace + ".attributes" );
}
namespaces.add( namespace + typeLoader.getTypesNamespacePrefix() + ".simple" );
namespaces.add( namespace + typeLoader.getTypesNamespacePrefix() + ".complex" );
namespaces.add( namespace + typeLoader.getAnonymousNamespacePrefix() );
namespaces.add( namespace + typeLoader.getAnonymousNamespacePrefix() + ".types" );
namespaces.add( namespace + typeLoader.getAnonymousNamespacePrefix() + ".types.simple" );
namespaces.add( namespace + typeLoader.getAnonymousNamespacePrefix() + ".types.complex" );
namespaces.add( namespace + typeLoader.getAnonymousNamespacePrefix() + ".elements" );
namespaces.add( namespace + typeLoader.getAnonymousNamespacePrefix() + ".attributes" );
for ( String suffix : additional ) {
namespaces.add( namespace + "." + suffix );
}
}
private static void addAllSubNamespaces(Set<String> namespaces, String namespace) {
while ( true ) { // for gw.foo.bar, also add gw.foo and gw
namespaces.add( namespace );
int idx = namespace.lastIndexOf( '.' );
if ( idx < 0 ) {
break;
}
namespace = namespace.substring( 0, idx );
}
}
public static void refreshSchemasFromAllTypeLoaders() {
TypeSystem.lock();
try {
for ( XmlSchemaResourceTypeLoaderBase typeloader : getAllXmlSchemaTypeLoaders() ) {
typeloader._allSchemaNamespacesAndSubNamespaces.clear();
typeloader._schemasByNamespaceCache.clear();
typeloader._schemaNamespaces.clear();
typeloader.clearTypeNames();
typeloader._allNamespaces.clear();
TypeSystem.clearErrorTypes();
}
XmlSchemaIndex.clearNormalizedSchemaNamespaces();
TypeSystem.refresh( true );
}
finally {
TypeSystem.unlock();
}
}
private static Iterable<XmlSchemaResourceTypeLoaderBase> getAllXmlSchemaTypeLoaders() {
List<XmlSchemaResourceTypeLoaderBase> xmlSchemaTypeLoaders = new ArrayList<XmlSchemaResourceTypeLoaderBase>();
for ( XmlSchemaResourceTypeLoaderBase loader : TypeSystem.getGlobalModule().getTypeLoaders(XmlSchemaResourceTypeLoaderBase.class) ) {
xmlSchemaTypeLoaders.add( loader );
}
return xmlSchemaTypeLoaders;
}
public static XmlSchemaIndex<?> findSchemaForNamespace( IModule module, String gosuNamespace ) {
if ( module == null ) {
throw new IllegalArgumentException( "module cannot be null" );
}
for (IModule m : module.getModuleTraversalList()) {
// Skip global module, but only if requested module is not global module itself
if (m == TypeSystem.getGlobalModule() && module != m) {
continue;
}
XmlSchemaIndex<?> schemaIndex;
List<? extends XmlSchemaResourceTypeLoaderBase> typeLoaders = m.getTypeLoaders(XmlSchemaResourceTypeLoaderBase.class);
for ( XmlSchemaResourceTypeLoaderBase loader : typeLoaders ) {
schemaIndex = loader.getSchemaForNamespace( gosuNamespace );
if ( schemaIndex != null ) {
return schemaIndex;
}
}
}
return null;
}
protected Map<String,IFile> loadAllSchemaNamespaces() {
Map<String,IFile> allSchemaNamespaces = new HashMap<String, IFile>();
HashMap<String,IDirectory> nonOverridden = getNonOverriddenCollectionPaths();
List<Pair<String,IFile>> pairs = getModule().getFileRepository().findAllFilesByExtension(_fileExtension);
for ( Pair<String, IFile> pair : pairs ) {
IFile file = pair.getSecond();
String packageName = pair.getFirst();
String path = getParentPath(packageName);
IDirectory useDirectory = nonOverridden.get(path);
if (useDirectory != null && !useDirectory.equals(file.getParent())) {
continue;
}
packageName = convertPathToPackage( packageName );
// TODO-dp the second check is a hack to prevent wsdls from pc-cmdline from being processed here
if ( packageName.startsWith( "config." ) || packageName.startsWith( "webservices." )) {
continue;
}
String schemaNamespace = XmlSchemaIndex.normalizeSchemaNamespace(packageName, pair.getFirst());
if ( ! CommonServices.getXmlSchemaCompatibilityConfig().useCompatibilityMode(schemaNamespace) ) {
if ( ! allSchemaNamespaces.containsKey( schemaNamespace ) ) {
allSchemaNamespaces.put( schemaNamespace, file );
}
}
}
return allSchemaNamespaces;
}
private HashMap<String, IDirectory> getNonOverriddenCollectionPaths() {
HashMap<String, IDirectory> map = new HashMap<String, IDirectory>();
List<Pair<String,IFile>> pairs = getModule().getFileRepository().findAllFilesByExtension(WSC_EXTENSION);
for ( Pair<String, IFile> pair : pairs ) {
IFile file = pair.getSecond();
IDirectory dir = file.getParent().dir(file.getBaseName());
String collectionPath = getCollectionPath(pair.getFirst());
if (!map.containsKey(collectionPath)) {
map.put(collectionPath, dir);
}
}
return map;
}
private String getParentPath(String path) {
int pos = path.lastIndexOf('/');
return pos == -1 ? "" : path.substring(0, pos);
}
private String getCollectionPath(String path) {
return path.substring(0, path.length() - WSC_EXTENSION.length() - 1);
}
private String convertPathToPackage( String packageName ) {
packageName = packageName.substring( 0, packageName.length() - _fileExtension.length() - 1 ).replace( '.', '_' ).replace( '/', '.' );
return packageName;
}
// called from XmlSchemaTestUtil.gs
@SuppressWarnings( { "UnusedDeclaration" } )
public void addSchemas( List<IFile> resourceFiles ) {
HashMap<Pair<URL, String>, XmlSchema> caches = new HashMap<Pair<URL, String>, XmlSchema>();
List<XmlSchemaIndex> schemaIndexes = new ArrayList<XmlSchemaIndex>();
for ( IFile resourceFile : resourceFiles ) {
try {
String relativePath = TypeSystem.getGlobalModule().getFileRepository().getResourceName(resourceFile.toURI().toURL());
String packageName = convertPathToPackage( relativePath );
String schemaNamespace = XmlSchemaIndex.normalizeSchemaNamespace( packageName, relativePath );
XmlSchemaIndex<T> schemaIndex = addSchemaToCacheIfNeeded( schemaNamespace, resourceFile, caches );
if ( schemaIndex != null ) {
schemaIndexes.add( schemaIndex );
addAllNamespacesForSchema( true, this, _allNamespaces.get(), schemaNamespace, getAdditionalSubPackages() );
addAllNamespacesForSchema( false, this, _allSchemaNamespacesAndSubNamespaces.get(), schemaNamespace, getAdditionalSubPackages() );
}
}
catch ( MalformedURLException e ) {
throw GosuExceptionUtil.forceThrow( e );
}
}
clearTypeNames();
for ( XmlSchemaIndex schemaIndex : schemaIndexes ) {
schemaIndex.validate( caches );
}
}
protected String[] getAdditionalSubPackages() {
return new String[] {};
}
public IXmlTypeData loadTypeData( String fullyQualifiedTypeName ) {
if ( ! getAllSchemaNamespacesAndSubNamespaces().contains( GosuClassUtil.getPackage( fullyQualifiedTypeName ) ) ) {
return null;
}
startProcessingTypeData();
try {
XmlSchemaIndex<?> schemaIndex = getSchemaForType( fullyQualifiedTypeName );
if ( schemaIndex == null ) {
return null;
}
return schemaIndex.getTypeData( fullyQualifiedTypeName );
}
finally {
endProcessingTypeData();
}
}
public Set<String> getAllSchemaNamespacesAndSubNamespaces() {
return _allSchemaNamespacesAndSubNamespaces.get();
}
public XmlSchemaIndex<T> getSchemaForNamespace( String packageName ) {
return getSchemaForNamespace(packageName, null);
}
XmlSchemaIndex<T> getSchemaForNamespace( String packageName, Map<Pair<URL,String>, XmlSchema> caches ) {
XmlSchemaIndex<T> schemaIndex = null;
IFile resourceFile = _schemaNamespaces.get().get(packageName);
if ( resourceFile != null ) {
schemaIndex = addSchemaToCacheIfNeeded( packageName, resourceFile, caches );
}
return schemaIndex;
}
protected XmlSchemaIndex<T> addSchemaToCacheIfNeeded( String packageName, IFile resourceFile, Map<Pair<URL, String>, XmlSchema> caches ) {
XmlSchemaIndex<T> schemaIndex = _schemasByNamespaceCache.get(packageName);
if ( schemaIndex == null ) {
TypeSystem.lock();
try {
schemaIndex = loadSchemaForNamespace( packageName, resourceFile, caches );
if ( schemaIndex != null ) {
_schemasByNamespaceCache.put( packageName, schemaIndex );
_schemaNamespaces.get().put( packageName, resourceFile );
}
}
finally {
TypeSystem.unlock();
}
}
return schemaIndex;
}
protected abstract XmlSchemaIndex<T> loadSchemaForNamespace( String namespace, IFile resourceFile, Map<Pair<URL,String>, XmlSchema> caches );
public Set<String> computeTypeNames() {
Map<Pair<URL,String>,XmlSchema> caches = new HashMap<Pair<URL, String>, XmlSchema>(); // key <URL, namespace>
startProcessingTypeData();
try {
Set<String> names = new HashSet<String>();
for ( String namespace : getAllSchemaNamespaces() ) {
XmlSchemaIndex<T> schemaIndex = getSchemaForNamespace( namespace, caches );
if ( schemaIndex != null ) {
names.addAll( schemaIndex.getAllTypeNames( caches ) );
}
}
return names;
} finally {
endProcessingTypeData();
}
}
public Collection<String> getAllSchemaNamespaces() {
return _schemaNamespaces.get().keySet();
}
@Override
public List<String> getHandledPrefixes() {
return Collections.emptyList();
}
public String getDeprecatedReason() {
return null;
}
public Set<String> getAllNamespaces() {
return _allNamespaces.get();
}
public String getSchemaSchemaTypeName() {
return "gw.xsd.w3c.xmlschema.Schema";
}
public String getElementsNamespacePrefix() {
return "";
}
public String getTypesNamespacePrefix() {
return ".types";
}
public String getEnumerationsNamespacePrefix() {
return ".enums";
}
public String getAnonymousNamespacePrefix() {
return ".anonymous";
}
public XmlSchemaIndex<?> getSchemaForType( String fullyQualifiedTypeName ) {
Set<String> namespacesAndSubNamespaces = getAllSchemaNamespacesAndSubNamespaces();
XmlSchemaIndex<?> schemaIndex = null;
String packageName = fullyQualifiedTypeName;
while ( true ) {
int idx = packageName.lastIndexOf( '.' );
if ( idx < 0 ) {
break;
}
packageName = packageName.substring( 0, idx );
if ( ! namespacesAndSubNamespaces.contains( packageName ) ) {
return null;
}
schemaIndex = getSchemaForNamespace( packageName );
if ( schemaIndex != null ) {
break;
}
}
return schemaIndex;
}
protected void endProcessingTypeData() {
_processingTypeData = false;
}
protected void startProcessingTypeData() {
if ( _processingTypeData ) {
throw new RuntimeException( "Attempt to loadTypeData() while resolving types" );
}
_processingTypeData = true;
}
@Override
public final IType getType( String fullyQualifiedName ) {
final IXmlTypeData typeData = loadTypeData( fullyQualifiedName );
if ( typeData == null ) {
return null;
}
try {
Class<?> typeProxy = _typeProxies.get( typeData.getClass() );
if ( typeProxy == null ) {
typeProxy = generateTypeProxy( typeData.getClass(), typeData.getAdditionalInterfaces() );
_typeProxies.put( typeData.getClass(), typeProxy );
}
return TypeSystem.getOrCreateTypeReference( (IType) typeProxy.getConstructor( ITypeLoader.class, IXmlTypeData.class ).newInstance( this, typeData ) );
}
catch ( Exception ex ) {
throw new RuntimeException( ex );
}
}
private Class<?> generateTypeProxy( Class<?> typeDataClass, List<Class<?>> additionalInterfaces ) throws NoSuchMethodException {
LinkedHashSet<Class<?>> ifaces = new LinkedHashSet<Class<?>>();
ifaces.addAll( additionalInterfaces ); // wish we get these from the typedata class instead of the typedata instance
ifaces.add( IXmlType.class );
Set<Method> generatedMethods = new HashSet<Method>();
IRClassBuilder classBuilder = new IRClassBuilder( typeDataClass.getName() + ITypeRefFactory.USER_PROXY_SUFFIX, getClassOfType());
for ( Constructor ctor : getClassOfType().getDeclaredConstructors() ) {
classBuilder.createConstructor()._public().copyParameters( ctor ).body(
_superInit( passArgs( IJavaClassInfo.Util.get( ctor ) ) ),
_return()
) ;
}
for ( Class<?> iface : ifaces ) {
classBuilder.withInterface( iface );
for ( Method method : iface.getMethods() ) {
if ( generatedMethods.add( method ) ) {
generateMethod( classBuilder, method, typeDataClass );
}
}
}
return classBuilder.define( typeDataClass.getClassLoader() );
}
protected Class<?> getClassOfType() {
return XmlType.class;
}
private void generateMethod( IRClassBuilder classBuilder, Method methodToImplement, Class<?> typeDataClass ) throws NoSuchMethodException {
// if method exists on XmlType, don't generate it
Method methodToDelegateTo;
try {
getClassOfType().getMethod(methodToImplement.getName(), methodToImplement.getParameterTypes());
return;
}
catch ( NoSuchMethodException e ) {
methodToDelegateTo = typeDataClass.getMethod( methodToImplement.getName(), methodToImplement.getParameterTypes() );
}
// We need to preserve synthetic/bridge modifiers for the sake of covariant methods
int modifiers = Modifier.PUBLIC;
if (methodToImplement.isSynthetic() || methodToImplement.isBridge()) {
modifiers = modifiers | Modifier.VOLATILE;
}
IRMethodBuilder builder = classBuilder.createMethod().withModifiers( modifiers ).
name( methodToImplement.getName()).copyParameters( methodToImplement ).returns( methodToImplement.getReturnType() );
List<IRElementBuilder> bodyElements = new ArrayList<IRElementBuilder>();
//noinspection ObjectEquality
if (methodToImplement.getReturnType() == void.class) {
bodyElements.add( call( "getTypeData" ).cast( methodToDelegateTo.getDeclaringClass() ).call( IJavaClassInfo.Util.get( methodToDelegateTo ), passArgs( IJavaClassInfo.Util.get( methodToImplement ) ) ) );
bodyElements.add( _return() );
}
else {
bodyElements.add( _return( call( "getTypeData" ).cast( methodToDelegateTo.getDeclaringClass() ).call( IJavaClassInfo.Util.get( methodToDelegateTo ), passArgs( IJavaClassInfo.Util.get( methodToImplement ) ) ) ) );
}
builder.body( bodyElements );
}
public Collection<? extends IConstructorInfo> getAdditionalConstructors( IXmlSchemaTypeData<T> typeData ) {
return Collections.emptyList();
}
public Collection<? extends IPropertyInfo> getAdditionalProperties( IXmlSchemaTypeData<T> typeData ) {
return Collections.emptyList();
}
public Collection<? extends IMethodInfo> getAdditionalMethods( IXmlSchemaTypeData<T> typeData ) {
return Collections.emptyList();
}
public XmlSchemaIndex.NormalizationMode getPropertyNameNormalizationMode() {
return XmlSchemaIndex.NormalizationMode.PROPERCASE;
}
@Override
public final boolean isCaseSensitive() {
return true;
}
@Override
public void uninitialize() {
XmlSchemaIndex.clear();
}
public static void addExceptionListener( IXmlSchemaExceptionListener listener ) {
_exceptionListeners.add(listener);
}
public static void removeExceptionListener( IXmlSchemaExceptionListener listener ) {
_exceptionListeners.remove(listener);
}
public void schemaIndexingExceptionOccurred( String namespace, IFile resourceFile, Throwable t ) {
for (IXmlSchemaExceptionListener exceptionListener : _exceptionListeners) {
exceptionListener.exceptionOccurred( namespace, resourceFile, t );
}
}
public String getFileExtension() {
return _fileExtension;
}
@Override
public boolean hasNamespace(String namespace) {
//TODO-dp can we do better?
return getAllNamespaces().contains(namespace);
}
@Override
public void refreshedNamespace(String namespace, IDirectory dir, RefreshKind kind) {
// anything to do ?
}
@Override
public boolean handlesFile(IFile file) {
return getFileExtension().equals( file.getExtension() );
}
@Override
public String[] getTypesForFile(IFile file) {
String type = TypeSystem.getGlobalModule().pathRelativeToRoot(file.getParent());
String packageName = (type.replace('/', '.') + '.' + file.getBaseName()).toLowerCase();
XmlSchemaIndex<?> index = getSchemaForNamespace(packageName);
if (index != null) {
Set<String> allTypeNames = index.getAllTypeNames(null);
return allTypeNames.toArray(new String[allTypeNames.size()]);
}
// No schema index yet -- must be a new type, need to refresh namespace type at least
return new String[] { packageName };
}
protected void createdType( String typeName ) {
if ( _typeNames != null ) {
_typeNames.add( typeName );
}
}
@Override
public RefreshKind refreshedFile(IFile file, String[] types, RefreshKind kind) {
_allSchemaNamespacesAndSubNamespaces.clear();
_schemasByNamespaceCache.clear();
_schemaNamespaces.clear();
clearTypeNames();
_allNamespaces.clear();
TypeSystem.clearErrorTypes();
XmlSchemaIndex.clearNormalizedSchemaNamespaces();
return kind;
}
@Override
protected boolean shouldCacheTypeNames() {
return false;
}
@Override
public boolean showTypeNamesInIDE() {
return true;
}
@Override
public List<String> verifyResources() throws Exception {
List<String> errorMessages = new ArrayList<String>();
for ( Map.Entry<String, IFile> entry : loadAllSchemaNamespaces().entrySet() ) {
IFile schemaSourceFile = entry.getValue();
URL url = schemaSourceFile.toURI().toURL();
String resourceName = getModule().getFileRepository().getResourceName( url );
verifySchemaSourceFile( entry.getKey(), schemaSourceFile, errorMessages, resourceName, url );
}
return errorMessages;
}
protected void verifySchemaSourceFile( String packageName, IFile schemaSourceFile, List<String> errorMessages, String resourceName, URL url ) throws Exception {
try {
XmlSchemaIndex<T> schemaIndex = new XmlSchemaIndex<T>( this, packageName, new ResourceFileXmlSchemaSource( schemaSourceFile ), null );
schemaIndex.validate( null );
}
catch ( Exception ex ) {
ex.printStackTrace();
Throwable t = ex;
while ( t.getCause() != null ) {
t = t.getCause();
}
errorMessages.add( resourceName + ": " + t.toString() );
}
}
}