001: /*
002: * $Id: NotificationServices.java,v 1.5 2003/12/23 12:34:17 jonesde Exp $
003: *
004: * Copyright (c) 2003 The Open For Business Project - www.ofbiz.org
005: *
006: * Permission is hereby granted, free of charge, to any person obtaining a
007: * copy of this software and associated documentation files (the "Software"),
008: * to deal in the Software without restriction, including without limitation
009: * the rights to use, copy, modify, merge, publish, distribute, sublicense,
010: * and/or sell copies of the Software, and to permit persons to whom the
011: * Software is furnished to do so, subject to the following conditions:
012: *
013: * The above copyright notice and this permission notice shall be included
014: * in all copies or substantial portions of the Software.
015: *
016: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
017: * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
018: * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
019: * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
020: * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
021: * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
022: * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
023: *
024: */
025: package org.ofbiz.content.email;
026:
027: import java.io.IOException;
028: import java.io.InputStreamReader;
029: import java.io.StringWriter;
030: import java.io.Writer;
031: import java.net.InetAddress;
032: import java.net.URL;
033: import java.net.UnknownHostException;
034: import java.util.HashMap;
035: import java.util.Map;
036:
037: import org.ofbiz.base.util.Debug;
038: import org.ofbiz.base.util.UtilMisc;
039: import org.ofbiz.base.util.UtilProperties;
040: import org.ofbiz.base.util.UtilURL;
041: import org.ofbiz.entity.GenericDelegator;
042: import org.ofbiz.entity.GenericEntityException;
043: import org.ofbiz.entity.GenericValue;
044: import org.ofbiz.service.DispatchContext;
045: import org.ofbiz.service.GenericServiceException;
046: import org.ofbiz.service.LocalDispatcher;
047: import org.ofbiz.service.ModelService;
048: import org.ofbiz.service.ServiceUtil;
049: import org.ofbiz.content.webapp.ftl.FreeMarkerWorker;
050: import org.ofbiz.content.webapp.ftl.OfbizCurrencyTransform;
051:
052: import freemarker.ext.beans.BeansWrapper;
053: import freemarker.template.Configuration;
054: import freemarker.template.Template;
055: import freemarker.template.TemplateException;
056: import freemarker.template.TemplateHashModel;
057:
058: /**
059: * Provides generic services related to preparing and
060: * delivering notifications via email.
061: * <p>
062: * To use the NotificationService, a message specific service should be
063: * defined for a particular <a href="http://freemarker.sourceforge.net/docs/dgui_quickstart_template.html">
064: * Freemarker Template</a> mapping the required fields of the
065: * template to the required attributes of the service.
066: * <p>
067: * This service definition should extend the <code>sendNotificationInterface<code>
068: * or the <code>prepareNotificationInterface</code> service interface
069: * and simply invoke the associated method defined in this class.
070: * <p>
071: * <blockquote>
072: * <pre>
073: * <service name="sendPoPickupNotification" engine="java"
074: * location="org.ofbiz.content.email.NotificationServices" invoke="sendNotification">
075: * <description>Sends notification based on a message template</description>
076: * <implements service="sendNotificationInterface"/>
077: * <attribute name="orderId" type="String" mode="IN" optional="false"/>
078: * </service>
079: * </pre>
080: * </blockquote>
081: * <p>
082: * An optional parameter available to all message templates is <code>baseUrl</code>
083: * which can either be specified when the service is invoked or let the
084: * <code>NotificationService</code> attempt to resolve it as best it can,
085: * see {@link #setBaseUrl(GenericDelegator, String, Map) setBaseUrl(Map)} for details on how this is achieved.
086: * <p>
087: * The following example shows what a simple notification message template,
088: * associated with the above service, might contain:
089: * <blockquote>
090: * <pre>
091: * Please use the following link to schedule a delivery date:
092: * <p>
093: * ${baseUrl}/ordermgr/control/schedulepo?order_id=${orderId}"
094: * </pre>
095: * </blockquote>
096: * <p>
097: * The template file must be found on the classpath at runtime and
098: * match the "templateName" field passed to the service when it
099: * is invoked.
100: * <p>
101: * For complex messages with a large number of dynamic fields, it may be wise
102: * to implement a custom service that takes one or two parameters that can
103: * be used to resolve the rest of the required fields and then pass them to
104: * the {@link #prepareNotification(DispatchContext, Map) prepareNotification(DispatchContext, Map)}
105: * or {@link #sendNotification(DispatchContext, Map) sendNotification(DispatchContext, Map)}
106: * methods directly to generate or generate and send the notification respectively.
107: *
108: *
109: * @author <a href="mailto:tristana@twibble.org">Tristan Austin</a>
110: * @author <a href="mailto:jaz@ofbiz.org">Andy Zeneski</a>
111: * @version $Revision: 1.5 $
112: * @since 2.2
113: */
114: public class NotificationServices {
115:
116: public static final String module = NotificationServices.class
117: .getName();
118:
119: /**
120: * This will use the {@link #prepareNotification(DispatchContext, Map) prepareNotification(DispatchContext, Map)}
121: * method to generate the body of the notification message to send
122: * and then deliver it via the "sendMail" service.
123: * <p>
124: * If the "body" parameter is already specified, the message body generation
125: * phase will be skipped and the notification will be sent with the
126: * specified body instead. This can be used to combine both service
127: * calls in a decoupled manner if other steps are required between
128: * generating the message body and sending the notification.
129: *
130: * @param ctx The dispatching context of the service
131: * @param context The map containing all the fields associated with
132: * the sevice
133: * @return A Map with the service response messages in it
134: */
135: public static Map sendNotification(DispatchContext ctx, Map context) {
136: LocalDispatcher dispatcher = ctx.getDispatcher();
137: Map result = null;
138:
139: try {
140: // see whether the optional 'body' attribute was specified or needs to be processed
141: // nulls are handled the same as not specified
142: String body = (String) context.get("body");
143:
144: if (body == null) {
145: // prepare the body of the notification email
146: Map bodyResult = prepareNotification(ctx, context);
147:
148: // ensure the body was generated successfully
149: if (bodyResult.get(ModelService.RESPONSE_MESSAGE)
150: .equals(ModelService.RESPOND_SUCCESS)) {
151: body = (String) bodyResult.get("body");
152: } else {
153: // otherwise just report the error
154: Debug.logError("prepareNotification failed: "
155: + bodyResult
156: .get(ModelService.ERROR_MESSAGE),
157: module);
158: body = null;
159: }
160: }
161:
162: // make sure we have a valid body before sending
163: if (body != null) {
164: // retain only the required attributes for the sendMail service
165: Map emailContext = new HashMap();
166: emailContext.put("sendTo", context.get("sendTo"));
167: emailContext.put("body", body);
168: emailContext.put("sendCc", context.get("sendCc"));
169: emailContext.put("sendBcc", context.get("sendBcc"));
170: emailContext.put("sendFrom", context.get("sendFrom"));
171: emailContext.put("subject", context.get("subject"));
172: emailContext.put("sendVia", context.get("sendVia"));
173: emailContext.put("sendType", context.get("sendType"));
174: emailContext.put("contentType", context
175: .get("contentType"));
176:
177: // pass on to the sendMail service
178: result = dispatcher.runSync("sendMail", emailContext);
179: } else {
180: Debug.logError(
181: "Invalid email body; null is not allowed",
182: module);
183: result = ServiceUtil
184: .returnError("Invalid email body; null is not allowed");
185: }
186: } catch (GenericServiceException serviceException) {
187: Debug.logError(serviceException, "Error sending email",
188: module);
189: result = ServiceUtil
190: .returnError("Email delivery error, see error log");
191: }
192:
193: return result;
194: }
195:
196: /**
197: * This will process the associated notification template definition
198: * with all the fields contained in the given context and generate
199: * the message body of the notification.
200: * <p>
201: * The result returned will contain the appropriate response
202: * messages indicating succes or failure and the OUT parameter,
203: * "body" containing the generated message.
204: *
205: * @param ctx The dispatching context of the service
206: * @param context The map containing all the fields associated with
207: * the sevice
208: * @return A new Map indicating success or error containing the
209: * body generated from the template and the input parameters.
210: */
211: public static Map prepareNotification(DispatchContext ctx,
212: Map context) {
213: GenericDelegator delegator = ctx.getDelegator();
214: String templateName = (String) context.get("templateName");
215: Map templateData = (Map) context.get("templateData");
216: String webSiteId = (String) context.get("webSiteId");
217:
218: Map result = null;
219: if (templateData == null) {
220: templateData = new HashMap();
221: }
222:
223: try {
224: // ensure the baseURl is defined
225: setBaseUrl(delegator, webSiteId, templateData);
226:
227: // initialize the template reader and processor
228: URL templateUrl = UtilURL.fromResource(templateName);
229:
230: if (templateUrl == null) {
231: Debug.logError("Problem getting the template URL: "
232: + templateName + " not found", module);
233: return ServiceUtil
234: .returnError("Problem finding template; see logs");
235: }
236:
237: InputStreamReader templateReader = new InputStreamReader(
238: templateUrl.openStream());
239:
240: // process the template with the given data and write
241: // the email body to the String buffer
242: Writer writer = new StringWriter();
243: FreeMarkerWorker.renderTemplate(templateName,
244: templateReader, templateData, writer);
245:
246: // extract the newly created body for the notification email
247: String notificationBody = writer.toString();
248:
249: // generate the successfull reponse
250: result = ServiceUtil
251: .returnSuccess("Message body generated successfully");
252: result.put("body", notificationBody);
253: } catch (IOException ie) {
254: Debug.logError(ie, "Problems reading template", module);
255: result = ServiceUtil
256: .returnError("Template reading problem, see error logs");
257: } catch (TemplateException te) {
258: Debug.logError(te, "Problems processing template", module);
259: result = ServiceUtil
260: .returnError("Template processing problem, see error log");
261: }
262:
263: return result;
264: }
265:
266: /**
267: * The expectation is that a lot of notification messages will include
268: * a link back to one or more pages in the system, which will require knowledge
269: * of the base URL to extrapolate. This method will ensure that the
270: * <code>baseUrl</code> field is set in the given context.
271: * <p>
272: * If it has been specified a default <code>baseUrl</code> will be
273: * set using a best effort approach. If it is specified in the
274: * url.properties configuration files of the system, that will be
275: * used, otherwise it will attempt resolve the fully qualified
276: * local host name.
277: * <p>
278: * <i>Note:</i> I thought it might be useful to have some dynamic way
279: * of extending the default properties provided by the NotificationService,
280: * such as the <code>baseUrl</code>, perhaps using the standard
281: * <code>ResourceBundle</code> java approach so that both classes
282: * and static files may be invoked.
283: *
284: * @param context The context to check and, if necessary, set the
285: * <code>baseUrl</code>.
286: */
287: public static void setBaseUrl(GenericDelegator delegator,
288: String webSiteId, Map context) {
289: StringBuffer httpBase = null;
290: StringBuffer httpsBase = null;
291:
292: String localServer = null;
293:
294: String httpsPort = null;
295: String httpsServer = null;
296: String httpPort = null;
297: String httpServer = null;
298: Boolean enableHttps = null;
299:
300: // If the baseUrl was not specified we can do a best effort instead
301: if (!context.containsKey("baseUrl")) {
302: try {
303: // using just the IP address of localhost if we don't have a defined server
304: InetAddress localHost = InetAddress.getLocalHost();
305: localServer = localHost.getHostAddress();
306: } catch (UnknownHostException hostException) {
307: Debug
308: .logWarning(
309: hostException,
310: "Could not determine localhost, using '127.0.0.1'",
311: module);
312: localServer = "127.0.0.1";
313: }
314:
315: // load the properties from the website entity
316: GenericValue webSite = null;
317: if (webSiteId != null) {
318: try {
319: webSite = delegator.findByPrimaryKeyCache(
320: "WebSite", UtilMisc.toMap("webSiteId",
321: webSiteId));
322: if (webSite != null) {
323: httpsPort = webSite.getString("httpsPort");
324: httpsServer = webSite.getString("httpsHost");
325: httpPort = webSite.getString("httpPort");
326: httpServer = webSite.getString("httpHost");
327: enableHttps = webSite.getBoolean("enableHttps");
328: }
329: } catch (GenericEntityException e) {
330: Debug
331: .logWarning(
332: e,
333: "Problems with WebSite entity; using global defaults",
334: module);
335: }
336: }
337:
338: // fill in any missing properties with fields from the global file
339: if (httpsPort == null) {
340: httpsPort = UtilProperties.getPropertyValue(
341: "url.properties", "port.https", "443");
342: }
343: if (httpServer == null) {
344: httpsServer = UtilProperties.getPropertyValue(
345: "url.properties", "force.https.host",
346: localServer);
347: }
348: if (httpPort == null) {
349: httpPort = UtilProperties.getPropertyValue(
350: "url.properties", "port.http", "80");
351: }
352: if (httpServer == null) {
353: httpServer = UtilProperties.getPropertyValue(
354: "url.properties", "force.http.host",
355: localServer);
356: }
357: if (enableHttps == null) {
358: enableHttps = new Boolean(UtilProperties
359: .propertyValueEqualsIgnoreCase(
360: "url.properties", "port.https.enabled",
361: "Y"));
362: }
363:
364: // prepare the (non-secure) URL
365: httpBase = new StringBuffer("http://");
366: httpBase.append(httpServer);
367: if (!"80".equals(httpPort)) {
368: httpBase.append(":");
369: httpBase.append(httpPort);
370: }
371:
372: // set the base (non-secure) URL for any messages requiring it
373: context.put("baseUrl", httpBase.toString());
374:
375: if (enableHttps.booleanValue()) {
376: // prepare the (secure) URL
377: httpsBase = new StringBuffer("https://");
378: httpsBase.append(httpsServer);
379: if (!"443".equals(httpsPort)) {
380: httpsBase.append(":");
381: httpsBase.append(httpsPort);
382: }
383:
384: // set the base (secure) URL for any messages requiring it
385: context.put("baseSecureUrl", httpsBase.toString());
386: } else {
387: context.put("baseSecureUrl", httpBase.toString());
388: }
389: }
390: }
391: }
|