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.text.DateFormat;
024import java.text.SimpleDateFormat;
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.io.IOUtils;
033import org.apache.commons.lang.StringUtils;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import org.ametys.core.util.I18nUtils;
038import org.ametys.plugins.newsletter.category.Category;
039import org.ametys.plugins.newsletter.category.CategoryProviderExtensionPoint;
040import org.ametys.plugins.newsletter.ga.GAUriBuilder;
041import org.ametys.plugins.repository.metadata.CompositeMetadata;
042import org.ametys.web.repository.content.WebContent;
043import org.ametys.web.site.SiteConfigurationExtensionPoint;
044
045/**
046 * Send a google analytics event for every newsletter e-mail sent.
047 */
048public class SendGAEventsEngine implements Runnable
049{
050    
051    private static final Logger _LOGGER = LoggerFactory.getLogger(SendGAEventsEngine.class);
052    
053    private static final DateFormat _DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
054    
055    /** The avalon context. */
056    protected Context _context;
057    
058    /** The cocoon environment context. */
059    protected org.apache.cocoon.environment.Context _environmentContext;
060    
061    /** The service manager. */
062    protected ServiceManager _manager;
063    
064    /** Is the engine initialized ? */
065    protected boolean _initialized;
066    
067    /** The google analytics URI builder. */
068    protected GAUriBuilder _gaUriBuilder;
069    
070    /** The category provider extension point. */
071    protected CategoryProviderExtensionPoint _categoryProviderEP;
072    
073    /** The site configuration extension point. */
074    protected SiteConfigurationExtensionPoint _siteConf;
075    
076    /** The i18n utils component. */
077    protected I18nUtils _i18nUtils;
078    
079    private boolean _parametrized;
080    
081    private String _siteName;
082    
083    private Date _newsletterDate;
084    private long _newsletterNumber;
085    private String _newsletterTitle;
086    
087    private Category _category;
088    
089    private int _eventCount;
090    
091    /**
092     * Initialize the engine.
093     * @param manager the avalon service manager.
094     * @param context the avalon context.
095     * @throws ContextException if an error occurred during initialization
096     * @throws ServiceException if an error occurred during initialization
097     */
098    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
099    {
100        _manager = manager;
101        _context = context;
102        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
103        
104        // Lookup the needed components.
105        _gaUriBuilder = (GAUriBuilder) manager.lookup(GAUriBuilder.ROLE);
106        _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE);
107        _siteConf = (SiteConfigurationExtensionPoint) manager.lookup(SiteConfigurationExtensionPoint.ROLE);
108        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
109        
110        _initialized = true;
111    }
112    
113    /**
114     * Parametrize engine
115     * @param siteName The site name
116     * @param newsletterContent the newsletter content
117     * @param category the newsletter category
118     * @param eventCount the number of events
119     */
120    public void parametrize(String siteName, WebContent newsletterContent, Category category, int eventCount)
121    {
122        _siteName = siteName;
123        _category = category;
124        _eventCount = eventCount;
125        
126        CompositeMetadata meta = newsletterContent.getMetadataHolder();
127        _newsletterDate = meta.getDate("newsletter-date", null);
128        _newsletterNumber = meta.getLong("newsletter-number", 0);
129        _newsletterTitle = meta.getString("title", "");
130        
131        _parametrized = true;
132    }
133    
134    private void _checkInitialization()
135    {
136        if (!_initialized)
137        {
138            String message = "The GA events engine has to be properly initialized before it's run.";
139            _LOGGER.error(message);
140            throw new IllegalStateException(message);
141        }
142        if (!_parametrized)
143        {
144            String message = "The GA events engine has to be parametrized before it's run.";
145            _LOGGER.error(message);
146            throw new IllegalStateException(message);
147        }
148    }
149    
150    public void run()
151    {
152        _checkInitialization();
153        
154        if (_LOGGER.isInfoEnabled())
155        {
156            _LOGGER.info("Starting to send GA newsletter events.");
157        }
158        
159        boolean trackingEnabled = _siteConf.getValueAsBoolean(_siteName, "newsletter-enable-tracking");
160        String gaWebId = _siteConf.getValueAsString(_siteName, "google-web-property-id");
161        
162        if (trackingEnabled && StringUtils.isNotBlank(gaWebId))
163        {
164            sendEvents(gaWebId);
165        }
166        
167        if (_LOGGER.isInfoEnabled())
168        {
169            _LOGGER.info("All mails are sent at " + new Date());
170        }
171    }
172    
173    /**
174     * Send GA events for a given GA user account.
175     * @param gaWebId the google web ID.
176     */
177    protected void sendEvents(String gaWebId)
178    {
179        // Compute the event identifier.
180        String eventIdentifier = computeEventIdentifier();
181        
182        for (int i = 0; i < _eventCount; i++)
183        {
184            try
185            {
186                if (i > 0)
187                {
188                    Thread.sleep(1100);
189                }
190                
191                // Build the URI and send the request.
192                String uri = _gaUriBuilder.getEventGifUri(gaWebId, eventIdentifier, false);
193                sendEvent(uri);
194            }
195            catch (IOException e)
196            {
197                _LOGGER.error("IO error sending the event.", e);
198            }
199            catch (InterruptedException e)
200            {
201                _LOGGER.error("Error while waiting for sending mails.", e);
202            }
203        }
204    }
205    
206    /**
207     * Send the event request, given the full GIF URL.
208     * @param eventUrl the full event GIF URL (with parameters).
209     * @throws IOException if an error occurs sending the GIF HTTP request.
210     */
211    protected void sendEvent(String eventUrl) throws IOException
212    {
213        URL url = new URL(eventUrl);
214        
215        // Use a custom user agent to avoid 
216        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
217        connection.setRequestProperty("User-Agent", "Ametys/3 (compatible; MSIE 6.0)");
218        connection.setRequestMethod("GET");
219        connection.connect();
220        
221        int responseCode = connection.getResponseCode();
222        
223        if (responseCode != HttpURLConnection.HTTP_OK)
224        {
225            _LOGGER.error("Google analytics image request returned with error (code: " + responseCode + ").");
226        }
227        else
228        {
229            // Quietly consume the stream, ignoring errors.
230            consumeQuietly(connection.getInputStream());
231        }
232    }
233    
234    /**
235     * Compute the event identifier for the newsletter sending.
236     * @return the event identifier.
237     */
238    protected String computeEventIdentifier()
239    {
240        String newsletterTitle = _i18nUtils.translate(_category.getTitle(), _category.getLang());
241        
242        String category = "Newsletters / " + newsletterTitle;
243        String action = "Sending";
244        
245        StringBuilder label = new StringBuilder();
246        label.append(_newsletterTitle);
247        if (_newsletterNumber > 0)
248        {
249            label.append(" / ").append(_newsletterNumber);
250        }
251        if (_newsletterDate != null)
252        {
253            label.append(" / ").append(_DATE_FORMAT.format(_newsletterDate));
254        }
255        
256        try
257        {
258            return _gaUriBuilder.getEventIdentifier(category, action, label.toString());
259        }
260        catch (URISyntaxException e)
261        {
262            // Should never happen.
263            _LOGGER.error("Unsupported encoding.", e);
264        }
265        
266        return "";
267    }
268    
269    /**
270     * Consume a stream, reading all its data and ignoring errors. 
271     * @param input the input stream to consume.
272     */
273    protected void consumeQuietly(InputStream input)
274    {
275        try
276        {
277            byte[] buffer = new byte[256];
278            while (input.read(buffer) != -1)
279            {
280                // Ignore.
281            }
282        }
283        catch (IOException e)
284        {
285            // Ignore.
286        }
287        finally
288        {
289            IOUtils.closeQuietly(input);
290        }
291    }
292    
293}