On Thu, Sep 21, 2006 at 09:38:55AM +0000, David Holroyd wrote:
> Has anyone done work on generating AS classes from a schema, or seen
> such a tool?
> 
> I'm thinking of trying to build something do do this if nothing exists.

Attached is the code I hacked together.  Notes:

 - Horrible code!  :)
 - Creates classes for named top-level complex types *only*
 - Depends on unreleased version of metaas[1], so you'll need to furtle
   in SVN to actually use the attached!
 - Depends on the Eclipse XSD infoset model, as noted at the top of the
   attached file.

Is anyone interested in turning this into a proper project?


ta,
dave

[1] http://www.badgers-in-foil.co.uk/projects/metaas/

-- 
http://david.holroyd.me.uk/
/*
 * Copyright (c) David Holroyd 2006
 */

package uk.co.badgersinfoil.asxsd;


import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.xsd.XSDAnnotation;
import org.eclipse.xsd.XSDAttributeDeclaration;
import org.eclipse.xsd.XSDAttributeGroupContent;
import org.eclipse.xsd.XSDAttributeUse;
import org.eclipse.xsd.XSDComplexTypeContent;
import org.eclipse.xsd.XSDComplexTypeDefinition;
import org.eclipse.xsd.XSDCompositor;
import org.eclipse.xsd.XSDElementDeclaration;
import org.eclipse.xsd.XSDModelGroup;
import org.eclipse.xsd.XSDNamedComponent;
import org.eclipse.xsd.XSDParticle;
import org.eclipse.xsd.XSDParticleContent;
import org.eclipse.xsd.XSDSchema;
import org.eclipse.xsd.XSDSimpleTypeDefinition;
import org.eclipse.xsd.XSDTypeDefinition;
import org.eclipse.xsd.util.XSDResourceFactoryImpl;
import org.eclipse.xsd.util.XSDResourceImpl;
import org.eclipse.xsd.util.XSDSchemaQueryTools;
import org.w3c.dom.Element;
import uk.co.badgersinfoil.metaas.ASClassType;
import uk.co.badgersinfoil.metaas.ASField;
import uk.co.badgersinfoil.metaas.ASMethod;
import uk.co.badgersinfoil.metaas.ASSourceFactory;
import uk.co.badgersinfoil.metaas.CompilationUnit;
import uk.co.badgersinfoil.metaas.StatementContainer;
import uk.co.badgersinfoil.metaas.Visibility;


/*
 * To use this class, you will need both metaas, and the Eclpise XSD infoset
 * model, and supporting packages.  I used emf-sdo-xsd-Standalone-2.2.0,
 * available at,
 * 
 * http://www.eclipse.org/downloads/download.php?file=/tools/emf/downloads/drops/2.2.0/R200606271057/emf-sdo-xsd-Standalone-2.2.0.zip
 * 
 * After unpacking the contents of this archive, the emf_common, emf_ecore and
 * xsd jars (from the archive's 'emf/bin' directory) must be added to your
 * classpath.
 * 
 * NOTE: this code is a bit ugly; a proper imlementation might be allowed to
 * grow to more than a single class ;)
 */


/*
 * TODOs:
 * - WSDL?  [ http://dev.eclipse.org/newslists/news.eclipse.technology.xsd/msg00401.html ]
 */


public class Main {
	private static final String SCHEMA_NAMESPACE = "http://www.w3.org/2001/XMLSchema";;
	private static String destDir;

	public static void main(String[] args) throws IOException {
		// need to do this in order to have Eclipse's XSD 'resource'
		// support work,
		Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("xsd", new XSDResourceFactoryImpl());

		String filename = args[0];
		if (args.length > 1) {
			destDir = args[1];
		} else {
			destDir = ".";
		}
		XSDSchema mainSchema = loadSchema(filename);
		processSchema(mainSchema);
	}

	private static XSDSchema loadSchema(String filename) throws IOException {
		ResourceSet resourceSet = new ResourceSetImpl();
		XSDResourceImpl resource = (XSDResourceImpl)resourceSet.createResource(URI.createURI("*.xsd"));
		resource.setURI(URI.createFileURI(filename));
		resource.load(resourceSet.getLoadOptions());
		XSDSchema mainSchema = resource.getSchema();
		return mainSchema;
	}

