package ru.dreamteam.couch;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.FilterReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.BasicHttpEntity;
import ru.dreamteam.couch.query.Query;
import static org.apache.http.HttpStatus.*;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Control updates for design documents
* @author dooman
*/
class DbManager {
public final Logger log = Logger.getLogger(DbManager.class.getName());
private static class SkipNewLinesFilterReader extends FilterReader {
private boolean stringInProgress;
/**
* Creates a new filtered reader.
*
* @param in a reader object providing the underlying stream
* @throws NullPointerException if <code>in</code> is <code>null</code>
*/
public SkipNewLinesFilterReader(Reader in) {
super(in);
}
@Override
public int read() throws IOException {
int cChar = in.read();
if (cChar == '\"') {
stringInProgress = !stringInProgress;
}
return ((cChar == '\n' || cChar == '\r') && stringInProgress) ? ' ' : cChar;
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
for(int i=0; i < len; i++)
cbuf[off + i] = (char)read();
return len;
}
}
private String dbName;
private Couch dbInstance;
private ObjectMapper mapper;
public DbManager(String dbName, Couch dbInstance, ObjectMapper mapper) {
super();
this.dbName = dbName;
this.dbInstance = dbInstance;
this.mapper = mapper;
}
/**
* <p>
* Update design documents from files /views/[DBNAME]/*.json.
* Each JSON file is one design document which will be called as filename.
* Design document will be updated only if content of JSON file was updated.
* </p>
* <p>
* Also, this method remove all design documents which listed in file /views/[DBNAME]/views.delete.
* Each design document for remove should be starts on new line
* </p>
* @return Number of created/updated/deleted design documents.
*/
public int updateViews() {
int updated = 0;
URL viewsUrl = getClass().getResource("/views/" + dbName + "/");
if (viewsUrl != null) {
File viewsDir = new File(viewsUrl.getFile());
if (viewsDir.isDirectory() && viewsDir.exists()) {
File[] viewsFiles = viewsDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".json");
}
});
log.info("Importing database views");
for (File view : viewsFiles) {
if (importView(view)) {
updated++;
}
}
log.info("Importing database views [complete]");
log.info("Deleting database views");
File deleteViewsFile = new File(viewsDir, "views.delete");
try (FileInputStream s = new FileInputStream(deleteViewsFile)) {
for (String line : (List<String>) IOUtils.readLines(s)) {
String viewId = line.trim();
if (deleteDesignDocument(viewId)) {
updated++;
}
}
} catch (IOException e) {
log.log(Level.SEVERE, "Error while reading delete file [" + deleteViewsFile.getAbsolutePath() + "]", e);
}
log.info("Deleting database views [complete]");
}
}
return updated;
}
private boolean importView(File view) {
try {
if (needViewUpdate(view)) {
log.info("The view import from: [" + view.getAbsolutePath() + "]");
createView(view);
return true;
}
} catch (Exception e) {
log.log(Level.SEVERE, "Error the view update [" + view.getName() + "]", e);
}
return false;
}
/**
* Returns current list of design documents in database
* @return list of design documents in database
*/
public List<ViewStore> getDesignDocuments() {
Query<ViewStore> query = new Query<ViewStore>(dbInstance, "/" + dbName + "/_all_docs", ViewStore.class);
return query.startKey("_design/").endKey("_design0").includeDocs(true).list();
}
private static final String NOT_FOUND_DOC = "404DOC";
/**
* Delete design document from database.
* @param docId id of design document <b>without</b> _design/ prefix
* @return {@code true} if design document was deleted successfully, {@code false} if design document does not exists
*/
public boolean deleteDesignDocument(final String docId) {
if (StringUtils.isBlank(docId)) {
return false;
}
final String revision = dbInstance.execute(new HttpCall<String>() {
@Override
public HttpRequest getRequest() throws URISyntaxException,
IOException {
return new HttpHead("/" + dbName + "/_design/" + docId);
}
@Override
public String doWithResponse(HttpResponse response)
throws IOException {
if (response.getStatusLine().getStatusCode() == SC_NOT_FOUND) {
return NOT_FOUND_DOC;
}
return StringUtils.remove(response.getHeaders("ETag")[0].getValue(), '"');
}
}, SC_OK, SC_NOT_FOUND);
if (StringUtils.equals(NOT_FOUND_DOC, revision)) {
return false;
}
dbInstance.execute(new HttpCall<Void>() {
@Override
public HttpRequest getRequest() throws URISyntaxException,
IOException {
return new HttpDelete("/" + dbName + "/_design/" + docId + "?" + CouchConstants.REV + "=" + revision);
}
@Override
public Void doWithResponse(HttpResponse response) { return null; }
}, SC_OK, SC_ACCEPTED);
log.info("Deleted design document [" + docId + "]");
return true;
}
private boolean needViewUpdate(final File viewFile) throws ParseException, IOException, NoSuchAlgorithmException {
final String viewId = viewFile.getName().replace(".json", "");
return dbInstance.execute(new HttpCall<Boolean>() {
@Override
public HttpRequest getRequest() throws URISyntaxException, IOException {
return new HttpGet(Db.buildUri("/" + dbName + "/_design/" + viewId));
}
@Override
public Boolean doWithResponse(HttpResponse response) throws Exception {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == SC_OK) {
ViewStore oldStore = mapper.readValue(response.getEntity().getContent(), ViewStore.class);
byte[] fileContent = IOUtils.toByteArray(new FileInputStream(viewFile));
String md5 = md5(fileContent);
return !md5.equals(oldStore.getFileMd5());
}
return true;
}
}, SC_OK, SC_NOT_FOUND);
}
private void createView(File file) throws IOException, ParseException, NoSuchAlgorithmException {
InputStream fileInputStream = new FileInputStream(file);
try {
byte[] fileContent = IOUtils.toByteArray(fileInputStream);
final ViewStore viewStore = mapper.readValue(new SkipNewLinesFilterReader(new InputStreamReader(new ByteArrayInputStream(fileContent), Charset.forName("UTF-8"))), ViewStore.class);
viewStore.setId(file.getName().replace(".json", ""));
viewStore.setFileMd5(md5(fileContent));
final String viewUri = "/" + dbName + "/_design/" + viewStore.getId();
//Setting last revision to view
dbInstance.execute(new HttpCall<Void>() {
@Override
public HttpRequest getRequest() throws URISyntaxException, IOException {
return new HttpGet(Db.buildUri(viewUri));
}
@Override
public Void doWithResponse(HttpResponse response) throws IOException {
if (response.getStatusLine().getStatusCode() == SC_OK) {
ViewStore oldStore = mapper.readValue(response.getEntity().getContent(), ViewStore.class);
viewStore.setRevision(oldStore.getRevision());
}
return null;
}
}, SC_OK, SC_NOT_FOUND);
//Store view to database
dbInstance.execute(new HttpCall<Void>() {
@Override
public HttpRequest getRequest() throws URISyntaxException, IOException {
HttpPut command = new HttpPut(Db.buildUri(viewUri));
BasicHttpEntity entity = new BasicHttpEntity();
entity.setContentType("application/json");
byte[] data = Db.toBytes(viewStore, mapper);
entity.setContent(new ByteArrayInputStream(data));
entity.setContentLength(data.length);
command.setEntity(entity);
return command;
}
@Override
public Void doWithResponse(HttpResponse response) { return null; }
}, SC_CREATED);
} finally {
IOUtils.closeQuietly(fileInputStream);
}
}
private String md5(byte[] content) throws FileNotFoundException, IOException, NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] md5Bytes = messageDigest.digest(content);
StringBuilder md5String = new StringBuilder();
for (byte b : md5Bytes) {
md5String.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1,3));
}
return md5String.toString();
}
}