aed365ca175e07ea81abe05b7535b707227887dd
[fw/altos] / altoslib / AltosMapStore.java
1 /*
2  * Copyright © 2014 Keith Packard <keithp@keithp.com>
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; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package org.altusmetrum.altoslib_10;
19
20 import java.io.*;
21 import java.net.*;
22 import java.util.*;
23
24 public class AltosMapStore {
25         String                                  url;
26         public File                             file;
27         LinkedList<AltosMapStoreListener>       listeners = new LinkedList<AltosMapStoreListener>();
28
29         int                                     status;
30
31         public int status() {
32                 return status;
33         }
34
35         public synchronized void add_listener(AltosMapStoreListener listener) {
36                 if (!listeners.contains(listener))
37                         listeners.add(listener);
38                 listener.notify_store(this, status);
39         }
40
41         public synchronized void remove_listener(AltosMapStoreListener listener) {
42                 listeners.remove(listener);
43         }
44
45         private synchronized void notify_listeners(int status) {
46                 this.status = status;
47                 for (AltosMapStoreListener listener : listeners)
48                         listener.notify_store(this, status);
49         }
50
51         static Object   forbidden_lock = new Object();
52         static long     forbidden_time;
53         static boolean  forbidden_set;
54
55         private int fetch_url() {
56                 URL u;
57
58                 try {
59                         u = new URL(url);
60                 } catch (java.net.MalformedURLException e) {
61                         return AltosMapTile.bad_request;
62                 }
63
64                 byte[] data;
65                 URLConnection uc = null;
66                 try {
67                         uc = u.openConnection();
68                         String type = uc.getContentType();
69                         int contentLength = uc.getContentLength();
70                         if (uc instanceof HttpURLConnection) {
71                                 int response = ((HttpURLConnection) uc).getResponseCode();
72                                 switch (response) {
73                                 case HttpURLConnection.HTTP_FORBIDDEN:
74                                 case HttpURLConnection.HTTP_PAYMENT_REQUIRED:
75                                 case HttpURLConnection.HTTP_UNAUTHORIZED:
76                                         synchronized (forbidden_lock) {
77                                                 forbidden_time = System.nanoTime();
78                                                 forbidden_set = true;
79                                                 return AltosMapTile.forbidden;
80                                         }
81                                 }
82                         }
83                         InputStream in = new BufferedInputStream(uc.getInputStream());
84                         int bytesRead = 0;
85                         int offset = 0;
86                         data = new byte[contentLength];
87                         while (offset < contentLength) {
88                                 bytesRead = in.read(data, offset, data.length - offset);
89                                 if (bytesRead == -1)
90                                         break;
91                                 offset += bytesRead;
92                         }
93                         in.close();
94
95                         if (offset != contentLength)
96                                 return AltosMapTile.failed;
97
98                 } catch (IOException e) {
99                         return AltosMapTile.failed;
100                 }
101
102                 try {
103                         FileOutputStream out = new FileOutputStream(file);
104                         out.write(data);
105                         out.flush();
106                         out.close();
107                 } catch (FileNotFoundException e) {
108                         return AltosMapTile.bad_request;
109                 } catch (IOException e) {
110                         if (file.exists())
111                                 file.delete();
112                         return AltosMapTile.bad_request;
113                 }
114                 return AltosMapTile.fetched;
115         }
116
117         static Object   fetch_lock = new Object();
118
119         static final long       forbidden_interval = 60l * 1000l * 1000l * 1000l;
120         static final long       google_maps_ratelimit_ms = 1200;
121
122         static Object   fetcher_lock = new Object();
123
124         static LinkedList<AltosMapStore> waiting = new LinkedList<AltosMapStore>();
125         static LinkedList<AltosMapStore> running = new LinkedList<AltosMapStore>();
126
127         static final int concurrent_fetchers = 128;
128
129         static void start_fetchers() {
130                 while (!waiting.isEmpty() && running.size() < concurrent_fetchers) {
131                         AltosMapStore   s = waiting.remove();
132                         running.add(s);
133                         Thread lt = s.make_fetcher_thread();
134                         lt.start();
135                 }
136         }
137
138         void finish_fetcher() {
139                 synchronized(fetcher_lock) {
140                         running.remove(this);
141                         start_fetchers();
142                 }
143         }
144
145         void add_fetcher() {
146                 synchronized(fetcher_lock) {
147                         waiting.add(this);
148                         start_fetchers();
149                 }
150         }
151
152         class fetcher implements Runnable {
153
154                 public void run() {
155                         try {
156                                 if (file.exists()) {
157                                         notify_listeners(AltosMapTile.fetched);
158                                         return;
159                                 }
160
161                                 synchronized(forbidden_lock) {
162                                         if (forbidden_set && (System.nanoTime() - forbidden_time) < forbidden_interval) {
163                                                 notify_listeners(AltosMapTile.forbidden);
164                                                 return;
165                                         }
166                                 }
167
168                                 int new_status;
169
170                                 if (!AltosVersion.has_google_maps_api_key()) {
171                                         synchronized (fetch_lock) {
172                                                 long startTime = System.nanoTime();
173                                                 new_status = fetch_url();
174                                                 if (new_status == AltosMapTile.fetched) {
175                                                         long duration_ms = (System.nanoTime() - startTime) / 1000000;
176                                                         if (duration_ms < google_maps_ratelimit_ms) {
177                                                                 try {
178                                                                         Thread.sleep(google_maps_ratelimit_ms - duration_ms);
179                                                                 } catch (InterruptedException e) {
180                                                                         Thread.currentThread().interrupt();
181                                                                 }
182                                                         }
183                                                 }
184                                         }
185                                 } else {
186                                         new_status = fetch_url();
187                                 }
188                                 notify_listeners(new_status);
189                         } finally {
190                                 finish_fetcher();
191                         }
192                 }
193         }
194
195         private Thread make_fetcher_thread() {
196                 return new Thread(new fetcher());
197         }
198
199         private void fetch() {
200                 add_fetcher();
201         }
202
203         private AltosMapStore (String url, File file) {
204                 this.url = url;
205                 this.file = file;
206
207                 if (file.exists())
208                         status = AltosMapTile.fetched;
209                 else {
210                         status = AltosMapTile.fetching;
211                         fetch();
212                 }
213         }
214
215         public int hashCode() {
216                 return url.hashCode();
217         }
218
219         public boolean equals(Object o) {
220                 if (o == null)
221                         return false;
222
223                 if (!(o instanceof AltosMapStore))
224                         return false;
225
226                 AltosMapStore other = (AltosMapStore) o;
227                 return url.equals(other.url);
228         }
229
230         static HashMap<String,AltosMapStore> stores = new HashMap<String,AltosMapStore>();
231
232         public static AltosMapStore get(String url, File file) {
233                 AltosMapStore   store;
234                 synchronized(stores) {
235                         if (stores.containsKey(url)) {
236                                 store = stores.get(url);
237                         } else {
238                                 store = new AltosMapStore(url, file);
239                                 stores.put(url, store);
240                         }
241                 }
242                 return store;
243         }
244 }