	private static void processSchema(XSDSchema schema) throws IOException {
		processSchemaCreateTypes(schema);
		processSchemaCreateUnmarshaler(schema);
	}

	private static void processSchemaCreateTypes(XSDSchema schema) throws IOException {
		List types = schema.getTypeDefinitions();
		for (Iterator i = types.iterator(); i.hasNext(); ) {
			 XSDTypeDefinition typeDef = (XSDTypeDefinition)i.next();
			 if (typeDef instanceof XSDComplexTypeDefinition) {
				 processComplexType((XSDComplexTypeDefinition)typeDef);
			 }
		}
	}

	/**
	 * Handle a top-level complex type definition by creating an AS
	 * class.
	 */
	private static void processComplexType(XSDComplexTypeDefinition typeDef) throws IOException {
		ASSourceFactory fact = new ASSourceFactory();
		CompilationUnit unit = fact.newClass(typeName(typeDef));
		ASClassType clazz = (ASClassType)unit.getType();
		processComplexTypeBaseType(typeDef, clazz);
		processComplexTypeAnnotation(typeDef, clazz);
		processAllComplexTypeAttributes(typeDef, clazz);
		processAllComplexTypeElements(typeDef, clazz);
		fact.write(destDir, unit);
	}

	/**
	 * Super-type handling
	 */
	private static void processComplexTypeBaseType(XSDComplexTypeDefinition typeDef, ASClassType clazz) {
		XSDTypeDefinition baseType = typeDef.getBaseType();
		if (!isXSDAnyType(baseType)) {
			clazz.setSuperclass(typeName(baseType));
		}
	}

	/**
	 * turn attrubutes defined by the complexType into class properties
	 */
	private static void processAllComplexTypeAttributes(XSDComplexTypeDefinition typeDef,
	                                                 ASClassType clazz)
	{
		List attrs = typeDef.getAttributeContents();
		for (Iterator i=attrs.iterator(); i.hasNext(); ) {
			XSDAttributeGroupContent attrContent = (XSDAttributeGroupContent)i.next();
			if (attrContent instanceof XSDAttributeUse) {
				processComplexTypeAttribute((XSDAttributeUse)attrContent, clazz);
			}
		}
	}

	/**
	 * turn this particular attribute into a property on the given AS class
	 */
	private static void processComplexTypeAttribute(XSDAttributeUse attrUse,
	                                                ASClassType clazz)
	{
		XSDAttributeDeclaration attrDecl = attrUse.getAttributeDeclaration();
		ASField field = clazz.newField(fieldName(attrDecl), Visibility.PUBLIC,
		                               typeName(attrDecl.getTypeDefinition()));
		String doc = findDocumentation(attrDecl.getAnnotation());
		if (doc != null) {
			field.setDocComment(doc);
		}
	}

	/**
	 * create a field name based on the given attribute declaration
	 */
	private static String fieldName(XSDAttributeDeclaration attrDecl) {
		// TODO: name sanitization etc.
		return attrDecl.getName();
	}

	/**
	 * add any annotation on the given complexType as the documentation
	 * comment for the given AS class.
	 */
	private static void processComplexTypeAnnotation(XSDComplexTypeDefinition typeDef,
	                                                 ASClassType clazz)
	{
		String doc = findDocumentation(typeDef.getAnnotation());
		if (doc != null) {
			clazz.setDocComment("\n"+doc+"\n");
		}
	}

	/**
	 * attempt to extract simple text from the documentation element of the
	 * given annotation.
	 */
	private static String findDocumentation(XSDAnnotation annotation) {
		if (annotation != null) {
			List docs = annotation.getUserInformation();
			for (Iterator i = docs.iterator(); i.hasNext(); ) {
				// maybe we can do better..?
				Element doc = (Element)i.next();
				return preProcessComment(doc.getTextContent());
			}
		}
		return null;
	}

	/**
	 * Strip initial whitespace from all lines in the given string, and
	 * return a string which starts each line with a single space character,
	 * ready to go into a javadoc comment.
	 */
	private static String preProcessComment(String text) {
		return text.replaceFirst("\\A\\s*", " ").replaceAll("([\n\r])\\s+", "$1 ");
	}

