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