1    /*
2     * Copyright 2009 :torweg free software group
3     *
4     * This program is free software: you can redistribute it and/or modify
5     * it under the terms of the GNU General Public License as published by
6     * the Free Software Foundation, either version 3 of the License, or
7     * (at your option) any later version.
8     * 
9     * This program is distributed in the hope that it will be useful,
10    * but WITHOUT ANY WARRANTY; without even the implied warranty of
11    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    * GNU General Public License for more details.
13    * 
14    * You should have received a copy of the GNU General Public License
15    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16    *
17    */
18   package org.torweg.pulse.component.util.vfs;
19   
20   import java.io.File;
21   import java.io.FileInputStream;
22   import java.io.IOException;
23   import java.io.InputStream;
24   import java.io.OutputStream;
25   import java.net.URI;
26   import java.util.HashMap;
27   import java.util.List;
28   import java.util.Map;
29   import java.util.TreeMap;
30   
31   import javax.servlet.http.HttpServletResponse;
32   
33   import org.apache.poi.ss.usermodel.Cell;
34   import org.apache.poi.ss.usermodel.Row;
35   import org.apache.poi.xssf.usermodel.XSSFSheet;
36   import org.apache.poi.xssf.usermodel.XSSFWorkbook;
37   import org.hibernate.Session;
38   import org.hibernate.Transaction;
39   import org.slf4j.Logger;
40   import org.slf4j.LoggerFactory;
41   import org.torweg.pulse.accesscontrol.User;
42   import org.torweg.pulse.annotations.Action;
43   import org.torweg.pulse.annotations.Permission;
44   import org.torweg.pulse.bundle.Controller;
45   import org.torweg.pulse.invocation.lifecycle.Lifecycle;
46   import org.torweg.pulse.service.PulseException;
47   import org.torweg.pulse.service.event.Event;
48   import org.torweg.pulse.service.request.CacheMode;
49   import org.torweg.pulse.service.request.Command;
50   import org.torweg.pulse.service.request.ServiceRequest;
51   import org.torweg.pulse.vfs.VirtualFile;
52   import org.torweg.pulse.vfs.VirtualFileSystem;
53   
54   /**
55    * is an import tool for the {@code VirtualFileSystem}.
56    * <p>
57    * It supports the recursive import of a given directory of the servers file
58    * system into a given directory of the {@code VirtualFileSystem}
59    * sanitising the file and directory names.
60    * </p>
61    * <p>
62    * The results of the import can be outputted as XML, Excel 97/2007 or CSV.
63    * </p>
64    * 
65    * @author Thomas Weber
66    * @version $Revision: 1581 $
67    */
68   public final class VFSImporter extends Controller {
69   
70       /**
71        * the logger.
72        */
73       private static final Logger LOGGER = LoggerFactory
74               .getLogger(VFSImporter.class);
75   
76       /**
77        * all URL safe characters.
78        */
79       private static final String URL_SAFE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz"
80               + "ABCDEFGHIJKLMNOPQRSTUWVXYZ" + "1234567890-_.";
81   
82       /**
83        * the default substitution character.
84        */
85       private static final String SUBSTITUTION_CHARCTER = "_";
86   
87       /**
88        * performs the import (see details for required {@code Parameter}s.
89        * <p>
90        * The import {@code Command} requires the following
91        * {@code Parameter}s:
92        * </p>
93        * <ul type="disc">
94        * <li><tt>source</tt>: the source folder in the file system as a
95        * {@code URI}</li>
96        * <li><tt>destination</tt>: the destination folder in the
97        * {@code VirtualFileSystem} as a {@code URI}</li>
98        * </ul>
99        * 
100       * @param request
101       *            the current request
102       * @return {@code null}
103       */
104      @Action(value = "importIntoVFS")
105      @Permission(value = "importIntoVFS")
106      public Object importIntoVFS(final ServiceRequest request) {
107          Command command = request.getCommand();
108          File source;
109          VirtualFile destination;
110          try {
111              source = new File(new URI(command.getParameter("source")
112                      .getFirstValue()));
113              destination = VirtualFileSystem.getInstance()
114                      .getVirtualFile(
115                              new URI(command.getParameter("destination")
116                                      .getFirstValue()), request.getUser());
117          } catch (Exception e) {
118              throw new PulseException("Invalid input: "
119                      + e.getLocalizedMessage(), e);
120          }
121          if (!destination.isDirectory()) {
122              throw new PulseException("Destination must be a directory!");
123          }
124          if (!source.exists() || !source.canRead()) {
125              throw new PulseException("Source '" + source.getAbsolutePath()
126                      + "' must exist and be readable!");
127          }
128          if (!destination.exists() || !destination.canWrite(request.getUser())) {
129              throw new PulseException("Destination '"
130                      + destination.getURI().toASCIIString()
131                      + "' must exist and be writeable!");
132          }
133          // String outputFormat = command.getParameter("output").getFirstValue();
134          // if (!outputFormat.matches("^xml$|^xls$|^xlsx$|^csv$")) {
135          // throw new PulseException(outputFormat
136          // + "is not valid value for 'output'");
137          // }
138          Map<String, String> sourceDestMap = importFiles(source, destination,
139                  request.getUser());
140  
141          request.getEventManager().addEvent(new ExcelOutputEvent(sourceDestMap));
142  
143          return null;
144      }
145  
146      /**
147       * updates the file size of all {@code VirtualFile}s.
148       * 
149       * @param request
150       *            the current request
151       * @return an {@code null}
152       */
153      @Action(value = "updateFileSize")
154      @Permission(value = "importIntoVFS")
155      public Object updateFileSize(final ServiceRequest request) {
156          Session s = Lifecycle.getHibernateDataSource().createNewSession();
157          Transaction tx = s.beginTransaction();
158          VirtualFileSystem vfs = VirtualFileSystem.getInstance();
159          try {
160              @SuppressWarnings("unchecked")
161              List<VirtualFile> files = (List<VirtualFile>) s.createCriteria(
162                      VirtualFile.class).list();
163              for (VirtualFile f : files) {
164                  if (f.isFile()) {
165                      vfs.updateFileSize(f);
166                      s.saveOrUpdate(f);
167                  }
168              }
169              tx.commit();
170          } catch (Exception e) {
171              tx.rollback();
172              throw new PulseException("Error: " + e.getLocalizedMessage(), e);
173          } finally {
174              s.close();
175          }
176          return null;
177      }
178  
179      /**
180       * @param source
181       *            the source
182       * @param destination
183       *            the destination
184       * @param user
185       *            the user for access checks
186       * @return the mapping of sources to destinations
187       */
188      private Map<String, String> importFiles(final File source,
189              final VirtualFile destination, final User user) {
190          Map<String, String> sourceDestMap = new HashMap<String, String>();
191          if (source.isFile()) {
192              importFile(source, destination, user, sourceDestMap);
193          }
194          if (source.isDirectory()) {
195              importDirectory(source, destination, user, sourceDestMap);
196          }
197          return sourceDestMap;
198      }
199  
200      /**
201       * @param source
202       *            the source
203       * @param destination
204       *            the destination
205       * @param user
206       *            the user for access checks
207       * @param sourceDestMap
208       *            the mapping
209       */
210      private void importDirectory(final File source,
211              final VirtualFile destination, final User user,
212              final Map<String, String> sourceDestMap) {
213          VirtualFileSystem vfs = VirtualFileSystem.getInstance();
214          String filename = normalizeFileName(source.getName());
215          VirtualFile nextDestination = null;
216          try {
217              URI oldDestinationURI = destination.getURI();
218              if (!oldDestinationURI.toASCIIString().endsWith("/")) {
219                  oldDestinationURI = new URI(oldDestinationURI.toASCIIString()
220                          + "/");
221              }
222              URI nextDestURI = oldDestinationURI.resolve(filename);
223              nextDestination = vfs.getVirtualFile(nextDestURI, user);
224              if (vfs.mkdir(vfs.getVirtualFile(nextDestURI, user), user)) {
225                  nextDestination = vfs.getVirtualFile(nextDestURI, user);
226              }
227          } catch (Exception e) {
228              LOGGER.error(e.getLocalizedMessage(), e);
229          }
230          if (nextDestination != null) {
231              sourceDestMap.put(source.getAbsolutePath(), nextDestination
232                      .getURI().toASCIIString());
233              for (File file : source.listFiles()) {
234                  if (file.isFile()) {
235                      importFile(file, nextDestination, user, sourceDestMap);
236                  } else if (file.isDirectory()) {
237                      importDirectory(file, nextDestination, user, sourceDestMap);
238                  }
239              }
240          }
241  
242      }
243  
244      /**
245       * @param source
246       *            the source
247       * @param destination
248       *            the destination
249       * @param user
250       *            the user for access checks
251       * @param sourceDestMap
252       *            the mapping
253       */
254      private void importFile(final File source, final VirtualFile destination,
255              final User user, final Map<String, String> sourceDestMap) {
256          try {
257              VirtualFileSystem vfs = VirtualFileSystem.getInstance();
258              String filename = normalizeFileName(source.getName());
259              URI destinationURI = destination.getURI();
260              if (!destinationURI.toASCIIString().endsWith("/")) {
261                  destinationURI = new URI(destinationURI.toASCIIString() + "/");
262              }
263              VirtualFile vf = vfs.getVirtualFile(destinationURI
264                      .resolve(filename), user);
265              OutputStream out = vfs.getOutputStream(vf, user);
266              InputStream in = new FileInputStream(source);
267              try {
268                  byte[] buffer = new byte[65536];
269                  int available = in.read(buffer);
270                  while (available > 0) {
271                      out.write(buffer, 0, available);
272                      available = in.read(buffer);
273                  }
274              } finally {
275                  out.close();
276                  in.close();
277              }
278              sourceDestMap.put(source.getAbsolutePath(), vf.getURI()
279                      .toASCIIString());
280          } catch (Exception e) {
281              LOGGER.error(e.getLocalizedMessage(), e);
282          }
283  
284      }
285  
286      /**
287       * conversion method which is run after the locale based
288       * {@code SuffixConverter} to ensure that the suffix does only contain
289       * URL safe characters.
290       * 
291       * @param suffix
292       *            the suffix
293       * @return the converted suffix
294       */
295      private String normalizeFileName(final String suffix) {
296          StringBuilder converted = new StringBuilder();
297          for (int i = 0; i < suffix.length(); i++) {
298              String c = suffix.substring(i, i + 1);
299              if (URL_SAFE_CHARACTERS.indexOf(c) > -1) {
300                  converted.append(c);
301              } else if (c.equals(" ")) {
302                  converted.append("-");
303              } else {
304                  converted.append(SUBSTITUTION_CHARCTER);
305              }
306          }
307          return converted.toString();
308      }
309  
310      /* -------------------------- Excel output event ----------------- */
311  
312      /**
313       * creates the Excel output.
314       */
315      public static final class ExcelOutputEvent implements Event {
316  
317          /**
318           * the file mapping.
319           */
320          private final Map<String, String> fileMap;
321  
322          /**
323           * the cache mode.
324           */
325          private CacheMode cache = CacheMode.NONE;
326  
327          /**
328           * creates the {@code ExcelOutputEvent}.
329           * 
330           * @param mapping
331           *            the mapping
332           */
333          public ExcelOutputEvent(final Map<String, String> mapping) {
334              super();
335              this.fileMap = new TreeMap<String, String>();
336              this.fileMap.putAll(mapping);
337          }
338  
339          /**
340           * @return {@code true}
341           * @see org.torweg.pulse.service.event.Event#isOutputEvent()
342           */
343          public boolean isOutputEvent() {
344              return true;
345          }
346  
347          /**
348           * @return {@code true}
349           * @see org.torweg.pulse.service.event.Event#isStopEvent()
350           */
351          public boolean isStopEvent() {
352              return true;
353          }
354  
355          /**
356           * @see org.torweg.pulse.service.event.Event#isSingularEvent()
357           * @return {@code true}
358           */
359          public boolean isSingularEvent() {
360              return true;
361          }
362  
363          /**
364           * produces the Excel output.
365           * 
366           * @param req
367           *            the current request
368           * @see org.torweg.pulse.service.event.Event#run(org.torweg.pulse.service.request.ServiceRequest)
369           */
370          public void run(final ServiceRequest req) {
371              XSSFWorkbook workbook = new XSSFWorkbook();
372              XSSFSheet sheet = workbook.createSheet("file mappings");
373              int rowNum = 0;
374              for (Map.Entry<String, String> entry : this.fileMap.entrySet()) {
375                  Row row = sheet.createRow(rowNum);
376                  Cell source = row.createCell(0);
377                  source.setCellValue(entry.getKey());
378                  Cell destination = row.createCell(1);
379                  destination.setCellValue(entry.getValue());
380                  rowNum++;
381              }
382              HttpServletResponse response = req.getHttpServletResponse();
383              response.setContentType("application/"
384                      + "vnd.openxmlformats-officedocument."
385                      + "spreadsheetml.sheet");
386              response.addHeader("Content-disposition", "attachment; filename="
387                      + "file-mappings.xlsx");
388              OutputStream out = null;
389              try {
390                  out = response.getOutputStream();
391                  workbook.write(out);
392              } catch (Exception e) {
393                  throw new PulseException("Error generating Excel output: "
394                          + e.getLocalizedMessage(), e);
395              } finally {
396                  try {
397                      out.close();
398                  } catch (IOException e) {
399                      throw new PulseException("Error generating Excel output: "
400                              + e.getLocalizedMessage(), e);
401                  }
402              }
403          }
404  
405          /**
406           * returns the {@code CacheMode}.
407           * 
408           * @return the cache mode
409           * @see org.torweg.pulse.service.event.Event#getCacheMode()
410           */
411          public CacheMode getCacheMode() {
412              return this.cache;
413          }
414  
415          /**
416           * sets the {@code CacheMode}.
417           * 
418           * @param c
419           *            the cache mode to set
420           * @see org.torweg.pulse.service.event.Event#setCacheMode(org.torweg.pulse.service.request.CacheMode)
421           */
422          public void setCacheMode(final CacheMode c) {
423              this.cache = c;
424          }
425  
426      }
427  }
428