001/* 002 * Copyright 2012 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.newsletter.workflow; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.net.HttpURLConnection; 021import java.net.URISyntaxException; 022import java.net.URL; 023import java.time.LocalDate; 024import java.time.format.DateTimeFormatter; 025import java.util.Date; 026 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.cocoon.Constants; 032import org.apache.commons.lang.StringUtils; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036import org.ametys.core.util.I18nUtils; 037import org.ametys.plugins.newsletter.category.Category; 038import org.ametys.plugins.newsletter.category.CategoryProviderExtensionPoint; 039import org.ametys.plugins.newsletter.ga.GAUriBuilder; 040import org.ametys.web.repository.content.WebContent; 041import org.ametys.web.repository.site.Site; 042import org.ametys.web.repository.site.SiteManager; 043 044/** 045 * Send a google analytics event for every newsletter e-mail sent. 046 */ 047public class SendGAEventsEngine implements Runnable 048{ 049 050 private static final Logger _LOGGER = LoggerFactory.getLogger(SendGAEventsEngine.class); 051 052 /** The avalon context. */ 053 protected Context _context; 054 055 /** The cocoon environment context. */ 056 protected org.apache.cocoon.environment.Context _environmentContext; 057 058 /** The service manager. */ 059 protected ServiceManager _manager; 060 061 /** Is the engine initialized ? */ 062 protected boolean _initialized; 063 064 /** The google analytics URI builder. */ 065 protected GAUriBuilder _gaUriBuilder; 066 067 /** The category provider extension point. */ 068 protected CategoryProviderExtensionPoint _categoryProviderEP; 069 070 /** The site Manager. */ 071 protected SiteManager _siteManager; 072 073 /** The i18n utils component. */ 074 protected I18nUtils _i18nUtils; 075 076 private boolean _parametrized; 077 078 private Site _site; 079 080 private LocalDate _newsletterDate; 081 private long _newsletterNumber; 082 private String _newsletterTitle; 083 084 private Category _category; 085 086 private int _eventCount; 087 088 /** 089 * Initialize the engine. 090 * @param manager the avalon service manager. 091 * @param context the avalon context. 092 * @throws ContextException if an error occurred during initialization 093 * @throws ServiceException if an error occurred during initialization 094 */ 095 public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException 096 { 097 _manager = manager; 098 _context = context; 099 _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 100 101 // Lookup the needed components. 102 _gaUriBuilder = (GAUriBuilder) manager.lookup(GAUriBuilder.ROLE); 103 _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE); 104 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 105 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 106 107 _initialized = true; 108 } 109 110 /** 111 * Parameterize engine 112 * @param siteName The site name 113 * @param newsletterContent the newsletter content 114 * @param category the newsletter category 115 * @param eventCount the number of events 116 */ 117 public void parametrize(String siteName, WebContent newsletterContent, Category category, int eventCount) 118 { 119 if (siteName != null) 120 { 121 _site = _siteManager.getSite(siteName); 122 } 123 _category = category; 124 _eventCount = eventCount; 125 126 _newsletterDate = newsletterContent.getValue("newsletter-date"); 127 _newsletterNumber = newsletterContent.getValue("newsletter-number", false, 0); 128 _newsletterTitle = newsletterContent.getValue("title", false, ""); 129 130 _parametrized = true; 131 } 132 133 private void _checkInitialization() 134 { 135 if (!_initialized) 136 { 137 String message = "The GA events engine has to be properly initialized before it's run."; 138 _LOGGER.error(message); 139 throw new IllegalStateException(message); 140 } 141 if (!_parametrized) 142 { 143 String message = "The GA events engine has to be parametrized before it's run."; 144 _LOGGER.error(message); 145 throw new IllegalStateException(message); 146 } 147 } 148 149 public void run() 150 { 151 _checkInitialization(); 152 153 if (_LOGGER.isInfoEnabled()) 154 { 155 _LOGGER.info("Starting to send GA newsletter events."); 156 } 157 158 boolean trackingEnabled = _site.getValue("newsletter-enable-tracking", false, false); 159 String gaWebId = _site.getValue("google-web-property-id"); 160 161 if (trackingEnabled && StringUtils.isNotBlank(gaWebId)) 162 { 163 sendEvents(gaWebId); 164 } 165 166 if (_LOGGER.isInfoEnabled()) 167 { 168 _LOGGER.info("All mails are sent at " + new Date()); 169 } 170 } 171 172 /** 173 * Send GA events for a given GA user account. 174 * @param gaWebId the google web ID. 175 */ 176 protected void sendEvents(String gaWebId) 177 { 178 // Compute the event identifier. 179 String eventIdentifier = computeEventIdentifier(); 180 181 for (int i = 0; i < _eventCount; i++) 182 { 183 try 184 { 185 if (i > 0) 186 { 187 Thread.sleep(1100); 188 } 189 190 // Build the URI and send the request. 191 String uri = _gaUriBuilder.getEventGifUri(gaWebId, eventIdentifier, false); 192 sendEvent(uri); 193 } 194 catch (IOException e) 195 { 196 _LOGGER.error("IO error sending the event.", e); 197 } 198 catch (InterruptedException e) 199 { 200 _LOGGER.error("Error while waiting for sending mails.", e); 201 } 202 } 203 } 204 205 /** 206 * Send the event request, given the full GIF URL. 207 * @param eventUrl the full event GIF URL (with parameters). 208 * @throws IOException if an error occurs sending the GIF HTTP request. 209 */ 210 protected void sendEvent(String eventUrl) throws IOException 211 { 212 URL url = new URL(eventUrl); 213 214 // Use a custom user agent to avoid 215 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 216 connection.setRequestProperty("User-Agent", "Ametys/3 (compatible; MSIE 6.0)"); 217 connection.setRequestMethod("GET"); 218 connection.connect(); 219 220 int responseCode = connection.getResponseCode(); 221 222 if (responseCode != HttpURLConnection.HTTP_OK) 223 { 224 _LOGGER.error("Google analytics image request returned with error (code: " + responseCode + ")."); 225 } 226 else 227 { 228 // Quietly consume the stream, ignoring errors. 229 consumeQuietly(connection.getInputStream()); 230 } 231 } 232 233 /** 234 * Compute the event identifier for the newsletter sending. 235 * @return the event identifier. 236 */ 237 protected String computeEventIdentifier() 238 { 239 String newsletterTitle = _i18nUtils.translate(_category.getTitle(), _category.getLang()); 240 241 String category = "Newsletters / " + newsletterTitle; 242 String action = "Sending"; 243 244 StringBuilder label = new StringBuilder(); 245 label.append(_newsletterTitle); 246 if (_newsletterNumber > 0) 247 { 248 label.append(" / ").append(_newsletterNumber); 249 } 250 if (_newsletterDate != null) 251 { 252 label.append(" / ").append(_newsletterDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); 253 } 254 255 try 256 { 257 return _gaUriBuilder.getEventIdentifier(category, action, label.toString()); 258 } 259 catch (URISyntaxException e) 260 { 261 // Should never happen. 262 _LOGGER.error("Unsupported encoding.", e); 263 } 264 265 return ""; 266 } 267 268 /** 269 * Consume a stream, reading all its data and ignoring errors. 270 * @param input the input stream to consume. 271 */ 272 protected void consumeQuietly(InputStream input) 273 { 274 try 275 { 276 byte[] buffer = new byte[256]; 277 while (input.read(buffer) != -1) 278 { 279 // Ignore. 280 } 281 } 282 catch (IOException e) 283 { 284 // Ignore. 285 } 286 finally 287 { 288 // Close quietly 289 try 290 { 291 if (input != null) 292 { 293 input.close(); 294 } 295 } 296 catch (IOException ioe) 297 { 298 // ignore 299 } 300 } 301 } 302 303}