	/**
	 * returns the name of the AS type that holds data for the given
	 * component of the schema
	 */
	private static String typeName(XSDNamedComponent named) {
		if (named instanceof XSDSimpleTypeDefinition) {
			XSDSimpleTypeDefinition simpleType = (XSDSimpleTypeDefinition)named;
			return lookupTypeName(simpleType);
		}
		if (isXSDAnyType(named)) {
			return "XML";
		}
		String pkgName = toPackageName(named.getTargetNamespace());
		return pkgName + "." + named.getName();
	}

	private static boolean isXSDAnyType(XSDNamedComponent named) {
		return "anyType".equals(named.getName())
		    && named.getTargetNamespace().equals(SCHEMA_NAMESPACE);
	}

	private static String toPackageName(String targetNamespace) {
		URI uri = URI.createURI(targetNamespace);
		String name = reverseJoin(sanitize(uri.host().split("\\.")), ".");
		if (uri.hasPath()) {
			String path = uri.path().replaceAll("/+", "/").replaceFirst("\\A/", "");
			name = name + "." + join(sanitize(path.split("/")), ".");
		}
		return name;
	}

	private static String[] sanitize(String[] strings) {
		for (int i=0; i<strings.length; i++) {
			strings[i] = sanitize(strings[i]);
		}
		return strings;
	}

	private static String sanitize(String string) {
		StringBuffer result = new StringBuffer();
		if (!Character.isJavaIdentifierStart(string.charAt(0))
		  && Character.isJavaIdentifierPart(string.charAt(0)))
		{
			// e.g. if this fragment starts with a number, prefix
			// it with an underscore to create a valid identifier
			result.append("_");
		}
		for (int i=0; i<string.length(); i++) {
			char c = string.charAt(i);
			if (Character.isJavaIdentifierPart(c)) {
				result.append(c);
			} else {
				result.append("_");
			}
		}
		return result.toString();
	}

	private static String reverseJoin(String[] strings, String delimiter) {
		StringBuffer result = new StringBuffer();
		for (int i=strings.length-1; i>=0; i--) {
			result.append(strings[i]);
			if (i>0) {
				result.append(delimiter);
			}
		}
		return result.toString();
	}

	private static String join(String[] strings, String delimiter) {
		StringBuffer result = new StringBuffer();
		for (int i=0; i<strings.length; i++) {
			if (i>0) {
				result.append(delimiter);
			}
			result.append(strings[i]);
		}
		return result.toString();
	}

	/**
	 * tries to map XML Schema standard types to AS types
	 */
	private static String lookupTypeName(XSDSimpleTypeDefinition simpleType) {
		if (simpleType == null) {
			return null;
		}
		if (simpleType.getTargetNamespace().equals(SCHEMA_NAMESPACE)) {
			if (simpleType.getName().equals("string")) {
				return "String";
			}
			if (simpleType.getName().equals("int")) {
				return "Number";
			}
			if (simpleType.getName().equals("float")) {
				return "Number";
			}
System.err.println("Unhandled type "+simpleType.getURI());
			return null;
		}
		return lookupTypeName(simpleType.getBaseTypeDefinition());
	}

	/**
	 * adds all elements declared by the given complexType as properties
	 * to the given AS class
	 */
	private static void processAllComplexTypeElements(XSDComplexTypeDefinition typeDef,
	                                                  ASClassType clazz)
	{
		XSDComplexTypeContent complexContent = typeDef.getContent();
		if (complexContent instanceof XSDParticle) {
			XSDParticle particle = (XSDParticle)complexContent;
			XSDParticleContent particleContent = particle.getContent();
			if (particleContent instanceof XSDModelGroup) {
				XSDModelGroup modelGroup = (XSDModelGroup)particleContent;
				if (modelGroup.getCompositor().equals(XSDCompositor.SEQUENCE_LITERAL)) {
					processComplexTypeSequence(typeDef, modelGroup, clazz);
				}
			}
		}
	}

