001// Copyright 2012 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.util; 016 017import org.apache.tapestry5.commons.Messages; 018import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; 019import org.apache.tapestry5.json.JSONObject; 020import org.apache.tapestry5.services.messages.ComponentMessagesSource; 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.net.URL; 025import java.util.Locale; 026 027public class MessageCatalogResource extends VirtualResource 028{ 029 private final Locale locale; 030 031 private final ComponentMessagesSource messagesSource; 032 033 private final boolean compactJSON; 034 035 private volatile byte[] bytes; 036 037 public MessageCatalogResource(Locale locale, ComponentMessagesSource messagesSource, final ResourceChangeTracker changeTracker, boolean compactJSON) 038 { 039 this.locale = locale; 040 this.messagesSource = messagesSource; 041 this.compactJSON = compactJSON; 042 043 messagesSource.getInvalidationEventHub().addInvalidationCallback(new Runnable() 044 { 045 public void run() 046 { 047 bytes = null; 048 049 // What's all this then? This MessageCatalogResource is converted, as if it was a real file, into 050 // a StreamableResource by the StreamableResourceService service; as a virtual resource, we don't have 051 // any date-time-modified information for the application message catalog (as if that was possible, 052 // given that its composed of multiple files). The ComponentMessagesSource can tell us when 053 // *some* properties file has changed (not necessarily one used in the application message catalog, 054 // but that's the breaks). When that occurs, we tell the ResourceChangeTracker to fire its invalidation 055 // event. That flushes out all the assets it has cached, including StreamableResources for JavaScript files, 056 // including the one created here to represent the application message catalog. 057 changeTracker.forceInvalidationEvent(); 058 } 059 }); 060 } 061 062 @Override 063 public String toString() 064 { 065 return String.format("MessageCatalogResource[%s]", locale); 066 } 067 068 /** 069 * StreamableResourceSourceImpl needs a file, and a suffix, to determine the content type. 070 */ 071 @Override 072 public String getFile() 073 { 074 return String.format("virtual-%s.js", locale); 075 } 076 077 /** 078 * To work as a streamable resource, must return a value (or null) from toURL. 079 */ 080 @Override 081 public URL toURL() 082 { 083 return null; 084 } 085 086 public InputStream openStream() throws IOException 087 { 088 return toInputStream(getBytes()); 089 } 090 091 private byte[] getBytes() throws IOException 092 { 093 if (bytes == null) 094 { 095 bytes = assembleCatalog().getBytes(UTF8); 096 } 097 098 return bytes; 099 } 100 101 private String assembleCatalog() 102 { 103 Messages messages = messagesSource.getApplicationCatalog(locale); 104 105 JSONObject catalog = new JSONObject(); 106 107 for (String key : messages.getKeys()) 108 { 109 if (key.startsWith("private-")) 110 { 111 continue; 112 } 113 114 String value = messages.get(key); 115 116 if (value.contains("%")) 117 { 118 continue; 119 } 120 121 catalog.put(key, value); 122 } 123 124 StringBuilder builder = new StringBuilder(2000); 125 126 builder.append("define(").append(catalog.toString(compactJSON)).append(");"); 127 128 return builder.toString(); 129 } 130}