1
2
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
37
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
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
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
396
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
621
622
623
624
625
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 }