	/**
	 * auxiliary function used by processAllComplexTypeElements() to handle
	 * the xs:sequence within an xs:complexType
	 */
	private static void processComplexTypeSequence(XSDComplexTypeDefinition typeDef,
	                                               XSDModelGroup modelGroup,
	                                               ASClassType clazz)
	{
		List particles = modelGroup.getParticles();
		for (Iterator i=particles.iterator(); i.hasNext(); ) {
			XSDParticle part = (XSDParticle)i.next();
			XSDParticleContent partContent = part.getContent();
			if (partContent instanceof XSDElementDeclaration) {
				processComplexTypeElementDeclaration(part, (XSDElementDeclaration)partContent, clazz);
			}
		}
	}

	/**
	 * handles an xs:element within the xs:sequence of an xs:complexType
	 * by adding a property to the given AS class.
	 */
	private static void processComplexTypeElementDeclaration(XSDParticle part,
	                                                         XSDElementDeclaration decl,
	                                                         ASClassType clazz)
	{
		if (decl.isElementDeclarationReference()) {
			decl = decl.getResolvedElementDeclaration();
		}
		XSDElementDeclaration listElement = getElementIfContainerForList(decl);
		String typeName = null;
		String doc = findDocumentation(decl.getAnnotation());
		if (listElement != null) {
			typeName = "Array";
			if (doc == null) {
				doc = "";
			} else {
				doc += "\n\n";
			}
			doc += " Elements of type [EMAIL PROTECTED] " + typeName(listElement.getType())+"}";
		} else if (isMultiplyOccuring(part)) {
			typeName = "Array";
			if (doc == null) {
				doc = "";
			} else {
				doc += "\n\n";
			}
			if (decl.getType() != null) {
				doc += "Elements of type [EMAIL PROTECTED] " + typeName(decl.getType()) + "}\n";
			}
			doc += "minOccurs "+describeMultiplicity(part.getMinOccurs())+", maxOccurs "+describeMultiplicity(part.getMaxOccurs());
		} else if (decl.getType() != null) {
			typeName = typeName(decl.getType());
		}
if (typeName == null) {
	System.err.println("no AS type resulted from: "+decl.getType());
}
		ASField field = clazz.newField(fieldName(decl), Visibility.PUBLIC, typeName);
		if (doc != null) {
			field.setDocComment(doc);
		}
	}

	/**
	 * returns the name of the AS field which should hold values of the
	 * given element declaration.
	 */
	private static String fieldName(XSDElementDeclaration decl) {
		// TODO: sanitise value, etc.
		return decl.getName();
	}

	/**
	 * If the given element appears to be implementing a list-container
	 * design pattern then return the definition of the element repeated in
	 * the list
	 */
	private static XSDElementDeclaration getElementIfContainerForList(XSDElementDeclaration decl) {
		XSDTypeDefinition typeDef = decl.getAnonymousTypeDefinition();
		if (typeDef == null) {
			return null;
		}
		// look for the definition pattern which allows documents like:
		// ...<eggs><egg/><egg/><egg/></eggs>...
		// so that we can add a 'eggs' array to the defining class,
		// rather than creating a useless 'Eggs' class which just holds
		// the array.
		// TODO: check for attrs on container,
		XSDParticle ctype = typeDef.getComplexType();
		XSDParticleContent particleContent = ctype.getContent();
		if (particleContent instanceof XSDModelGroup) {
			XSDModelGroup modelGroup = (XSDModelGroup)particleContent;
			if (modelGroup.getCompositor().equals(XSDCompositor.SEQUENCE_LITERAL)) {
				List particles = modelGroup.getParticles();
				if (particles.size() == 1) {
					XSDParticle part = (XSDParticle)particles.get(0);
					XSDParticleContent partContent = part.getContent();
					if (partContent instanceof XSDElementDeclaration) {
						if (isMultiplyOccuring(part)) {
							return (XSDElementDeclaration)partContent;
						}
					}
				}
			}
		}
		
		return null;
	}

	/**
	 * returns true if the given particle's maxOccurs attribute is either
	 * 'unbounded', or an integer greater than 1.
	 */
	private static boolean isMultiplyOccuring(XSDParticle part) {
		int max = part.getMaxOccurs();
		return max == -1 || max > 1;  // -1 : unbounded
	}

	/**
	 * returns a text string with either the value "unbounded" if the
	 * given integer is -1, or the decimal representation of the given
	 * integer otherwise.
	 */
	private static String describeMultiplicity(int occurs) {
		if (occurs == -1) {
			return "unbounded";
		} else {
			return String.valueOf(occurs);
		}
	}


