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.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, String> 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            return Collections.singletonMap("error", "rejected-file");
092        }
093        
094        PartOnDisk uploadedFilePart = (PartOnDisk) part;
095        File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null;
096        String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null;
097        
098        if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS))
099        {
100            return Collections.singletonMap("error", "invalid-extension");
101        }
102        
103        // Test if the category exists.
104        Category category = _categoryProviderEP.getCategory(categoryId);
105        if (category == null)
106        {
107            return Collections.singletonMap("error", "unknown-category");
108        }
109        
110        FileInputStream fileIS =  new FileInputStream(uploadedFile);
111        BOMInputStream bomIS = new BOMInputStream(fileIS);
112        
113        // Extract the emails from the import file.
114        Collection<String> emails = getEmails(bomIS);
115        
116        if (!emails.isEmpty())
117        {
118            // Empty the category subscribers if needed.
119            if (cleanTable)
120            {
121                _subscribersDao.empty(categoryId, siteName);
122            }
123            
124            // Insert the emails.
125            insertSubscribers(emails, categoryId, siteName, result);
126        }
127        
128        result.put("success", "true");
129        result.put("categoryId", categoryId);
130        
131        return result;
132    }
133    
134    /**
135     * Extract the emails from the file.
136     * @param bomIS the input stream 
137     * @return a collection of the emails.
138     * @throws IOException if an error occurs reading the file.
139     */
140    protected Collection<String> getEmails(BOMInputStream bomIS) throws IOException
141    {
142        Set<String> emails = new LinkedHashSet<>();
143        
144        for (String line : IOUtils.readLines(bomIS, StandardCharsets.UTF_8))
145        {
146            String[] part = line.split(";");
147            String email = StringUtils.trimToEmpty(part[0]);
148            if (__EMAIL_VALIDATOR.matcher(email.toLowerCase()).matches() && StringUtils.isNotBlank(email))
149            {
150                emails.add(email);
151            }
152        }
153        
154        return emails;
155    }
156    
157    /**
158     * Insert subscribers
159     * @param emails the emails to insert
160     * @param categoryId the id of category
161     * @param siteName the site name of category
162     * @param result the result map
163     */
164    protected void insertSubscribers(Collection<String> emails, String categoryId, String siteName, Map<String, String> result)
165    {
166        int subscribedCount = 0;
167        int existingCount = 0;
168        int errorCount = 0;
169        
170        for (String email : emails)
171        {
172            try
173            {
174                if (_subscribersDao.getSubscriber(email, siteName, categoryId) == null)
175                {
176                    Subscriber subscriber = new Subscriber();
177                    subscriber.setEmail(email);
178                    subscriber.setSiteName(siteName);
179                    subscriber.setCategoryId(categoryId);
180                    subscriber.setSubscribedAt(new Date());
181                    
182                    // Generate unique token.
183                    String token = UUID.randomUUID().toString();
184                    subscriber.setToken(token);
185                    
186                    _subscribersDao.subscribe(subscriber);
187                    
188                    if (getLogger().isInfoEnabled())
189                    {
190                        getLogger().info("The user with email '" + email + "' subscribed to the newsletter with the token " + token);
191                    }
192                    
193                    subscribedCount++;
194                }
195                else
196                {
197                    existingCount++;
198                }
199            }
200            catch (Exception e)
201            {
202                getLogger().error("Impossible to add as a subscriber the email " + email + " in category " + categoryId + " of site " + siteName, e);
203                errorCount++;
204            }
205        }
206        
207        result.put("subscribedCount", Integer.toString(subscribedCount));
208        result.put("existingCount", Integer.toString(existingCount));
209        result.put("errorCount", Integer.toString(errorCount));
210    }
211
212}