/* * � Copyright IBM Corp. 2010 * * 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 com.ibm.xsp.extlib.javascript; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import lotus.domino.Database; import lotus.domino.Document; import lotus.domino.Session; import com.ibm.commons.Platform; import com.ibm.commons.util.StringUtil; import com.ibm.jscript.InterpretException; import com.ibm.jscript.JSContext; import com.ibm.jscript.JavaScriptException; import com.ibm.jscript.engine.IExecutionContext; import com.ibm.jscript.types.BuiltinFunction; import com.ibm.jscript.types.FBSDefaultObject; import com.ibm.jscript.types.FBSGlobalObject; import com.ibm.jscript.types.FBSNull; import com.ibm.jscript.types.FBSObject; import com.ibm.jscript.types.FBSUndefined; import com.ibm.jscript.types.FBSUtility; import com.ibm.jscript.types.FBSValue; import com.ibm.jscript.types.FBSValueVector; import com.ibm.xsp.FacesExceptionEx; import com.ibm.xsp.component.UIViewRootEx; import com.ibm.xsp.extlib.util.ExtLibUtil; import com.ibm.xsp.model.domino.DatabaseConstants; import com.ibm.xsp.model.domino.wrapped.DominoDocument; import com.ibm.xsp.renderkit.html_extended.RenderUtil; import com.ibm.xsp.util.FacesUtil; import com.ibm.xsp.webapp.FacesResourceServlet; /** * Extended Notes/Domino formula language. * <p> * This class implements a set of new functions available to the JavaScript interpreter. They become available to Domino * Designer in the category "@NotesFunctionEx". * </p> */ public class NotesFunctionsEx extends FBSDefaultObject { // Functions IDs private static final int FCT_TOPPARENTID = 1; private static final int FCT_TOPPARENTUNID = 2; private static final int FCT_FULLURL = 3; private static final int FCT_ABSOLUTEURL = 4; private static final int FCT_ENCODEURL = 5; private static final int FCT_ISABSOLUTEURL = 6; private static final int FCT_ERRORMESSAGE = 7; private static final int FCT_WARNINGMESSAGE = 8; private static final int FCT_INFORMATIONMESSAGE = 9; private static final int FCT_VIEWICONURL = 10; private static final int FCT_NORMALIZESUBJECT = 11; public static final String FULLURL_DEFAULT_FORMAT = "DEFAULT_URL_FORMAT"; // $NON-NLS-1$ public static final String FULLURL_NOTES_FORMAT = "NOTES_URL_FORMAT"; // $NON-NLS-1$ // ============================= CODE COMPLETION ========================== // // Even though JavaScript is an untyped language, the XPages JavaScript // interpreter can make use of symbolic information defining the // objects/functions exposed. This is particularly used by Domino Designer // to provide the code completion facility and help the user writing code. // // Each function expose by a library can then have one or multiple // "prototypes", defining its parameters and the returned value type. To // make this definition as efficient as possible, the parameter definition // is compacted within a string, where all the parameters are defined // within parenthesis followed by the returned value type. // A parameter is defined by its name, followed by a colon and its type. // Generally, the type is defined by a single character (see bellow) or a // full Java class name. The returned type is defined right after the // closing parameter parenthesis. // // Here is, for example, the definition of the "@Date" function which can // take 3 different set of parameters: // "(time:Y):Y", // "(years:Imonths:Idays:I):Y", // "(years:Imonths:Idays:Ihours:Iminutes:Iseconds:I):Y"); // // List of types // V void // C char // B byte // S short // I int // J long // F float // D double // Z boolean // T string // Y date/time // W any (variant) // N multiple (...) // L<name>; object // ex: // (entries:[Lcom.ibm.xsp.extlib.MyClass;):V // // ========================================================================= public NotesFunctionsEx(JSContext jsContext) { super(jsContext, null, false); // Document helpers addFunction(FCT_TOPPARENTID, "@TopParentID", "(doc:W):T"); // $NON-NLS-1$ $NON-NLS-2$ addFunction(FCT_TOPPARENTUNID, "@TopParentUNID", "(doc:W):T"); // $NON-NLS-1$ $NON-NLS-2$ // URL handling addFunction(FCT_FULLURL, "@FullUrl", "(str:T):T", "(str:T, form:T):T"); // $NON-NLS-1$ $NON-NLS-2$ $NON-NLS-3$ addFunction(FCT_ABSOLUTEURL, "@AbsoluteUrl", "(str:T):T"); // $NON-NLS-1$ $NON-NLS-2$ addFunction(FCT_ENCODEURL, "@EncodeUrl", "(str:T):T"); // $NON-NLS-1$ $NON-NLS-2$ addFunction(FCT_ISABSOLUTEURL, "@IsAbsoluteUrl", "(str:T):T"); // $NON-NLS-1$ $NON-NLS-2$ // XPages helpers addFunction(FCT_ERRORMESSAGE, "@ErrorMessage", "(str:Tcomp:W):V"); // $NON-NLS-1$ $NON-NLS-2$ addFunction(FCT_WARNINGMESSAGE, "@WarningMessage", "(str:Tcomp:W):V"); // $NON-NLS-1$ $NON-NLS-2$ addFunction(FCT_INFORMATIONMESSAGE, "@InfoMessage", "(str:Tcomp:W):V"); // $NON-NLS-1$ $NON-NLS-2$ // Domino View addFunction(FCT_VIEWICONURL, "@ViewIconUrl", "(icon:I):T"); // $NON-NLS-1$ $NON-NLS-2$ addFunction(FCT_NORMALIZESUBJECT, "@NormalizeSubject", "(subject:T):T", "(subject:T, maxlength:I):T"); // $NON-NLS-1$ // $NON-NLS-2$ // $NON-NLS-3$ } private void addFunction(int index, String functionName, String... params) { createMethod(functionName, FBSObject.P_NODELETE | FBSObject.P_READONLY, new NotesFunction(getJSContext(), index, functionName, params)); } @Override public boolean hasInstance(FBSValue v) { return v instanceof FBSGlobalObject; } @Override public boolean isJavaNative() { return false; } // ================================================================================= // Functions implementation // For optimization reasons, there is one NotesFunction instance per function, // instead of one class (this avoids loading to many classes). To then distinguish // the actual function, it uses an index member. // ================================================================================= public static class NotesFunction extends BuiltinFunction { private String functionName; private int index; private String[] params; NotesFunction(JSContext jsContext, int index, String functionName, String[] params) { super(jsContext); this.functionName = functionName; this.index = index; this.params = params; } /** * Index of the function. * <p> * There must be one instanceof this class per index. * </p> */ public int getIndex() { return this.index; } /** * Return the list of the function parameters. * <p> * Note that this list is not used at runtime, at least for now, but consumed by Designer code completion.<br> * A function can expose multiple parameter sets. * </p> */ @Override protected String[] getCallParameters() { return this.params; } /** * Function name, as exposed by Designer and use at runtime. * <p> * This function is exposed in the JavaScript global namespace, so you should be careful to avoid any name * conflict. * </p> */ @Override public String getFunctionName() { return this.functionName; } /** * Actual code execution. * <p> * The JS runtime calls this method when the method is executed within a JavaScript formula. * </p> * * @param context * the JavaScript execution context (global variables, function...) * @param args * the arguments passed to the function * @params _this the "this" object when the method is called as a "this" member */ @Override public FBSValue call(IExecutionContext context, FBSValueVector args, FBSObject _this) throws JavaScriptException { try { // Else execute the formulas switch (index) { // //////////////////////////////////////////////////////////////////////////////////////// // Document IDs // //////////////////////////////////////////////////////////////////////////////////////// case FCT_TOPPARENTID: { if (args.size() <= 1) { Document d = args.size() == 0 ? getCurrentDocument() : getDocument(args.get(0) .toJavaObject()); while (d != null) { String unid = d.getParentDocumentUNID(); if (StringUtil.isEmpty(unid)) { return FBSUtility.wrap(d.getNoteID()); } d = d.getParentDatabase().getDocumentByUNID(unid); } // we should never be here return FBSNull.nullValue; } } break; case FCT_TOPPARENTUNID: { if (args.size() <= 1) { Document d = args.size() == 0 ? getCurrentDocument() : getDocument(args.get(0) .toJavaObject()); while (d != null) { String unid = d.getParentDocumentUNID(); if (StringUtil.isEmpty(unid)) { return FBSUtility.wrap(d.getUniversalID()); } d = d.getParentDatabase().getDocumentByUNID(unid); } // we should never be here return FBSNull.nullValue; } } break; // //////////////////////////////////////////////////////////////////////////////////////// // URL handling // //////////////////////////////////////////////////////////////////////////////////////// case FCT_FULLURL: { if (args.size() == 1) { String url = args.get(0).stringValue(); return FBSUtility.wrap(fullUrl(url)); } else if (args.size() == 2) { String url = args.get(0).stringValue(); String urlFormat = args.get(1).stringValue(); return FBSUtility.wrap(fullUrl(url, urlFormat)); } } break; case FCT_ABSOLUTEURL: { if (args.size() == 1) { String url = args.get(0).stringValue(); return FBSUtility.wrap(absoluteUrl(url)); } } break; case FCT_ENCODEURL: { if (args.size() == 1) { String url = args.get(0).stringValue(); return FBSUtility.wrap(encodeUrl(url)); } } break; case FCT_ISABSOLUTEURL: { if (args.size() == 1) { String url = args.get(0).stringValue(); return FBSUtility.wrap(FacesUtil.isAbsoluteUrl(url)); } } break; // //////////////////////////////////////////////////////////////////////////////////////// // XPages Helpers // //////////////////////////////////////////////////////////////////////////////////////// case FCT_ERRORMESSAGE: { if (args.size() >= 1) { FacesContext ctx = FacesContext.getCurrentInstance(); String msg = getErrorMessage(args.get(0)); UIComponent c = null; if (args.size() >= 2) { FBSValue v = args.get(1); if (v.isString()) { Object _thisObject = context.getThis() != null ? context.getThis().toJavaObject() : null; UIComponent start = (_thisObject instanceof UIComponent) ? (UIComponent) _thisObject : ctx.getViewRoot(); c = FacesUtil.getComponentFor(start, v.stringValue()); } } FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); ctx.addMessage(c != null ? c.getClientId(ctx) : null, m); return FBSUndefined.undefinedValue; } } break; case FCT_WARNINGMESSAGE: { if (args.size() >= 1) { FacesContext ctx = FacesContext.getCurrentInstance(); String msg = getErrorMessage(args.get(0)); UIComponent c = null; if (args.size() >= 2) { FBSValue v = args.get(1); if (v.isString()) { Object _thisObject = context.getThis() != null ? context.getThis().toJavaObject() : null; UIComponent start = (_thisObject instanceof UIComponent) ? (UIComponent) _thisObject : ctx.getViewRoot(); c = FacesUtil.getComponentFor(start, v.stringValue()); } } FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_WARN, msg, msg); ctx.addMessage(c != null ? c.getClientId(ctx) : null, m); return FBSUndefined.undefinedValue; } } break; case FCT_INFORMATIONMESSAGE: { if (args.size() >= 1) { FacesContext ctx = FacesContext.getCurrentInstance(); String msg = getErrorMessage(args.get(0)); UIComponent c = null; if (args.size() >= 2) { FBSValue v = args.get(1); if (v.isString()) { Object _thisObject = context.getThis() != null ? context.getThis().toJavaObject() : null; UIComponent start = (_thisObject instanceof UIComponent) ? (UIComponent) _thisObject : ctx.getViewRoot(); c = FacesUtil.getComponentFor(start, v.stringValue()); } } FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_INFO, msg, msg); ctx.addMessage(c != null ? c.getClientId(ctx) : null, m); return FBSUndefined.undefinedValue; } } break; // //////////////////////////////////////////////////////////////////////////////////////// // Domino View // //////////////////////////////////////////////////////////////////////////////////////// case FCT_VIEWICONURL: { if (args.size() >= 1) { int icon = args.get(0).intValue(); if (icon >= 1 && icon <= 212) { String idx = StringUtil.toString(icon, 3, '0'); String url = "/.ibmxspres/domino/icons/vwicn" + idx + ".gif"; // $NON-NLS-1$ $NON-NLS-2$ return FBSUtility.wrap(url); } return FBSUtility.wrap("/.ibmxspres/domino/icons/vwicn999.gif"); // $NON-NLS-1$ } } break; case FCT_NORMALIZESUBJECT: { if (args.size() == 1) { String subject = args.get(0).stringValue(); if (null != subject) { return FBSUtility.wrap(normalizeSubject(subject, 80)); } return FBSUndefined.undefinedValue; } else if (args.size() == 2) { String subject = args.get(0).stringValue(); int maxlength = args.get(1).intValue(); if (null != subject && maxlength > 0) { return FBSUtility.wrap(normalizeSubject(subject, maxlength)); } else if (null != subject) { return FBSUtility.wrap(normalizeSubject(subject, 80)); } return FBSUndefined.undefinedValue; } } break; default: { throw new InterpretException(null, StringUtil.format( "Internal error: unknown function \'{0}\'", functionName)); // $NLX-NotesFunctionEx_InternalErrorUnknownFunction-1$ } } // } catch (InterpretException e) { // throw e; // } catch (NotesException e) { // // This case covers where a call to session.evaluate() throws a NotesException // // We want to continue rendering the page but allow @IsError to pick up on this issue // // so we return @Error (NaN / FBSUndefined.undefinedValue) // return FBSUndefined.undefinedValue; } catch (Exception e) { throw new InterpretException(e, StringUtil.format("Error while executing function \'{0}\'", // $NLX-NotesFunctionEx_ErrorExecutingFunction-1$ functionName)); } throw new InterpretException(null, StringUtil.format("Cannot evaluate function \'{0}\'", functionName)); // $NLX-NotesFunctionEx_CannotEvaluateFunction-1$ } } /** * This methods calculates the full URL path, relative to the server. * * @param url * @return */ public static String fullUrl(String url) { return fullUrl(url, FULLURL_DEFAULT_FORMAT); } /** * This methods calculates the full URL path, relative to the server. * * @param url * @param urlFormat * @return */ public static String fullUrl(String url, String urlFormat) { FacesContext context = FacesContext.getCurrentInstance(); // If already absolute, leave... if (FacesUtil.isAbsoluteUrl(url)) { return url; } // SPR # PEDS8WXD6J - @FullUrl("foo.nsf") returns: /foo.nsf/foo.nsf // See here if the url passed contains the name of an NSF String nsfName = null; if (url.toLowerCase().contains(".nsf")) { // $NON-NLS-1$ // if so extract the name and check for duplicates later int eon = url.toLowerCase().lastIndexOf(".nsf"); // $NON-NLS-1$ int son = eon; while (son >= 0) { char c = url.charAt(son); if (c == '!' || c == '/' || c == '\\') { son++; break; } son--; } if (son == -1) { son++; } nsfName = url.substring(son, eon+4); } // If it is a global URL (/.ibmxspres/...) if (url.startsWith(FacesResourceServlet.RESOURCE_PREFIX)) { // It says as is and it will be server relative return url; } // If it is not an absolute URL, then make it absolute based on the current page if (!url.startsWith("/")) { // $NON-NLS-1$ UIViewRootEx vex = (UIViewRootEx) context.getViewRoot(); String pageName = vex.getPageName(); int idx = pageName.lastIndexOf("/"); // $NON-NLS-1$ if (idx >= 0) { String path = pageName.substring(0, idx + 1); url = path + url; } else { url = "/" + url; // $NON-NLS-1$ } } // Then, make it relative to the current application url = context.getApplication().getViewHandler().getResourceURL(context, url); if(StringUtil.equalsIgnoreCase(urlFormat, FULLURL_NOTES_FORMAT) && Platform.getInstance().isPlatform("Notes")){ // $NON-NLS-1$ // use a format that can be included in a notes://blah... URL // i.e. remove any server symbols (!!) and any /xsp prefixes int posServerSymbol = url.indexOf("!!"); // $NON-NLS-1$ if (posServerSymbol >= 0) { url = url.substring(posServerSymbol+2); if (!url.startsWith("/")) { // $NON-NLS-1$ url = "/" + url; // $NON-NLS-1$ } } else if (url.startsWith("/xsp")) { // $NON-NLS-1$ // Need to chop off any leading "/xsp" tokens url = url.substring(4); } } // If the input URL is an NSF name, check that it is not duplicated in output if (StringUtil.isNotEmpty(nsfName)) { int first, last; do { first = url.toLowerCase().indexOf(nsfName.toLowerCase()); last = url.toLowerCase().lastIndexOf(nsfName.toLowerCase()); if (first >= 0 && first != last ) { url = url.replaceFirst(nsfName, ""); }} while (first != last); // be consistent with non-duplicated use case if (!url.endsWith("/")) { // $NON-NLS-1$ url = url + "/"; // $NON-NLS-1$ } } // SPR# EGLN8DEN5U - if the NSF is in a subfolder then this can cause problems in XPiNC // ... where backslashes in the path name, e.g. subfolder\\foo.nsf cause get escaped in URL // various other "funnies" can occur - e.g. empty slots, slashes after the server symbol etc url = url.replaceAll("\\\\", "/"); url = url.replaceAll("//", "/"); url = url.replaceAll("!!/", "!!"); //System.out.println(url); return url; } /** * This methods make a URL absolute, by prefix it with the protocol/server name. * * @param url * @return */ public static String absoluteUrl(String url) { return FacesUtil.makeUrlAbsolute(FacesContext.getCurrentInstance(), url); } /** * This methods encode the URL, but adding the necessary attributes when appropriate (session id...). * * @param url * @return */ public static String encodeUrl(String url) { FacesContext context = FacesContext.getCurrentInstance(); if (RenderUtil.isXspUrl(url)) { url = context.getExternalContext().encodeActionURL(url); } else { url = context.getExternalContext().encodeResourceURL(url); } return url; } /** * This method trims a subject to the specified maxlength if it exceeds it * * @param subject * @param maxlength * @return normalized subject */ public static String normalizeSubject(String subject, int maxlength) { if (subject.length() > maxlength) { subject = subject.substring(0, maxlength) + "..."; //$NON-NLS-1$ } else if (subject.length() == 0) { subject = "Untitled"; // $NLS-NotesFunctionEx_SubjectUntitled-1$ } return subject; } /** * Get an error message from a JavaScript value. Such a message is used to display an error message in JSF. * * @return */ public static String getErrorMessage(FBSValue v) throws InterpretException { if (v.isJavaObject()) { Object o = v.toJavaObject(); if (o instanceof Throwable) { StringBuilder b = new StringBuilder(); for (Throwable t = (Throwable) o; t != null; t = getCause(t)) { if (b.length() > 0) { b.append('\n'); } b.append(t.toString()); } return b.toString(); } } return v.stringValue(); } private static Throwable getCause(Throwable t) { Throwable c = t.getCause(); return c != t ? c : null; } // ========================================================================= // Some useful helpers // ========================================================================= private static Session getCurrentSession() { return ExtLibUtil.getCurrentSession(); } private static Database getCurrentDatabase() { return ExtLibUtil.getCurrentDatabase(); } public static Document getCurrentDocument() { return getDocument(DatabaseConstants.CURRENT_DOCUMENT); } public static Document getDocument(Object var) { if (var instanceof Document) { return (Document) var; } if (var instanceof DominoDocument) { return ((DominoDocument) var).getDocument(); } if (var instanceof String) { Object data = FacesUtil.resolveRequestMapVariable(FacesContext.getCurrentInstance(), (String) var); if (data instanceof DominoDocument) { DominoDocument dd = (DominoDocument) data; return (Document) dd.getDocument(); } if (data instanceof Document) { return (Document) data; } } throw new FacesExceptionEx(null, "Cannot find document {0}", var); // $NLX-NotesFunctionEx_CannotFindNotesDocument-1$ } private static DominoDocument getCurrentDominoDocument() { return getDominoDocument(DatabaseConstants.CURRENT_DOCUMENT); } private static DominoDocument getDominoDocument(Object var) { if (var instanceof DominoDocument) { return (DominoDocument) var; } if (var instanceof String) { Object data = FacesUtil.resolveRequestMapVariable(FacesContext.getCurrentInstance(), (String) var); if (data instanceof DominoDocument) { return (DominoDocument) data; } } throw new FacesExceptionEx(null, "Cannot find Domino document {0}", var); // $NLX-NotesFunctionEx_CannotFindXPagesDocumentObject-1$ } }