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.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.plugins.repository.metadata.CompositeMetadata;
041import org.ametys.web.repository.content.WebContent;
042import org.ametys.web.site.SiteConfigurationExtensionPoint;
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    private static final DateFormat _DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
053    
054    /** The avalon context. */
055    protected Context _context;
056    
057    /** The cocoon environment context. */
058    protected org.apache.cocoon.environment.Context _environmentContext;
059    
060    /** The service manager. */
061    protected ServiceManager _manager;
062    
063    /** Is the engine initialized ? */
064    protected boolean _initialized;
065    
066    /** The google analytics URI builder. */
067    protected GAUriBuilder _gaUriBuilder;
068    
069    /** The category provider extension point. */
070    protected CategoryProviderExtensionPoint _categoryProviderEP;
071    
072    /** The site configuration extension point. */
073    protected SiteConfigurationExtensionPoint _siteConf;
074    
075    /** The i18n utils component. */
076    protected I18nUtils _i18nUtils;
077    
078    private boolean _parametrized;
079    
080    private String _siteName;
081    
082    private Date _newsletterDate;
083    private long _newsletterNumber;
084    private String _newsletterTitle;
085    
086    private Category _category;
087    
088    private int _eventCount;
089    
090    /**
091     * Initialize the engine.
092     * @param manager the avalon service manager.
093     * @param context the avalon context.
094     * @throws ContextException if an error occurred during initialization
095     * @throws ServiceException if an error occurred during initialization
096     */
097    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
098    {
099        _manager = manager;
100        _context = context;
101        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
102        
103        // Lookup the needed components.
104        _gaUriBuilder = (GAUriBuilder) manager.lookup(GAUriBuilder.ROLE);
105        _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE);
106        _siteConf = (SiteConfigurationExtensionPoint) manager.lookup(SiteConfigurationExtensionPoint.ROLE);
107        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
108        
109        _initialized = true;
110    }
111    
112    /**
113     * Parametrize engine
114     * @param siteName The site name
115     * @param newsletterContent the newsletter content
116     * @param category the newsletter category
117     * @param eventCount the number of events
118     */
119    public void parametrize(String siteName, WebContent newsletterContent, Category category, int eventCount)
120    {
121        _siteName = siteName;
122        _category = category;
123        _eventCount = eventCount;
124        
125        CompositeMetadata meta = newsletterContent.getMetadataHolder();
126        _newsletterDate = meta.getDate("newsletter-date", null);
127        _newsletterNumber = meta.getLong("newsletter-number", 0);
128        _newsletterTitle = meta.getString("title", "");
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 = _siteConf.getValueAsBoolean(_siteName, "newsletter-enable-tracking");
159        String gaWebId = _siteConf.getValueAsString(_siteName, "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(_DATE_FORMAT.format(_newsletterDate));
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}