Package org.openstreetmap.osmosis.replicationhttp.v0_6.impl

Source Code of org.openstreetmap.osmosis.replicationhttp.v0_6.impl.ReplicationDataServerHandler

// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.replicationhttp.v0_6.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.Queue;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.WriteCompletionEvent;
import org.jboss.netty.handler.codec.http.DefaultHttpChunk;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.util.CharsetUtil;
import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.util.PropertiesPersister;
import org.openstreetmap.osmosis.replication.common.ReplicationSequenceFormatter;
import org.openstreetmap.osmosis.replication.common.ReplicationState;


/**
* A sequence server handler implementation that sends the replication data
* associated with sequence numbers.
*
* @author Brett Henderson
*/
public class ReplicationDataServerHandler extends SequenceServerHandler {

  private static final Logger LOG = Logger.getLogger(ReplicationDataServerHandler.class.getName());
  private static final String REQUEST_DATE_FORMAT = "yyyy-MM-dd-HH-mm-ss";
  private static final int CHUNK_SIZE = 4096;

  private File dataDirectory;
  private ReplicationSequenceFormatter sequenceFormatter;
  private FileChannel chunkedFileChannel;
  private boolean fileSizeSent;
  private boolean includeData;
  private ChannelFuture sequenceFuture;


  /**
   * Creates a new instance.
   *
   * @param control
   *            Provides the Netty handlers with access to the controller.
   * @param dataDirectory
   *            The directory containing the replication data files.
   */
  public ReplicationDataServerHandler(SequenceServerControl control, File dataDirectory) {
    super(control);

    this.dataDirectory = dataDirectory;

    sequenceFormatter = new ReplicationSequenceFormatter(9, 3);
  }


  private DateFormat getRequestDateParser() {
    SimpleDateFormat dateParser = new SimpleDateFormat(REQUEST_DATE_FORMAT);
    Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
    dateParser.setCalendar(calendar);

    return dateParser;
  }


  private File getStateFile(long sequenceNumber) {
    return new File(dataDirectory, sequenceFormatter.getFormattedName(sequenceNumber, ".state.txt"));
  }


  private File getDataFile(long sequenceNumber) {
    return new File(dataDirectory, sequenceFormatter.getFormattedName(sequenceNumber, ".osc.gz"));
  }


  private ReplicationState getReplicationState(long sequenceNumber) {
    PropertiesPersister persister = new PropertiesPersister(getStateFile(sequenceNumber));
    ReplicationState state = new ReplicationState();
    state.load(persister.loadMap());

    return state;
  }


  /**
   * Search through the replication state records and find the nearest
   * replication number with a timestamp earlier or equal to the requested
   * date. It is not sufficient to find the minimum known sequence record with
   * a timestamp greater than the requested date because there may be missing
   * replication records in between.
   *
   * @param lastDate
   *            The last date known by the client.
   * @return The associated sequence number.
   */
  private long getNextSequenceNumberByDate(Date lastDate) {
    long startBound = 0;
    long endBound = getControl().getLatestSequenceNumber();

    // If the requested date is greater than or equal to the latest known
    // timestamp we should return our latest sequence number so that the
    // client will start receiving all new records as they arrive with
    // possibly some duplicated change records.
    if (lastDate.compareTo(getReplicationState(endBound).getTimestamp()) >= 0) {
      return endBound;
    }

    // Continue splitting our range in half until either we find the
    // requested record, or we only have one possibility remaining.
    while ((endBound - startBound) > 1) {
      // Calculate the current midpoint.
      long midPoint = startBound + ((endBound - startBound) / 2);

      // If the midpoint doesn't exist we need to reset the start bound to
      // the midpoint and search again.
      if (!getStateFile(midPoint).exists()) {
        startBound = midPoint;
        continue;
      }

      // If the midpoint timestamp is greater we search in the lower half,
      // otherwise the higher half.
      int comparison = lastDate.compareTo(getReplicationState(midPoint).getTimestamp());
      if (comparison == 0) {
        // We have an exact match so stop processing now.
        return midPoint;
      } else if (comparison < 0) {
        // We will now search in the lower half of the search range.
        // Even though we know the midpoint is not the right value, we
        // include it in the next range because our search assumes that
        // the right sequence number is less than the end point.
        endBound = midPoint;
      } else {
        // We will now search in the upper half of the search range.
        // Even though the mid point has a timestamp less than the
        // requested value, it still may be the selected value if the
        // next timestamp is greater.
        startBound = midPoint;
      }
    }

    // We only have one possibility remaining which is the start bound. This
    // is the requested record if it exists and has a timestamp less than or
    // equal to that requested.
    if (getStateFile(startBound).exists()
        && lastDate.compareTo(getReplicationState(startBound).getTimestamp()) >= 0) {
      return startBound;
    } else {
      // We cannot find any replication records with an early enough date.
      // This typically means that replication records for that time
      // period either no longer exist or never existed.
      throw new ResourceGoneException();
    }
  }


