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