	// ---- marshaling / unmarshaling ----


	private static void processSchemaCreateUnmarshaler(XSDSchema schema) throws IOException {
		ASSourceFactory fact = new ASSourceFactory();
		CompilationUnit unit = fact.newClass(toPackageName(schema.getTargetNamespace())+".Unmarshaler");
		ASClassType clazz = (ASClassType)unit.getType();
		List types = schema.getTypeDefinitions();
		for (Iterator i = types.iterator(); i.hasNext(); ) {
			XSDTypeDefinition typeDef = (XSDTypeDefinition)i.next();
			if (typeDef instanceof XSDComplexTypeDefinition) {
				XSDComplexTypeDefinition ctype = (XSDComplexTypeDefinition)typeDef;
				String typeName = typeName(ctype);
				ASMethod meth = clazz.newMethod(unmarshalerMethodFor(ctype), Visibility.PUBLIC, typeName(ctype));
				meth.addParam("thisElement", "XML");
				meth.addStmt("var _result:"+typeName+" = new "+typeName+"()");
				processComplexTypeBaseTypeUnmarshal(ctype, meth);
				processAllComplexTypeAttributesUnmarshal(ctype, meth);
				processAllComplexTypeElementsUnmarshal(ctype, meth);
				meth.addStmt("return _result");
			}
		}
		fact.write(destDir, unit);
	}

	private static void processComplexTypeBaseTypeUnmarshal(XSDComplexTypeDefinition ctype, ASMethod meth) {
		XSDTypeDefinition baseType = ctype.getBaseType();
		if (!isXSDAnyType(baseType)) {
			// TODO: handle base class initialisation
		}
	}

	private static void processAllComplexTypeAttributesUnmarshal(XSDComplexTypeDefinition ctype, ASMethod meth) {
		List attrs= ctype.getAttributeContents();
		for (Iterator i=attrs.iterator(); i.hasNext(); ) {
			XSDAttributeGroupContent attrContent = (XSDAttributeGroupContent)i.next();
			if (attrContent instanceof XSDAttributeUse) {
				processComplexTypeAttributeUnmarshal((XSDAttributeUse)attrContent, meth);
			}
		}
	}

	private static void processComplexTypeAttributeUnmarshal(XSDAttributeUse attrUse, ASMethod meth) {
		XSDAttributeDeclaration attrDecl = attrUse.getAttributeDeclaration();
		String accessExpr = "thisElement."+attrAccess(attrDecl);
		accessExpr = doBasicTypeCoercion(accessExpr, attrDecl.getType());
		meth.addStmt("_result."+fieldName(attrDecl)+" = "+accessExpr);
	}

	private static String doBasicTypeCoercion(String expr, XSDTypeDefinition type) {
		if (typeIsA(type, SCHEMA_NAMESPACE, "string")) {
			return expr;
		}
		if (typeIsA(type, SCHEMA_NAMESPACE, "int")
		   ||typeIsA(type, SCHEMA_NAMESPACE, "float")) {
			return "Number("+expr+")";
		}
		System.err.println("Unable to produce type convertion expression for "+type.getURI());
		return expr+" /* <-- didn't know how to convert */";
	}

	private static boolean typeIsA(XSDTypeDefinition type, String namespace, String name) {
		return (type.getName().equals(name) && type.getTargetNamespace().equals(namespace))
		    || XSDSchemaQueryTools.isTypeDerivedFrom(type, namespace, name);
	}

	private static String attrAccess(XSDAttributeDeclaration attrDecl) {
		if (isValidIdent(attrDecl.getName())) {
			return "@" + attrDecl.getName();
		}
		return "@[" + ASSourceFactory.str(attrDecl.getName()) + "]";
	}

	private static boolean isValidIdent(String name) {
		if (!Character.isJavaIdentifierStart(name.charAt(0))) {
			return false;
		}
		for (int i=1; i<name.length(); i++) {
			if (!Character.isJavaIdentifierPart(name.charAt(0))) {
				return false;
			}
		}
		return true;
	}