  private FileChannel openFileChannel(File file) {
    try {
      return new FileInputStream(file).getChannel();
    } catch (FileNotFoundException e) {
      throw new OsmosisRuntimeException("Unable to open file " + file, e);
    }
  }


  private ChannelBuffer readFromFile(FileChannel fileChannel, int bytesToRead) {
    try {
      // Allocate a buffer for the data to be read.
      byte[] rawBuffer = new byte[bytesToRead];

      // Copy data into the buffer using NIO.
      ByteBuffer nioBuffer = ByteBuffer.wrap(rawBuffer);
      for (int bytesRead = 0; bytesRead < bytesToRead;) {
        int lastBytesRead = fileChannel.read(nioBuffer);

        // We always expect to read data.
        if (lastBytesRead < 0) {
          throw new OsmosisRuntimeException("Unexpectedly reached the end of the replication data file");
        }
        if (lastBytesRead == 0) {
          throw new OsmosisRuntimeException("Last read of the replication data file returned 0 bytes");
        }

        bytesRead += lastBytesRead;
      }

      // Create and return a Netty buffer.
      return ChannelBuffers.wrappedBuffer(rawBuffer);

    } catch (IOException e) {
      throw new OsmosisRuntimeException("Unable to read from the replication data file", e);
    }
  }


  private ChannelBuffer loadFile(File file) {
    FileChannel fileChannel = openFileChannel(file);

    try {
      if (fileChannel.size() > Integer.MAX_VALUE) {
        throw new OsmosisRuntimeException("Maximum file size supported is " + Integer.MAX_VALUE + " bytes");
      }

      // Determine the size of the file.
      int fileSize = (int) fileChannel.size();

      // Read the entire file.
      ChannelBuffer buffer = readFromFile(fileChannel, fileSize);

      // We no longer need access to the file.
      fileChannel.close();

      return buffer;

    } catch (IOException e) {
      throw new OsmosisRuntimeException("Unable to read from file " + file, e);
    } finally {
      try {
        fileChannel.close();
      } catch (IOException e) {
        LOG.log(Level.WARNING, "Unable to close channel for file " + file, e);
      }
    }
  }


  private ChannelBuffer getFileChunk() {
    try {
      // Determine how many bytes are left in the file.
      long remaining = chunkedFileChannel.size() - chunkedFileChannel.position();

      // We will only send up to our maximum chunk size.
      if (remaining > CHUNK_SIZE) {
        remaining = CHUNK_SIZE;
      }

      // Read the next data for the next chunk.
      ChannelBuffer buffer = readFromFile(chunkedFileChannel, (int) remaining);

      // Close the file if we've reached the end.
      if (chunkedFileChannel.position() >= chunkedFileChannel.size()) {
        chunkedFileChannel.close();
        chunkedFileChannel = null;
      }

      return buffer;

    } catch (IOException e) {
      throw new OsmosisRuntimeException("Unable to read from the replication data file", e);
    }
  }
 
 
  private ChannelBuffer buildChunkHeader(long chunkSize) {
    return ChannelBuffers.copiedBuffer(Long.toString(chunkSize) + "\r\n", CharsetUtil.UTF_8);
  }


  @Override
  protected void handleRequest(ChannelHandlerContext ctx, HttpRequest request) {
    final String replicationStateUri = "replicationState";
    final String replicationDataUri = "replicationData";
    final String textContentType = "text/plain";
    final String dataContentType = "application/octet-stream";

    // Split the request Uri into its path elements.
    String uri = request.getUri();
    if (!uri.startsWith("/")) {
      throw new OsmosisRuntimeException("Uri doesn't start with a / character: " + uri);
    }
    Queue<String> uriElements = new LinkedList<String>(Arrays.asList(uri.split("/")));
    uriElements.remove(); // First element is empty due to leading '/'.

    // First element must be either the replication state or replication
    // data uri which determines whether replication data will be included
    // or just the replication state.
    String contentType;
    if (uriElements.isEmpty()) {
      throw new ResourceNotFoundException();
    }
    String requestTypeString = uriElements.remove();
    if (replicationStateUri.equals(requestTypeString)) {
      contentType = textContentType;
      includeData = false;
    } else if (replicationDataUri.equals(requestTypeString)) {
      contentType = dataContentType;
      includeData = true;
    } else {
      throw new ResourceNotFoundException();
    }

    /*
     * The next element determines which replication number to start from.
     * The request is one of "current" or N where is the last sequence
     * number received by the client.
     */
    long nextSequenceNumber;
    if (uriElements.isEmpty()) {
      throw new ResourceNotFoundException();
    }
    String sequenceStartString = uriElements.remove();
    if ("current".equals(sequenceStartString)) {
      nextSequenceNumber = getControl().getLatestSequenceNumber();
    } else {
      // Try to parse the sequence start string as a number. If that fails
      // try to parse as a date.
      try {
        nextSequenceNumber = Long.parseLong(sequenceStartString);
      } catch (NumberFormatException e) {
        try {
          Date lastDate = getRequestDateParser().parse(sequenceStartString);
          nextSequenceNumber = getNextSequenceNumberByDate(lastDate);

        } catch (ParseException e1) {
          throw new BadRequestException("Requested sequence number of " + sequenceStartString
              + " is not a number, or a date in format yyyy-MM-dd-HH-mm-ss.");
        }

      }
    }

    // If the next element exists and is "tail" it means that the client
    // wants to stay connected and receive updated sequences as they become
    // available.
    boolean follow;
    if (!uriElements.isEmpty()) {
      String tailElement = uriElements.remove();
      if ("tail".equals(tailElement)) {
        follow = true;
      } else {
        throw new ResourceNotFoundException();
      }
    } else {
      follow = false;
    }

    // Validate that that no more URI elements are available.
    if (!uriElements.isEmpty()) {
      throw new ResourceNotFoundException();
    }

    // Begin sending replication sequence information to the client.
    if (LOG.isLoggable(Level.FINER)) {
      LOG.finer("New request details, includeData=" + includeData + ", sequenceNumber=" + nextSequenceNumber
          + ", tail=" + follow);
    }
    initiateSequenceWriting(ctx, contentType, nextSequenceNumber, follow);
  }


