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}