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, 0L);
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}