weblib: separate html code from lib code
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibraryServer.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.util.ArrayList;
7 import java.util.HashMap;
8 import java.util.LinkedList;
9 import java.util.List;
10 import java.util.Map;
11
12 import org.json.JSONArray;
13 import org.json.JSONObject;
14
15 import be.nikiroo.fanfix.Instance;
16 import be.nikiroo.fanfix.bundles.Config;
17 import be.nikiroo.fanfix.data.Chapter;
18 import be.nikiroo.fanfix.data.JsonIO;
19 import be.nikiroo.fanfix.data.MetaData;
20 import be.nikiroo.fanfix.data.Paragraph;
21 import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
22 import be.nikiroo.fanfix.data.Story;
23 import be.nikiroo.utils.Image;
24 import be.nikiroo.utils.LoginResult;
25 import be.nikiroo.utils.NanoHTTPD;
26 import be.nikiroo.utils.NanoHTTPD.Response;
27 import be.nikiroo.utils.NanoHTTPD.Response.Status;
28
29 public class WebLibraryServer extends WebLibraryServerHtml {
30 class WLoginResult extends LoginResult {
31 private boolean rw;
32 private boolean wl;
33 private boolean bl;
34
35 public WLoginResult(boolean badLogin, boolean badCookie) {
36 super(badLogin, badCookie);
37 }
38
39 public WLoginResult(String who, String key, String subkey, boolean rw,
40 boolean wl, boolean bl) {
41 super(who, key, subkey, (rw ? "|rw" : "") + (wl ? "|wl" : "")
42 + (bl ? "|bl" : "") + "|");
43 this.rw = rw;
44 this.wl = wl;
45 this.bl = bl;
46 }
47
48 public WLoginResult(String cookie, String who, String key,
49 List<String> subkeys) {
50 super(cookie, who, key, subkeys,
51 subkeys == null || subkeys.isEmpty());
52 }
53
54 public boolean isRw() {
55 return getOption().contains("|rw|");
56 }
57
58 public boolean isWl() {
59 return getOption().contains("|wl|");
60 }
61
62 public boolean isBl() {
63 return getOption().contains("|bl|");
64 }
65 }
66
67 private Map<String, Story> storyCache = new HashMap<String, Story>();
68 private LinkedList<String> storyCacheOrder = new LinkedList<String>();
69 private long storyCacheSize = 0;
70 private long maxStoryCacheSize;
71
72 private List<String> whitelist;
73 private List<String> blacklist;
74
75 public WebLibraryServer(boolean secure) throws IOException {
76 super(secure);
77
78 int cacheMb = Instance.getInstance().getConfig()
79 .getInteger(Config.SERVER_MAX_CACHE_MB, 100);
80 maxStoryCacheSize = cacheMb * 1024 * 1024;
81
82 setTraceHandler(Instance.getInstance().getTraceHandler());
83
84 whitelist = Instance.getInstance().getConfig()
85 .getList(Config.SERVER_WHITELIST, new ArrayList<String>());
86 blacklist = Instance.getInstance().getConfig()
87 .getList(Config.SERVER_BLACKLIST, new ArrayList<String>());
88 }
89
90 /**
91 * Start the server (listen on the network for new connections).
92 * <p>
93 * Can only be called once.
94 * <p>
95 * This call is asynchronous, and will just start a new {@link Thread} on
96 * itself (see {@link WebLibraryServer#run()}).
97 */
98 public void start() {
99 new Thread(this).start();
100 }
101
102 @Override
103 protected WLoginResult login(boolean badLogin, boolean badCookie) {
104 return new WLoginResult(false, false);
105 }
106
107 @Override
108 protected WLoginResult login(String who, String cookie) {
109 List<String> subkeys = Instance.getInstance().getConfig()
110 .getList(Config.SERVER_ALLOWED_SUBKEYS);
111 String realKey = Instance.getInstance().getConfig()
112 .getString(Config.SERVER_KEY);
113
114 return new WLoginResult(cookie, who, realKey, subkeys);
115 }
116
117 // allow rw/wl
118 @Override
119 protected WLoginResult login(String who, String key, String subkey) {
120 String realKey = Instance.getInstance().getConfig()
121 .getString(Config.SERVER_KEY, "");
122
123 // I don't like NULLs...
124 key = key == null ? "" : key;
125 subkey = subkey == null ? "" : subkey;
126
127 if (!realKey.equals(key)) {
128 return new WLoginResult(true, false);
129 }
130
131 // defaults are true (as previous versions without the feature)
132 boolean rw = true;
133 boolean wl = true;
134 boolean bl = true;
135
136 rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
137 rw);
138
139 List<String> allowed = Instance.getInstance().getConfig().getList(
140 Config.SERVER_ALLOWED_SUBKEYS, new ArrayList<String>());
141
142 if (!allowed.isEmpty()) {
143 if (!allowed.contains(subkey)) {
144 return new WLoginResult(true, false);
145 }
146
147 if ((subkey + "|").contains("|rw|")) {
148 rw = true;
149 }
150 if ((subkey + "|").contains("|wl|")) {
151 wl = false; // |wl| = bypass whitelist
152 }
153 if ((subkey + "|").contains("|bl|")) {
154 bl = false; // |bl| = bypass blacklist
155 }
156 }
157
158 return new WLoginResult(who, key, subkey, rw, wl, bl);
159 }
160
161 @Override
162 protected Response getList(String uri, WLoginResult login)
163 throws IOException {
164 if (WebLibraryUrls.LIST_URL_METADATA.equals(uri)) {
165 List<JSONObject> jsons = new ArrayList<JSONObject>();
166 for (MetaData meta : metas(login)) {
167 jsons.add(JsonIO.toJson(meta));
168 }
169
170 return newInputStreamResponse("application/json",
171 new ByteArrayInputStream(
172 new JSONArray(jsons).toString().getBytes()));
173 }
174
175 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
176 NanoHTTPD.MIME_PLAINTEXT, null);
177 }
178
179 // /story/luid/chapter/para <-- text/image
180 // /story/luid/cover <-- image
181 // /story/luid/metadata <-- json
182 // /story/luid/json <-- json, whole chapter (no images)
183 @Override
184 protected Response getStoryPart(String uri, WLoginResult login) {
185 String[] cover = uri.split("/");
186 int off = 2;
187
188 if (cover.length < off + 2) {
189 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
190 NanoHTTPD.MIME_PLAINTEXT, null);
191 }
192
193 String luid = cover[off + 0];
194 String chapterStr = cover[off + 1];
195 String imageStr = cover.length < off + 3 ? null : cover[off + 2];
196
197 // 1-based (0 = desc)
198 int chapter = 0;
199 if (chapterStr != null && !"cover".equals(chapterStr)
200 && !"metadata".equals(chapterStr)
201 && !"json".equals(chapterStr)) {
202 try {
203 chapter = Integer.parseInt(chapterStr);
204 if (chapter < 0) {
205 throw new NumberFormatException();
206 }
207 } catch (NumberFormatException e) {
208 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
209 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
210 }
211 }
212
213 // 1-based
214 int paragraph = 1;
215 if (imageStr != null) {
216 try {
217 paragraph = Integer.parseInt(imageStr);
218 if (paragraph < 0) {
219 throw new NumberFormatException();
220 }
221 } catch (NumberFormatException e) {
222 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
223 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
224 }
225 }
226
227 String mimeType = NanoHTTPD.MIME_PLAINTEXT;
228 InputStream in = null;
229 try {
230 if ("cover".equals(chapterStr)) {
231 Image img = cover(luid, login);
232 if (img != null) {
233 in = img.newInputStream();
234 }
235 // TODO: get correct image type
236 mimeType = "image/png";
237 } else if ("metadata".equals(chapterStr)) {
238 MetaData meta = meta(luid, login);
239 JSONObject json = JsonIO.toJson(meta);
240 mimeType = "application/json";
241 in = new ByteArrayInputStream(json.toString().getBytes());
242 } else if ("json".equals(chapterStr)) {
243 Story story = story(luid, login);
244 JSONObject json = JsonIO.toJson(story);
245 mimeType = "application/json";
246 in = new ByteArrayInputStream(json.toString().getBytes());
247 } else {
248 Story story = story(luid, login);
249 if (story != null) {
250 if (chapter == 0) {
251 StringBuilder builder = new StringBuilder();
252 for (Paragraph p : story.getMeta().getResume()) {
253 if (builder.length() == 0) {
254 builder.append("\n");
255 }
256 builder.append(p.getContent());
257 }
258
259 in = new ByteArrayInputStream(
260 builder.toString().getBytes("utf-8"));
261 } else {
262 Paragraph para = story.getChapters().get(chapter - 1)
263 .getParagraphs().get(paragraph - 1);
264 Image img = para.getContentImage();
265 if (para.getType() == ParagraphType.IMAGE) {
266 // TODO: get correct image type
267 mimeType = "image/png";
268 in = img.newInputStream();
269 } else {
270 in = new ByteArrayInputStream(
271 para.getContent().getBytes("utf-8"));
272 }
273 }
274 }
275 }
276 } catch (IndexOutOfBoundsException e) {
277 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
278 NanoHTTPD.MIME_PLAINTEXT,
279 "Chapter or paragraph does not exist");
280 } catch (IOException e) {
281 Instance.getInstance().getTraceHandler()
282 .error(new IOException("Cannot get image: " + uri, e));
283 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
284 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
285 }
286
287 return newInputStreamResponse(mimeType, in);
288 }
289
290 @Override
291 protected List<MetaData> metas(WLoginResult login) throws IOException {
292 BasicLibrary lib = Instance.getInstance().getLibrary();
293 List<MetaData> metas = new ArrayList<MetaData>();
294 for (MetaData meta : lib.getList().getMetas()) {
295 if (isAllowed(meta, login)) {
296 metas.add(meta);
297 }
298 }
299
300 return metas;
301 }
302
303 // NULL if not whitelist OK or if not found
304 @Override
305 protected Story story(String luid, WLoginResult login) throws IOException {
306 synchronized (storyCache) {
307 if (storyCache.containsKey(luid)) {
308 Story story = storyCache.get(luid);
309 if (!isAllowed(story.getMeta(), login))
310 return null;
311
312 return story;
313 }
314 }
315
316 Story story = null;
317 MetaData meta = meta(luid, login);
318 if (meta != null) {
319 BasicLibrary lib = Instance.getInstance().getLibrary();
320 story = lib.getStory(luid, null);
321 long size = sizeOf(story);
322
323 synchronized (storyCache) {
324 // Could have been added by another request
325 if (!storyCache.containsKey(luid)) {
326 while (!storyCacheOrder.isEmpty()
327 && storyCacheSize + size > maxStoryCacheSize) {
328 String oldestLuid = storyCacheOrder.removeFirst();
329 Story oldestStory = storyCache.remove(oldestLuid);
330 maxStoryCacheSize -= sizeOf(oldestStory);
331 }
332
333 storyCacheOrder.add(luid);
334 storyCache.put(luid, story);
335 }
336 }
337 }
338
339 return story;
340 }
341
342 private MetaData meta(String luid, WLoginResult login) throws IOException {
343 BasicLibrary lib = Instance.getInstance().getLibrary();
344 MetaData meta = lib.getInfo(luid);
345 if (!isAllowed(meta, login))
346 return null;
347
348 return meta;
349 }
350
351 private Image cover(String luid, WLoginResult login) throws IOException {
352 MetaData meta = meta(luid, login);
353 if (meta != null) {
354 BasicLibrary lib = Instance.getInstance().getLibrary();
355 return lib.getCover(meta.getLuid());
356 }
357
358 return null;
359 }
360
361 private boolean isAllowed(MetaData meta, WLoginResult login) {
362 if (login.isWl() && !whitelist.isEmpty()
363 && !whitelist.contains(meta.getSource())) {
364 return false;
365 }
366 if (login.isBl() && blacklist.contains(meta.getSource())) {
367 return false;
368 }
369
370 return true;
371 }
372
373 private long sizeOf(Story story) {
374 long size = 0;
375 for (Chapter chap : story) {
376 for (Paragraph para : chap) {
377 if (para.getType() == ParagraphType.IMAGE) {
378 size += para.getContentImage().getSize();
379 } else {
380 size += para.getContent().length();
381 }
382 }
383 }
384
385 return size;
386 }
387
388 public static void main(String[] args) throws IOException {
389 Instance.init();
390 WebLibraryServer web = new WebLibraryServer(false);
391 web.run();
392 }
393 }