  @Override
  protected void writeSequence(ChannelHandlerContext ctx, ChannelFuture future, long sequenceNumber) {
    // We do not support sending new replication data until the previous
    // send has completed.
    if (chunkedFileChannel != null) {
      throw new OsmosisRuntimeException(
          "We cannot send new replication data until the previous write has completed");
    }
   
    if (LOG.isLoggable(Level.FINEST)) {
      LOG.finest("Sequence being written, includeData=" + includeData + ", sequenceNumber="
          + sequenceNumber);
    }
   
    // We must save the future to attach to the final write.
    sequenceFuture = future;

    // Get the name of the replication data file.
    File stateFile = getStateFile(sequenceNumber);
    File dataFile = getDataFile(sequenceNumber);

    // Load the contents of the state file.
    ChannelBuffer stateFileBuffer = loadFile(stateFile);
   
    // Add a chunk length header.
    stateFileBuffer = ChannelBuffers.wrappedBuffer(buildChunkHeader(stateFileBuffer.readableBytes()),
        stateFileBuffer);

    // Only include replication data if initially requested by the client
    // and if this is not sequence 0.
    if (includeData && sequenceNumber > 0) {
      // Open the data file read for sending.
      chunkedFileChannel = openFileChannel(dataFile);
      fileSizeSent = false;
    }

    /*
     * Send the state file to the client. If replication data is to be sent
     * we will continue when we receive completion information via the
     * writeComplete method. We must create a new future now if we have more
     * data coming because we don't want the future of the current event to
     * fire until we're completely finished processing.
     */
    ChannelFuture writeFuture;
    if (chunkedFileChannel != null) {
      writeFuture = Channels.future(ctx.getChannel());
    } else {
      writeFuture = sequenceFuture;
    }
    Channels.write(ctx, writeFuture, new DefaultHttpChunk(stateFileBuffer));
  }


  @Override
  public void writeComplete(ChannelHandlerContext ctx, WriteCompletionEvent e) throws Exception {
    if (chunkedFileChannel != null) {
      // We have an open file channel so we are still sending replication
      // data.
      ChannelBuffer buffer;
      ChannelFuture future;
      if (!fileSizeSent) {
        // Send a chunk header containing the size of the file.
        ChannelBuffer fileSizeBuffer = buildChunkHeader(chunkedFileChannel.size());
        fileSizeSent = true;
        future = Channels.future(ctx.getChannel());
        buffer = fileSizeBuffer;
      } else {
        // Send the next chunk to the client.
        buffer = getFileChunk();
        if (chunkedFileChannel != null) {
          future = Channels.future(ctx.getChannel());
        } else {
          // This is the last write for this sequence so attach the original future.
          future = sequenceFuture;
        }
      }
     
      // Write the data to the channel.
      Channels.write(ctx, future, new DefaultHttpChunk(buffer));
    } else {
      super.writeComplete(ctx, e);
    }
  }


  @Override
  public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
    // Close the in-progress chunk file channel if it exists.
    if (chunkedFileChannel != null) {
      try {
        chunkedFileChannel.close();
      } catch (IOException ex) {
        LOG.log(Level.WARNING, "Unable to close the replication data file.", ex);
      }
      chunkedFileChannel = null;
    }
    super.channelClosed(ctx, e);
  }
}
TOP

Related Classes of org.openstreetmap.osmosis.replicationhttp.v0_6.impl.ReplicationDataServerHandler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.