001/*
002 *  Copyright 2011 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.subscribe;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.nio.charset.StandardCharsets;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.LinkedHashSet;
027import java.util.Map;
028import java.util.Set;
029import java.util.UUID;
030import java.util.regex.Pattern;
031
032import org.apache.avalon.framework.parameters.Parameters;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.cocoon.acting.ServiceableAction;
036import org.apache.cocoon.environment.ObjectModelHelper;
037import org.apache.cocoon.environment.Redirector;
038import org.apache.cocoon.environment.Request;
039import org.apache.cocoon.environment.SourceResolver;
040import org.apache.cocoon.servlet.multipart.Part;
041import org.apache.cocoon.servlet.multipart.PartOnDisk;
042import org.apache.cocoon.servlet.multipart.RejectedPart;
043import org.apache.commons.io.FilenameUtils;
044import org.apache.commons.io.IOUtils;
045import org.apache.commons.io.input.BOMInputStream;
046import org.apache.commons.lang.StringUtils;
047
048import org.ametys.plugins.newsletter.category.Category;
049import org.ametys.plugins.newsletter.category.CategoryProviderExtensionPoint;
050import org.ametys.plugins.newsletter.daos.Subscriber;
051import org.ametys.plugins.newsletter.daos.SubscribersDAO;
052
053/**
054 * Import subscribers from a CSV or text file.
055 */
056public class ImportSubscribersAction extends ServiceableAction
057{
058    private static final String[] _ALLOWED_EXTENSIONS = new String[] {"txt", "csv"};
059    
060    private static final Pattern __EMAIL_VALIDATOR = Pattern.compile("^([a-z0-9._-]+@[a-z0-9.-]{2,}[.][a-zA-Z0-9]{2,})?$");
061    
062    /** The subscribers DAO. */
063    protected SubscribersDAO _subscribersDao;
064    
065    /** The category provider extension point. */
066    protected CategoryProviderExtensionPoint _categoryProviderEP;
067
068    @Override
069    public void service(ServiceManager smanager) throws ServiceException
070    {
071        super.service(smanager);
072        _subscribersDao = (SubscribersDAO) smanager.lookup(SubscribersDAO.ROLE);
073        _categoryProviderEP = (CategoryProviderExtensionPoint) smanager.lookup(CategoryProviderExtensionPoint.ROLE);
074    }
075    
076    @Override
077    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
078    {
079        Map<String, String> result = new HashMap<>();
080
081        Request request = ObjectModelHelper.getRequest(objectModel);
082
083        String siteName = parameters.getParameter("siteName", request.getParameter("siteName"));
084        String categoryId = request.getParameter("categoryId");
085        boolean cleanTable = "true".equals(request.getParameter("cleanSubscribers"));
086        
087        Part part = (Part) request.get("importFile");
088        if (part instanceof RejectedPart)
089        {
090            return Collections.singletonMap("error", "rejected-file");
091        }
092        
093        PartOnDisk uploadedFilePart = (PartOnDisk) part;
094        File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null;
095        String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null;
096        
097        if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS))
098        {
099            return Collections.singletonMap("error", "invalid-extension");
100        }
101        
102        // Test if the category exists.
103        Category category = _categoryProviderEP.getCategory(categoryId);
104        if (category == null)
105        {
106            return Collections.singletonMap("error", "unknown-category");
107        }
108        
109        FileInputStream fileIS =  new FileInputStream(uploadedFile);
110        BOMInputStream bomIS = new BOMInputStream(fileIS);
111        
112        // Extract the emails from the import file.
113        Collection<String> emails = getEmails(bomIS);
114        
115        if (!emails.isEmpty())
116        {
117            // Empty the category subscribers if needed.
118            if (cleanTable)
119            {
120                _subscribersDao.empty(categoryId, siteName);
121            }
122            
123            // Insert the emails.
124            insertSubscribers(emails, categoryId, siteName, result);
125        }
126        
127        result.put("success", "true");
128        result.put("categoryId", categoryId);
129        
130        return result;
131    }
132    
133    /**
134     * Extract the emails from the file.
135     * @param bomIS the input stream 
136     * @return a collection of the emails.
137     * @throws IOException if an error occurs reading the file.
138     */
139    protected Collection<String> getEmails(BOMInputStream bomIS) throws IOException
140    {
141        Set<String> emails = new LinkedHashSet<>();
142        
143        for (String line : IOUtils.readLines(bomIS, StandardCharsets.UTF_8))
144        {
145            String[] part = line.split(";");
146            String email = StringUtils.trimToEmpty(part[0]);
147            if (__EMAIL_VALIDATOR.matcher(email.toLowerCase()).matches() && StringUtils.isNotBlank(email))
148            {
149                emails.add(email);
150            }
151        }
152        
153        return emails;
154    }
155    
156    /**
157     * Insert subscribers
158     * @param emails the emails to insert
159     * @param categoryId the id of category
160     * @param siteName the site name of category
161     * @param result the result map
162     */
163    protected void insertSubscribers(Collection<String> emails, String categoryId, String siteName, Map<String, String> result)
164    {
165        int subscribedCount = 0;
166        int existingCount = 0;
167        int errorCount = 0;
168        
169        for (String email : emails)
170        {
171            try
172            {
173                if (_subscribersDao.getSubscriber(email, siteName, categoryId) == null)
174                {
175                    Subscriber subscriber = new Subscriber();
176                    subscriber.setEmail(email);
177                    subscriber.setSiteName(siteName);
178                    subscriber.setCategoryId(categoryId);
179                    subscriber.setSubscribedAt(new Date());
180                    
181                    // Generate unique token.
182                    String token = UUID.randomUUID().toString();
183                    subscriber.setToken(token);
184                    
185                    _subscribersDao.subscribe(subscriber);
186                    
187                    if (getLogger().isInfoEnabled())
188                    {
189                        getLogger().info("The user with email '" + email + "' subscribed to the newsletter with the token " + token);
190                    }
191                    
192                    subscribedCount++;
193                }
194                else
195                {
196                    existingCount++;
197                }
198            }
199            catch (Exception e)
200            {
201                getLogger().error("Impossible to add as a subscriber the email " + email + " in category " + categoryId + " of site " + siteName, e);
202                errorCount++;
203            }
204        }
205        
206        result.put("subscribedCount", Integer.toString(subscribedCount));
207        result.put("existingCount", Integer.toString(existingCount));
208        result.put("errorCount", Integer.toString(errorCount));
209    }
210
211}