1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package com.tomgibara.pronto.config.source;
19
20 import java.io.BufferedInputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.net.URL;
24 import java.net.URLConnection;
25 import java.util.Map;
26
27 import com.tomgibara.pronto.config.ConfigSource;
28 import com.tomgibara.pronto.util.Arguments;
29 import com.tomgibara.pronto.util.Streams;
30
31 /**
32 * Accesses a (possibly) remote resource that is been specified via a URL and
33 * parses it into a collection of property values.
34 *
35 * Instances of this class are not safe for concurrent use. Wrap instances in a
36 * SynchronousConfigSource if thread safety is required.
37 *
38 * @author Tom Gibara
39 */
40
41 public class URLConfigSource implements ConfigSource {
42
43 /**
44 * The URL from which the properties resource is loaded.
45 */
46
47 private final URL url;
48
49 /**
50 * The reader which converts the connection streams into properties.
51 */
52
53 private final PropertiesReader reader;
54
55 /**
56 * The time at which the properties resource was last modified.
57 *
58 * Value of -1 indicates never retrieved. Value of 0 indicates retrieved but
59 * unknown.
60 */
61
62 private long lastModified = -1L;
63
64 /**
65 * The time at which the properties resource is to expire.
66 *
67 * Value of -1 indicates never retrieved. Value of 0 indicates retrieved but
68 * unknown.
69 */
70
71 private long expires = -1L;
72
73 /**
74 * The properties that were last read from the resource. A null value
75 * indicates that the properties have never before been retrieved.
76 */
77
78 private Map<String, String> properties = null;
79
80
81
82 /**
83 * Constructs a source which reads its properties from a URL using a
84 * DefaultPropertiesReader.
85 *
86 * @param url
87 * the location of the resource which contains the properties to
88 * be loaded, not null
89 */
90
91 public URLConfigSource(final URL url) {
92 this(url, null);
93 }
94
95 /**
96 * Constructs a source which reads its properties from a URL using the
97 * supplied properties reader. If a null reader is supplied to this
98 * constructor, a DefaultPropertiesReader is used.
99 *
100 * @param url
101 * the location of the resource which contains the properties to
102 * be loaded, not null
103 * @param reader
104 * a properties reader which can convert the resource into a set
105 * of properties, may be null
106 */
107
108 public URLConfigSource(final URL url, final PropertiesReader reader) {
109 Arguments.notNull(url, "url");
110 this.url = url;
111 this.reader = reader == null ? DefaultPropertiesReader.PLAIN : reader;
112 }
113
114
115
116 /**
117 * The URL from which the properties are loaded.
118 *
119 * @return the url for this source, never null
120 */
121
122 public URL getURL() {
123 return url;
124 }
125
126 /**
127 * The reader which converts this source's resources into properties.
128 *
129 * @return the properties reader for this source
130 */
131
132 public PropertiesReader getReader() {
133 return reader;
134 }
135
136
137
138 /**
139 * @return the last time at which the resource identified by this source's
140 * URL was declared to have changed
141 * @throws RuntimeException
142 * if an IOException was raised when accessing the resource
143 */
144
145 public long lastModified() throws RuntimeException {
146 update();
147 return lastModified;
148 }
149
150 /**
151 * @return the properties read from the resource identified by this source's
152 * URL
153 * @throws RuntimeException
154 * if an IOException was raised when accessing the resource
155 */
156
157 public Map<String, String> getProperties() throws RuntimeException {
158 update();
159 return properties;
160 }
161
162
163
164 /**
165 * This method is responsible for obtaining a connection to the URL for this
166 * source, configuring it, calling the connect() method on it and then
167 * returning the connection. This method may be overridden to customize the
168 * connection to the URL.
169 *
170 * The default implementation simply opens the connection, calls connect and
171 * then returns.
172 *
173 * @return an opened, connected <code>URLConnection</code>
174 * @throws IOException
175 * if the connection raises an IOException
176 */
177
178 protected URLConnection connectToURL() throws IOException {
179 URLConnection connection = getURL().openConnection();
180 connection.connect();
181 return connection;
182 }
183
184
185
186 /**
187 * Updates the lastModified, expires and properties fields of this object. A
188 * good attempt is made at
189 *
190 *
191 * @throws RuntimeException
192 * in the event that the an IOException occurs
193 */
194
195 private void update() throws RuntimeException {
196 long now = 0L;
197 if (properties != null) {
198
199 if (expires > 0L) {
200 now = System.currentTimeMillis();
201 if (now < expires) return;
202 }
203 }
204
205 if (now == 0L) now = System.currentTimeMillis();
206
207 try {
208
209 URLConnection connection = connectToURL();
210
211
212
213
214 boolean changed;
215 long newLastModified = connection.getLastModified();
216
217
218
219
220 if (lastModified < newLastModified || newLastModified == 0) {
221 lastModified = newLastModified;
222 expires = connection.getExpiration();
223 changed = true;
224 } else {
225
226
227 expires = connection.getExpiration();
228 changed = false;
229 }
230
231
232
233 InputStream in = null;
234 try {
235 in = connection.getInputStream();
236 if (properties == null || changed) {
237 in = new BufferedInputStream(in);
238 properties = reader.read(in);
239 }
240 } finally {
241 Streams.safeClose(in);
242 }
243 } catch (IOException e) {
244 throw new RuntimeException(e);
245 }
246 }
247 }