Java tutorial
/* * Copyright 2013 David Tinker * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.qdb.server.controller; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import io.qdb.server.databind.DataBinder; import io.qdb.server.databind.DurationParser; import io.qdb.server.databind.HasAnySetter; import io.qdb.server.input.InputHandler; import io.qdb.server.input.InputHandlerFactory; import io.qdb.server.input.InputStatusMonitor; import io.qdb.server.model.Input; import io.qdb.server.model.Queue; import io.qdb.server.monitor.Status; import io.qdb.server.repo.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; import java.util.*; import java.util.regex.Pattern; @Singleton public class InputController extends CrudController { private final Repository repo; private final InputHandlerFactory handlerFactory; private final InputStatusMonitor inputStatusMonitor; private static final Logger log = LoggerFactory.getLogger(InputController.class); public static class InputDTO implements Comparable<InputDTO>, HasAnySetter { public String id; public Integer version; public String type; public String url; public Boolean enabled; public Integer updateIntervalMs; public Object warnAfter; public Object errorAfter; public transient Map<String, Object> params; public String status; public Long lastMessageId; public Date lastMessageTimestamp; public Object lastMessageAppended; @SuppressWarnings("UnusedDeclaration") public InputDTO() { } public InputDTO(String id, Input in, boolean borg) { this.id = id; version = in.getVersion(); type = in.getType(); url = in.getUrl(); enabled = in.isEnabled(); updateIntervalMs = in.getUpdateIntervalMs(); params = in.getParams(); lastMessageId = null0(in.getLastMessageId()); lastMessageTimestamp = toDate(in.getLastMessageTimestamp()); if (borg) { this.warnAfter = null0(in.getWarnAfter()); this.errorAfter = null0(in.getErrorAfter()); } else { int secs = in.getWarnAfter(); if (secs > 0) this.warnAfter = DurationParser.formatHumanMs(secs * 1000L); secs = in.getErrorAfter(); if (secs > 0) this.errorAfter = DurationParser.formatHumanMs(secs * 1000L); } } private Date toDate(long ms) { return ms == 0 ? null : new Date(ms); } private Integer null0(int x) { return x == 0 ? null : x; } private Long null0(long x) { return x == 0 ? null : x; } @JsonAnySetter public void set(String key, Object value) { if (params == null) params = new HashMap<String, Object>(); if (value == null) params.remove(key); else params.put(key, value); } @JsonAnyGetter public Map<String, Object> getParams() { return params; } @Override public int compareTo(InputDTO in) { return id.compareTo(in.id); } } private static final Pattern VALID_INPUT_ID = Pattern.compile("[0-9a-z\\-_]+", Pattern.CASE_INSENSITIVE); @Inject public InputController(JsonService jsonService, Repository repo, InputHandlerFactory handlerFactory, InputStatusMonitor inputStatusMonitor) { super(jsonService); this.repo = repo; this.handlerFactory = handlerFactory; this.inputStatusMonitor = inputStatusMonitor; } @SuppressWarnings("unchecked") @Override protected void list(Call call, int offset, int limit) throws IOException { List<InputDTO> ans = new ArrayList<InputDTO>(); Queue q = call.getQueue(); Map<String, String> inputs = q.getInputs(); if (inputs != null) { for (Map.Entry<String, String> e : inputs.entrySet()) { Input in = repo.findInput(e.getValue()); if (in != null) ans.add(createInputDTO(call, e.getKey(), in)); } Collections.sort(ans); int last = Math.min(offset + limit, ans.size()); if (offset > 0 || last < ans.size()) { if (offset >= ans.size()) ans = Collections.EMPTY_LIST; else ans = ans.subList(offset, last); } } call.setJson(ans); } @Override protected void count(Call call) throws IOException { Map<String, String> inputs = call.getQueue().getInputs(); call.setJson(new Count(inputs == null ? 0 : inputs.size())); } @Override protected void show(Call call, String id) throws IOException { Queue q = call.getQueue(); Map<String, String> inputs = q.getInputs(); if (inputs != null) { String inputId = inputs.get(id); if (inputId != null) { Input in = repo.findInput(inputId); if (in != null) { call.setJson(createInputDTO(call, id, in)); return; } } } call.setCode(404); } private InputDTO createInputDTO(Call call, String id, Input in) throws IOException { boolean borg = call.getBoolean("borg"); InputDTO dto = new InputDTO(id, in, borg); if (dto.lastMessageTimestamp != null) { long ms = System.currentTimeMillis() - dto.lastMessageTimestamp.getTime(); dto.lastMessageAppended = borg ? ms : DurationParser.formatHumanMs(ms) + " ago"; } Status status = inputStatusMonitor.getStatus(in); if (status != null) dto.status = status.toString(); return dto; } @Override protected void createOrUpdate(Call call, String id) throws IOException { InputDTO dto = getBodyObject(call, InputDTO.class); boolean create; Input in; Queue q; synchronized (repo) { // re-lookup queue inside sync block in case we need to update it q = repo.findQueue(call.getQueue().getId()); if (q == null) { // this isn't likely but isn't impossible either call.setCode(404); return; } String inputId = q.getInputIdForInput(id); if (create = inputId == null) { if (call.isPut()) { call.setCode(404); return; } if (!VALID_INPUT_ID.matcher(id).matches()) { call.setCode(400, "Input id must contain only letters, numbers, hyphens and underscores"); return; } if (dto.type == null) { call.setCode(400, "type is required"); return; } in = new Input(); in.setQueue(q.getId()); in.setEnabled(true); in.setUpdateIntervalMs(1000); } else { in = repo.findInput(inputId); if (in == null) { // this shouldn't happen String msg = "Input /db/" + q.getDatabase() + "/q/" + q.getId() + "/in/" + id + " inputId [" + inputId + "] not found"; log.error(msg); call.setCode(500, msg); return; } if (dto.version != null && !dto.version.equals(in.getVersion())) { call.setCode(409, createInputDTO(call, id, in)); return; } in = in.deepCopy(); } boolean changed = create; if (dto.type != null && !dto.type.equals(in.getType())) { try { handlerFactory.createHandler(dto.type); } catch (IllegalArgumentException e) { call.setCode(400, e.getMessage()); return; } in.setType(dto.type); changed = true; } if (dto.url != null && !dto.url.equals(in.getUrl())) { in.setUrl(dto.url); changed = true; } if (dto.enabled != null && dto.enabled != in.isEnabled()) { in.setEnabled(dto.enabled); changed = true; } if (dto.updateIntervalMs != null && dto.updateIntervalMs != in.getUpdateIntervalMs()) { in.setUpdateIntervalMs(dto.updateIntervalMs); changed = true; } if (dto.warnAfter != null) { try { int secs = convertDuration(dto.warnAfter); if (secs != in.getWarnAfter()) { in.setWarnAfter(secs); changed = true; } } catch (IllegalArgumentException e) { call.setCode(422, "Invalid warnAfter value, expected duration"); return; } } if (dto.errorAfter != null) { try { int secs = convertDuration(dto.errorAfter); if (secs != in.getErrorAfter()) { in.setErrorAfter(secs); changed = true; } } catch (IllegalArgumentException e) { call.setCode(422, "Invalid errorAfter value, expected duration"); return; } } if (dto.params != null) { InputHandler h = handlerFactory.createHandler(in.getType()); new DataBinder(jsonService).updateMap(true).bind(dto.params, h).check(); Map<String, Object> params = in.getParams(); if (params == null) { in.setParams(dto.params); changed = true; } else { for (Map.Entry<String, Object> e : dto.params.entrySet()) { String key = e.getKey(); Object v = e.getValue(); Object existing = params.get(key); if (!v.equals(existing)) { params.put(key, v); changed = true; } } } } if (create) { for (int attempt = 0;;) { in.setId(generateId()); if (repo.findInput(in.getId()) == null) break; if (++attempt == 20) throw new IOException("Got " + attempt + " dup id's attempting to create input?"); } } if (create) { q = q.deepCopy(); // make a copy before we modify it Map<String, String> inputs = q.getInputs(); if (inputs == null) q.setInputs(inputs = new HashMap<String, String>()); inputs.put(id, in.getId()); repo.updateQueue(q); } if (changed) repo.updateInput(in); } call.setCode(create ? 201 : 200, createInputDTO(call, id, in)); } @Override protected void delete(Call call, String id) throws IOException { String inputId = call.getQueue().getInputIdForInput(id); if (inputId == null) { call.setCode(404); return; } repo.deleteInput(inputId); } }