Java tutorial
/* Copyright 2011 Anton S. Kraievoy akraievoy@gmail.com This file is part of org.akraievoy:couch. org.akraievoy:couch is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. org.akraievoy:couch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with org.akraievoy:couch. If not, see <http://www.gnu.org/licenses/>. */ package org.akraievoy.couch; import com.google.common.base.Charsets; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.io.*; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.SerializationConfig; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.*; import java.util.concurrent.TimeUnit; /** * CouchDB entity persistence. * * Please note that for optimal performance you have to override * default CouchDB configuration. * * <pre><code>[httpd] * socket_options = [{nodelay, true}]</code></pre> * * http://stackoverflow.com/questions/8992122/couchdb-mochiweb-negative-effect-of-persistent-connections */ @SuppressWarnings("UnusedDeclaration") public class CouchDao { private final ObjectMapper mapper = new ObjectMapper(); { mapper.getSerializationConfig().enable(SerializationConfig.Feature.INDENT_OUTPUT); } private final CouchSetup couchSetup; protected String dbName = "elw-data"; public void setDbName(String dbName) { this.dbName = dbName; } protected int concurrencyLevel = 3; public void setConcurrencyLevel(int concurrencyLevel) { this.concurrencyLevel = concurrencyLevel; } protected long cacheValidityMinutes = 1; public void setCacheValidityMinutes(long cacheValidityMinutes) { this.cacheValidityMinutes = cacheValidityMinutes; } private Cache<Squab.Path, List<Squab.Path>> cachePaths; private Cache<Squab.Path, List<List<String>>> cacheAxes; private Cache<Squab.Path, List<? extends Squab>> cacheSquabs; private Cache<Squab.Path, SortedMap<Long, ? extends Squab.Stamped>> cacheStamped; private static final SortedMap<Long, ? extends Squab.Stamped> EMPTY_MAP = Collections .unmodifiableSortedMap(new TreeMap<Long, Squab.Stamped>()); public CouchDao(final CouchSetup couchSetup) { this.couchSetup = couchSetup; } public void invalidateCaches() { cachePaths.invalidateAll(); cachePaths.cleanUp(); cacheAxes.invalidateAll(); cacheAxes.cleanUp(); cacheSquabs.invalidateAll(); cacheSquabs.cleanUp(); cacheStamped.invalidateAll(); cacheStamped.cleanUp(); } public void start() { final CacheBuilder<Object, Object> caches = CacheBuilder.newBuilder().concurrencyLevel(concurrencyLevel) .expireAfterWrite(cacheValidityMinutes, TimeUnit.MINUTES); cachePaths = caches.build(); cacheAxes = caches.build(); cacheSquabs = caches.build(); cacheStamped = caches.build(); } protected void invalidate(final Squab.Path path) { invalidate_(path, cacheAxes.asMap().keySet()); invalidate_(path, cachePaths.asMap().keySet()); invalidate_(path, cacheSquabs.asMap().keySet()); invalidate_(path, cacheStamped.asMap().keySet()); } protected void invalidate_(final Squab.Path path, final Set<Squab.Path> paths) { for (Iterator<Squab.Path> iterator = paths.iterator(); iterator.hasNext();) { if (iterator.next().intersects(path)) { iterator.remove(); } } } // Squab public static class UpdateStatus { public final String rev; public final Long stamp; public UpdateStatus(String rev, Long stamp) { this.rev = rev; this.stamp = stamp; } } public UpdateStatus createOrUpdate(final Squab squab) { return createOrUpdate(squab, true); } public UpdateStatus createOrUpdate(final Squab squab, final boolean updateStamp) { try { Long stamp = null; if (squab instanceof Squab.Stamped) { final Squab.Stamped stamped = (Squab.Stamped) squab; if (!updateStamp && stamped.getStamp() != null) { stamp = stamped.getStamp(); } else { stamp = stamped.updateStamp(); } } final String couchRev = couchPut(squab.getCouchPath().id(), squab); return new UpdateStatus(couchRev, stamp); } finally { invalidate(squab.getCouchPath()); } } @Deprecated public Long update(final Squab squab) { return update(squab, true); } public Long update(final Squab squab, final boolean updateStamp) { Long stamp = null; if (squab instanceof Squab.Stamped) { final Squab.Stamped stamped = (Squab.Stamped) squab; if (!updateStamp && stamped.getStamp() != null) { stamp = stamped.getStamp(); } else { stamp = stamped.updateStamp(); } } couchPut(squab.getCouchPath().id(), squab); // LATER we should do these operations in finally block invalidate(squab.getCouchPath()); return stamp; } /** * Send attachments out of band of the main document update/create request * http://wiki.apache.org/couchdb/HTTP_Document_API#Standalone_Attachments * More efficient than encoding byte stream within document JSON. * * @param squab to attach the stream to (only id/rev fields are used) * @param fileName attachment fileName * @param contentType content type of the attachment * @param streamBytes containing the attachment bytes * * @return new revision of the main document */ public String attachStream(final Squab squab, final String fileName, final String contentType, final InputSupplier<InputStream> streamBytes) { final Squab.Path couchPath = squab.getCouchPath(); try { final String newCouchRev = couchFilePut(couchPath.id(), squab.getCouchRev(), fileName, contentType, streamBytes); return newCouchRev; } finally { invalidate(couchPath); } } public <S extends Squab> S findOne(final Class<S> squabClass, final String... path) { final List<S> all = findAll(squabClass, path); if (all.isEmpty()) { throw new IllegalStateException( "no records: " + squabClass.getSimpleName() + " '" + Arrays.toString(path) + "'"); } return all.get(0); } protected <S extends Squab> S findOne(final Class<S> squabClass, final Squab.Path fullPath) { final List<S> all = findAll(squabClass, fullPath); if (all.isEmpty()) { throw new IllegalStateException("no records: '" + fullPath + "'"); } return all.get(0); } public InputSupplier<InputStream> file(final Squab squab, final String fileName) { return couchFileGet(squab.getCouchPath(), fileName); } public byte[] fileBytes(final Squab squab, final String fileName) throws IOException { return ByteStreams.toByteArray(couchFileGet(squab.getCouchPath(), fileName)); } public List<String> fileLines(final Squab squab, final String fileName) throws IOException { return CharStreams.readLines( CharStreams.newReaderSupplier(couchFileGet(squab.getCouchPath(), fileName), Charsets.UTF_8)); } public String fileText(final Squab squab, final String fileName) throws IOException { return CharStreams.toString( CharStreams.newReaderSupplier(couchFileGet(squab.getCouchPath(), fileName), Charsets.UTF_8)); } public <S extends Squab> S findSome(final Class<S> squabClass, final String... path) { final List<S> all = findAll(squabClass, path); return all.isEmpty() ? null : all.get(0); } public <S extends Squab> List<S> findAll(final Class<S> squabClass, final String... path) { final Squab.Path fullPath = new Squab.Path(squabClass, path); return findAll(squabClass, fullPath); } protected <S extends Squab> List<S> findAll(final Class<S> squabClass, final Squab.Path fullPath) { final List<? extends Squab> allCached = cacheSquabs.getIfPresent(fullPath); if (allCached != null) { //noinspection unchecked return (List<S>) allCached; } final List<Squab.Path> allPaths = findAllPaths(fullPath); final ArrayList<S> all = new ArrayList<S>(); for (Squab.Path aPath : allPaths) { all.add(couchGet(aPath, squabClass)); } final List<S> allRO = Collections.unmodifiableList(all); cacheSquabs.put(fullPath, allRO); return allRO; } public <S extends Squab> List<List<String>> axes(final Class<S> squabClass, final String... path) { return axes(new Squab.Path(squabClass, path)); } /** * Computes possible criteria positions, enumerating undefined ones. * * @param fullPath criteria to inspect * @return all possible values for each criteria path position */ public List<List<String>> axes(final Squab.Path fullPath) { final List<List<String>> cachedAxes = cacheAxes.getIfPresent(fullPath); if (cachedAxes != null) { return cachedAxes; } final List<Squab.Path> paths = findAllPaths(fullPath); final SortedMap<Integer, TreeSet<String>> resultMap = new TreeMap<Integer, TreeSet<String>>(); for (Squab.Path matchPath : paths) { for (int i = 0; i < matchPath.len(); i++) { final TreeSet<String> axis = resultMap.get(i); if (axis == null) { TreeSet<String> newAxis = new TreeSet<String>(); newAxis.add(matchPath.elem(i)); resultMap.put(i, newAxis); } else { axis.add(matchPath.elem(i)); } } } final List<List<String>> axes = new ArrayList<List<String>>(resultMap.size()); for (int i = 0; i < resultMap.size(); i++) { axes.add(Collections.unmodifiableList(new ArrayList<String>(resultMap.get(i)))); } final List<List<String>> axesRO = Collections.unmodifiableList(axes); cacheAxes.put(fullPath, axesRO); return axesRO; } public <S extends Squab> List<Squab.Path> findAllPaths(final Class<S> squabClass, final String... path) { return findAllPaths(new Squab.Path(squabClass, path)); } public List<Squab.Path> findAllPaths(final Squab.Path fullPath) { final List<Squab.Path> cachedIds = cachePaths.getIfPresent(fullPath); if (cachedIds != null) { return cachedIds; } final Squab.RespViewList viewList = couchList(fullPath); final List<Squab.Path> paths = new ArrayList<Squab.Path>(viewList.getRows().size()); for (Squab.RespViewList.Row row : viewList.getRows()) { final Squab.Path rowPath = Squab.Path.fromId(row.getId()); if (rowPath.len() < fullPath.len()) { continue; } boolean afterWild = false; boolean matches = true; for (int i = 0; i < fullPath.len(); i++) { // path may have some wilds in the middle, so // we have to filter even after couch range-query afterWild |= i > 0 && fullPath.elem(i - 1) == null; if (afterWild && i < fullPath.len() && fullPath.elem(i) != null && !fullPath.elem(i).equals(rowPath.elem(i))) { matches = false; break; } } if (matches) { paths.add(rowPath); } } final List<Squab.Path> pathsRO = Collections.unmodifiableList(paths); cachePaths.put(fullPath, pathsRO); return pathsRO; } // Squab.Stamped public <S extends Squab.Stamped> S findLast(final Class<S> squabClass, final String... path) { final Squab.Path fullPath = new Squab.Path(squabClass, path); final List<Squab.Path> allPaths = findAllPaths(fullPath); Squab.Path lastPath = null; long lastStamp = 0; for (Squab.Path somePath : allPaths) { final long someStamp = Squab.Stamped.parse(somePath.getLast()); if (lastPath == null || someStamp >= lastStamp) { lastPath = somePath; lastStamp = someStamp; } } return lastPath == null ? null : findOne(squabClass, lastPath); } public <S extends Squab.Stamped> S findByStamp(final long stamp, final Class<S> squabClass, final String... path) { final Squab.Path fullPath = new Squab.Path(squabClass, path); final List<Squab.Path> allPaths = findAllPaths(fullPath); Squab.Path stampPath = null; for (Squab.Path somePath : allPaths) { final long someStamp = Squab.Stamped.parse(somePath.getLast()); if (someStamp == stamp) { stampPath = somePath; break; } } return stampPath == null ? null : findOne(squabClass, fullPath); } public <S extends Squab.Stamped> SortedMap<Long, S> findAllStamped(final Long sinceTime, final Long untilTime, final Class<S> squabClass, final String... path) { final SortedMap<Long, S> stampToSquab = findAllStamped(squabClass, path); if (sinceTime != null && untilTime != null) { return stampToSquab.subMap(sinceTime, untilTime); } if (sinceTime != null) { return stampToSquab.tailMap(sinceTime); } if (untilTime != null) { return stampToSquab.headMap(untilTime); } return stampToSquab; } public <S extends Squab.Stamped> SortedMap<Long, S> findAllStamped(final Class<S> squabClass, final String... path) { final Squab.Path fullPath = new Squab.Path(squabClass, path); final SortedMap<Long, ? extends Squab.Stamped> stampedCached = cacheStamped.getIfPresent(fullPath); if (stampedCached != null) { //noinspection unchecked return (SortedMap<Long, S>) stampedCached; } final List<S> squabs = findAll(squabClass, path); final SortedMap<Long, S> timeToSquab; if (squabs.isEmpty()) { //noinspection unchecked timeToSquab = (SortedMap<Long, S>) EMPTY_MAP; } else { timeToSquab = byStamp(squabs); } final SortedMap<Long, S> timeToSquabRO = Collections.unmodifiableSortedMap(timeToSquab); cacheStamped.put(fullPath, timeToSquabRO); return timeToSquabRO; } protected static <S extends Squab.Stamped> SortedMap<Long, S> byStamp(final Collection<S> squabs) { final SortedMap<Long, S> result = new TreeMap<Long, S>(); for (S squab : squabs) { result.put(squab.getStamp(), squab); } return result; } // Couch HTTP protected Squab.RespViewList couchList(Squab.Path path) { HttpURLConnection connection = null; try { URL url = new URL(couchSetup.getCouchDbUrl() + dbNameDynamic() + "/" + "_all_docs/?" + "startkey=\"" + url(path.rangeMin()) + "\"&" + "endkey=\"" + url(path.rangeMax()) + "\""); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setUseCaches(false); connection.setDoInput(true); connection.setDoOutput(false); final InputStream is = connection.getInputStream(); final Squab.RespViewList list = mapper.readValue(is, Squab.RespViewList.class); is.close(); return list; } catch (IOException e) { throw new IllegalStateException("list failed", e); } finally { if (connection != null) { connection.disconnect(); } } } protected String dbNameDynamic() { final Map<String, String> dbNames = couchSetup.getCouchDbNames(); if (dbNames != null && dbNames.containsKey(dbName)) { final String dbNameOverride = dbNames.get(dbName); if (dbNameOverride != null && dbNameOverride.trim().length() > 0) { return dbNameOverride; } } return dbName; } private static String url(final String urlElem) { try { return URLEncoder.encode(urlElem, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("UTF-8 not supported?!"); } } protected <S extends Squab> S couchGet(final Squab.Path path, final Class<S> squabClass) { HttpURLConnection connection = null; try { URL url = new URL(couchSetup.getCouchDbUrl() + dbNameDynamic() + "/" + url(path.id())); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setUseCaches(true); connection.setDoInput(true); connection.setDoOutput(false); final BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream(), Charsets.UTF_8)); final S squab = mapper.readValue(reader, squabClass); reader.close(); return squab; } catch (IOException e) { throw new IllegalStateException("GET failed", e); } finally { if (connection != null) { connection.disconnect(); } } } public InputSupplier<InputStream> couchFileGet(final Squab.Path path, final String fileName) { try { final URL url = new URL( couchSetup.getCouchDbUrl() + dbNameDynamic() + "/" + url(path.id()) + "/" + url(fileName)); return new InputSupplier<InputStream>() { public InputStream getInput() throws IOException { final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setUseCaches(true); connection.setDoInput(true); connection.setDoOutput(false); return new FilterInputStream(connection.getInputStream()) { @Override public void close() throws IOException { connection.disconnect(); super.close(); } }; } }; } catch (IOException e) { throw new IllegalStateException("failed to construct URL", e); } } protected String couchPut(final String couchId, final Squab squab) { HttpURLConnection connection = null; try { final String squabJSON = mapper.writeValueAsString(squab); URL url = new URL(couchSetup.getCouchDbUrl() + dbNameDynamic() + "/" + url(couchId)); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("PUT"); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Content-Length", Integer.toString(squabJSON.getBytes().length)); storeAuth(connection); connection.setUseCaches(false); connection.setDoInput(true); connection.setDoOutput(true); BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream()); bos.write(squabJSON.getBytes(Charsets.UTF_8)); bos.flush(); bos.close(); return expectCouchOkResponse(connection); } catch (IOException e) { throw new IllegalStateException("PUT failed", e); } finally { if (connection != null) { connection.disconnect(); } } } /** * Send attachments out of band of the main document update * http://wiki.apache.org/couchdb/HTTP_Document_API#Standalone_Attachments * * @param couchId id of the main document * @param couchRev revision of the main document * @param fileName attachment fileName * @param contentType content type of the attachment * @param dataStream containing the attachment bytes * @return new revision of the main document */ protected String couchFilePut(final String couchId, final String couchRev, final String fileName, final String contentType, final InputSupplier<InputStream> dataStream) { HttpURLConnection connection = null; try { //Create connection URL url = new URL(couchSetup.getCouchDbUrl() + dbNameDynamic() + "/" + url(couchId) + "/" + url(fileName) + "?rev=" + url(couchRev)); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("PUT"); connection.setRequestProperty("Content-Type", contentType); storeAuth(connection); connection.setUseCaches(false); connection.setDoInput(true); connection.setDoOutput(true); ByteStreams.copy(dataStream, connOutput(connection)); return expectCouchOkResponse(connection); } catch (IOException e) { throw new IllegalStateException("PUT failed", e); } finally { if (connection != null) { connection.disconnect(); } } } protected String expectCouchOkResponse(final HttpURLConnection connection) throws IOException { InputStream is = null; try { is = connection.getInputStream(); final Squab.RespUpdate update = mapper.readValue(is, Squab.RespUpdate.class); if (!update.isOk()) { throw new IllegalStateException("PUT failed:" + update); } return update.getRev(); } finally { Closeables.closeQuietly(is); } } protected static OutputSupplier<OutputStream> connOutput(final HttpURLConnection connection) { return new OutputSupplier<OutputStream>() { public OutputStream getOutput() throws IOException { return new BufferedOutputStream(connection.getOutputStream()); } }; } protected void storeAuth(final HttpURLConnection connection) throws IOException { // use Base64 codec bundled with Jackson: bytes -> "base64" final String authStr = couchSetup.getCouchDbUser() + ":" + couchSetup.getCouchDbPassword(); final String base64Str = mapper.writeValueAsString(authStr.getBytes()); connection.addRequestProperty("Authorization", // drop first and last chars as those are JSON quotas "Basic " + base64Str.substring(1, base64Str.length() - 1)); } }