001/*
002 *  Copyright 2015 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.sms.action;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.avalon.framework.parameters.Parameters;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.acting.ServiceableAction;
032import org.apache.cocoon.environment.ObjectModelHelper;
033import org.apache.cocoon.environment.Redirector;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.environment.SourceResolver;
036import org.apache.cocoon.servlet.multipart.Part;
037import org.apache.cocoon.servlet.multipart.PartOnDisk;
038import org.apache.cocoon.servlet.multipart.RejectedPart;
039import org.apache.commons.io.FilenameUtils;
040import org.apache.commons.io.IOUtils;
041import org.apache.commons.io.input.BOMInputStream;
042
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.sms.SMSHelper;
045import org.ametys.plugins.sms.dao.SubscriberDAO;
046
047/**
048 * Action importing subscribers from a CSV file supposedly containing one valid number per line
049 */
050public class ImportSMSListSubscribersAction extends ServiceableAction
051{
052    private static final String[] __ALLOWED_EXTENSIONS = new String[] {"txt", "csv"};
053    
054    /** The subscribers DAO. */
055    private SubscriberDAO _subscriberDAO;
056    
057    /** The Ametys object resolver */
058    private AmetysObjectResolver _ametysObjectResolver;
059
060    @Override
061    public void service(ServiceManager smanager) throws ServiceException
062    {
063        super.service(smanager);
064        _subscriberDAO = (SubscriberDAO) smanager.lookup(SubscriberDAO.ROLE);
065        _ametysObjectResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
066    }
067    
068    @Override
069    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
070    {
071        Map<String, String> result = new HashMap<>();
072
073        Request request = ObjectModelHelper.getRequest(objectModel);
074
075        String smsListId = request.getParameter("smsListId");
076        boolean cleanTable = "true".equals(request.getParameter("cleanSubscribers"));
077        
078        Part part = (Part) request.get("importFile");
079        if (part instanceof RejectedPart)
080        {
081            return Collections.singletonMap("error", "rejected-file");
082        }
083        
084        PartOnDisk uploadedFilePart = (PartOnDisk) part;
085        File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null;
086        String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null;
087        
088        if (!FilenameUtils.isExtension(filename, __ALLOWED_EXTENSIONS))
089        {
090            return Collections.singletonMap("error", "invalid-extension");
091        }
092        
093        // Test if the sms list exists.
094        if (!_ametysObjectResolver.hasAmetysObjectForId(smsListId))
095        {
096            return Collections.singletonMap("error", "unknown-list");
097        }
098        
099        Collection<String> phoneNumbers = null;
100        try (FileInputStream fileIS = new FileInputStream(uploadedFile);
101             BOMInputStream bomIS = new BOMInputStream(fileIS);)
102        {
103            // Extract the phone numbers from the file.
104            phoneNumbers = _getPhoneNumbers(bomIS, result);
105        }
106        
107        if (!phoneNumbers.isEmpty())
108        {
109            // Empty the broadcasting list subscribers if needed.
110            if (cleanTable)
111            {
112                _subscriberDAO.deleteAllNumbers(smsListId);
113            }
114            
115            // Insert the phone numbers.
116            _insertSubscribers(phoneNumbers, smsListId, result);
117        }
118        else
119        {
120            result.put("subscribedCount", "0");
121            result.put("existingCount", "0");
122            result.put("errorCount", "0");
123        }
124        
125        result.put("success", "true");
126        result.put("smsListId", smsListId);
127        
128        return result;
129    }
130    
131    /**
132     * Extract the phone numbers from the file.
133     * @param bomIS the BOM input stream 
134     * @param result the result map
135     * @return the collection of phone numbers.
136     * @throws IOException if an error occurs reading the file.
137     */
138    private Set<String> _getPhoneNumbers(BOMInputStream bomIS, Map<String, String> result) throws IOException
139    {
140        Set<String> phoneNumbers = new HashSet<>();
141        int errorCount = 0;
142        
143        for (String phoneNumber : IOUtils.readLines(bomIS))
144        {
145            // We check if the phone number is correct
146            String transformedNumber = phoneNumber.trim();           
147            transformedNumber = SMSHelper.transformPhoneNumber(transformedNumber);
148            
149            if (!SMSHelper.checkPhoneNumber(transformedNumber) && !SMSHelper.PHONE_NUMBER_INTERNATIONAL_VALIDATOR.matcher(transformedNumber).matches())
150            {   
151                getLogger().warn("Import subscribers phone number: '" + phoneNumber + "' is not a valid phone number; it will be ignored");
152                errorCount++;
153            }
154            else
155            {
156                phoneNumbers.add(transformedNumber);
157            }
158        }
159        result.put("errorCount", Integer.toString(errorCount));
160        return phoneNumbers;
161    }
162    
163    /**
164     * Insert subscribers in the broadcasting list
165     * @param phoneNumbers the list of phone numbers to insert
166     * @param smsListId the id of the sms list 
167     * @param result the server call result 
168     */
169    private void _insertSubscribers(Collection<String> phoneNumbers, String smsListId, Map<String, String> result)
170    {
171        int subscribedCount = 0;
172        int existingCount = 0;
173        int errorCount = Integer.parseInt(result.get("errorCount"));
174        
175        for (String phoneNumber : phoneNumbers)
176        {
177            try
178            {
179                if (_subscriberDAO.numberAlreadyExists(phoneNumber, smsListId))
180                {
181                    existingCount++;
182                }
183                else
184                {
185                    _subscriberDAO.insertNumber(phoneNumber, smsListId);
186                    
187                    if (getLogger().isInfoEnabled())
188                    {
189                        getLogger().info("The user with phoneNumber '" + phoneNumber + "' subscribed to the list '" + smsListId + "'.");
190                    }
191                    
192                    subscribedCount++;
193                }
194            }
195            catch (Exception e)
196            {
197                getLogger().error("Impossible to add as a subscriber the phoneNumber " + phoneNumber + " in list '" + smsListId + "'.", e);
198                errorCount++;
199            }
200        }
201        
202        result.put("subscribedCount", Integer.toString(subscribedCount));
203        result.put("existingCount", Integer.toString(existingCount));
204        result.put("errorCount", Integer.toString(errorCount));
205    }
206}