	private static void processAllComplexTypeElementsUnmarshal(XSDComplexTypeDefinition typeDef,
	                                                           ASMethod meth)
	{
		XSDComplexTypeContent complexContent = typeDef.getContent();
		if (complexContent instanceof XSDParticle) {
			XSDParticle particle = (XSDParticle)complexContent;
			XSDParticleContent particleContent = particle.getContent();
			if (particleContent instanceof XSDModelGroup) {
				XSDModelGroup modelGroup = (XSDModelGroup)particleContent;
				if (modelGroup.getCompositor().equals(XSDCompositor.SEQUENCE_LITERAL)) {
					processComplexTypeSequenceUnmarshal(typeDef, modelGroup, meth);
				}
			}
		}
	}

	private static void processComplexTypeSequenceUnmarshal(XSDComplexTypeDefinition typeDef,
	                                                        XSDModelGroup modelGroup,
	                                                        ASMethod meth)
	{
		List particles = modelGroup.getParticles();
		if (!particles.isEmpty()) {
			//meth.addComment(" process child elements,");
			meth.addStmt("var _children:XMLList = thisElement.children()");
			meth.addStmt("var _seq:int = 0");
		}
		for (Iterator i=particles.iterator(); i.hasNext(); ) {
			StatementContainer block = meth;
			XSDParticle part = (XSDParticle)i.next();
			XSDParticleContent partContent = part.getContent();
			if (partContent instanceof XSDElementDeclaration) {
				XSDElementDeclaration elementDecl = (XSDElementDeclaration)partContent;
				if (elementDecl.isElementDeclarationReference()) {
					elementDecl = elementDecl.getResolvedElementDeclaration();
				}
				if (isOptional(part)) {
					block = meth.newIf("_children[_seq].name()==new QName("+ASSourceFactory.str(elementDecl.getTargetNamespace())+", "+ASSourceFactory.str(elementDecl.getName()));
				}
				processComplexTypeSequenceElementDeclarationUnmarshal(part, elementDecl, block);
			} else {
				meth.addStmt("_seq++");
			}
		}
	}

	private static boolean isOptional(XSDParticle part) {
		return part.getMinOccurs()==0 && part.getMaxOccurs()==1;
	}

	private static void processComplexTypeSequenceElementDeclarationUnmarshal(XSDParticle part,
	                                                                  XSDElementDeclaration decl,
	                                                                  StatementContainer block)
	{
		String accessExpr = "_children[_seq++]";
		XSDTypeDefinition typeDef = decl.getType();
		String fieldAccess = "_result."+fieldName(decl);
		if (typeDef instanceof XSDSimpleTypeDefinition) {
			accessExpr = doBasicTypeCoercion(accessExpr+".text()", typeDef);
			block.addStmt(fieldAccess+" = "+accessExpr);
		} else {
			XSDElementDeclaration listElement = getElementIfContainerForList(decl);
			if (listElement == null) {
				String unmarshaled = unmarshaledFrom(typeDef, accessExpr);
				if (isMultiplyOccuring(part)) {
					block.addStmt(fieldAccess+" = new Array()");
					block = block.newWhile("_seq<_children.length() && _children[_seq].name()==new QName("+ASSourceFactory.str(decl.getTargetNamespace())+", "+ASSourceFactory.str(decl.getName())+")");
					block.addStmt(fieldAccess+".push("+unmarshaled+")");
				} else {
					block.addStmt(fieldAccess+" = "+unmarshaled);
				}
			} else {
				block.addStmt(fieldAccess + " = new Array()");
				StatementContainer loop = block.newForEachIn("var _child:XML", accessExpr+".elements()");
				String unmarshaled = unmarshaledFrom(listElement.getType(), "_child");
				loop.addStmt(fieldAccess + ".push("+unmarshaled+")");
			}
		}
	}

	private static String unmarshaledFrom(XSDTypeDefinition typeDef, String expr) {
		if (isXSDAnyType(typeDef)) {
			// result is just the XML node
			return expr;
		}
		return unmarshalerMethodFor(typeDef)+"("+expr+")";
	}

	private static String unmarshalerMethodFor(XSDTypeDefinition typeDef) {
		return "unmarshal"+typeDef.getName();
	}
}
_______________________________________________
osflash mailing list
[email protected]
http://osflash.org/mailman/listinfo/osflash_osflash.org

Reply via email to