| /* |
| * Copyright (C) 2020 The Baremaps Authors |
| * |
| * 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 com.baremaps.server.editor; |
| |
| import static com.baremaps.server.ogcapi.Conversions.asPostgresQuery; |
| import static com.google.common.net.HttpHeaders.CONTENT_ENCODING; |
| import static com.google.common.net.HttpHeaders.CONTENT_TYPE; |
| import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; |
| |
| import com.baremaps.blob.Blob; |
| import com.baremaps.blob.BlobStore; |
| import com.baremaps.blob.BlobStoreException; |
| import com.baremaps.model.MbStyle; |
| import com.baremaps.model.TileJSON; |
| import com.baremaps.tile.Tile; |
| import com.baremaps.tile.TileStore; |
| import com.baremaps.tile.postgres.PostgresQuery; |
| import com.baremaps.tile.postgres.PostgresTileStore; |
| import com.fasterxml.jackson.core.JsonProcessingException; |
| import com.fasterxml.jackson.databind.JsonNode; |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import com.fasterxml.jackson.databind.node.ObjectNode; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URI; |
| import java.nio.file.FileSystems; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.WatchEvent; |
| import java.nio.file.WatchKey; |
| import java.nio.file.WatchService; |
| import java.util.List; |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| import javax.sql.DataSource; |
| import javax.ws.rs.Consumes; |
| import javax.ws.rs.GET; |
| import javax.ws.rs.PUT; |
| import javax.ws.rs.PathParam; |
| import javax.ws.rs.Produces; |
| import javax.ws.rs.core.Context; |
| import javax.ws.rs.core.MediaType; |
| import javax.ws.rs.core.Response; |
| import javax.ws.rs.sse.OutboundSseEvent; |
| import javax.ws.rs.sse.Sse; |
| import javax.ws.rs.sse.SseBroadcaster; |
| import javax.ws.rs.sse.SseEventSink; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @javax.ws.rs.Path("/") |
| public class EditorResources { |
| |
| private static final Logger logger = LoggerFactory.getLogger(EditorResources.class); |
| |
| private final URI style; |
| |
| private final URI tileset; |
| |
| private final BlobStore blobStore; |
| |
| private final DataSource dataSource; |
| |
| private final ObjectMapper objectMapper; |
| |
| private final SseBroadcaster sseBroadcaster; |
| |
| private final Thread fileWatcher; |
| |
| @Inject |
| public EditorResources( |
| @Named("tileset") URI tileset, |
| @Named("style") URI style, |
| BlobStore blobStore, |
| DataSource dataSource, |
| ObjectMapper objectMapper, |
| Sse sse) { |
| this.tileset = tileset; |
| this.style = style; |
| this.blobStore = blobStore; |
| this.dataSource = dataSource; |
| this.objectMapper = objectMapper; |
| |
| // Observe the file system for changes |
| OutboundSseEvent.Builder sseEventBuilder = sse.newEventBuilder(); |
| this.sseBroadcaster = sse.newBroadcaster(); |
| fileWatcher = |
| new Thread( |
| () -> { |
| try { |
| Path tilesetFile = Paths.get(tileset.getPath()).toAbsolutePath(); |
| Path styleFile = Paths.get(style.getPath()).toAbsolutePath(); |
| WatchService watchService = FileSystems.getDefault().newWatchService(); |
| tilesetFile.getParent().register(watchService, ENTRY_MODIFY); |
| styleFile.getParent().register(watchService, ENTRY_MODIFY); |
| WatchKey key; |
| while ((key = watchService.take()) != null) { |
| Path dir = (Path) key.watchable(); |
| for (WatchEvent<?> event : key.pollEvents()) { |
| Path path = dir.resolve((Path) event.context()); |
| try (InputStream inputStream = blobStore.get(style).getInputStream()) { |
| ObjectNode jsonNode = objectMapper.readValue(inputStream, ObjectNode.class); |
| jsonNode.put("reload", path.endsWith(tilesetFile.getFileName())); |
| sseBroadcaster.broadcast(sseEventBuilder.data(jsonNode.toString()).build()); |
| } |
| } |
| key.reset(); |
| } |
| } catch (InterruptedException e) { |
| logger.error(e.getMessage()); |
| Thread.currentThread().interrupt(); |
| } catch (BlobStoreException | IOException e) { |
| logger.error(e.getMessage()); |
| } |
| }); |
| fileWatcher.start(); |
| } |
| |
| @GET |
| @javax.ws.rs.Path("changes") |
| @Produces("text/event-stream") |
| public void changes(@Context SseEventSink sseEventSink) { |
| sseBroadcaster.register(sseEventSink); |
| } |
| |
| @PUT |
| @Consumes(MediaType.APPLICATION_JSON) |
| @javax.ws.rs.Path("style.json") |
| public void putStyle(MbStyle json) throws JsonProcessingException, BlobStoreException { |
| byte[] value = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(json); |
| blobStore.put(style, Blob.builder().withByteArray(value).build()); |
| } |
| |
| @PUT |
| @javax.ws.rs.Path("tiles.json") |
| public void putTiles(JsonNode json) throws JsonProcessingException, BlobStoreException { |
| byte[] value = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(json); |
| blobStore.put(tileset, Blob.builder().withByteArray(value).build()); |
| } |
| |
| @GET |
| @javax.ws.rs.Path("style.json") |
| @Produces(MediaType.APPLICATION_JSON) |
| public MbStyle getStyle() throws BlobStoreException, IOException { |
| try (InputStream inputStream = blobStore.get(style).getInputStream()) { |
| return objectMapper.readValue(inputStream, MbStyle.class); |
| } |
| } |
| |
| @GET |
| @javax.ws.rs.Path("tiles.json") |
| @Produces(MediaType.APPLICATION_JSON) |
| public TileJSON getTileset() throws BlobStoreException, IOException { |
| try (InputStream inputStream = blobStore.get(tileset).getInputStream()) { |
| return objectMapper.readValue(inputStream, TileJSON.class); |
| } |
| } |
| |
| @GET |
| @javax.ws.rs.Path("/tiles/{z}/{x}/{y}.mvt") |
| public Response getTile(@PathParam("z") int z, @PathParam("x") int x, @PathParam("y") int y) { |
| try { |
| List<PostgresQuery> queries = asPostgresQuery(getTileset()); |
| TileStore tileStore = new PostgresTileStore(dataSource, queries); |
| Tile tile = new Tile(x, y, z); |
| Blob blob = tileStore.read(tile); |
| if (blob != null) { |
| return Response.status(200) |
| .header(CONTENT_TYPE, blob.getContentType()) |
| .header(CONTENT_ENCODING, blob.getContentEncoding()) |
| .entity(blob.getInputStream()) |
| .build(); |
| } else { |
| return Response.status(204).build(); |
| } |
| } catch (Exception ex) { |
| logger.error("Tile error", ex); |
| return Response.status(404).build(); |
| } |
| } |
| |
| @GET |
| @javax.ws.rs.Path("{path:.*}") |
| public Response get(@PathParam("path") String path) throws IOException { |
| if (path.equals("") || path.endsWith("/")) { |
| path += "index.html"; |
| } |
| path = String.format("maputnik/%s", path); |
| try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path)) { |
| var bytes = inputStream.readAllBytes(); |
| return Response.ok().entity(bytes).build(); |
| } catch (IOException e) { |
| return Response.status(404).build(); |
| } |
| } |
| } |