View Javadoc

1   /*
2    * $Id: RepositoryServlet.java,v 1.2 2009/03/04 13:45:49 oeuillot Exp $
3    * 
4    */
5   package org.rcfaces.core.internal.repository;
6   
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.io.OutputStream;
10  import java.text.DateFormat;
11  import java.util.Date;
12  import java.util.HashMap;
13  import java.util.Locale;
14  import java.util.Map;
15  import java.util.zip.GZIPOutputStream;
16  
17  import javax.faces.FacesException;
18  import javax.servlet.ServletConfig;
19  import javax.servlet.ServletException;
20  import javax.servlet.http.HttpServletRequest;
21  import javax.servlet.http.HttpServletResponse;
22  
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.rcfaces.core.internal.Constants;
26  import org.rcfaces.core.internal.lang.ByteBufferOutputStream;
27  import org.rcfaces.core.internal.lang.StringAppender;
28  import org.rcfaces.core.internal.repository.IRepository.IContent;
29  import org.rcfaces.core.internal.repository.IRepository.IContentProvider;
30  import org.rcfaces.core.internal.repository.IRepository.IFile;
31  import org.rcfaces.core.internal.webapp.ConfiguredHttpServlet;
32  import org.rcfaces.core.internal.webapp.ExpirationDate;
33  import org.rcfaces.core.internal.webapp.URIParameters;
34  
35  /**
36   * @author Olivier Oeuillot (latest modification by $Author: oeuillot $)
37   * @version $Revision: 1.2 $ $Date: 2009/03/04 13:45:49 $
38   */
39  public abstract class RepositoryServlet extends ConfiguredHttpServlet {
40  
41      private static final String REVISION = "$Revision: 1.2 $";
42  
43      private static final long serialVersionUID = 7028775289298926045L;
44  
45      private static final Log LOG = LogFactory.getLog(RepositoryServlet.class);
46  
47      private static final byte[] BYTE_EMPTY_ARRAY = new byte[0];
48  
49      private static final String SET_PREFIX = ".sets";
50  
51      private static final String MODULES_PREFIX = ".modules";
52  
53      private static final String NO_CACHE_PARAMETER = Constants
54              .getPackagePrefix()
55              + ".NO_CACHE";
56  
57      private static final String GROUP_ALL_DEFAULT_VALUE = null;
58  
59      private static final String BOOT_SET_DEFAULT_VALUE = null;
60  
61      private static final int CONTENT_INITIAL_SIZE = 16000;
62  
63      private final Map fileToRecordByLocale = new HashMap(128);
64  
65      private IRepository repository;
66  
67      private boolean noCache;
68  
69      private boolean devMode;
70  
71      public void init(ServletConfig config) throws ServletException {
72          super.init(config);
73  
74          String nc = getParameter(getNoCacheParameterName());
75          if ("true".equalsIgnoreCase(nc)) {
76              noCache = true;
77  
78              LOG.info("Enable NO_CACHE for servlet '" + getServletName() + "'.");
79          }
80  
81          String repositoryDevModePropertyName = getRepositoryDevModeParameterName();
82          if (repositoryDevModePropertyName != null) {
83              String dev = getParameter(repositoryDevModePropertyName);
84  
85              if ("true".equalsIgnoreCase(dev)) {
86                  devMode = true;
87  
88                  LOG.info("Enable REPOSITORY_DEV_MODE for servlet '"
89                          + getServletName() + "'.");
90              }
91          }
92  
93          try {
94              repository = initializeRepository(config);
95  
96          } catch (IOException e) {
97              throw new ServletException(
98                      "Can not initialize repository for servlet '"
99                              + getServletName() + "'.", e);
100         }
101     }
102 
103     protected String getNoCacheParameterName() {
104         return NO_CACHE_PARAMETER;
105     }
106 
107     protected String getRepositoryDevModeParameterName() {
108         return null;
109     }
110 
111     protected abstract String getParameterPrefix();
112 
113     protected final IRepository getRepository() {
114         return repository;
115     }
116 
117     protected abstract IRepository initializeRepository(ServletConfig config)
118             throws IOException;
119 
120     protected void doHead(HttpServletRequest request,
121             HttpServletResponse response) throws IOException {
122 
123         // On gere le fonctionement en interne !
124         doGet(request, response);
125     }
126 
127     protected void doGet(HttpServletRequest request,
128             HttpServletResponse response) throws IOException {
129 
130         String uri = request.getRequestURI();
131 
132         String contextPath = request.getContextPath();
133         if (contextPath != null) {
134             uri = uri.substring(contextPath.length());
135         }
136 
137         String servletPath = request.getServletPath();
138         if (servletPath != null) {
139             uri = uri.substring(servletPath.length());
140         }
141 
142         int idx = uri.indexOf('/');
143         if (idx >= 0) {
144             uri = uri.substring(idx + 1);
145         }
146 
147         Locale locale = null;
148 
149         boolean isVersioned = false;
150         String version = null;
151         if (getVersionSupport()) {
152             idx = uri.indexOf('/');
153             if (idx < 0) {
154                 response.setStatus(HttpServletResponse.SC_NOT_FOUND);
155                 return;
156             }
157 
158             version = uri.substring(0, idx);
159             uri = uri.substring(idx + 1);
160             isVersioned = true;
161         }
162 
163         URIParameters up = URIParameters.parseURI(uri);
164         if (up != null) {
165             if (localeSupport) {
166                 String localeName = up.getLocaleName();
167                 if (localeName != null) {
168                     locale = convertLocaleName(localeName, true);
169                 }
170             }
171 
172             if (version == null) {
173                 version = up.getVersion();
174             }
175 
176             uri = up.getURI();
177         }
178 
179         if (version != null) {
180             String repositoryVersion = repository.getVersion();
181             if (repositoryVersion != null) {
182                 if (repositoryVersion.equals(version) == false) {
183                     LOG.error("Not same repository version ! (current="
184                             + repositoryVersion + " request=" + version + ")");
185 
186                     setNoCache(response);
187                     response.sendError(HttpServletResponse.SC_CONFLICT,
188                             "Invalid RCFaces version !");
189                     return;
190                 }
191             }
192         }
193 
194         IFile file = repository.getFileByURI(uri);
195         if (file == null) {
196             setNoCache(response);
197             response.setStatus(HttpServletResponse.SC_NOT_FOUND);
198             return;
199         }
200 
201         if (locale == null) {
202             locale = getDefaultLocale(request, response);
203         }
204 
205         Record record = getFileRecord(file, locale);
206 
207         sendRecord(request, response, record, isVersioned);
208     }
209 
210     protected abstract boolean getVersionSupport();
211 
212     protected Record getFileRecord(IFile file, Locale locale) {
213         Record record;
214 
215         if (locale == null) {
216             throw new FacesException("Locale is NULL for file '"
217                     + file.getFilename() + "'.");
218         }
219 
220         synchronized (fileToRecordByLocale) {
221             Map fileToRecord = (Map) fileToRecordByLocale.get(locale);
222             if (fileToRecord == null) {
223                 fileToRecord = new HashMap();
224                 fileToRecordByLocale.put(locale, fileToRecord);
225             }
226 
227             record = (Record) fileToRecord.get(file);
228             if (record == null) {
229                 record = newRecord(file, locale);
230 
231                 fileToRecord.put(file, record);
232             }
233 
234             if (devMode) {
235                 record.verifyModifications();
236             }
237         }
238 
239         return record;
240     }
241 
242     private void sendRecord(HttpServletRequest request,
243             HttpServletResponse response, Record record, boolean isVersioned)
244             throws IOException {
245 
246         byte buf[] = null;
247         long modificationDate;
248         boolean noHeader = false;
249         String etag;
250         String hash;
251 
252         synchronized (record) {
253             etag = record.getETag();
254             hash = record.getHash();
255 
256             modificationDate = record.getLastModificationDate();
257             if (modificationDate > 0)
258                 modificationDate -= (modificationDate % 1000);
259 
260             if (hasGZipSupport() && hasGzipSupport(request)) {
261                 byte jsGZip[] = record.getGZipedBuffer();
262                 if (jsGZip != null) {
263                     setGzipContentEncoding(response, true);
264 
265                     buf = jsGZip;
266                     noHeader = true;
267                 }
268             }
269             if (buf == null) {
270                 buf = record.getBuffer();
271             }
272         }
273 
274         if (buf == null) {
275             if (LOG.isDebugEnabled()) {
276                 LOG.debug("Set no cache for response.");
277             }
278 
279             setNoCache(response);
280             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
281             return;
282         }
283 
284         if (noCache) {
285             setNoCache(response);
286 
287         } else {
288             if (modificationDate > 0) {
289                 response.setDateHeader(HTTP_LAST_MODIFIED, modificationDate);
290             }
291             ExpirationDate expirationDate = record.getExpirationDate();
292             if (expirationDate == null) {
293                 expirationDate = getDefaultExpirationDate(isVersioned);
294             }
295 
296             if (expirationDate != null) {
297                 expirationDate.sendExpires(response);
298             }
299         }
300 
301         boolean different = false;
302 
303         if (different == false && etag != null) {
304             String ifETag = request.getHeader(HTTP_IF_NONE_MATCH);
305             if (ifETag != null) {
306                 if (etag.equals(ifETag)) {
307                     response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
308                     return;
309                 }
310 
311                 different = true;
312             }
313         }
314 
315         if (different == false && hash != null) {
316             String isHash = request.getHeader(HTTP_IF_NOT_HASH);
317             if (isHash != null) {
318                 if (hash.equals(isHash)) {
319                     response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
320                     return;
321                 }
322 
323                 different = true;
324             }
325         }
326 
327         if (different == false && modificationDate > 0) {
328             long ifModifiedSince = request
329                     .getDateHeader(HTTP_IF_MODIFIED_SINCE);
330             if (ifModifiedSince > 0) {
331                 ifModifiedSince -= (ifModifiedSince % 1000);
332 
333                 if (ifModifiedSince >= modificationDate) {
334                     response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
335                     return;
336                 }
337             }
338         }
339 
340         // String acceptCharset=request.getHeader("Accept-Charset");
341 
342         String contentType = getContentType(record);
343         response.setContentType(contentType);
344 
345         if (etag != null) {
346             response.setHeader(HTTP_ETAG, etag);
347         }
348 
349         if (hash != null) {
350             response.setHeader(HTTP_HASH, hash);
351         }
352 
353         byte prolog[] = null;
354         byte epilog[] = null;
355         int length = buf.length;
356 
357         if (noHeader == false) {
358             prolog = record.getProlog();
359             if (prolog != null) {
360                 length += prolog.length;
361             }
362 
363             epilog = record.getEpilog();
364             if (epilog != null) {
365                 length += epilog.length;
366             }
367         }
368 
369         response.setContentLength(length);
370 
371         if (request.getMethod().equals(HEAD_HTTP_METHOD)) {
372             return;
373         }
374 
375         OutputStream outputStream = response.getOutputStream();
376         if (prolog != null) {
377             outputStream.write(prolog);
378         }
379 
380         outputStream.write(buf);
381 
382         if (epilog != null) {
383             outputStream.write(epilog);
384         }
385     }
386 
387     protected Record newRecord(IFile file, Locale locale) {
388         return new Record(file, locale);
389     }
390 
391     protected abstract String getContentType(Record record);
392 
393     /**
394      * 
395      * @author Olivier Oeuillot (latest modification by $Author: oeuillot $)
396      * @version $Revision: 1.2 $ $Date: 2009/03/04 13:45:49 $
397      */
398     protected class Record {
399         private static final String REVISION = "$Revision: 1.2 $";
400 
401         protected final IFile file;
402 
403         protected final Locale locale;
404 
405         protected byte buffer[];
406 
407         private byte gzippedBuffer[];
408 
409         private long lastModificationDate;
410 
411         protected ExpirationDate expirationDate;
412 
413         private String etag;
414 
415         private String hash;
416 
417         public Record(IFile file, Locale locale) {
418             this.file = file;
419             this.locale = locale;
420         }
421 
422         protected final IFile getFile() {
423             return file;
424         }
425 
426         public ExpirationDate getExpirationDate() {
427             return expirationDate;
428         }
429 
430         public void verifyModifications() {
431 
432             boolean modified = verifyFileModifications();
433 
434             if (modified == false) {
435                 return;
436             }
437 
438             if (LOG.isDebugEnabled()) {
439                 LOG.debug("Modification detected: " + getFile());
440             }
441             resetRecord();
442         }
443 
444         protected void resetRecord() {
445             buffer = null;
446             gzippedBuffer = null;
447             lastModificationDate = 0;
448             etag = null;
449             hash = null;
450 
451         }
452 
453         private boolean verifyFileModifications() {
454 
455             Object urls[] = getFileContentReferences(file);
456 
457             IContentProvider contentProvider = file.getContentProvider();
458             for (int i = 0; i < urls.length; i++) {
459                 long l;
460                 try {
461                     IContent content = contentProvider.getContent(urls[i],
462                             locale);
463                     try {
464                         l = content.getLastModified();
465 
466                     } finally {
467                         content.release();
468                     }
469 
470                 } catch (IOException ex) {
471                     LOG.error("Can not get lastModified date of '" + urls[i]
472                             + "'.", ex);
473                     l = -1;
474                 }
475 
476                 if (l < 1) {
477                     l = System.currentTimeMillis();
478                 }
479 
480                 LOG.debug("Verify file '" + file.getFilename() + "' delta="
481                         + (l - lastModificationDate) + " ms.");
482 
483                 if (lastModificationDate == l) {
484                     return false;
485                 }
486 
487                 if (lastModificationDate > 0) {
488                     LOG.debug("File '" + file.getFilename()
489                             + "' has been modified !");
490                 }
491             }
492 
493             return true;
494         }
495 
496         protected boolean verifyFilesModifications(IFile[] files) {
497             boolean modified = false;
498 
499             for (int i = 0; i < files.length; i++) {
500                 Record record = getFileRecord(files[i], locale);
501 
502                 if (record.verifyFileModifications()) {
503                     record.resetRecord();
504                     modified = true;
505                 }
506             }
507 
508             return modified;
509         }
510 
511         public byte[] getBuffer() throws IOException {
512             if (buffer != null) {
513                 return buffer;
514             }
515 
516             Object urls[] = getFileContentReferences(file);
517 
518             ByteBufferOutputStream bos = new ByteBufferOutputStream(
519                     CONTENT_INITIAL_SIZE);
520             lastModificationDate = -1;
521 
522             for (int i = 0; i < urls.length; i++) {
523                 IContent contentProvider = file.getContentProvider()
524                         .getContent(urls[i], locale);
525                 try {
526                     long date = contentProvider.getLastModified();
527                     if (date < 1) {
528                         date = System.currentTimeMillis();
529                     }
530                     if (lastModificationDate < date) {
531                         lastModificationDate = date;
532                     }
533 
534                     long size = contentProvider.getLength();
535                     if (size == 0) {
536                         continue;
537                     }
538 
539                     InputStream in = contentProvider.getInputStream();
540                     try {
541                         byte buf[];
542                         if (size > 0) {
543                             buf = new byte[(int) size];
544 
545                         } else {
546                             buf = new byte[4096];
547                         }
548 
549                         for (;;) {
550                             int ret = in.read(buf);
551                             if (ret < 0) {
552                                 break;
553                             }
554                             bos.write(buf, 0, ret);
555                         }
556 
557                     } finally {
558                         try {
559                             in.close();
560 
561                         } catch (Exception ex) {
562                             LOG.error("Can not close inputstream '" + urls[i]
563                                     + "'.", ex);
564                         }
565                     }
566                 } finally {
567                     contentProvider.release();
568                 }
569             }
570 
571             bos.close();
572 
573             buffer = bos.toByteArray();
574 
575             int beforeUpdate = buffer.length;
576 
577             buffer = updateBuffer(buffer);
578 
579             if (LOG.isInfoEnabled()) {
580                 DateFormat dateFormat = DateFormat.getDateTimeInstance(
581                         DateFormat.SHORT, DateFormat.MEDIUM);
582 
583                 LOG.debug("Load record '"
584                         + file.getFilename()
585                         + "' into "
586                         + buffer.length
587                         + " bytes, modified date="
588                         + dateFormat.format(new Date(lastModificationDate))
589                         + ((beforeUpdate > 0) ? ("  (update-ratio "
590                                 + (buffer.length * 100 / beforeUpdate) + "%)")
591                                 : ""));
592             }
593 
594             if (hasEtagSupport()) {
595                 etag = computeETag(buffer);
596             }
597 
598             if (hasHashSupport()) {
599                 hash = computeHash(buffer);
600             }
601 
602             return buffer;
603         }
604 
605         protected byte[] getFilesBuffer(IFile files[]) throws IOException {
606             byte buffers[][] = new byte[files.length][];
607             int size = 0;
608             lastModificationDate = 0;
609 
610             for (int i = 0; i < files.length; i++) {
611                 Record record = getFileRecord(files[i], locale);
612 
613                 synchronized (record) {
614                     buffers[i] = record.getBuffer();
615 
616                     size += buffers[i].length;
617 
618                     long lm = record.getLastModificationDate();
619 
620                     // plusieurs cas :
621                     // * on ne connait encore aucune date
622                     // * une des dates est inconnue, la date globale est
623                     // donc inconnue
624                     // * la nouvelle date est plus recente que les
625                     // precedentes
626                     if (lm > lastModificationDate) {
627                         lastModificationDate = lm;
628                     }
629                 }
630             }
631 
632             buffer = new byte[size];
633             int offset = 0;
634             for (int i = 0; i < files.length; i++) {
635                 byte b[] = buffers[i];
636 
637                 System.arraycopy(b, 0, buffer, offset, b.length);
638                 offset += b.length;
639             }
640 
641             if (LOG.isInfoEnabled()) {
642                 DateFormat dateFormat = DateFormat.getDateTimeInstance(
643                         DateFormat.SHORT, DateFormat.MEDIUM);
644 
645                 StringAppender sb = new StringAppender(files.length * 32);
646                 for (int i = 0; i < files.length; i++) {
647                     Record record = getFileRecord(files[i], locale);
648 
649                     if (sb.length() > 0) {
650                         sb.append(", ");
651                     }
652                     sb.append(record.file.getFilename());
653                 }
654 
655                 LOG.debug("Merge records for '" + file.getFilename()
656                         + "' into " + buffer.length + " bytes, modified date="
657                         + dateFormat.format(new Date(lastModificationDate))
658                         + ", files '" + sb.toString() + "'.");
659             }
660 
661             if (hasEtagSupport()) {
662                 etag = computeETag(buffer);
663             }
664 
665             if (hasHashSupport()) {
666                 hash = computeHash(buffer);
667             }
668 
669             return buffer;
670         }
671 
672         protected Object[] getFileContentReferences(IFile file) {
673             return file.getContentReferences(locale);
674         }
675 
676         protected byte[] updateBuffer(byte[] buffer) throws IOException {
677             return buffer;
678         }
679 
680         public final long getLastModificationDate() throws IOException {
681             getBuffer();
682 
683             return lastModificationDate;
684         }
685 
686         public final String getETag() throws IOException {
687             getBuffer();
688 
689             return etag;
690         }
691 
692         public final String getHash() throws IOException {
693             getBuffer();
694 
695             return hash;
696         }
697 
698         public final byte[] getGZipedBuffer() throws IOException {
699             if (gzippedBuffer != null) {
700                 return gzippedBuffer;
701             }
702 
703             byte buf[] = getBuffer();
704             if (buf == null || buf.length < 1) {
705                 return buf;
706             }
707 
708             ByteBufferOutputStream bos = new ByteBufferOutputStream(buf.length);
709             GZIPOutputStream gzos = new GZIPOutputStream(bos, buf.length);
710 
711             byte prolog[] = getProlog();
712             if (prolog.length > 0) {
713                 gzos.write(prolog);
714             }
715 
716             gzos.write(buf);
717 
718             byte epilog[] = getEpilog();
719             if (epilog.length > 0) {
720                 gzos.write(epilog);
721             }
722             gzos.close();
723 
724             gzippedBuffer = bos.toByteArray();
725 
726             LOG.debug("GZIP record '" + file.getFilename() + "' into "
727                     + gzippedBuffer.length + " bytes (compression-ratio "
728                     + (gzippedBuffer.length * 100 / buffer.length)
729                     + "% , original size=" + buffer.length + " bytes)");
730 
731             return gzippedBuffer;
732         }
733 
734         public byte[] getProlog() throws IOException {
735             return BYTE_EMPTY_ARRAY;
736         }
737 
738         public byte[] getEpilog() throws IOException {
739             return BYTE_EMPTY_ARRAY;
740         }
741 
742         public String getCharset() {
743             return null;
744         }
745 
746         public String toString() {
747             return "[Record file='"
748                     + file
749                     + "' expiration='"
750                     + expirationDate
751                     + "' lastModication='"
752                     + lastModificationDate
753                     + "' buffer.size="
754                     + ((buffer == null) ? "null" : String
755                             .valueOf(buffer.length)) + "]";
756         }
757 
758     }
759 }