kinman 2002/06/13 11:56:18 Modified: jasper2/src/share/org/apache/jasper/compiler Generator.java JspDocumentParser.java Node.java Parser.java Log: - Patch by Jan Luehe, to fix 2 problems related to scripting variables. 1. AT_BEGIN and AT_END variables are not accessible after tag end. 2. Tags within the same tag cuases Javac compilation errors. Revision Changes Path 1.29 +351 -64 jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/Generator.java Index: Generator.java =================================================================== RCS file: /home/cvs/jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/Generator.java,v retrieving revision 1.28 retrieving revision 1.29 diff -u -r1.28 -r1.29 --- Generator.java 12 Jun 2002 23:17:36 -0000 1.28 +++ Generator.java 13 Jun 2002 18:56:18 -0000 1.29 @@ -185,12 +185,13 @@ public void visit(Node.CustomTag n) throws JasperException { String name = createTagHandlerPoolName(n.getPrefix(), - n.getShortName(), - n.getAttributes()); + n.getShortName(), + n.getAttributes()); n.setTagHandlerPoolName(name); if (!names.contains(name)) { names.add(name); } + visitBody(n); } @@ -234,6 +235,80 @@ page.visit(new TagHandlerPoolVisitor(tagHandlerPoolNames)); } + + /* + * For every custom tag, declares its scripting variables with AT_BEGIN + * and AT_END scopes. + */ + private void declareAtBeginAtEndScriptingVariables(Node.Nodes page) + throws JasperException { + + class ScriptingVariableDeclarationVisitor extends Node.Visitor { + + /* + * Vector keeping track of which scripting variables have already + * been declared + */ + private Vector scriptVars; + + /* + * Constructor. + */ + public ScriptingVariableDeclarationVisitor() { + scriptVars = new Vector(); + } + + public void visit(Node.CustomTag n) throws JasperException { + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + + if ((varInfos == null) && (tagVarInfos == null)) { + visitBody(n); + } + + if (varInfos != null) { + for (int i=0; i<varInfos.length; i++) { + int scope = varInfos[i].getScope(); + String varName = varInfos[i].getVarName(); + if (((scope == VariableInfo.AT_BEGIN) + || (scope == VariableInfo.AT_END)) + && varInfos[i].getDeclare() + && !scriptVars.contains(varName)) { + out.printin(varInfos[i].getClassName()); + out.print(" "); + out.print(varName); + out.println(" = null;"); + scriptVars.add(varName); + } + } + } else { + for (int i=0; i<tagVarInfos.length; i++) { + int scope = tagVarInfos[i].getScope(); + String varName = tagVarInfos[i].getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfos[i].getNameFromAttribute()); + } + if (((scope == VariableInfo.AT_BEGIN) + || (scope == VariableInfo.AT_END)) + && tagVarInfos[i].getDeclare() + && !scriptVars.contains(varName)) { + out.printin(tagVarInfos[i].getClassName()); + out.print(" "); + out.print(varName); + out.println(" = null;"); + scriptVars.add(varName); + } + } + } + + visitBody(n); + } + } + + page.visit(new ScriptingVariableDeclarationVisitor()); + } /** * Generates the destroy() method which is responsible for calling the @@ -379,6 +454,10 @@ } */ out.printil("JspWriter _jspx_out = null;"); + out.println(); + + declareAtBeginAtEndScriptingVariables(page); + out.println(); out.printil("try {"); out.pushIndent(); @@ -464,15 +543,26 @@ private MethodsBuffer methodsBuffer; private int methodNesting; + /* + * Maps temporary scripting variable to parent of custom tag that + * declared it + */ + private Hashtable tmpVars; + + // Maps NESTED scripting var to parent of custom tag that declared it + private Hashtable nestedVars; + /** * Constructor. */ public GenerateVisitor(ServletWriter out, MethodsBuffer methodsBuffer) { this.out = out; this.methodsBuffer = methodsBuffer; + methodNesting = 0; handlerInfos = new Hashtable(); tagVarNumbers = new Hashtable(); - methodNesting = 0; + tmpVars = new Hashtable(); + nestedVars = new Hashtable(); } /** @@ -984,16 +1074,8 @@ public void visit(Node.CustomTag n) throws JasperException { - TagLibraryInfo tagLibInfo = (TagLibraryInfo) - pageInfo.getTagLibraries().get(n.getPrefix()); - TagInfo tagInfo = tagLibInfo.getTag(n.getShortName()); - - // Get info on scripting variables created/manipulated by tag - VariableInfo[] varInfos = tagInfo.getVariableInfo(n.getTagData()); - TagVariableInfo[] tagVarInfos = tagInfo.getTagVariableInfos(); - - Hashtable handlerInfosByShortName - = (Hashtable) handlerInfos.get(n.getPrefix()); + Hashtable handlerInfosByShortName = (Hashtable) + handlerInfos.get(n.getPrefix()); if (handlerInfosByShortName == null) { handlerInfosByShortName = new Hashtable(); handlerInfos.put(n.getPrefix(), handlerInfosByShortName); @@ -1001,8 +1083,11 @@ TagHandlerInfo handlerInfo = (TagHandlerInfo) handlerInfosByShortName.get(n.getShortName()); if (handlerInfo == null) { - handlerInfo = new TagHandlerInfo(n, tagInfo.getTagClassName(), - ctxt.getClassLoader(), err); + handlerInfo = new TagHandlerInfo( + n, + n.getTagInfo().getTagClassName(), + ctxt.getClassLoader(), + err); handlerInfosByShortName.put(n.getShortName(), handlerInfo); } @@ -1016,8 +1101,10 @@ // to a method. ServletWriter outSave = null; MethodsBuffer methodsBufferSave = null; - if (n.isScriptless() && varInfos == null && - (tagVarInfos == null || tagVarInfos.length == 0)) { + boolean generateTagMethod = false; + if (n.isScriptless() && n.getVariableInfos() == null && + (n.getTagVariableInfos() == null + || n.getTagVariableInfos().length == 0)) { // The tag handler and its body code can reside in a separate // method if it is scriptless and does not have any scripting // variable defined. @@ -1025,6 +1112,7 @@ // in TEI, but tagVarInfos is empty array when var is not // defined in tld. + generateTagMethod = true; String tagMethod = "_jspx_meth_" + baseVar; // Generate a call to this method @@ -1079,20 +1167,17 @@ } // Generate code for start tag, body, and end tag - generateCustomStart(n, varInfos, tagVarInfos, handlerInfo, - tagHandlerVar, tagEvalVar); + generateCustomStart(n, handlerInfo, tagHandlerVar, tagEvalVar); String tmpParent = parent; parent = tagHandlerVar; visitBody(n); parent = tmpParent; - generateCustomEnd(n, varInfos, tagVarInfos, - handlerInfo.getTagHandlerClass(), tagHandlerVar, - tagEvalVar); + generateCustomEnd(n, handlerInfo.getTagHandlerClass(), + tagHandlerVar, tagEvalVar); - if (n.isScriptless() && varInfos == null && - (tagVarInfos == null || tagVarInfos.length == 0)) { + if (generateTagMethod) { // Generate end of method if (methodNesting > 0) { out.printil("return false;"); @@ -1213,23 +1298,33 @@ } private void generateCustomStart(Node.CustomTag n, - VariableInfo[] varInfos, - TagVariableInfo[] tagVarInfos, TagHandlerInfo handlerInfo, String tagHandlerVar, String tagEvalVar) throws JasperException { + Class tagHandlerClass = handlerInfo.getTagHandlerClass(); + n.setBeginJavaLine(out.getJavaLine()); out.printin("/* ---- "); out.print(n.getName()); out.println(" ---- */"); - Class tagHandlerClass = handlerInfo.getTagHandlerClass(); - boolean implementsTryCatchFinally = TryCatchFinally.class.isAssignableFrom(tagHandlerClass); + /* + * Declare variables where current contents of scripting variables + * will be temporarily saved + */ + declareTemporaryScriptingVariables(n); + + // Declare scripting variables with NESTED scope + declareNestedScriptingVariables(n); + + // Save current value of scripting variables if required + saveScriptingVariables(n); + out.printin(tagHandlerClass.getName()); out.print(" "); out.print(tagHandlerVar); @@ -1256,10 +1351,9 @@ boolean isBodyTag = BodyTag.class.isAssignableFrom(tagHandlerClass); - // Declare and synchronize AT_BEGIN scripting variables - syncScriptingVariables(varInfos, tagVarInfos, n.getTagData(), - VariableInfo.AT_BEGIN, true); - + // Synchronize AT_BEGIN scripting variables + syncScriptingVariables(n, VariableInfo.AT_BEGIN); + if (n.getBody() != null) { out.printin("if ("); out.print(tagEvalVar); @@ -1291,23 +1385,21 @@ } } - - // Declare and synchronize NESTED scripting variables - syncScriptingVariables(varInfos, tagVarInfos, n.getTagData(), - VariableInfo.NESTED, true); + // Synchronize NESTED scripting variables + syncScriptingVariables(n, VariableInfo.NESTED); // Synchronize AT_BEGIN scripting variables - syncScriptingVariables(varInfos, tagVarInfos, n.getTagData(), - VariableInfo.AT_BEGIN, false); + syncScriptingVariables(n, VariableInfo.AT_BEGIN); }; private void generateCustomEnd(Node.CustomTag n, - VariableInfo[] varInfos, - TagVariableInfo[] tagVarInfos, Class tagHandlerClass, String tagHandlerVar, String tagEvalVar) { + VariableInfo[] varInfos = n.getVariableInfos(); + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + boolean implementsIterationTag = IterationTag.class.isAssignableFrom(tagHandlerClass); boolean implementsBodyTag = @@ -1323,8 +1415,7 @@ } // Synchronize AT_BEGIN scripting variables - syncScriptingVariables(varInfos, tagVarInfos, n.getTagData(), - VariableInfo.AT_BEGIN, false); + syncScriptingVariables(n, VariableInfo.AT_BEGIN); if (n.getBody() != null) { if (implementsBodyTag) { @@ -1364,7 +1455,7 @@ out.print(tagHandlerVar); out.println(");"); out.popIndent(); - out.printil("}"); + out.println("}"); } else { out.printin(n.getTagHandlerPoolName()); out.print(".reuse("); @@ -1372,27 +1463,226 @@ out.println(");"); } - // Declare and synchronize AT_END variables - syncScriptingVariables(varInfos, tagVarInfos, n.getTagData(), - VariableInfo.AT_END, true); + // Synchronize AT_END variables + syncScriptingVariables(n, VariableInfo.AT_END); + + restoreScriptingVariables(n); n.setEndJavaLine(out.getJavaLine()); } - private void syncScriptingVariables(VariableInfo[] varInfos, - TagVariableInfo[] tagVarInfos, - TagData tagData, - int scope, - boolean declare) { + /* + * Declares any NESTED scripting variables of the given custom tag, + * if the given custom tag is not nested inside itself (i.e, has a + * nesting level of zero). In addition, a NESTED scripting variable is + * declared only if it has not already been declared in the same scope + * in the generated code, that is, if this custom tag's parent is + * different from the parent of the custom tag that may already have + * declared this variable. + */ + private void declareNestedScriptingVariables(Node.CustomTag n) { + if (n.getCustomNestingLevel() > 0) { + return; + } + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); if ((varInfos == null) && (tagVarInfos == null)) { return; } + if (varInfos != null) { for (int i=0; i<varInfos.length; i++) { - if (varInfos[i].getScope() == scope) { - if (declare && varInfos[i].getDeclare()) { - out.printin(varInfos[i].getClassName() + " "); + if ((varInfos[i].getScope() == VariableInfo.NESTED) + && varInfos[i].getDeclare()) { + String name = varInfos[i].getVarName(); + Node parent = (Node) nestedVars.get(name); + if ((parent == null) || (parent != n.getParent())) { + out.printin(varInfos[i].getClassName()); + out.print(" "); + out.print(name); + out.println(";"); + nestedVars.put(name, n.getParent()); + } + } + } + } else { + for (int i=0; i<tagVarInfos.length; i++) { + if ((tagVarInfos[i].getScope() == VariableInfo.NESTED) + && tagVarInfos[i].getDeclare()) { + String name = tagVarInfos[i].getNameGiven(); + if (name == null) { + name = n.getTagData().getAttributeString( + tagVarInfos[i].getNameFromAttribute()); } + Node parent = (Node) nestedVars.get(name); + if ((parent == null) || (parent != n.getParent())) { + out.printin(tagVarInfos[i].getClassName()); + out.print(" "); + out.print(name); + out.println(";"); + nestedVars.put(name, n.getParent()); + } + } + } + } + } + + /* + * For every scripting variable exposed by this custom tag, declares + * a variable where the current value of the scripting variable may + * be saved, so it can later be restored in this custom tag's end + * element. + */ + private void declareTemporaryScriptingVariables(Node.CustomTag n) { + if (n.getCustomNestingLevel() == 0) { + return; + } + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + if ((varInfos == null) && (tagVarInfos == null)) { + return; + } + + if (varInfos != null) { + for (int i=0; i<varInfos.length; i++) { + String tmpVarName = "_jspx_" + varInfos[i].getVarName() + + "_" + n.getCustomNestingLevel(); + Node parent = (Node) tmpVars.get(tmpVarName); + if ((parent == null) || (parent != n.getParent())) { + out.printin(varInfos[i].getClassName()); + out.print(" "); + out.print(tmpVarName); + out.println(";"); + tmpVars.put(tmpVarName, n.getParent()); + } + } + } else { + for (int i=0; i<tagVarInfos.length; i++) { + String varName = tagVarInfos[i].getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfos[i].getNameFromAttribute()); + } + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + Node parent = (Node) tmpVars.get(tmpVarName); + if ((parent == null) || (parent != n.getParent())) { + out.printin(tagVarInfos[i].getClassName()); + out.print(" "); + out.print(tmpVarName); + out.println(";"); + tmpVars.put(tmpVarName, n.getParent()); + } + } + } + } + + /* + * For each scripting variable of a custom tag with a nesting level + * greater than 0, save its value to a temporary variable so that the + * scripting variable can be synchronized inside the nested custom tag + * without affecting the value it had at the start element of the + * custom tag, which will be restored when the end element of the + * custom tag is reached. + */ + private void saveScriptingVariables(Node.CustomTag n) { + if (n.getCustomNestingLevel() == 0) { + return; + } + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + if ((varInfos == null) && (tagVarInfos == null)) { + return; + } + + if (varInfos != null) { + for (int i=0; i<varInfos.length; i++) { + String varName = varInfos[i].getVarName(); + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(tmpVarName); + out.print(" = "); + out.print(varName); + out.println(";"); + } + } else { + for (int i=0; i<tagVarInfos.length; i++) { + String varName = tagVarInfos[i].getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfos[i].getNameFromAttribute()); + } + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(tmpVarName); + out.print(" = "); + out.print(varName); + out.println(";"); + } + } + } + + /* + * For each scripting variable of a custom tag with a nesting level + * greater than 0, restore its original value that was saved in the + * start element of the custom tag. + */ + private void restoreScriptingVariables(Node.CustomTag n) { + if (n.getCustomNestingLevel() == 0) { + return; + } + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + if ((varInfos == null) && (tagVarInfos == null)) { + return; + } + + if (varInfos != null) { + for (int i=0; i<varInfos.length; i++) { + String varName = varInfos[i].getVarName(); + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(varName); + out.print(" = "); + out.print(tmpVarName); + out.println(";"); + } + } else { + for (int i=0; i<tagVarInfos.length; i++) { + String varName = tagVarInfos[i].getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfos[i].getNameFromAttribute()); + } + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(varName); + out.print(" = "); + out.print(tmpVarName); + out.println(";"); + } + } + } + + /* + * Synchronizes the scripting variables of the given custom tag for + * the given scope. + */ + private void syncScriptingVariables(Node.CustomTag n, int scope) { + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + + if ((varInfos == null) && (tagVarInfos == null)) { + return; + } + + if (varInfos != null) { + for (int i=0; i<varInfos.length; i++) { + if (varInfos[i].getScope() == scope) { out.printin(varInfos[i].getVarName()); out.print(" = ("); out.print(varInfos[i].getClassName()); @@ -1403,14 +1693,11 @@ } } else { for (int i=0; i<tagVarInfos.length; i++) { - String name = tagVarInfos[i].getNameGiven(); - if (name == null) { - name = tagData.getAttributeString( - tagVarInfos[i].getNameFromAttribute()); - } if (tagVarInfos[i].getScope() == scope) { - if (declare && tagVarInfos[i].getDeclare()) { - out.printin(tagVarInfos[i].getClassName() + " "); + String name = tagVarInfos[i].getNameGiven(); + if (name == null) { + name = n.getTagData().getAttributeString( + tagVarInfos[i].getNameFromAttribute()); } out.printin(name); out.print(" = ("); 1.2 +7 -6 jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/JspDocumentParser.java Index: JspDocumentParser.java =================================================================== RCS file: /home/cvs/jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/JspDocumentParser.java,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- JspDocumentParser.java 28 Mar 2002 18:46:15 -0000 1.1 +++ JspDocumentParser.java 13 Jun 2002 18:56:18 -0000 1.2 @@ -62,7 +62,7 @@ import java.io.*; import java.util.Hashtable; -import javax.servlet.jsp.tagext.TagLibraryInfo; +import javax.servlet.jsp.tagext.*; import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; @@ -401,13 +401,14 @@ if (tagLibInfo == null) { return null; } - if (tagLibInfo.getTag(shortName) == null) { + TagInfo tagInfo = tagLibInfo.getTag(shortName); + if (tagInfo == null) { throw new SAXException(err.getString("jsp.error.bad_tag", shortName, prefix)); } return new Node.CustomTag(attrs, start, qName, prefix, shortName, - parent); + tagInfo, parent); } /* 1.13 +68 -7 jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/Node.java Index: Node.java =================================================================== RCS file: /home/cvs/jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/Node.java,v retrieving revision 1.12 retrieving revision 1.13 diff -u -r1.12 -r1.13 --- Node.java 8 Jun 2002 00:14:35 -0000 1.12 +++ Node.java 13 Jun 2002 18:56:18 -0000 1.13 @@ -62,7 +62,7 @@ import java.util.*; import java.io.CharArrayWriter; -import javax.servlet.jsp.tagext.TagData; +import javax.servlet.jsp.tagext.*; import org.xml.sax.Attributes; import org.apache.jasper.JasperException; @@ -670,13 +670,19 @@ private boolean hasIncludeAction; private boolean hasSetProperty; private String tagHandlerPoolName; + private TagInfo tagInfo; + private VariableInfo[] varInfos; + private int nestingLevel; public CustomTag(Attributes attrs, Mark start, String name, - String prefix, String shortName, Node parent) { + String prefix, String shortName, + TagInfo tagInfo, Node parent) { super(attrs, start, parent); this.name = name; this.prefix = prefix; this.shortName = shortName; + this.tagInfo = tagInfo; + this.nestingLevel = computeCustomNestingLevel(); } public void accept(Visitor v) throws JasperException { @@ -714,6 +720,7 @@ public void setTagData(TagData tagData) { this.tagData = tagData; + this.varInfos = tagInfo.getVariableInfo(tagData); } public TagData getTagData() { @@ -752,14 +759,68 @@ return hasSetProperty; } + public void setTagHandlerPoolName(String s) { + tagHandlerPoolName = s; + } + public String getTagHandlerPoolName() { return tagHandlerPoolName; } - public void setTagHandlerPoolName(String s) { - tagHandlerPoolName = s; + public TagInfo getTagInfo() { + return tagInfo; + } + + public TagVariableInfo[] getTagVariableInfos() { + return tagInfo.getTagVariableInfos(); + } + + public VariableInfo[] getVariableInfos() { + return varInfos; } + /* + * Gets this custom tag's nesting level. + */ + public int getCustomNestingLevel() { + return nestingLevel; + } + + /* + * Computes this custom tag's nesting level, which corresponds to the + * number of times this custom tag is nested inside itself. + * + * Example: + * + * <g:h> + * <a:b> -- nesting level 0 + * <c:d> + * <e:f> + * <a:b> -- nesting level 1 + * <a:b> -- nesting level 2 + * </a:b> + * </a:b> + * <a:b> -- nesting level 1 + * </a:b> + * </e:f> + * </c:d> + * </a:b> + * </g:h> + * + * @return Custom tag's nesting level + */ + private int computeCustomNestingLevel() { + int n = 0; + Node p = parent; + while (p != null) { + if ((p instanceof Node.CustomTag) + && name.equals(((Node.CustomTag) p).name)) { + n++; + } + p = p.parent; + } + return n; + } } /** 1.5 +8 -7 jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/Parser.java Index: Parser.java =================================================================== RCS file: /home/cvs/jakarta-tomcat-jasper/jasper2/src/share/org/apache/jasper/compiler/Parser.java,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- Parser.java 7 Jun 2002 20:04:27 -0000 1.4 +++ Parser.java 13 Jun 2002 18:56:18 -0000 1.5 @@ -704,7 +704,8 @@ reader.reset(start); return false; } - if (tagLibInfo.getTag(shortTagName) == null) { + TagInfo tagInfo = tagLibInfo.getTag(shortTagName); + if (tagInfo == null) { err.jspError(start, "jsp.error.bad_tag", shortTagName, prefix); } @@ -716,7 +717,7 @@ if (reader.matches("/>")) { // EmptyElemTag ::= '<' Name ( S Attribute )* S? '/>'# new Node.CustomTag(attrs, start, tagName, prefix, shortTagName, - parent); + tagInfo, parent); return true; } @@ -729,10 +730,10 @@ // Looking for a body, it still can be empty; but if there is a // a tag body, its syntax would be dependent on the type of // body content declared in TLD. - String bc = ((TagLibraryInfo) taglibs.get(prefix)).getTag(shortTagName).getBodyContent(); + String bc = tagInfo.getBodyContent(); Node tagNode = new Node.CustomTag(attrs, start, tagName, prefix, - shortTagName, parent); + shortTagName, tagInfo, parent); // There are 3 body content types: empty, jsp, or tag-dependent. if (bc.equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY)) { if (!reader.matchesETag(tagName)) {
-- To unsubscribe, e-mail: <mailto:[EMAIL PROTECTED]> For additional commands, e-mail: <mailto:[EMAIL PROTECTED]>