/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.hdfs.server.namenode;

import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ADD;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ADD_ERASURE_CODING_POLICY;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_APPEND;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ADD_BLOCK;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ADD_CACHE_DIRECTIVE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ADD_CACHE_POOL;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ALLOCATE_BLOCK_ID;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ALLOW_SNAPSHOT;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_CANCEL_DELEGATION_TOKEN;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_CLEAR_NS_QUOTA;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_CLOSE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_CONCAT_DELETE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_CREATE_SNAPSHOT;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_DELETE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_DELETE_SNAPSHOT;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_DISABLE_ERASURE_CODING_POLICY;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_DISALLOW_SNAPSHOT;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ENABLE_ERASURE_CODING_POLICY;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_END_LOG_SEGMENT;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_GET_DELEGATION_TOKEN;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_INVALID;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_MKDIR;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_MODIFY_CACHE_DIRECTIVE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_MODIFY_CACHE_POOL;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_REASSIGN_LEASE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_REMOVE_CACHE_DIRECTIVE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_REMOVE_CACHE_POOL;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_REMOVE_ERASURE_CODING_POLICY;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_REMOVE_XATTR;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_RENAME;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_RENAME_OLD;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_RENAME_SNAPSHOT;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_RENEW_DELEGATION_TOKEN;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ROLLING_UPGRADE_FINALIZE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_ROLLING_UPGRADE_START;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_ACL;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_GENSTAMP_V1;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_GENSTAMP_V2;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_NS_QUOTA;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_OWNER;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_PERMISSIONS;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_QUOTA;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_REPLICATION;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_XATTR;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_START_LOG_SEGMENT;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SYMLINK;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_TIMES;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_TRUNCATE;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_UPDATE_BLOCKS;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_UPDATE_MASTER_KEY;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_STORAGE_POLICY;
import static org.apache.hadoop.hdfs.server.namenode.FSEditLogOpCodes.OP_SET_QUOTA_BY_STORAGETYPE;

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.CheckedInputStream;
import java.util.zip.Checksum;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.fs.ChecksumException;
import org.apache.hadoop.fs.Options.Rename;
import org.apache.hadoop.fs.XAttr;
import org.apache.hadoop.fs.XAttrCodec;
import org.apache.hadoop.fs.permission.AclEntry;
import org.apache.hadoop.fs.permission.AclEntryScope;
import org.apache.hadoop.fs.permission.AclEntryType;
import org.apache.hadoop.fs.permission.FsAction;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.fs.permission.PermissionStatus;
import org.apache.hadoop.fs.StorageType;
import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.apache.hadoop.hdfs.DeprecatedUTF8;
import org.apache.hadoop.hdfs.protocol.Block;
import org.apache.hadoop.hdfs.protocol.CacheDirectiveInfo;
import org.apache.hadoop.hdfs.protocol.CachePoolInfo;
import org.apache.hadoop.hdfs.protocol.ClientProtocol;
import org.apache.hadoop.hdfs.protocol.ErasureCodingPolicy;
import org.apache.hadoop.hdfs.protocol.HdfsConstants;
import org.apache.hadoop.hdfs.protocol.LayoutVersion;
import org.apache.hadoop.hdfs.protocol.LayoutVersion.Feature;
import org.apache.hadoop.hdfs.protocol.proto.EditLogProtos.AclEditLogProto;
import org.apache.hadoop.hdfs.protocol.proto.EditLogProtos.XAttrEditLogProto;
import org.apache.hadoop.hdfs.protocolPB.PBHelperClient;
import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
import org.apache.hadoop.hdfs.server.common.HdfsServerConstants;
import org.apache.hadoop.hdfs.util.XMLUtils;
import org.apache.hadoop.hdfs.util.XMLUtils.InvalidXmlException;
import org.apache.hadoop.hdfs.util.XMLUtils.Stanza;
import org.apache.hadoop.io.ArrayWritable;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.DataOutputBuffer;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableFactories;
import org.apache.hadoop.io.WritableFactory;
import org.apache.hadoop.io.erasurecode.ECSchema;
import org.apache.hadoop.io.erasurecode.ErasureCodeConstants;
import org.apache.hadoop.ipc.ClientId;
import org.apache.hadoop.ipc.RpcConstants;
import org.apache.hadoop.security.token.delegation.DelegationKey;
import org.apache.hadoop.util.DataChecksum;
import org.apache.hadoop.util.StringUtils;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

/**
 * Helper classes for reading the ops from an InputStream.
 * All ops derive from FSEditLogOp and are only
 * instantiated from Reader#readOp()
 */
@InterfaceAudience.Private
@InterfaceStability.Unstable
public abstract class FSEditLogOp {
  public final FSEditLogOpCodes opCode;
  long txid;
  byte[] rpcClientId;
  int rpcCallId;

  public static class OpInstanceCache {
    private static final ThreadLocal<OpInstanceCacheMap> CACHE =
        new ThreadLocal<OpInstanceCacheMap>() {
      @Override
      protected OpInstanceCacheMap initialValue() {
        return new OpInstanceCacheMap();
      }
    };

    @SuppressWarnings("serial")
    static final class OpInstanceCacheMap extends
        EnumMap<FSEditLogOpCodes, FSEditLogOp> {
      OpInstanceCacheMap() {
        super(FSEditLogOpCodes.class);
        for (FSEditLogOpCodes opCode : FSEditLogOpCodes.values()) {
          put(opCode, newInstance(opCode));
        }
      }
    }

    private boolean useCache = true;

    void disableCache() {
      useCache = false;
    }

    public OpInstanceCache get() {
      return this;
    }

    @SuppressWarnings("unchecked")
    public <T extends FSEditLogOp> T get(FSEditLogOpCodes opCode) {
      return useCache ? (T)CACHE.get().get(opCode) : (T)newInstance(opCode);
    }

    private static FSEditLogOp newInstance(FSEditLogOpCodes opCode) {
      FSEditLogOp instance = null;
      Class<? extends FSEditLogOp> clazz = opCode.getOpClass();
      if (clazz != null) {
        try {
          instance = clazz.newInstance();
        } catch (Exception ex) {
          throw new RuntimeException("Failed to instantiate "+opCode, ex);
        }
      }
      return instance;
    }
  }

  final void reset() {
    txid = HdfsServerConstants.INVALID_TXID;
    rpcClientId = RpcConstants.DUMMY_CLIENT_ID;
    rpcCallId = RpcConstants.INVALID_CALL_ID;
    resetSubFields();
  }

  abstract void resetSubFields();

  private static ImmutableMap<String, FsAction> fsActionMap() {
    ImmutableMap.Builder<String, FsAction> b = ImmutableMap.builder();
    for (FsAction v : FsAction.values())
      b.put(v.SYMBOL, v);
    return b.build();
  }

  private static final ImmutableMap<String, FsAction> FSACTION_SYMBOL_MAP
    = fsActionMap();

  /**
   * Constructor for an EditLog Op. EditLog ops cannot be constructed
   * directly, but only through Reader#readOp.
   */
  @VisibleForTesting
  protected FSEditLogOp(FSEditLogOpCodes opCode) {
    this.opCode = opCode;
    reset();
  }

  public long getTransactionId() {
    Preconditions.checkState(txid != HdfsServerConstants.INVALID_TXID);
    return txid;
  }

  public String getTransactionIdStr() {
    return (txid == HdfsServerConstants.INVALID_TXID) ? "(none)" : "" + txid;
  }
  
  public boolean hasTransactionId() {
    return (txid != HdfsServerConstants.INVALID_TXID);
  }

  public void setTransactionId(long txid) {
    this.txid = txid;
  }
  
  public boolean hasRpcIds() {
    return rpcClientId != RpcConstants.DUMMY_CLIENT_ID
        && rpcCallId != RpcConstants.INVALID_CALL_ID;
  }
  
  /** this has to be called after calling {@link #hasRpcIds()} */
  public byte[] getClientId() {
    Preconditions.checkState(rpcClientId != RpcConstants.DUMMY_CLIENT_ID);
    return rpcClientId;
  }
  
  public void setRpcClientId(byte[] clientId) {
    this.rpcClientId = clientId;
  }
  
  /** this has to be called after calling {@link #hasRpcIds()} */
  public int getCallId() {
    Preconditions.checkState(rpcCallId != RpcConstants.INVALID_CALL_ID);
    return rpcCallId;
  }
  
  public void setRpcCallId(int callId) {
    this.rpcCallId = callId;
  }

  abstract void readFields(DataInputStream in, int logVersion)
      throws IOException;

  public abstract void writeFields(DataOutputStream out)
      throws IOException;

  static interface BlockListUpdatingOp {
    Block[] getBlocks();
    String getPath();
    boolean shouldCompleteLastBlock();
  }
  
  private static void writeRpcIds(final byte[] clientId, final int callId,
      DataOutputStream out) throws IOException {
    FSImageSerialization.writeBytes(clientId, out);
    FSImageSerialization.writeInt(callId, out);
  }
  
  void readRpcIds(DataInputStream in, int logVersion)
      throws IOException {
    if (NameNodeLayoutVersion.supports(
        LayoutVersion.Feature.EDITLOG_SUPPORT_RETRYCACHE, logVersion)) {
      this.rpcClientId = FSImageSerialization.readBytes(in);
      this.rpcCallId = FSImageSerialization.readInt(in);
    }
  }
  
  void readRpcIdsFromXml(Stanza st) {
    this.rpcClientId = st.hasChildren("RPC_CLIENTID") ? 
        ClientId.toBytes(st.getValue("RPC_CLIENTID"))
        : RpcConstants.DUMMY_CLIENT_ID;
    this.rpcCallId = st.hasChildren("RPC_CALLID") ? 
        Integer.parseInt(st.getValue("RPC_CALLID"))
        : RpcConstants.INVALID_CALL_ID;
  }
  
  private static void appendRpcIdsToString(final StringBuilder builder,
      final byte[] clientId, final int callId) {
    builder.append(", RpcClientId=")
        .append(ClientId.toString(clientId))
        .append(", RpcCallId=")
        .append(callId);
  }
  
  private static void appendRpcIdsToXml(ContentHandler contentHandler,
      final byte[] clientId, final int callId) throws SAXException {
    XMLUtils.addSaxString(contentHandler, "RPC_CLIENTID",
        ClientId.toString(clientId));
    XMLUtils.addSaxString(contentHandler, "RPC_CALLID", 
        Integer.toString(callId));
  }

  private static final class AclEditLogUtil {
    private static final int ACL_EDITLOG_ENTRY_HAS_NAME_OFFSET = 6;
    private static final int ACL_EDITLOG_ENTRY_TYPE_OFFSET = 3;
    private static final int ACL_EDITLOG_ENTRY_SCOPE_OFFSET = 5;
    private static final int ACL_EDITLOG_PERM_MASK = 7;
    private static final int ACL_EDITLOG_ENTRY_TYPE_MASK = 3;
    private static final int ACL_EDITLOG_ENTRY_SCOPE_MASK = 1;

    private static final FsAction[] FSACTION_VALUES = FsAction.values();
    private static final AclEntryScope[] ACL_ENTRY_SCOPE_VALUES = AclEntryScope
        .values();
    private static final AclEntryType[] ACL_ENTRY_TYPE_VALUES = AclEntryType
        .values();

    private static List<AclEntry> read(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(Feature.EXTENDED_ACL, logVersion)) {
        return null;
      }

      int size = in.readInt();
      if (size == 0) {
        return null;
      }

      List<AclEntry> aclEntries = Lists.newArrayListWithCapacity(size);
      for (int i = 0; i < size; ++i) {
        int v = in.read();
        int p = v & ACL_EDITLOG_PERM_MASK;
        int t = (v >> ACL_EDITLOG_ENTRY_TYPE_OFFSET)
            & ACL_EDITLOG_ENTRY_TYPE_MASK;
        int s = (v >> ACL_EDITLOG_ENTRY_SCOPE_OFFSET)
            & ACL_EDITLOG_ENTRY_SCOPE_MASK;
        boolean hasName = ((v >> ACL_EDITLOG_ENTRY_HAS_NAME_OFFSET) & 1) == 1;
        String name = hasName ? FSImageSerialization.readString(in) : null;
        aclEntries.add(new AclEntry.Builder().setName(name)
            .setPermission(FSACTION_VALUES[p])
            .setScope(ACL_ENTRY_SCOPE_VALUES[s])
            .setType(ACL_ENTRY_TYPE_VALUES[t]).build());
      }

      return aclEntries;
    }

    private static void write(List<AclEntry> aclEntries, DataOutputStream out)
        throws IOException {
      if (aclEntries == null) {
        out.writeInt(0);
        return;
      }

      out.writeInt(aclEntries.size());
      for (AclEntry e : aclEntries) {
        boolean hasName = e.getName() != null;
        int v = (e.getScope().ordinal() << ACL_EDITLOG_ENTRY_SCOPE_OFFSET)
            | (e.getType().ordinal() << ACL_EDITLOG_ENTRY_TYPE_OFFSET)
            | e.getPermission().ordinal();

        if (hasName) {
          v |= 1 << ACL_EDITLOG_ENTRY_HAS_NAME_OFFSET;
        }
        out.write(v);
        if (hasName) {
          FSImageSerialization.writeString(e.getName(), out);
        }
      }
    }
  }

  private static List<XAttr> readXAttrsFromEditLog(DataInputStream in,
      int logVersion) throws IOException {
    if (!NameNodeLayoutVersion.supports(NameNodeLayoutVersion.Feature.XATTRS,
        logVersion)) {
      return null;
    }
    XAttrEditLogProto proto = XAttrEditLogProto.parseDelimitedFrom(in);
    return PBHelperClient.convertXAttrs(proto.getXAttrsList());
  }

  @SuppressWarnings("unchecked")
  static abstract class AddCloseOp
         extends FSEditLogOp
          implements BlockListUpdatingOp {
    int length;
    long inodeId;
    String path;
    short replication;
    long mtime;
    long atime;
    long blockSize;
    Block[] blocks;
    PermissionStatus permissions;
    List<AclEntry> aclEntries;
    List<XAttr> xAttrs;
    String clientName;
    String clientMachine;
    boolean overwrite;
    byte storagePolicyId;
    byte erasureCodingPolicyId;
    
    private AddCloseOp(FSEditLogOpCodes opCode) {
      super(opCode);
      storagePolicyId = HdfsConstants.BLOCK_STORAGE_POLICY_ID_UNSPECIFIED;
      erasureCodingPolicyId = ErasureCodeConstants.REPLICATION_POLICY_ID;
      assert(opCode == OP_ADD || opCode == OP_CLOSE || opCode == OP_APPEND);
    }

    @Override
    void resetSubFields() {
      length = 0;
      inodeId = 0L;
      path = null;
      replication = 0;
      mtime = 0L;
      atime = 0L;
      blockSize = 0L;
      blocks = null;
      permissions = null;
      aclEntries = null;
      xAttrs = null;
      clientName = null;
      clientMachine = null;
      overwrite = false;
      storagePolicyId = 0;
      erasureCodingPolicyId = ErasureCodeConstants.REPLICATION_POLICY_ID;
    }

    <T extends AddCloseOp> T setInodeId(long inodeId) {
      this.inodeId = inodeId;
      return (T)this;
    }

    <T extends AddCloseOp> T setPath(String path) {
      this.path = path;
      return (T)this;
    }
    
    @Override
    public String getPath() {
      return path;
    }

    <T extends AddCloseOp> T setReplication(short replication) {
      this.replication = replication;
      return (T)this;
    }

    <T extends AddCloseOp> T setModificationTime(long mtime) {
      this.mtime = mtime;
      return (T)this;
    }

    <T extends AddCloseOp> T setAccessTime(long atime) {
      this.atime = atime;
      return (T)this;
    }

    <T extends AddCloseOp> T setBlockSize(long blockSize) {
      this.blockSize = blockSize;
      return (T)this;
    }

    <T extends AddCloseOp> T setBlocks(Block[] blocks) {
      if (blocks.length > MAX_BLOCKS) {
        throw new RuntimeException("Can't have more than " + MAX_BLOCKS +
            " in an AddCloseOp.");
      }
      this.blocks = blocks;
      return (T)this;
    }
    
    @Override
    public Block[] getBlocks() {
      return blocks;
    }

    <T extends AddCloseOp> T setPermissionStatus(PermissionStatus permissions) {
      this.permissions = permissions;
      return (T)this;
    }

    <T extends AddCloseOp> T setAclEntries(List<AclEntry> aclEntries) {
      this.aclEntries = aclEntries;
      return (T)this;
    }

    <T extends AddCloseOp> T setXAttrs(List<XAttr> xAttrs) {
      this.xAttrs = xAttrs;
      return (T)this;
    }

    <T extends AddCloseOp> T setClientName(String clientName) {
      this.clientName = clientName;
      return (T)this;
    }

    <T extends AddCloseOp> T setClientMachine(String clientMachine) {
      this.clientMachine = clientMachine;
      return (T)this;
    }
    
    <T extends AddCloseOp> T setOverwrite(boolean overwrite) {
      this.overwrite = overwrite;
      return (T)this;
    }

    <T extends AddCloseOp> T setStoragePolicyId(byte storagePolicyId) {
      this.storagePolicyId = storagePolicyId;
      return (T)this;
    }

    <T extends AddCloseOp> T setErasureCodingPolicyId(byte ecPolicyId) {
      this.erasureCodingPolicyId = ecPolicyId;
      return (T)this;
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(inodeId, out);
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeShort(replication, out);
      FSImageSerialization.writeLong(mtime, out);
      FSImageSerialization.writeLong(atime, out);
      FSImageSerialization.writeLong(blockSize, out);
      new ArrayWritable(Block.class, blocks).write(out);
      permissions.write(out);

      if (this.opCode == OP_ADD) {
        AclEditLogUtil.write(aclEntries, out);
        XAttrEditLogProto.Builder b = XAttrEditLogProto.newBuilder();
        b.addAllXAttrs(PBHelperClient.convertXAttrProto(xAttrs));
        b.build().writeDelimitedTo(out);
        FSImageSerialization.writeString(clientName,out);
        FSImageSerialization.writeString(clientMachine,out);
        FSImageSerialization.writeBoolean(overwrite, out);
        FSImageSerialization.writeByte(storagePolicyId, out);
        FSImageSerialization.writeByte(erasureCodingPolicyId, out);
        // write clientId and callId
        writeRpcIds(rpcClientId, rpcCallId, out);
      }
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
      }
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.ADD_INODE_ID, logVersion)) {
        this.inodeId = in.readLong();
      } else {
        // The inodeId should be updated when this editLogOp is applied
        this.inodeId = HdfsConstants.GRANDFATHER_INODE_ID;
      }
      if ((-17 < logVersion && length != 4) ||
          (logVersion <= -17 && length != 5 && !NameNodeLayoutVersion.supports(
              LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion))) {
        throw new IOException("Incorrect data format."  +
                              " logVersion is " + logVersion +
                              " but writables.length is " +
                              length + ". ");
      }
      this.path = FSImageSerialization.readString(in);

      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.replication = FSImageSerialization.readShort(in);
        this.mtime = FSImageSerialization.readLong(in);
      } else {
        this.replication = readShort(in);
        this.mtime = readLong(in);
      }

      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.FILE_ACCESS_TIME, logVersion)) {
        if (NameNodeLayoutVersion.supports(
            LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
          this.atime = FSImageSerialization.readLong(in);
        } else {
          this.atime = readLong(in);
        }
      } else {
        this.atime = 0;
      }

      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.blockSize = FSImageSerialization.readLong(in);
      } else {
        this.blockSize = readLong(in);
      }

      this.blocks = readBlocks(in, logVersion);
      this.permissions = PermissionStatus.read(in);

      if (this.opCode == OP_ADD) {
        aclEntries = AclEditLogUtil.read(in, logVersion);
        this.xAttrs = readXAttrsFromEditLog(in, logVersion);
        this.clientName = FSImageSerialization.readString(in);
        this.clientMachine = FSImageSerialization.readString(in);
        if (NameNodeLayoutVersion.supports(
            NameNodeLayoutVersion.Feature.CREATE_OVERWRITE, logVersion)) {
          this.overwrite = FSImageSerialization.readBoolean(in);
        } else {
          this.overwrite = false;
        }
        if (NameNodeLayoutVersion.supports(
            NameNodeLayoutVersion.Feature.BLOCK_STORAGE_POLICY, logVersion)) {
          this.storagePolicyId = FSImageSerialization.readByte(in);
        } else {
          this.storagePolicyId =
              HdfsConstants.BLOCK_STORAGE_POLICY_ID_UNSPECIFIED;
        }

        if (NameNodeLayoutVersion.supports(
            NameNodeLayoutVersion.Feature.ERASURE_CODING, logVersion)) {
          this.erasureCodingPolicyId = FSImageSerialization.readByte(in);
        } else {
          this.erasureCodingPolicyId =
              ErasureCodeConstants.REPLICATION_POLICY_ID;
        }
        // read clientId and callId
        readRpcIds(in, logVersion);
      } else {
        this.clientName = "";
        this.clientMachine = "";
      }
    }

    static final public int MAX_BLOCKS = 1024 * 1024 * 64;
    
    private static Block[] readBlocks(
        DataInputStream in,
        int logVersion) throws IOException {
      int numBlocks = in.readInt();
      if (numBlocks < 0) {
        throw new IOException("invalid negative number of blocks");
      } else if (numBlocks > MAX_BLOCKS) {
        throw new IOException("invalid number of blocks: " + numBlocks +
            ".  The maximum number of blocks per file is " + MAX_BLOCKS);
      }
      Block[] blocks = new Block[numBlocks];
      for (int i = 0; i < numBlocks; i++) {
        Block blk = new Block();
        blk.readFields(in);
        blocks[i] = blk;
      }
      return blocks;
    }

    public String stringifyMembers() {
      StringBuilder builder = new StringBuilder();
      builder.append("[length=")
          .append(length)
          .append(", inodeId=")
          .append(inodeId)
          .append(", path=")
          .append(path)
          .append(", replication=")
          .append(replication)
          .append(", mtime=")
          .append(mtime)
          .append(", atime=")
          .append(atime)
          .append(", blockSize=")
          .append(blockSize)
          .append(", blocks=")
          .append(Arrays.toString(blocks))
          .append(", permissions=")
          .append(permissions)
          .append(", aclEntries=")
          .append(aclEntries)
          .append(", clientName=")
          .append(clientName)
          .append(", clientMachine=")
          .append(clientMachine)
          .append(", overwrite=")
          .append(overwrite);
      if (this.opCode == OP_ADD) {
        appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      }
      builder.append(", storagePolicyId=")
          .append(storagePolicyId)
          .append(", erasureCodingPolicyId=")
          .append(erasureCodingPolicyId)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "INODEID",
          Long.toString(inodeId));
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "REPLICATION",
          Short.toString(replication));
      XMLUtils.addSaxString(contentHandler, "MTIME",
          Long.toString(mtime));
      XMLUtils.addSaxString(contentHandler, "ATIME",
          Long.toString(atime));
      XMLUtils.addSaxString(contentHandler, "BLOCKSIZE",
          Long.toString(blockSize));
      XMLUtils.addSaxString(contentHandler, "CLIENT_NAME", clientName);
      XMLUtils.addSaxString(contentHandler, "CLIENT_MACHINE", clientMachine);
      XMLUtils.addSaxString(contentHandler, "OVERWRITE", 
          Boolean.toString(overwrite));
      for (Block b : blocks) {
        FSEditLogOp.blockToXml(contentHandler, b);
      }
      FSEditLogOp.permissionStatusToXml(contentHandler, permissions);
      if (this.opCode == OP_ADD) {
        if (aclEntries != null) {
          appendAclEntriesToXml(contentHandler, aclEntries);
        }
        XMLUtils.addSaxString(contentHandler, "ERASURE_CODING_POLICY_ID",
            Byte.toString(erasureCodingPolicyId));
        appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
      }
    }

    @Override 
    void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.inodeId = Long.parseLong(st.getValue("INODEID"));
      this.path = st.getValue("PATH");
      this.replication = Short.parseShort(st.getValue("REPLICATION"));
      this.mtime = Long.parseLong(st.getValue("MTIME"));
      this.atime = Long.parseLong(st.getValue("ATIME"));
      this.blockSize = Long.parseLong(st.getValue("BLOCKSIZE"));

      this.clientName = st.getValue("CLIENT_NAME");
      this.clientMachine = st.getValue("CLIENT_MACHINE");
      this.overwrite = Boolean.parseBoolean(st.getValueOrNull("OVERWRITE"));
      if (st.hasChildren("BLOCK")) {
        List<Stanza> blocks = st.getChildren("BLOCK");
        this.blocks = new Block[blocks.size()];
        for (int i = 0; i < blocks.size(); i++) {
          this.blocks[i] = FSEditLogOp.blockFromXml(blocks.get(i));
        }
      } else {
        this.blocks = new Block[0];
      }
      this.permissions = permissionStatusFromXml(st);
      aclEntries = readAclEntriesFromXml(st);
      if (st.hasChildren("ERASURE_CODING_POLICY_ID")) {
        this.erasureCodingPolicyId = Byte.parseByte(st.getValue(
            "ERASURE_CODING_POLICY_ID"));
      }
      readRpcIdsFromXml(st);
    }
  }

  /**
   * {@literal @AtMostOnce} for {@link ClientProtocol#create} and
   * {@link ClientProtocol#append}
   */
  static class AddOp extends AddCloseOp {
    AddOp() {
      super(OP_ADD);
    }

    static AddOp getInstance(OpInstanceCache cache) {
      return (AddOp) cache.get(OP_ADD);
    }

    @Override
    public boolean shouldCompleteLastBlock() {
      return false;
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("AddOp ")
          .append(stringifyMembers());
      return builder.toString();
    }
  }

  /**
   * Although {@link ClientProtocol#append} may also log a close op, we do
   * not need to record the rpc ids here since a successful appendFile op will
   * finally log an AddOp.
   */
  static class CloseOp extends AddCloseOp {
    CloseOp() {
      super(OP_CLOSE);
    }

    static CloseOp getInstance(OpInstanceCache cache) {
      return (CloseOp)cache.get(OP_CLOSE);
    }

    @Override
    public boolean shouldCompleteLastBlock() {
      return true;
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("CloseOp ")
          .append(stringifyMembers());
      return builder.toString();
    }
  }

  static class AppendOp extends FSEditLogOp {
    String path;
    String clientName;
    String clientMachine;
    boolean newBlock;

    AppendOp() {
      super(OP_APPEND);
    }

    static AppendOp getInstance(OpInstanceCache cache) {
      return (AppendOp) cache.get(OP_APPEND);
    }

    AppendOp setPath(String path) {
      this.path = path;
      return this;
    }

    AppendOp setClientName(String clientName) {
      this.clientName = clientName;
      return this;
    }

    AppendOp setClientMachine(String clientMachine) {
      this.clientMachine = clientMachine;
      return this;
    }

    AppendOp setNewBlock(boolean newBlock) {
      this.newBlock = newBlock;
      return this;
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("AppendOp ")
          .append("[path=").append(path)
          .append(", clientName=").append(clientName)
          .append(", clientMachine=").append(clientMachine)
          .append(", newBlock=").append(newBlock).append("]");
      return builder.toString();
    }

    @Override
    void resetSubFields() {
      this.path = null;
      this.clientName = null;
      this.clientMachine = null;
      this.newBlock = false;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      this.path = FSImageSerialization.readString(in);
      this.clientName = FSImageSerialization.readString(in);
      this.clientMachine = FSImageSerialization.readString(in);
      this.newBlock = FSImageSerialization.readBoolean(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeString(clientName, out);
      FSImageSerialization.writeString(clientMachine, out);
      FSImageSerialization.writeBoolean(newBlock, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "CLIENT_NAME", clientName);
      XMLUtils.addSaxString(contentHandler, "CLIENT_MACHINE", clientMachine);
      XMLUtils.addSaxString(contentHandler, "NEWBLOCK",
          Boolean.toString(newBlock));
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.path = st.getValue("PATH");
      this.clientName = st.getValue("CLIENT_NAME");
      this.clientMachine = st.getValue("CLIENT_MACHINE");
      this.newBlock = Boolean.parseBoolean(st.getValue("NEWBLOCK"));
      readRpcIdsFromXml(st);
    }
  }
  
  static class AddBlockOp extends FSEditLogOp {
    private String path;
    private Block penultimateBlock;
    private Block lastBlock;
    
    AddBlockOp() {
      super(OP_ADD_BLOCK);
    }
    
    static AddBlockOp getInstance(OpInstanceCache cache) {
      return (AddBlockOp) cache.get(OP_ADD_BLOCK);
    }

    @Override
    void resetSubFields() {
      path = null;
      penultimateBlock = null;
      lastBlock = null;
    }
    
    AddBlockOp setPath(String path) {
      this.path = path;
      return this;
    }
    
    public String getPath() {
      return path;
    }

    AddBlockOp setPenultimateBlock(Block pBlock) {
      this.penultimateBlock = pBlock;
      return this;
    }
    
    Block getPenultimateBlock() {
      return penultimateBlock;
    }
    
    AddBlockOp setLastBlock(Block lastBlock) {
      this.lastBlock = lastBlock;
      return this;
    }
    
    Block getLastBlock() {
      return lastBlock;
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(path, out);
      int size = penultimateBlock != null ? 2 : 1;
      Block[] blocks = new Block[size];
      if (penultimateBlock != null) {
        blocks[0] = penultimateBlock;
      }
      blocks[size - 1] = lastBlock;
      FSImageSerialization.writeCompactBlockArray(blocks, out);
      // clientId and callId
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      path = FSImageSerialization.readString(in);
      Block[] blocks = FSImageSerialization.readCompactBlockArray(in,
          logVersion);
      Preconditions.checkState(blocks.length == 2 || blocks.length == 1);
      penultimateBlock = blocks.length == 1 ? null : blocks[0];
      lastBlock = blocks[blocks.length - 1];
      readRpcIds(in, logVersion);
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append("AddBlockOp [path=")
          .append(path)
          .append(", penultimateBlock=")
          .append(penultimateBlock == null ? "NULL" : penultimateBlock)
          .append(", lastBlock=")
          .append(lastBlock);
      appendRpcIdsToString(sb, rpcClientId, rpcCallId);
      sb.append("]");
      return sb.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      if (penultimateBlock != null) {
        FSEditLogOp.blockToXml(contentHandler, penultimateBlock);
      }
      FSEditLogOp.blockToXml(contentHandler, lastBlock);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }
    
    @Override 
    void fromXml(Stanza st) throws InvalidXmlException {
      this.path = st.getValue("PATH");
      List<Stanza> blocks = st.getChildren("BLOCK");
      int size = blocks.size();
      Preconditions.checkState(size == 1 || size == 2);
      this.penultimateBlock = size == 2 ? 
          FSEditLogOp.blockFromXml(blocks.get(0)) : null;
      this.lastBlock = FSEditLogOp.blockFromXml(blocks.get(size - 1));
      readRpcIdsFromXml(st);
    }
  }
  
  /**
   * {@literal @AtMostOnce} for {@link ClientProtocol#updatePipeline}, but 
   * {@literal @Idempotent} for some other ops.
   */
  static class UpdateBlocksOp extends FSEditLogOp implements BlockListUpdatingOp {
    String path;
    Block[] blocks;
    
    UpdateBlocksOp() {
      super(OP_UPDATE_BLOCKS);
    }
    
    static UpdateBlocksOp getInstance(OpInstanceCache cache) {
      return (UpdateBlocksOp)cache.get(OP_UPDATE_BLOCKS);
    }

    @Override
    void resetSubFields() {
      path = null;
      blocks = null;
    }
    
    UpdateBlocksOp setPath(String path) {
      this.path = path;
      return this;
    }
    
    @Override
    public String getPath() {
      return path;
    }

    UpdateBlocksOp setBlocks(Block[] blocks) {
      this.blocks = blocks;
      return this;
    }
    
    @Override
    public Block[] getBlocks() {
      return blocks;
    }

    @Override
    public
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeCompactBlockArray(blocks, out);
      // clientId and callId
      writeRpcIds(rpcClientId, rpcCallId, out);
    }
    
    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      path = FSImageSerialization.readString(in);
      this.blocks = FSImageSerialization.readCompactBlockArray(
          in, logVersion);
      readRpcIds(in, logVersion);
    }

    @Override
    public boolean shouldCompleteLastBlock() {
      return false;
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append("UpdateBlocksOp [path=")
        .append(path)
        .append(", blocks=")
        .append(Arrays.toString(blocks));
      appendRpcIdsToString(sb, rpcClientId, rpcCallId);
      sb.append("]");
      return sb.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      for (Block b : blocks) {
        FSEditLogOp.blockToXml(contentHandler, b);
      }
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.path = st.getValue("PATH");
      List<Stanza> blocks = st.hasChildren("BLOCK") ?
          st.getChildren("BLOCK") : new ArrayList<Stanza>();
      this.blocks = new Block[blocks.size()];
      for (int i = 0; i < blocks.size(); i++) {
        this.blocks[i] = FSEditLogOp.blockFromXml(blocks.get(i));
      }
      readRpcIdsFromXml(st);
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#setReplication} */
  static class SetReplicationOp extends FSEditLogOp {
    String path;
    short replication;

    SetReplicationOp() {
      super(OP_SET_REPLICATION);
    }

    static SetReplicationOp getInstance(OpInstanceCache cache) {
      return (SetReplicationOp)cache.get(OP_SET_REPLICATION);
    }

    @Override
    void resetSubFields() {
      path = null;
      replication = 0;
    }

    SetReplicationOp setPath(String path) {
      this.path = path;
      return this;
    }

    SetReplicationOp setReplication(short replication) {
      this.replication = replication;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeShort(replication, out);
    }
    
    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.path = FSImageSerialization.readString(in);
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.replication = FSImageSerialization.readShort(in);
      } else {
        this.replication = readShort(in);
      }
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetReplicationOp [path=")
          .append(path)
          .append(", replication=")
          .append(replication)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "REPLICATION",
          Short.toString(replication));
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.path = st.getValue("PATH");
      this.replication = Short.parseShort(st.getValue("REPLICATION"));
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#concat} */
  static class ConcatDeleteOp extends FSEditLogOp {
    int length;
    String trg;
    String[] srcs;
    long timestamp;
    final static public int MAX_CONCAT_SRC = 1024 * 1024;

    ConcatDeleteOp() {
      super(OP_CONCAT_DELETE);
    }

    static ConcatDeleteOp getInstance(OpInstanceCache cache) {
      return (ConcatDeleteOp)cache.get(OP_CONCAT_DELETE);
    }

    @Override
    void resetSubFields() {
      length = 0;
      trg = null;
      srcs = null;
      timestamp = 0L;
    }

    ConcatDeleteOp setTarget(String trg) {
      this.trg = trg;
      return this;
    }

    ConcatDeleteOp setSources(String[] srcs) {
      if (srcs.length > MAX_CONCAT_SRC) {
        throw new RuntimeException("ConcatDeleteOp can only have " +
            MAX_CONCAT_SRC + " sources at most.");
      }
      this.srcs = srcs;

      return this;
    }

    ConcatDeleteOp setTimestamp(long timestamp) {
      this.timestamp = timestamp;
      return this;
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(trg, out);
            
      DeprecatedUTF8 info[] = new DeprecatedUTF8[srcs.length];
      int idx = 0;
      for(int i=0; i<srcs.length; i++) {
        info[idx++] = new DeprecatedUTF8(srcs[i]);
      }
      new ArrayWritable(DeprecatedUTF8.class, info).write(out);

      FSImageSerialization.writeLong(timestamp, out);
      
      // rpc ids
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
        if (length < 3) { // trg, srcs.., timestamp
          throw new IOException("Incorrect data format " +
              "for ConcatDeleteOp.");
        }
      }
      this.trg = FSImageSerialization.readString(in);
      int srcSize = 0;
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        srcSize = in.readInt();
      } else {
        srcSize = this.length - 1 - 1; // trg and timestamp
      }
      if (srcSize < 0) {
          throw new IOException("Incorrect data format. "
              + "ConcatDeleteOp cannot have a negative number of data " +
              " sources.");
      } else if (srcSize > MAX_CONCAT_SRC) {
          throw new IOException("Incorrect data format. "
              + "ConcatDeleteOp can have at most " + MAX_CONCAT_SRC +
              " sources, but we tried to have " + (length - 3) + " sources.");
      }
      this.srcs = new String [srcSize];
      for(int i=0; i<srcSize;i++) {
        srcs[i]= FSImageSerialization.readString(in);
      }
      
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.timestamp = FSImageSerialization.readLong(in);
      } else {
        this.timestamp = readLong(in);
      }
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("ConcatDeleteOp [length=")
          .append(length)
          .append(", trg=")
          .append(trg)
          .append(", srcs=")
          .append(Arrays.toString(srcs))
          .append(", timestamp=")
          .append(timestamp);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "TRG", trg);
      XMLUtils.addSaxString(contentHandler, "TIMESTAMP",
          Long.toString(timestamp));
      contentHandler.startElement("", "", "SOURCES", new AttributesImpl());
      for (int i = 0; i < srcs.length; ++i) {
        XMLUtils.addSaxString(contentHandler,
            "SOURCE" + (i + 1), srcs[i]);
      }
      contentHandler.endElement("", "", "SOURCES");
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.trg = st.getValue("TRG");
      this.timestamp = Long.parseLong(st.getValue("TIMESTAMP"));
      List<Stanza> sources = st.getChildren("SOURCES");
      int i = 0;
      while (true) {
        if (!sources.get(0).hasChildren("SOURCE" + (i + 1)))
          break;
        i++;
      }
      srcs = new String[i];
      for (i = 0; i < srcs.length; i++) {
        srcs[i] = sources.get(0).getValue("SOURCE" + (i + 1));
      }
      readRpcIdsFromXml(st);
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#rename} */
  static class RenameOldOp extends FSEditLogOp {
    int length;
    String src;
    String dst;
    long timestamp;

    RenameOldOp() {
      super(OP_RENAME_OLD);
    }

    static RenameOldOp getInstance(OpInstanceCache cache) {
      return (RenameOldOp)cache.get(OP_RENAME_OLD);
    }

    @Override
    void resetSubFields() {
      length = 0;
      src = null;
      dst = null;
      timestamp = 0L;
    }

    RenameOldOp setSource(String src) {
      this.src = src;
      return this;
    }

    RenameOldOp setDestination(String dst) {
      this.dst = dst;
      return this;
    }

    RenameOldOp setTimestamp(long timestamp) {
      this.timestamp = timestamp;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(src, out);
      FSImageSerialization.writeString(dst, out);
      FSImageSerialization.writeLong(timestamp, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
        if (this.length != 3) {
          throw new IOException("Incorrect data format. "
              + "Old rename operation.");
        }
      }
      this.src = FSImageSerialization.readString(in);
      this.dst = FSImageSerialization.readString(in);
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.timestamp = FSImageSerialization.readLong(in);
      } else {
        this.timestamp = readLong(in);
      }
      
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("RenameOldOp [length=")
          .append(length)
          .append(", src=")
          .append(src)
          .append(", dst=")
          .append(dst)
          .append(", timestamp=")
          .append(timestamp);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      XMLUtils.addSaxString(contentHandler, "DST", dst);
      XMLUtils.addSaxString(contentHandler, "TIMESTAMP",
          Long.toString(timestamp));
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }
    
    @Override 
    void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.src = st.getValue("SRC");
      this.dst = st.getValue("DST");
      this.timestamp = Long.parseLong(st.getValue("TIMESTAMP"));
      
      readRpcIdsFromXml(st);
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#delete} */
  static class DeleteOp extends FSEditLogOp {
    int length;
    String path;
    long timestamp;

    DeleteOp() {
      super(OP_DELETE);
    }

    static DeleteOp getInstance(OpInstanceCache cache) {
      return (DeleteOp)cache.get(OP_DELETE);
    }

    @Override
    void resetSubFields() {
      length = 0;
      path = null;
      timestamp = 0L;
    }

    DeleteOp setPath(String path) {
      this.path = path;
      return this;
    }

    DeleteOp setTimestamp(long timestamp) {
      this.timestamp = timestamp;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeLong(timestamp, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
        if (this.length != 2) {
          throw new IOException("Incorrect data format. " + "delete operation.");
        }
      }
      this.path = FSImageSerialization.readString(in);
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.timestamp = FSImageSerialization.readLong(in);
      } else {
        this.timestamp = readLong(in);
      }
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("DeleteOp [length=")
          .append(length)
          .append(", path=")
          .append(path)
          .append(", timestamp=")
          .append(timestamp);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "TIMESTAMP",
          Long.toString(timestamp));
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.path = st.getValue("PATH");
      this.timestamp = Long.parseLong(st.getValue("TIMESTAMP"));
      
      readRpcIdsFromXml(st);
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#mkdirs} */
  static class MkdirOp extends FSEditLogOp {
    int length;
    long inodeId;
    String path;
    long timestamp;
    PermissionStatus permissions;
    List<AclEntry> aclEntries;
    List<XAttr> xAttrs;

    MkdirOp() {
      super(OP_MKDIR);
    }
    
    static MkdirOp getInstance(OpInstanceCache cache) {
      return (MkdirOp)cache.get(OP_MKDIR);
    }

    @Override
    void resetSubFields() {
      length = 0;
      inodeId = 0L;
      path = null;
      timestamp = 0L;
      permissions = null;
      aclEntries = null;
      xAttrs = null;
    }

    MkdirOp setInodeId(long inodeId) {
      this.inodeId = inodeId;
      return this;
    }
    
    MkdirOp setPath(String path) {
      this.path = path;
      return this;
    }

    MkdirOp setTimestamp(long timestamp) {
      this.timestamp = timestamp;
      return this;
    }

    MkdirOp setPermissionStatus(PermissionStatus permissions) {
      this.permissions = permissions;
      return this;
    }

    MkdirOp setAclEntries(List<AclEntry> aclEntries) {
      this.aclEntries = aclEntries;
      return this;
    }

    MkdirOp setXAttrs(List<XAttr> xAttrs) {
      this.xAttrs = xAttrs;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(inodeId, out);
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeLong(timestamp, out); // mtime
      FSImageSerialization.writeLong(timestamp, out); // atime, unused at this
      permissions.write(out);
      AclEditLogUtil.write(aclEntries, out);
      XAttrEditLogProto.Builder b = XAttrEditLogProto.newBuilder();
      b.addAllXAttrs(PBHelperClient.convertXAttrProto(xAttrs));
      b.build().writeDelimitedTo(out);
    }
    
    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
      }
      if (-17 < logVersion && length != 2 ||
          logVersion <= -17 && length != 3
          && !NameNodeLayoutVersion.supports(
              LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        throw new IOException("Incorrect data format. Mkdir operation.");
      }
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.ADD_INODE_ID, logVersion)) {
        this.inodeId = FSImageSerialization.readLong(in);
      } else {
        // This id should be updated when this editLogOp is applied
        this.inodeId = HdfsConstants.GRANDFATHER_INODE_ID;
      }
      this.path = FSImageSerialization.readString(in);
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.timestamp = FSImageSerialization.readLong(in);
      } else {
        this.timestamp = readLong(in);
      }

      // The disk format stores atimes for directories as well.
      // However, currently this is not being updated/used because of
      // performance reasons.
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.FILE_ACCESS_TIME, logVersion)) {
        if (NameNodeLayoutVersion.supports(
            LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
          FSImageSerialization.readLong(in);
        } else {
          readLong(in);
        }
      }

      this.permissions = PermissionStatus.read(in);
      aclEntries = AclEditLogUtil.read(in, logVersion);

      xAttrs = readXAttrsFromEditLog(in, logVersion);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("MkdirOp [length=")
          .append(length)
          .append(", inodeId=")
          .append(inodeId)
          .append(", path=")
          .append(path)
          .append(", timestamp=")
          .append(timestamp)
          .append(", permissions=")
          .append(permissions)
          .append(", aclEntries=")
          .append(aclEntries)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append(", xAttrs=")
          .append(xAttrs)
          .append("]");
      return builder.toString();
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "INODEID",
          Long.toString(inodeId));
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "TIMESTAMP",
          Long.toString(timestamp));
      FSEditLogOp.permissionStatusToXml(contentHandler, permissions);
      if (aclEntries != null) {
        appendAclEntriesToXml(contentHandler, aclEntries);
      }
      if (xAttrs != null) {
        appendXAttrsToXml(contentHandler, xAttrs);
      }
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.inodeId = Long.parseLong(st.getValue("INODEID"));
      this.path = st.getValue("PATH");
      this.timestamp = Long.parseLong(st.getValue("TIMESTAMP"));
      this.permissions = permissionStatusFromXml(st);
      aclEntries = readAclEntriesFromXml(st);
      xAttrs = readXAttrsFromXml(st);
    }
  }

  /**
   * The corresponding operations are either {@literal @Idempotent} (
   * {@link ClientProtocol#updateBlockForPipeline},
   * {@link ClientProtocol#recoverLease}, {@link ClientProtocol#addBlock}) or
   * already bound with other editlog op which records rpc ids (
   * {@link ClientProtocol#create}). Thus no need to record rpc ids here.
   */
  static class SetGenstampV1Op extends FSEditLogOp {
    long genStampV1;

    SetGenstampV1Op() {
      super(OP_SET_GENSTAMP_V1);
    }

    static SetGenstampV1Op getInstance(OpInstanceCache cache) {
      return (SetGenstampV1Op)cache.get(OP_SET_GENSTAMP_V1);
    }

    @Override
    void resetSubFields() {
      genStampV1 = 0L;
    }

    SetGenstampV1Op setGenerationStamp(long genStamp) {
      this.genStampV1 = genStamp;
      return this;
    }

    @Override
    public
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(genStampV1, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.genStampV1 = FSImageSerialization.readLong(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetGenstampOp [GenStamp=")
          .append(genStampV1)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "GENSTAMP",
                            Long.toString(genStampV1));
    }

    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.genStampV1 = Long.parseLong(st.getValue("GENSTAMP"));
    }
  }

  /** Similar with {@link SetGenstampV1Op} */
  static class SetGenstampV2Op extends FSEditLogOp {
    long genStampV2;

    SetGenstampV2Op() {
      super(OP_SET_GENSTAMP_V2);
    }

    static SetGenstampV2Op getInstance(OpInstanceCache cache) {
      return (SetGenstampV2Op)cache.get(OP_SET_GENSTAMP_V2);
    }

    @Override
    void resetSubFields() {
      genStampV2 = 0L;
    }

    SetGenstampV2Op setGenerationStamp(long genStamp) {
      this.genStampV2 = genStamp;
      return this;
    }

    @Override
    public
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(genStampV2, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.genStampV2 = FSImageSerialization.readLong(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetGenstampV2Op [GenStampV2=")
          .append(genStampV2)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "GENSTAMPV2",
                            Long.toString(genStampV2));
    }

    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.genStampV2 = Long.parseLong(st.getValue("GENSTAMPV2"));
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#addBlock} */
  static class AllocateBlockIdOp extends FSEditLogOp {
    long blockId;

    AllocateBlockIdOp() {
      super(OP_ALLOCATE_BLOCK_ID);
    }

    static AllocateBlockIdOp getInstance(OpInstanceCache cache) {
      return (AllocateBlockIdOp)cache.get(OP_ALLOCATE_BLOCK_ID);
    }

    @Override
    void resetSubFields() {
      blockId = 0L;
    }

    AllocateBlockIdOp setBlockId(long blockId) {
      this.blockId = blockId;
      return this;
    }

    @Override
    public
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(blockId, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.blockId = FSImageSerialization.readLong(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("AllocateBlockIdOp [blockId=")
          .append(blockId)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "BLOCK_ID",
                            Long.toString(blockId));
    }

    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.blockId = Long.parseLong(st.getValue("BLOCK_ID"));
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#setPermission} */
  static class SetPermissionsOp extends FSEditLogOp {
    String src;
    FsPermission permissions;

    SetPermissionsOp() {
      super(OP_SET_PERMISSIONS);
    }

    static SetPermissionsOp getInstance(OpInstanceCache cache) {
      return (SetPermissionsOp)cache.get(OP_SET_PERMISSIONS);
    }

    @Override
    void resetSubFields() {
      src = null;
      permissions = null;
    }

    SetPermissionsOp setSource(String src) {
      this.src = src;
      return this;
    }

    SetPermissionsOp setPermissions(FsPermission permissions) {
      this.permissions = permissions;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(src, out);
      permissions.write(out);
     }
 
    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.src = FSImageSerialization.readString(in);
      this.permissions = FsPermission.read(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetPermissionsOp [src=")
          .append(src)
          .append(", permissions=")
          .append(permissions)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      XMLUtils.addSaxString(contentHandler, "MODE",
          Short.toString(permissions.toShort()));
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.src = st.getValue("SRC");
      this.permissions = new FsPermission(
          Short.parseShort(st.getValue("MODE")));
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#setOwner} */
  static class SetOwnerOp extends FSEditLogOp {
    String src;
    String username;
    String groupname;

    SetOwnerOp() {
      super(OP_SET_OWNER);
    }

    static SetOwnerOp getInstance(OpInstanceCache cache) {
      return (SetOwnerOp)cache.get(OP_SET_OWNER);
    }

    @Override
    void resetSubFields() {
      src = null;
      username = null;
      groupname = null;
    }

    SetOwnerOp setSource(String src) {
      this.src = src;
      return this;
    }

    SetOwnerOp setUser(String username) {
      this.username = username;
      return this;
    }

    SetOwnerOp setGroup(String groupname) {
      this.groupname = groupname;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(src, out);
      FSImageSerialization.writeString(username == null ? "" : username, out);
      FSImageSerialization.writeString(groupname == null ? "" : groupname, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.src = FSImageSerialization.readString(in);
      this.username = FSImageSerialization.readString_EmptyAsNull(in);
      this.groupname = FSImageSerialization.readString_EmptyAsNull(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetOwnerOp [src=")
          .append(src)
          .append(", username=")
          .append(username)
          .append(", groupname=")
          .append(groupname)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      if (username != null) {
        XMLUtils.addSaxString(contentHandler, "USERNAME", username);
      }
      if (groupname != null) {
        XMLUtils.addSaxString(contentHandler, "GROUPNAME", groupname);
      }
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.src = st.getValue("SRC");
      this.username = (st.hasChildren("USERNAME")) ? 
          st.getValue("USERNAME") : null;
      this.groupname = (st.hasChildren("GROUPNAME")) ? 
          st.getValue("GROUPNAME") : null;
    }
  }
  
  static class SetNSQuotaOp extends FSEditLogOp {
    String src;
    long nsQuota;

    SetNSQuotaOp() {
      super(OP_SET_NS_QUOTA);
    }

    static SetNSQuotaOp getInstance(OpInstanceCache cache) {
      return (SetNSQuotaOp)cache.get(OP_SET_NS_QUOTA);
    }

    @Override
    void resetSubFields() {
      src = null;
      nsQuota = 0L;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      throw new IOException("Deprecated");      
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.src = FSImageSerialization.readString(in);
      this.nsQuota = FSImageSerialization.readLong(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetNSQuotaOp [src=")
          .append(src)
          .append(", nsQuota=")
          .append(nsQuota)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      XMLUtils.addSaxString(contentHandler, "NSQUOTA",
          Long.toString(nsQuota));
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.src = st.getValue("SRC");
      this.nsQuota = Long.parseLong(st.getValue("NSQUOTA"));
    }
  }

  static class ClearNSQuotaOp extends FSEditLogOp {
    String src;

    ClearNSQuotaOp() {
      super(OP_CLEAR_NS_QUOTA);
    }

    static ClearNSQuotaOp getInstance(OpInstanceCache cache) {
      return (ClearNSQuotaOp)cache.get(OP_CLEAR_NS_QUOTA);
    }

    @Override
    void resetSubFields() {
      src = null;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      throw new IOException("Deprecated");      
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.src = FSImageSerialization.readString(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("ClearNSQuotaOp [src=")
          .append(src)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.src = st.getValue("SRC");
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#setQuota} */
  static class SetQuotaOp extends FSEditLogOp {
    String src;
    long nsQuota;
    long dsQuota;

    SetQuotaOp() {
      super(OP_SET_QUOTA);
    }

    static SetQuotaOp getInstance(OpInstanceCache cache) {
      return (SetQuotaOp)cache.get(OP_SET_QUOTA);
    }

    @Override
    void resetSubFields() {
      src = null;
      nsQuota = 0L;
      dsQuota = 0L;
    }

    SetQuotaOp setSource(String src) {
      this.src = src;
      return this;
    }

    SetQuotaOp setNSQuota(long nsQuota) {
      this.nsQuota = nsQuota;
      return this;
    }

    SetQuotaOp setDSQuota(long dsQuota) {
      this.dsQuota = dsQuota;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(src, out);
      FSImageSerialization.writeLong(nsQuota, out);
      FSImageSerialization.writeLong(dsQuota, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.src = FSImageSerialization.readString(in);
      this.nsQuota = FSImageSerialization.readLong(in);
      this.dsQuota = FSImageSerialization.readLong(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetQuotaOp [src=")
          .append(src)
          .append(", nsQuota=")
          .append(nsQuota)
          .append(", dsQuota=")
          .append(dsQuota)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      XMLUtils.addSaxString(contentHandler, "NSQUOTA",
          Long.toString(nsQuota));
      XMLUtils.addSaxString(contentHandler, "DSQUOTA",
          Long.toString(dsQuota));
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.src = st.getValue("SRC");
      this.nsQuota = Long.parseLong(st.getValue("NSQUOTA"));
      this.dsQuota = Long.parseLong(st.getValue("DSQUOTA"));
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#setQuota} */
  static class SetQuotaByStorageTypeOp extends FSEditLogOp {
    String src;
    long dsQuota;
    StorageType type;

    SetQuotaByStorageTypeOp() {
      super(OP_SET_QUOTA_BY_STORAGETYPE);
    }

    static SetQuotaByStorageTypeOp getInstance(OpInstanceCache cache) {
      return (SetQuotaByStorageTypeOp)cache.get(OP_SET_QUOTA_BY_STORAGETYPE);
    }

    @Override
    void resetSubFields() {
      src = null;
      dsQuota = -1L;
      type = StorageType.DEFAULT;
    }

    SetQuotaByStorageTypeOp setSource(String src) {
      this.src = src;
      return this;
    }

    SetQuotaByStorageTypeOp setQuotaByStorageType(long dsQuota, StorageType type) {
      this.type = type;
      this.dsQuota = dsQuota;
      return this;
    }

    @Override
    public
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(src, out);
      FSImageSerialization.writeInt(type.ordinal(), out);
      FSImageSerialization.writeLong(dsQuota, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
      throws IOException {
      this.src = FSImageSerialization.readString(in);
      this.type = StorageType.parseStorageType(FSImageSerialization.readInt(in));
      this.dsQuota = FSImageSerialization.readLong(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetTypeQuotaOp [src=")
          .append(src)
          .append(", storageType=")
          .append(type)
          .append(", dsQuota=")
          .append(dsQuota)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      XMLUtils.addSaxString(contentHandler, "STORAGETYPE",
        Integer.toString(type.ordinal()));
      XMLUtils.addSaxString(contentHandler, "DSQUOTA",
        Long.toString(dsQuota));
    }

    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.src = st.getValue("SRC");
      this.type = StorageType.parseStorageType(
          Integer.parseInt(st.getValue("STORAGETYPE")));
      this.dsQuota = Long.parseLong(st.getValue("DSQUOTA"));
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#setTimes} */
  static class TimesOp extends FSEditLogOp {
    int length;
    String path;
    long mtime;
    long atime;

    TimesOp() {
      super(OP_TIMES);
    }

    static TimesOp getInstance(OpInstanceCache cache) {
      return (TimesOp)cache.get(OP_TIMES);
    }

    @Override
    void resetSubFields() {
      length = 0;
      path = null;
      mtime = 0L;
      atime = 0L;
    }

    TimesOp setPath(String path) {
      this.path = path;
      return this;
    }

    TimesOp setModificationTime(long mtime) {
      this.mtime = mtime;
      return this;
    }

    TimesOp setAccessTime(long atime) {
      this.atime = atime;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeLong(mtime, out);
      FSImageSerialization.writeLong(atime, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
        if (length != 3) {
          throw new IOException("Incorrect data format. " + "times operation.");
        }
      }
      this.path = FSImageSerialization.readString(in);

      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.mtime = FSImageSerialization.readLong(in);
        this.atime = FSImageSerialization.readLong(in);
      } else {
        this.mtime = readLong(in);
        this.atime = readLong(in);
      }
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("TimesOp [length=")
          .append(length)
          .append(", path=")
          .append(path)
          .append(", mtime=")
          .append(mtime)
          .append(", atime=")
          .append(atime)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid);
      builder.append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "MTIME",
          Long.toString(mtime));
      XMLUtils.addSaxString(contentHandler, "ATIME",
          Long.toString(atime));
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.path = st.getValue("PATH");
      this.mtime = Long.parseLong(st.getValue("MTIME"));
      this.atime = Long.parseLong(st.getValue("ATIME"));
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#createSymlink} */
  static class SymlinkOp extends FSEditLogOp {
    int length;
    long inodeId;
    String path;
    String value;
    long mtime;
    long atime;
    PermissionStatus permissionStatus;

    SymlinkOp() {
      super(OP_SYMLINK);
    }

    static SymlinkOp getInstance(OpInstanceCache cache) {
      return (SymlinkOp)cache.get(OP_SYMLINK);
    }

    @Override
    void resetSubFields() {
      length = 0;
      inodeId = 0L;
      path = null;
      value = null;
      mtime = 0L;
      atime = 0L;
      permissionStatus = null;
    }

    SymlinkOp setId(long inodeId) {
      this.inodeId = inodeId;
      return this;
    }
    
    SymlinkOp setPath(String path) {
      this.path = path;
      return this;
    }

    SymlinkOp setValue(String value) {
      this.value = value;
      return this;
    }

    SymlinkOp setModificationTime(long mtime) {
      this.mtime = mtime;
      return this;
    }

    SymlinkOp setAccessTime(long atime) {
      this.atime = atime;
      return this;
    }

    SymlinkOp setPermissionStatus(PermissionStatus permissionStatus) {
      this.permissionStatus = permissionStatus;
      return this;
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(inodeId, out);      
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeString(value, out);
      FSImageSerialization.writeLong(mtime, out);
      FSImageSerialization.writeLong(atime, out);
      permissionStatus.write(out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
        if (this.length != 4) {
          throw new IOException("Incorrect data format. "
              + "symlink operation.");
        }
      }
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.ADD_INODE_ID, logVersion)) {
        this.inodeId = FSImageSerialization.readLong(in);
      } else {
        // This id should be updated when the editLogOp is applied
        this.inodeId = HdfsConstants.GRANDFATHER_INODE_ID;
      }
      this.path = FSImageSerialization.readString(in);
      this.value = FSImageSerialization.readString(in);

      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.mtime = FSImageSerialization.readLong(in);
        this.atime = FSImageSerialization.readLong(in);
      } else {
        this.mtime = readLong(in);
        this.atime = readLong(in);
      }
      this.permissionStatus = PermissionStatus.read(in);
      
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SymlinkOp [length=")
          .append(length)
          .append(", inodeId=")
          .append(inodeId)
          .append(", path=")
          .append(path)
          .append(", value=")
          .append(value)
          .append(", mtime=")
          .append(mtime)
          .append(", atime=")
          .append(atime)
          .append(", permissionStatus=")
          .append(permissionStatus);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "INODEID",
          Long.toString(inodeId));
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "VALUE", value);
      XMLUtils.addSaxString(contentHandler, "MTIME",
          Long.toString(mtime));
      XMLUtils.addSaxString(contentHandler, "ATIME",
          Long.toString(atime));
      FSEditLogOp.permissionStatusToXml(contentHandler, permissionStatus);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override 
    void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.inodeId = Long.parseLong(st.getValue("INODEID"));
      this.path = st.getValue("PATH");
      this.value = st.getValue("VALUE");
      this.mtime = Long.parseLong(st.getValue("MTIME"));
      this.atime = Long.parseLong(st.getValue("ATIME"));
      this.permissionStatus = permissionStatusFromXml(st);
      
      readRpcIdsFromXml(st);
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#rename2} */
  static class RenameOp extends FSEditLogOp {
    int length;
    String src;
    String dst;
    long timestamp;
    Rename[] options;

    RenameOp() {
      super(OP_RENAME);
    }

    static RenameOp getInstance(OpInstanceCache cache) {
      return (RenameOp)cache.get(OP_RENAME);
    }

    @Override
    void resetSubFields() {
      length = 0;
      src = null;
      dst = null;
      timestamp = 0L;
      options = null;
    }

    RenameOp setSource(String src) {
      this.src = src;
      return this;
    }

    RenameOp setDestination(String dst) {
      this.dst = dst;
      return this;
    }
    
    RenameOp setTimestamp(long timestamp) {
      this.timestamp = timestamp;
      return this;
    }
    
    RenameOp setOptions(Rename[] options) {
      this.options = options;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(src, out);
      FSImageSerialization.writeString(dst, out);
      FSImageSerialization.writeLong(timestamp, out);
      toBytesWritable(options).write(out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.length = in.readInt();
        if (this.length != 3) {
          throw new IOException("Incorrect data format. " + "Rename operation.");
        }
      }
      this.src = FSImageSerialization.readString(in);
      this.dst = FSImageSerialization.readString(in);

      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.timestamp = FSImageSerialization.readLong(in);
      } else {
        this.timestamp = readLong(in);
      }
      this.options = readRenameOptions(in);
      
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    private static Rename[] readRenameOptions(DataInputStream in) throws IOException {
      BytesWritable writable = new BytesWritable();
      writable.readFields(in);

      byte[] bytes = writable.getBytes();
      int len = writable.getLength();
      Rename[] options = new Rename[len];

      for (int i = 0; i < len; i++) {
        options[i] = Rename.valueOf(bytes[i]);
      }
      return options;
    }

    static BytesWritable toBytesWritable(Rename... options) {
      byte[] bytes = new byte[options.length];
      for (int i = 0; i < options.length; i++) {
        bytes[i] = options[i].value();
      }
      return new BytesWritable(bytes);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("RenameOp [length=")
          .append(length)
          .append(", src=")
          .append(src)
          .append(", dst=")
          .append(dst)
          .append(", timestamp=")
          .append(timestamp)
          .append(", options=")
          .append(Arrays.toString(options));
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LENGTH",
          Integer.toString(length));
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      XMLUtils.addSaxString(contentHandler, "DST", dst);
      XMLUtils.addSaxString(contentHandler, "TIMESTAMP",
          Long.toString(timestamp));
      StringBuilder bld = new StringBuilder();
      String prefix = "";
      for (Rename r : options) {
        bld.append(prefix).append(r.toString());
        prefix = "|";
      }
      XMLUtils.addSaxString(contentHandler, "OPTIONS", bld.toString());
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.length = Integer.parseInt(st.getValue("LENGTH"));
      this.src = st.getValue("SRC");
      this.dst = st.getValue("DST");
      this.timestamp = Long.parseLong(st.getValue("TIMESTAMP"));
      String opts = st.getValue("OPTIONS");
      String o[] = opts.split("\\|");
      this.options = new Rename[o.length];
      for (int i = 0; i < o.length; i++) {
        if (o[i].equals(""))
          continue;
        try {
          this.options[i] = Rename.valueOf(o[i]);
        } finally {
          if (this.options[i] == null) {
            System.err.println("error parsing Rename value: \"" + o[i] + "\"");
          }
        }
      }
      readRpcIdsFromXml(st);
    }
  }

  static class TruncateOp extends FSEditLogOp {
    String src;
    String clientName;
    String clientMachine;
    long newLength;
    long timestamp;
    Block truncateBlock;

    TruncateOp() {
      super(OP_TRUNCATE);
    }

    static TruncateOp getInstance(OpInstanceCache cache) {
      return (TruncateOp)cache.get(OP_TRUNCATE);
    }

    @Override
    void resetSubFields() {
      src = null;
      clientName = null;
      clientMachine = null;
      newLength = 0L;
      timestamp = 0L;
    }

    TruncateOp setPath(String src) {
      this.src = src;
      return this;
    }

    TruncateOp setClientName(String clientName) {
      this.clientName = clientName;
      return this;
    }

    TruncateOp setClientMachine(String clientMachine) {
      this.clientMachine = clientMachine;
      return this;
    }

    TruncateOp setNewLength(long newLength) {
      this.newLength = newLength;
      return this;
    }

    TruncateOp setTimestamp(long timestamp) {
      this.timestamp = timestamp;
      return this;
    }

    TruncateOp setTruncateBlock(Block truncateBlock) {
      this.truncateBlock = truncateBlock;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      src = FSImageSerialization.readString(in);
      clientName = FSImageSerialization.readString(in);
      clientMachine = FSImageSerialization.readString(in);
      newLength = FSImageSerialization.readLong(in);
      timestamp = FSImageSerialization.readLong(in);
      Block[] blocks =
          FSImageSerialization.readCompactBlockArray(in, logVersion);
      assert blocks.length <= 1 : "Truncate op should have 1 or 0 blocks";
      truncateBlock = (blocks.length == 0) ? null : blocks[0];
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(src, out);
      FSImageSerialization.writeString(clientName, out);
      FSImageSerialization.writeString(clientMachine, out);
      FSImageSerialization.writeLong(newLength, out);
      FSImageSerialization.writeLong(timestamp, out);
      int size = truncateBlock != null ? 1 : 0;
      Block[] blocks = new Block[size];
      if (truncateBlock != null) {
        blocks[0] = truncateBlock;
      }
      FSImageSerialization.writeCompactBlockArray(blocks, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      XMLUtils.addSaxString(contentHandler, "CLIENTNAME", clientName);
      XMLUtils.addSaxString(contentHandler, "CLIENTMACHINE", clientMachine);
      XMLUtils.addSaxString(contentHandler, "NEWLENGTH",
          Long.toString(newLength));
      XMLUtils.addSaxString(contentHandler, "TIMESTAMP",
          Long.toString(timestamp));
      if(truncateBlock != null)
        FSEditLogOp.blockToXml(contentHandler, truncateBlock);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.src = st.getValue("SRC");
      this.clientName = st.getValue("CLIENTNAME");
      this.clientMachine = st.getValue("CLIENTMACHINE");
      this.newLength = Long.parseLong(st.getValue("NEWLENGTH"));
      this.timestamp = Long.parseLong(st.getValue("TIMESTAMP"));
      if (st.hasChildren("BLOCK"))
        this.truncateBlock = FSEditLogOp.blockFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("TruncateOp [src=")
          .append(src)
          .append(", clientName=")
          .append(clientName)
          .append(", clientMachine=")
          .append(clientMachine)
          .append(", newLength=")
          .append(newLength)
          .append(", timestamp=")
          .append(timestamp)
          .append(", truncateBlock=")
          .append(truncateBlock)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
  }
 
  /**
   * {@literal @Idempotent} for {@link ClientProtocol#recoverLease}. In the
   * meanwhile, startFile and appendFile both have their own corresponding
   * editlog op.
   */
  static class ReassignLeaseOp extends FSEditLogOp {
    String leaseHolder;
    String path;
    String newHolder;

    ReassignLeaseOp() {
      super(OP_REASSIGN_LEASE);
    }

    static ReassignLeaseOp getInstance(OpInstanceCache cache) {
      return (ReassignLeaseOp)cache.get(OP_REASSIGN_LEASE);
    }

    @Override
    void resetSubFields() {
      leaseHolder = null;
      path = null;
      newHolder = null;
    }

    ReassignLeaseOp setLeaseHolder(String leaseHolder) {
      this.leaseHolder = leaseHolder;
      return this;
    }

    ReassignLeaseOp setPath(String path) {
      this.path = path;
      return this;
    }

    ReassignLeaseOp setNewHolder(String newHolder) {
      this.newHolder = newHolder;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(leaseHolder, out);
      FSImageSerialization.writeString(path, out);
      FSImageSerialization.writeString(newHolder, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.leaseHolder = FSImageSerialization.readString(in);
      this.path = FSImageSerialization.readString(in);
      this.newHolder = FSImageSerialization.readString(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("ReassignLeaseOp [leaseHolder=")
          .append(leaseHolder)
          .append(", path=")
          .append(path)
          .append(", newHolder=")
          .append(newHolder)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid);
      builder.append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "LEASEHOLDER", leaseHolder);
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "NEWHOLDER", newHolder);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.leaseHolder = st.getValue("LEASEHOLDER");
      this.path = st.getValue("PATH");
      this.newHolder = st.getValue("NEWHOLDER");
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#getDelegationToken} */
  static class GetDelegationTokenOp extends FSEditLogOp {
    DelegationTokenIdentifier token;
    long expiryTime;

    GetDelegationTokenOp() {
      super(OP_GET_DELEGATION_TOKEN);
    }

    static GetDelegationTokenOp getInstance(OpInstanceCache cache) {
      return (GetDelegationTokenOp)cache.get(OP_GET_DELEGATION_TOKEN);
    }

    @Override
    void resetSubFields() {
      token = null;
      expiryTime = 0L;
    }

    GetDelegationTokenOp setDelegationTokenIdentifier(
        DelegationTokenIdentifier token) {
      this.token = token;
      return this;
    }

    GetDelegationTokenOp setExpiryTime(long expiryTime) {
      this.expiryTime = expiryTime;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      token.write(out);
      FSImageSerialization.writeLong(expiryTime, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.token = new DelegationTokenIdentifier();
      this.token.readFields(in);
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.expiryTime = FSImageSerialization.readLong(in);
      } else {
        this.expiryTime = readLong(in);
      }
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("GetDelegationTokenOp [token=")
          .append(token)
          .append(", expiryTime=")
          .append(expiryTime)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSEditLogOp.delegationTokenToXml(contentHandler, token);
      XMLUtils.addSaxString(contentHandler, "EXPIRY_TIME",
          Long.toString(expiryTime));
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.token = delegationTokenFromXml(st.getChildren(
          "DELEGATION_TOKEN_IDENTIFIER").get(0));
      this.expiryTime = Long.parseLong(st.getValue("EXPIRY_TIME"));
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#renewDelegationToken} */
  static class RenewDelegationTokenOp extends FSEditLogOp {
    DelegationTokenIdentifier token;
    long expiryTime;

    RenewDelegationTokenOp() {
      super(OP_RENEW_DELEGATION_TOKEN);
    }

    static RenewDelegationTokenOp getInstance(OpInstanceCache cache) {
      return (RenewDelegationTokenOp)cache.get(OP_RENEW_DELEGATION_TOKEN);
    }

    @Override
    void resetSubFields() {
      token = null;
      expiryTime = 0L;
    }

    RenewDelegationTokenOp setDelegationTokenIdentifier(
        DelegationTokenIdentifier token) {
      this.token = token;
      return this;
    }

    RenewDelegationTokenOp setExpiryTime(long expiryTime) {
      this.expiryTime = expiryTime;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      token.write(out);
      FSImageSerialization.writeLong(expiryTime, out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.token = new DelegationTokenIdentifier();
      this.token.readFields(in);
      if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITLOG_OP_OPTIMIZATION, logVersion)) {
        this.expiryTime = FSImageSerialization.readLong(in);
      } else {
        this.expiryTime = readLong(in);
      }
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("RenewDelegationTokenOp [token=")
          .append(token)
          .append(", expiryTime=")
          .append(expiryTime)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSEditLogOp.delegationTokenToXml(contentHandler, token);
      XMLUtils.addSaxString(contentHandler, "EXPIRY_TIME",
          Long.toString(expiryTime));
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.token = delegationTokenFromXml(st.getChildren(
          "DELEGATION_TOKEN_IDENTIFIER").get(0));
      this.expiryTime = Long.parseLong(st.getValue("EXPIRY_TIME"));
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#cancelDelegationToken} */
  static class CancelDelegationTokenOp extends FSEditLogOp {
    DelegationTokenIdentifier token;

    CancelDelegationTokenOp() {
      super(OP_CANCEL_DELEGATION_TOKEN);
    }

    static CancelDelegationTokenOp getInstance(OpInstanceCache cache) {
      return (CancelDelegationTokenOp)cache.get(OP_CANCEL_DELEGATION_TOKEN);
    }

    @Override
    void resetSubFields() {
      token = null;
    }

    CancelDelegationTokenOp setDelegationTokenIdentifier(
        DelegationTokenIdentifier token) {
      this.token = token;
      return this;
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      token.write(out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.token = new DelegationTokenIdentifier();
      this.token.readFields(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("CancelDelegationTokenOp [token=")
          .append(token)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSEditLogOp.delegationTokenToXml(contentHandler, token);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.token = delegationTokenFromXml(st.getChildren(
          "DELEGATION_TOKEN_IDENTIFIER").get(0));
    }
  }

  static class UpdateMasterKeyOp extends FSEditLogOp {
    DelegationKey key;

    UpdateMasterKeyOp() {
      super(OP_UPDATE_MASTER_KEY);
    }

    static UpdateMasterKeyOp getInstance(OpInstanceCache cache) {
      return (UpdateMasterKeyOp)cache.get(OP_UPDATE_MASTER_KEY);
    }

    @Override
    void resetSubFields() {
      key = null;
    }

    UpdateMasterKeyOp setDelegationKey(DelegationKey key) {
      this.key = key;
      return this;
    }
    
    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
      key.write(out);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.key = new DelegationKey();
      this.key.readFields(in);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("UpdateMasterKeyOp [key=")
          .append(key)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSEditLogOp.delegationKeyToXml(contentHandler, key);
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      this.key = delegationKeyFromXml(st.getChildren(
          "DELEGATION_KEY").get(0));
    }
  }
  
  static class LogSegmentOp extends FSEditLogOp {
    private LogSegmentOp(FSEditLogOpCodes code) {
      super(code);
      assert code == OP_START_LOG_SEGMENT ||
             code == OP_END_LOG_SEGMENT : "Bad op: " + code;
    }

    static LogSegmentOp getInstance(OpInstanceCache cache,
        FSEditLogOpCodes code) {
      return (LogSegmentOp)cache.get(code);
    }

    @Override
    void resetSubFields() {
      // no data stored in these ops yet
    }

    @Override
    public void readFields(DataInputStream in, int logVersion)
        throws IOException {
      // no data stored in these ops yet
    }

    @Override
    public
    void writeFields(DataOutputStream out) throws IOException {
      // no data stored
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("LogSegmentOp [opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      // no data stored
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      // do nothing
    }
  }

  static class StartLogSegmentOp extends LogSegmentOp {
    StartLogSegmentOp() {
      super(OP_START_LOG_SEGMENT);
    }
  }

  static class EndLogSegmentOp extends LogSegmentOp {
    EndLogSegmentOp() {
      super(OP_END_LOG_SEGMENT);
    }
  }

  static class InvalidOp extends FSEditLogOp {
    InvalidOp() {
      super(OP_INVALID);
    }

    static InvalidOp getInstance(OpInstanceCache cache) {
      return (InvalidOp)cache.get(OP_INVALID);
    }

    @Override
    void resetSubFields() {
    }

    @Override
    public 
    void writeFields(DataOutputStream out) throws IOException {
    }
    
    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      // nothing to read
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("InvalidOp [opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }
    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      // no data stored
    }
    
    @Override void fromXml(Stanza st) throws InvalidXmlException {
      // do nothing
    }
  }

  /**
   * Operation corresponding to creating a snapshot.
   * {@literal @AtMostOnce} for {@link ClientProtocol#createSnapshot}.
   */
  static class CreateSnapshotOp extends FSEditLogOp {
    String snapshotRoot;
    String snapshotName;
    
    public CreateSnapshotOp() {
      super(OP_CREATE_SNAPSHOT);
    }
    
    static CreateSnapshotOp getInstance(OpInstanceCache cache) {
      return (CreateSnapshotOp)cache.get(OP_CREATE_SNAPSHOT);
    }

    @Override
    void resetSubFields() {
      snapshotRoot = null;
      snapshotName = null;
    }

    CreateSnapshotOp setSnapshotName(String snapName) {
      this.snapshotName = snapName;
      return this;
    }

    public CreateSnapshotOp setSnapshotRoot(String snapRoot) {
      snapshotRoot = snapRoot;
      return this;
    }
    
    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      snapshotRoot = FSImageSerialization.readString(in);
      snapshotName = FSImageSerialization.readString(in);
      
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(snapshotRoot, out);
      FSImageSerialization.writeString(snapshotName, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTROOT", snapshotRoot);
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTNAME", snapshotName);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      snapshotRoot = st.getValue("SNAPSHOTROOT");
      snapshotName = st.getValue("SNAPSHOTNAME");
      
      readRpcIdsFromXml(st);
    }
    
    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("CreateSnapshotOp [snapshotRoot=")
          .append(snapshotRoot)
          .append(", snapshotName=")
          .append(snapshotName);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }
  
  /**
   * Operation corresponding to delete a snapshot.
   * {@literal @AtMostOnce} for {@link ClientProtocol#deleteSnapshot}.
   */
  static class DeleteSnapshotOp extends FSEditLogOp {
    String snapshotRoot;
    String snapshotName;
    
    DeleteSnapshotOp() {
      super(OP_DELETE_SNAPSHOT);
    }
    
    static DeleteSnapshotOp getInstance(OpInstanceCache cache) {
      return (DeleteSnapshotOp)cache.get(OP_DELETE_SNAPSHOT);
    }

    @Override
    void resetSubFields() {
      snapshotRoot = null;
      snapshotName = null;
    }
    
    DeleteSnapshotOp setSnapshotName(String snapName) {
      this.snapshotName = snapName;
      return this;
    }

    DeleteSnapshotOp setSnapshotRoot(String snapRoot) {
      snapshotRoot = snapRoot;
      return this;
    }
    
    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      snapshotRoot = FSImageSerialization.readString(in);
      snapshotName = FSImageSerialization.readString(in);
      
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(snapshotRoot, out);
      FSImageSerialization.writeString(snapshotName, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTROOT", snapshotRoot);
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTNAME", snapshotName);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      snapshotRoot = st.getValue("SNAPSHOTROOT");
      snapshotName = st.getValue("SNAPSHOTNAME");
      
      readRpcIdsFromXml(st);
    }
    
    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("DeleteSnapshotOp [snapshotRoot=")
          .append(snapshotRoot)
          .append(", snapshotName=")
          .append(snapshotName);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }
  
  /**
   * Operation corresponding to rename a snapshot.
   * {@literal @AtMostOnce} for {@link ClientProtocol#renameSnapshot}.
   */
  static class RenameSnapshotOp extends FSEditLogOp {
    String snapshotRoot;
    String snapshotOldName;
    String snapshotNewName;
    
    RenameSnapshotOp() {
      super(OP_RENAME_SNAPSHOT);
    }
    
    static RenameSnapshotOp getInstance(OpInstanceCache cache) {
      return (RenameSnapshotOp) cache.get(OP_RENAME_SNAPSHOT);
    }

    @Override
    void resetSubFields() {
      snapshotRoot = null;
      snapshotOldName = null;
      snapshotNewName = null;
    }
    
    RenameSnapshotOp setSnapshotOldName(String snapshotOldName) {
      this.snapshotOldName = snapshotOldName;
      return this;
    }

    RenameSnapshotOp setSnapshotNewName(String snapshotNewName) {
      this.snapshotNewName = snapshotNewName;
      return this;
    }
    
    RenameSnapshotOp setSnapshotRoot(String snapshotRoot) {
      this.snapshotRoot = snapshotRoot;
      return this;
    }
    
    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      snapshotRoot = FSImageSerialization.readString(in);
      snapshotOldName = FSImageSerialization.readString(in);
      snapshotNewName = FSImageSerialization.readString(in);
      
      // read RPC ids if necessary
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(snapshotRoot, out);
      FSImageSerialization.writeString(snapshotOldName, out);
      FSImageSerialization.writeString(snapshotNewName, out);
      
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTROOT", snapshotRoot);
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTOLDNAME", snapshotOldName);
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTNEWNAME", snapshotNewName);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      snapshotRoot = st.getValue("SNAPSHOTROOT");
      snapshotOldName = st.getValue("SNAPSHOTOLDNAME");
      snapshotNewName = st.getValue("SNAPSHOTNEWNAME");
      
      readRpcIdsFromXml(st);
    }
    
    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("RenameSnapshotOp [snapshotRoot=")
          .append(snapshotRoot)
          .append(", snapshotOldName=")
          .append(snapshotOldName)
          .append(", snapshotNewName=")
          .append(snapshotNewName);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /**
   * Operation corresponding to allow creating snapshot on a directory
   */
  static class AllowSnapshotOp extends FSEditLogOp { // @Idempotent
    String snapshotRoot;

    public AllowSnapshotOp() {
      super(OP_ALLOW_SNAPSHOT);
    }

    public AllowSnapshotOp(String snapRoot) {
      super(OP_ALLOW_SNAPSHOT);
      snapshotRoot = snapRoot;
    }

    static AllowSnapshotOp getInstance(OpInstanceCache cache) {
      return (AllowSnapshotOp) cache.get(OP_ALLOW_SNAPSHOT);
    }

    @Override
    void resetSubFields() {
      snapshotRoot = null;
    }

    public AllowSnapshotOp setSnapshotRoot(String snapRoot) {
      snapshotRoot = snapRoot;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      snapshotRoot = FSImageSerialization.readString(in);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(snapshotRoot, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTROOT", snapshotRoot);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      snapshotRoot = st.getValue("SNAPSHOTROOT");
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("AllowSnapshotOp [snapshotRoot=")
          .append(snapshotRoot)
          .append("]");
      return builder.toString();
    }
  }

  /**
   * Operation corresponding to disallow creating snapshot on a directory
   */
  static class DisallowSnapshotOp extends FSEditLogOp { // @Idempotent
    String snapshotRoot;

    public DisallowSnapshotOp() {
      super(OP_DISALLOW_SNAPSHOT);
    }

    public DisallowSnapshotOp(String snapRoot) {
      super(OP_DISALLOW_SNAPSHOT);
      snapshotRoot = snapRoot;
    }

    static DisallowSnapshotOp getInstance(OpInstanceCache cache) {
      return (DisallowSnapshotOp) cache.get(OP_DISALLOW_SNAPSHOT);
    }

    void resetSubFields() {
      snapshotRoot = null;
    }

    public DisallowSnapshotOp setSnapshotRoot(String snapRoot) {
      snapshotRoot = snapRoot;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      snapshotRoot = FSImageSerialization.readString(in);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(snapshotRoot, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SNAPSHOTROOT", snapshotRoot);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      snapshotRoot = st.getValue("SNAPSHOTROOT");
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("DisallowSnapshotOp [snapshotRoot=")
          .append(snapshotRoot)
          .append("]");
      return builder.toString();
    }
  }

  /**
   * {@literal @AtMostOnce} for
   * {@link ClientProtocol#addCacheDirective}
   */
  static class AddCacheDirectiveInfoOp extends FSEditLogOp {
    CacheDirectiveInfo directive;

    public AddCacheDirectiveInfoOp() {
      super(OP_ADD_CACHE_DIRECTIVE);
    }

    static AddCacheDirectiveInfoOp getInstance(OpInstanceCache cache) {
      return (AddCacheDirectiveInfoOp) cache.get(OP_ADD_CACHE_DIRECTIVE);
    }

    @Override
    void resetSubFields() {
      directive = null;
    }

    public AddCacheDirectiveInfoOp setDirective(
        CacheDirectiveInfo directive) {
      this.directive = directive;
      assert(directive.getId() != null);
      assert(directive.getPath() != null);
      assert(directive.getReplication() != null);
      assert(directive.getPool() != null);
      assert(directive.getExpiration() != null);
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      directive = FSImageSerialization.readCacheDirectiveInfo(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeCacheDirectiveInfo(out, directive);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSImageSerialization.writeCacheDirectiveInfo(contentHandler, directive);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      directive = FSImageSerialization.readCacheDirectiveInfo(st);
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("AddCacheDirectiveInfo [")
          .append("id=" + directive.getId() + ",")
          .append("path=" + directive.getPath().toUri().getPath() + ",")
          .append("replication=" + directive.getReplication() + ",")
          .append("pool=" + directive.getPool() + ",")
          .append("expiration=" + directive.getExpiration().getMillis());
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /**
   * {@literal @AtMostOnce} for
   * {@link ClientProtocol#modifyCacheDirective}
   */
  static class ModifyCacheDirectiveInfoOp extends FSEditLogOp {
    CacheDirectiveInfo directive;

    public ModifyCacheDirectiveInfoOp() {
      super(OP_MODIFY_CACHE_DIRECTIVE);
    }

    static ModifyCacheDirectiveInfoOp getInstance(OpInstanceCache cache) {
      return (ModifyCacheDirectiveInfoOp) cache.get(OP_MODIFY_CACHE_DIRECTIVE);
    }

    @Override
    void resetSubFields() {
      directive = null;
    }

    public ModifyCacheDirectiveInfoOp setDirective(
        CacheDirectiveInfo directive) {
      this.directive = directive;
      assert(directive.getId() != null);
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      this.directive = FSImageSerialization.readCacheDirectiveInfo(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeCacheDirectiveInfo(out, directive);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSImageSerialization.writeCacheDirectiveInfo(contentHandler, directive);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.directive = FSImageSerialization.readCacheDirectiveInfo(st);
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("ModifyCacheDirectiveInfoOp[")
          .append("id=").append(directive.getId());
      if (directive.getPath() != null) {
        builder.append(",").append("path=").append(directive.getPath());
      }
      if (directive.getReplication() != null) {
        builder.append(",").append("replication=").
            append(directive.getReplication());
      }
      if (directive.getPool() != null) {
        builder.append(",").append("pool=").append(directive.getPool());
      }
      if (directive.getExpiration() != null) {
        builder.append(",").append("expiration=").
            append(directive.getExpiration().getMillis());
      }
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /**
   * {@literal @AtMostOnce} for
   * {@link ClientProtocol#removeCacheDirective}
   */
  static class RemoveCacheDirectiveInfoOp extends FSEditLogOp {
    long id;

    public RemoveCacheDirectiveInfoOp() {
      super(OP_REMOVE_CACHE_DIRECTIVE);
    }

    static RemoveCacheDirectiveInfoOp getInstance(OpInstanceCache cache) {
      return (RemoveCacheDirectiveInfoOp) cache.get(OP_REMOVE_CACHE_DIRECTIVE);
    }

    @Override
    void resetSubFields() {
      id = 0L;
    }

    public RemoveCacheDirectiveInfoOp setId(long id) {
      this.id = id;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      this.id = FSImageSerialization.readLong(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(id, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "ID", Long.toString(id));
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.id = Long.parseLong(st.getValue("ID"));
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("RemoveCacheDirectiveInfo [")
          .append("id=" + Long.toString(id));
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#addCachePool} */
  static class AddCachePoolOp extends FSEditLogOp {
    CachePoolInfo info;

    public AddCachePoolOp() {
      super(OP_ADD_CACHE_POOL);
    }

    static AddCachePoolOp getInstance(OpInstanceCache cache) {
      return (AddCachePoolOp) cache.get(OP_ADD_CACHE_POOL);
    }

    @Override
    void resetSubFields() {
      info = null;
    }

    public AddCachePoolOp setPool(CachePoolInfo info) {
      this.info = info;
      assert(info.getPoolName() != null);
      assert(info.getOwnerName() != null);
      assert(info.getGroupName() != null);
      assert(info.getMode() != null);
      assert(info.getLimit() != null);
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      info = FSImageSerialization.readCachePoolInfo(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeCachePoolInfo(out, info);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSImageSerialization.writeCachePoolInfo(contentHandler, info);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.info = FSImageSerialization.readCachePoolInfo(st);
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("AddCachePoolOp [")
          .append("poolName=" + info.getPoolName() + ",")
          .append("ownerName=" + info.getOwnerName() + ",")
          .append("groupName=" + info.getGroupName() + ",")
          .append("mode=" + Short.toString(info.getMode().toShort()) + ",")
          .append("limit=" + Long.toString(info.getLimit()));
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#modifyCachePool} */
  static class ModifyCachePoolOp extends FSEditLogOp {
    CachePoolInfo info;

    public ModifyCachePoolOp() {
      super(OP_MODIFY_CACHE_POOL);
    }

    static ModifyCachePoolOp getInstance(OpInstanceCache cache) {
      return (ModifyCachePoolOp) cache.get(OP_MODIFY_CACHE_POOL);
    }

    @Override
    void resetSubFields() {
      info = null;
    }

    public ModifyCachePoolOp setInfo(CachePoolInfo info) {
      this.info = info;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      info = FSImageSerialization.readCachePoolInfo(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeCachePoolInfo(out, info);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      FSImageSerialization.writeCachePoolInfo(contentHandler, info);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.info = FSImageSerialization.readCachePoolInfo(st);
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("ModifyCachePoolOp [");
      ArrayList<String> fields = new ArrayList<String>(5);
      if (info.getPoolName() != null) {
        fields.add("poolName=" + info.getPoolName());
      }
      if (info.getOwnerName() != null) {
        fields.add("ownerName=" + info.getOwnerName());
      }
      if (info.getGroupName() != null) {
        fields.add("groupName=" + info.getGroupName());
      }
      if (info.getMode() != null) {
        fields.add("mode=" + info.getMode().toString());
      }
      if (info.getLimit() != null) {
        fields.add("limit=" + info.getLimit());
      }
      builder.append(Joiner.on(",").join(fields));
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /** {@literal @AtMostOnce} for {@link ClientProtocol#removeCachePool} */
  static class RemoveCachePoolOp extends FSEditLogOp {
    String poolName;

    public RemoveCachePoolOp() {
      super(OP_REMOVE_CACHE_POOL);
    }

    static RemoveCachePoolOp getInstance(OpInstanceCache cache) {
      return (RemoveCachePoolOp) cache.get(OP_REMOVE_CACHE_POOL);
    }

    @Override
    void resetSubFields() {
      poolName = null;
    }

    public RemoveCachePoolOp setPoolName(String poolName) {
      this.poolName = poolName;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      poolName = FSImageSerialization.readString(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(poolName, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "POOLNAME", poolName);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.poolName = st.getValue("POOLNAME");
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("RemoveCachePoolOp [")
          .append("poolName=" + poolName);
      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }
  
  static class RemoveXAttrOp extends FSEditLogOp {
    List<XAttr> xAttrs;
    String src;
    
    RemoveXAttrOp() {
      super(OP_REMOVE_XATTR);
    }
    
    static RemoveXAttrOp getInstance(OpInstanceCache cache) {
      return (RemoveXAttrOp) cache.get(OP_REMOVE_XATTR);
    }

    @Override
    void resetSubFields() {
      xAttrs = null;
      src = null;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      XAttrEditLogProto p = XAttrEditLogProto.parseDelimitedFrom(in);
      src = p.getSrc();
      xAttrs = PBHelperClient.convertXAttrs(p.getXAttrsList());
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      XAttrEditLogProto.Builder b = XAttrEditLogProto.newBuilder();
      if (src != null) {
        b.setSrc(src);
      }
      b.addAllXAttrs(PBHelperClient.convertXAttrProto(xAttrs));
      b.build().writeDelimitedTo(out);
      // clientId and callId
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      appendXAttrsToXml(contentHandler, xAttrs);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      src = st.getValue("SRC");
      xAttrs = readXAttrsFromXml(st);
      readRpcIdsFromXml(st);
    }
  }
  
  static class SetXAttrOp extends FSEditLogOp {
    List<XAttr> xAttrs;
    String src;
    
    SetXAttrOp() {
      super(OP_SET_XATTR);
    }
    
    static SetXAttrOp getInstance(OpInstanceCache cache) {
      return (SetXAttrOp) cache.get(OP_SET_XATTR);
    }

    @Override
    void resetSubFields() {
      xAttrs = null;
      src = null;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      XAttrEditLogProto p = XAttrEditLogProto.parseDelimitedFrom(in);
      src = p.getSrc();
      xAttrs = PBHelperClient.convertXAttrs(p.getXAttrsList());
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      XAttrEditLogProto.Builder b = XAttrEditLogProto.newBuilder();
      if (src != null) {
        b.setSrc(src);
      }
      b.addAllXAttrs(PBHelperClient.convertXAttrProto(xAttrs));
      b.build().writeDelimitedTo(out);
      // clientId and callId
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      appendXAttrsToXml(contentHandler, xAttrs);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      src = st.getValue("SRC");
      xAttrs = readXAttrsFromXml(st);
      readRpcIdsFromXml(st);
    }
  }

  static class SetAclOp extends FSEditLogOp {
    List<AclEntry> aclEntries = Lists.newArrayList();
    String src;

    SetAclOp() {
      super(OP_SET_ACL);
    }

    static SetAclOp getInstance(OpInstanceCache cache) {
      return (SetAclOp) cache.get(OP_SET_ACL);
    }

    @Override
    void resetSubFields() {
      aclEntries = null;
      src = null;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      AclEditLogProto p = AclEditLogProto.parseDelimitedFrom(in);
      if (p == null) {
        throw new IOException("Failed to read fields from SetAclOp");
      }
      src = p.getSrc();
      aclEntries = PBHelperClient.convertAclEntry(p.getEntriesList());
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      AclEditLogProto.Builder b = AclEditLogProto.newBuilder();
      if (src != null)
        b.setSrc(src);
      b.addAllEntries(PBHelperClient.convertAclEntryProto(aclEntries));
      b.build().writeDelimitedTo(out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "SRC", src);
      appendAclEntriesToXml(contentHandler, aclEntries);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      src = st.getValue("SRC");
      aclEntries = readAclEntriesFromXml(st);
      if (aclEntries == null) {
        aclEntries = Lists.newArrayList();
      }
    }
  }

  static private short readShort(DataInputStream in) throws IOException {
    return Short.parseShort(FSImageSerialization.readString(in));
  }

  static private long readLong(DataInputStream in) throws IOException {
    return Long.parseLong(FSImageSerialization.readString(in));
  }

  /**
   * A class to read in blocks stored in the old format. The only two
   * fields in the block were blockid and length.
   */
  static class BlockTwo implements Writable {
    long blkid;
    long len;

    static {                                      // register a ctor
      WritableFactories.setFactory
        (BlockTwo.class,
         new WritableFactory() {
           @Override
           public Writable newInstance() { return new BlockTwo(); }
         });
    }


    BlockTwo() {
      blkid = 0;
      len = 0;
    }
    /////////////////////////////////////
    // Writable
    /////////////////////////////////////
    @Override
    public void write(DataOutput out) throws IOException {
      out.writeLong(blkid);
      out.writeLong(len);
    }

    @Override
    public void readFields(DataInput in) throws IOException {
      this.blkid = in.readLong();
      this.len = in.readLong();
    }
  }

  /**
   * Operation corresponding to add an erasure coding policy.
   */
  static class AddErasureCodingPolicyOp extends FSEditLogOp {
    private ErasureCodingPolicy ecPolicy;

    AddErasureCodingPolicyOp() {
      super(OP_ADD_ERASURE_CODING_POLICY);
    }

    static AddErasureCodingPolicyOp getInstance(OpInstanceCache cache) {
      return (AddErasureCodingPolicyOp) cache
          .get(OP_ADD_ERASURE_CODING_POLICY);
    }

    @Override
    void resetSubFields() {
      this.ecPolicy = null;
    }

    public ErasureCodingPolicy getEcPolicy() {
      return this.ecPolicy;
    }

    public AddErasureCodingPolicyOp setErasureCodingPolicy(
        ErasureCodingPolicy policy) {
      Preconditions.checkNotNull(policy.getName());
      Preconditions.checkNotNull(policy.getSchema());
      Preconditions.checkArgument(policy.getCellSize() > 0);
      this.ecPolicy = policy;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      this.ecPolicy = FSImageSerialization.readErasureCodingPolicy(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      Preconditions.checkNotNull(ecPolicy);
      FSImageSerialization.writeErasureCodingPolicy(out, ecPolicy);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      Preconditions.checkNotNull(ecPolicy);
      XMLUtils.addSaxString(contentHandler, "CODEC", ecPolicy.getCodecName());
      XMLUtils.addSaxString(contentHandler, "DATAUNITS",
          Integer.toString(ecPolicy.getNumDataUnits()));
      XMLUtils.addSaxString(contentHandler, "PARITYUNITS",
          Integer.toString(ecPolicy.getNumParityUnits()));
      XMLUtils.addSaxString(contentHandler, "CELLSIZE",
          Integer.toString(ecPolicy.getCellSize()));

      Map<String, String> extraOptions = ecPolicy.getSchema().getExtraOptions();
      if (extraOptions == null || extraOptions.isEmpty()) {
        XMLUtils.addSaxString(contentHandler, "EXTRAOPTIONS",
            Integer.toString(0));
        appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
        return;
      }

      XMLUtils.addSaxString(contentHandler, "EXTRAOPTIONS",
          Integer.toString(extraOptions.size()));

      for (Map.Entry<String, String> entry : extraOptions.entrySet()) {
        contentHandler.startElement("", "", "EXTRAOPTION",
            new AttributesImpl());
        XMLUtils.addSaxString(contentHandler, "KEY", entry.getKey());
        XMLUtils.addSaxString(contentHandler, "VALUE", entry.getValue());
        contentHandler.endElement("", "", "EXTRAOPTION");
      }
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      final String codecName = st.getValue("CODEC");
      final int dataUnits = Integer.parseInt(st.getValue("DATAUNITS"));
      final int parityUnits = Integer.parseInt(st.getValue("PARITYUNITS"));
      final int cellSize = Integer.parseInt(st.getValue("CELLSIZE"));
      final int extraOptionNum = Integer.parseInt(st.getValue("EXTRAOPTIONS"));

      ECSchema schema;
      if (extraOptionNum == 0) {
        schema = new ECSchema(codecName, dataUnits, parityUnits, null);
      } else {
        Map<String, String> extraOptions = new HashMap<String, String>();
        List<Stanza> stanzas = st.getChildren("EXTRAOPTION");
        for (Stanza a: stanzas) {
          extraOptions.put(a.getValue("KEY"), a.getValue("VALUE"));
        }
        schema = new ECSchema(codecName, dataUnits, parityUnits, extraOptions);
      }
      this.ecPolicy = new ErasureCodingPolicy(schema, cellSize);
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("AddErasureCodingPolicy [")
          .append(ecPolicy.toString());

      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /**
   * Operation corresponding to enable an erasure coding policy.
   */
  static class EnableErasureCodingPolicyOp extends FSEditLogOp {
    private String ecPolicyName;

    EnableErasureCodingPolicyOp() {
      super(OP_ENABLE_ERASURE_CODING_POLICY);
    }

    static EnableErasureCodingPolicyOp getInstance(OpInstanceCache cache) {
      return (EnableErasureCodingPolicyOp) cache
          .get(OP_ENABLE_ERASURE_CODING_POLICY);
    }

    @Override
    void resetSubFields() {
      this.ecPolicyName = null;
    }

    public String getEcPolicy() {
      return this.ecPolicyName;
    }

    public EnableErasureCodingPolicyOp setErasureCodingPolicy(
        String policyName) {
      Preconditions.checkNotNull(policyName);
      this.ecPolicyName = policyName;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      this.ecPolicyName = FSImageSerialization.readString(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      Preconditions.checkNotNull(ecPolicyName);
      FSImageSerialization.writeString(ecPolicyName, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      Preconditions.checkNotNull(ecPolicyName);
      XMLUtils.addSaxString(contentHandler, "POLICYNAME", this.ecPolicyName);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.ecPolicyName = st.getValue("POLICYNAME");
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("EnableErasureCodingPolicy [")
          .append(ecPolicyName);

      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /**
   * Operation corresponding to disable an erasure coding policy.
   */
  static class DisableErasureCodingPolicyOp extends FSEditLogOp {
    private String ecPolicyName;

    DisableErasureCodingPolicyOp() {
      super(OP_DISABLE_ERASURE_CODING_POLICY);
    }

    static DisableErasureCodingPolicyOp getInstance(OpInstanceCache cache) {
      return (DisableErasureCodingPolicyOp) cache
          .get(OP_DISABLE_ERASURE_CODING_POLICY);
    }

    @Override
    void resetSubFields() {
      this.ecPolicyName = null;
    }

    public String getEcPolicy() {
      return this.ecPolicyName;
    }

    public DisableErasureCodingPolicyOp setErasureCodingPolicy(
        String policyName) {
      Preconditions.checkNotNull(policyName);
      this.ecPolicyName = policyName;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      this.ecPolicyName = FSImageSerialization.readString(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(ecPolicyName, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "POLICYNAME", this.ecPolicyName);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.ecPolicyName = st.getValue("POLICYNAME");
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("DisableErasureCodingPolicy [")
          .append(ecPolicyName);

      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /**
   * Operation corresponding to remove an erasure coding policy.
   */
  static class RemoveErasureCodingPolicyOp extends FSEditLogOp {
    private String ecPolicyName;

    RemoveErasureCodingPolicyOp() {
      super(OP_REMOVE_ERASURE_CODING_POLICY);
    }

    static RemoveErasureCodingPolicyOp getInstance(OpInstanceCache cache) {
      return (RemoveErasureCodingPolicyOp) cache
          .get(OP_REMOVE_ERASURE_CODING_POLICY);
    }

    @Override
    void resetSubFields() {
      this.ecPolicyName = null;
    }

    public String getEcPolicy() {
      return this.ecPolicyName;
    }

    public RemoveErasureCodingPolicyOp setErasureCodingPolicy(
        String policyName) {
      Preconditions.checkNotNull(policyName);
      this.ecPolicyName = policyName;
      return this;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      this.ecPolicyName = FSImageSerialization.readString(in);
      readRpcIds(in, logVersion);
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(ecPolicyName, out);
      writeRpcIds(rpcClientId, rpcCallId, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "POLICYNAME", this.ecPolicyName);
      appendRpcIdsToXml(contentHandler, rpcClientId, rpcCallId);
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.ecPolicyName = st.getValue("POLICYNAME");
      readRpcIdsFromXml(st);
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("RemoveErasureCodingPolicy [")
          .append(ecPolicyName);

      appendRpcIdsToString(builder, rpcClientId, rpcCallId);
      builder.append("]");
      return builder.toString();
    }
  }

  /**
   * Operation corresponding to upgrade
   */
  abstract static class RollingUpgradeOp extends FSEditLogOp { // @Idempotent
    private final String name;
    private long time;

    public RollingUpgradeOp(FSEditLogOpCodes code, String name) {
      super(code);
      this.name = StringUtils.toUpperCase(name);
    }

    @Override
    void resetSubFields() {
      time = 0L;
    }

    long getTime() {
      return time;
    }

    void setTime(long time) {
      this.time = time;
    }

    @Override
    void readFields(DataInputStream in, int logVersion) throws IOException {
      time = in.readLong();
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeLong(time, out);
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, name + "TIME",
          Long.toString(time));
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.time = Long.parseLong(st.getValue(name + "TIME"));
    }

    @Override
    public String toString() {
      return new StringBuilder().append("RollingUpgradeOp [").append(name)
          .append(", time=").append(time).append("]").toString();
    }
    
    static class RollbackException extends IOException {
      private static final long serialVersionUID = 1L;
    }
  }

  /** {@literal @Idempotent} for {@link ClientProtocol#setStoragePolicy} */
  static class SetStoragePolicyOp extends FSEditLogOp {
    String path;
    byte policyId;

    SetStoragePolicyOp() {
      super(OP_SET_STORAGE_POLICY);
    }

    static SetStoragePolicyOp getInstance(OpInstanceCache cache) {
      return (SetStoragePolicyOp) cache.get(OP_SET_STORAGE_POLICY);
    }

    @Override
    void resetSubFields() {
      path = null;
      policyId = 0;
    }

    SetStoragePolicyOp setPath(String path) {
      this.path = path;
      return this;
    }

    SetStoragePolicyOp setPolicyId(byte policyId) {
      this.policyId = policyId;
      return this;
    }

    @Override
    public void writeFields(DataOutputStream out) throws IOException {
      FSImageSerialization.writeString(path, out);
      out.writeByte(policyId);
    }

    @Override
    void readFields(DataInputStream in, int logVersion)
        throws IOException {
      this.path = FSImageSerialization.readString(in);
      this.policyId = in.readByte();
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("SetStoragePolicyOp [path=")
          .append(path)
          .append(", policyId=")
          .append(policyId)
          .append(", opCode=")
          .append(opCode)
          .append(", txid=")
          .append(txid)
          .append("]");
      return builder.toString();
    }

    @Override
    protected void toXml(ContentHandler contentHandler) throws SAXException {
      XMLUtils.addSaxString(contentHandler, "PATH", path);
      XMLUtils.addSaxString(contentHandler, "POLICYID",
          Byte.toString(policyId));
    }

    @Override
    void fromXml(Stanza st) throws InvalidXmlException {
      this.path = st.getValue("PATH");
      this.policyId = Byte.parseByte(st.getValue("POLICYID"));
    }
  }  

  static class RollingUpgradeStartOp extends RollingUpgradeOp {
    RollingUpgradeStartOp() {
      super(OP_ROLLING_UPGRADE_START, "start");
    }

    static RollingUpgradeStartOp getInstance(OpInstanceCache cache) {
      return (RollingUpgradeStartOp) cache.get(OP_ROLLING_UPGRADE_START);
    }
  }

  static class RollingUpgradeFinalizeOp extends RollingUpgradeOp {
    RollingUpgradeFinalizeOp() {
      super(OP_ROLLING_UPGRADE_FINALIZE, "finalize");
    }

    static RollingUpgradeFinalizeOp getInstance(OpInstanceCache cache) {
      return (RollingUpgradeFinalizeOp) cache.get(OP_ROLLING_UPGRADE_FINALIZE);
    }
  }

  /**
   * Class for writing editlog ops
   */
  public static class Writer {
    private final DataOutputBuffer buf;
    private final Checksum checksum;

    public Writer(DataOutputBuffer out) {
      this.buf = out;
      this.checksum = DataChecksum.newCrc32();
    }

    /**
     * Write an operation to the output stream
     * 
     * @param op The operation to write
     * @throws IOException if an error occurs during writing.
     */
    public void writeOp(FSEditLogOp op) throws IOException {
      int start = buf.getLength();
      // write the op code first to make padding and terminator verification
      // work
      buf.writeByte(op.opCode.getOpCode());
      buf.writeInt(0); // write 0 for the length first
      buf.writeLong(op.txid);
      op.writeFields(buf);
      int end = buf.getLength();
      
      // write the length back: content of the op + 4 bytes checksum - op_code
      int length = end - start - 1;
      buf.writeInt(length, start + 1);

      checksum.reset();
      checksum.update(buf.getData(), start, end-start);
      int sum = (int)checksum.getValue();
      buf.writeInt(sum);
    }
  }

  /**
   * Class for reading editlog ops from a stream
   */
  public abstract static class Reader {
    final DataInputStream in;
    final StreamLimiter limiter;
    final OpInstanceCache cache;
    final byte[] temp = new byte[4096];
    final int logVersion;
    int maxOpSize;

    public static Reader create(DataInputStream in, StreamLimiter limiter,
                                int logVersion) {
      if (logVersion < NameNodeLayoutVersion.CURRENT_LAYOUT_VERSION) {
        // Use the LengthPrefixedReader on edit logs which are newer than what
        // we can parse.  (Newer layout versions are represented by smaller
        // negative integers, for historical reasons.) Even though we can't
        // parse the Ops contained in them, we should still be able to call
        // scanOp on them.  This is important for the JournalNode during rolling
        // upgrade.
        return new LengthPrefixedReader(in, limiter, logVersion);
      } else if (NameNodeLayoutVersion.supports(
              NameNodeLayoutVersion.Feature.EDITLOG_LENGTH, logVersion)) {
        return new LengthPrefixedReader(in, limiter, logVersion);
      } else if (NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.EDITS_CHECKSUM, logVersion)) {
        Checksum checksum = DataChecksum.newCrc32();
        return new ChecksummedReader(checksum, in, limiter, logVersion);
      } else {
        return new LegacyReader(in, limiter, logVersion);
      }
    }

    /**
     * Construct the reader
     * @param in            The stream to read from.
     * @param limiter       The limiter for this stream.
     * @param logVersion    The version of the data coming from the stream.
     */
    Reader(DataInputStream in, StreamLimiter limiter, int logVersion) {
      this.in = in;
      this.limiter = limiter;
      this.logVersion = logVersion;
      this.cache = new OpInstanceCache();
      this.maxOpSize = DFSConfigKeys.DFS_NAMENODE_MAX_OP_SIZE_DEFAULT;
    }

    public void setMaxOpSize(int maxOpSize) {
      this.maxOpSize = maxOpSize;
    }

    /**
     * Read an operation from the input stream.
     * 
     * Note that the objects returned from this method may be re-used by future
     * calls to the same method.
     * 
     * @param skipBrokenEdits    If true, attempt to skip over damaged parts of
     * the input stream, rather than throwing an IOException
     * @return the operation read from the stream, or null at the end of the 
     *         file
     * @throws IOException on error.  This function should only throw an
     *         exception when skipBrokenEdits is false.
     */
    public FSEditLogOp readOp(boolean skipBrokenEdits) throws IOException {
      while (true) {
        try {
          return decodeOp();
        } catch (IOException e) {
          in.reset();
          if (!skipBrokenEdits) {
            throw e;
          }
        } catch (RuntimeException e) {
          // FSEditLogOp#decodeOp is not supposed to throw RuntimeException.
          // However, we handle it here for recovery mode, just to be more
          // robust.
          in.reset();
          if (!skipBrokenEdits) {
            throw e;
          }
        } catch (Throwable e) {
          in.reset();
          if (!skipBrokenEdits) {
            throw new IOException("got unexpected exception " +
                e.getMessage(), e);
          }
        }
        // Move ahead one byte and re-try the decode process.
        if (in.skip(1) < 1) {
          return null;
        }
      }
    }

    void verifyTerminator() throws IOException {
      /** The end of the edit log should contain only 0x00 or 0xff bytes.
       * If it contains other bytes, the log itself may be corrupt.
       * It is important to check this; if we don't, a stray OP_INVALID byte 
       * could make us stop reading the edit log halfway through, and we'd never
       * know that we had lost data.
       */
      limiter.clearLimit();
      int numRead = -1, idx = 0;
      while (true) {
        try {
          numRead = -1;
          idx = 0;
          numRead = in.read(temp);
          if (numRead == -1) {
            return;
          }
          while (idx < numRead) {
            if ((temp[idx] != (byte)0) && (temp[idx] != (byte)-1)) {
              throw new IOException("Read extra bytes after " +
                "the terminator!");
            }
            idx++;
          }
        } finally {
          // After reading each group of bytes, we reposition the mark one
          // byte before the next group.  Similarly, if there is an error, we
          // want to reposition the mark one byte before the error
          if (numRead != -1) { 
            in.reset();
            IOUtils.skipFully(in, idx);
            in.mark(temp.length + 1);
            IOUtils.skipFully(in, 1);
          }
        }
      }
    }

    /**
     * Read an opcode from the input stream.
     *
     * @return   the opcode, or null on EOF.
     *
     * If an exception is thrown, the stream's mark will be set to the first
     * problematic byte.  This usually means the beginning of the opcode.
     */
    public abstract FSEditLogOp decodeOp() throws IOException;

    /**
     * Similar to decodeOp(), but we only retrieve the transaction ID of the
     * Op rather than reading it.  If the edit log format supports length
     * prefixing, this can be much faster than full decoding.
     *
     * @return the last txid of the segment, or INVALID_TXID on EOF.
     */
    public abstract long scanOp() throws IOException;
  }

  /**
   * Reads edit logs which are prefixed with a length.  These edit logs also
   * include a checksum and transaction ID.
   */
  private static class LengthPrefixedReader extends Reader {
    /**
     * The minimum length of a length-prefixed Op.
     *
     * The minimum Op has:
     * 1-byte opcode
     * 4-byte length
     * 8-byte txid
     * 0-byte body
     * 4-byte checksum
     */
    private static final int MIN_OP_LENGTH = 17;

    /**
     * The op id length.
     *
     * Not included in the stored length.
     */
    private static final int OP_ID_LENGTH = 1;

    /**
     * The checksum length.
     *
     * Not included in the stored length.
     */
    private static final int CHECKSUM_LENGTH = 4;

    private final Checksum checksum;

    LengthPrefixedReader(DataInputStream in, StreamLimiter limiter,
                         int logVersion) {
      super(in, limiter, logVersion);
      this.checksum = DataChecksum.newCrc32();
    }

    @Override
    public FSEditLogOp decodeOp() throws IOException {
      long txid = decodeOpFrame();
      if (txid == HdfsServerConstants.INVALID_TXID) {
        return null;
      }
      in.reset();
      in.mark(maxOpSize);
      FSEditLogOpCodes opCode = FSEditLogOpCodes.fromByte(in.readByte());
      FSEditLogOp op = cache.get(opCode);
      if (op == null) {
        throw new IOException("Read invalid opcode " + opCode);
      }
      op.setTransactionId(txid);
      IOUtils.skipFully(in, 4 + 8); // skip length and txid
      op.readFields(in, logVersion);
      // skip over the checksum, which we validated above.
      IOUtils.skipFully(in, CHECKSUM_LENGTH);
      return op;
    }

    @Override
    public long scanOp() throws IOException {
      return decodeOpFrame();
    }

    /**
     * Decode the opcode "frame".  This includes reading the opcode and
     * transaction ID, and validating the checksum and length.  It does not
     * include reading the opcode-specific fields.
     * The input stream will be advanced to the end of the op at the end of this
     * function.
     *
     * @return        An op with the txid set, but none of the other fields
     *                  filled in, or null if we hit EOF.
     */
    private long decodeOpFrame() throws IOException {
      limiter.setLimit(maxOpSize);
      in.mark(maxOpSize);
      byte opCodeByte;
      try {
        opCodeByte = in.readByte();
      } catch (EOFException eof) {
        // EOF at an opcode boundary is expected.
        return HdfsServerConstants.INVALID_TXID;
      }
      if (opCodeByte == FSEditLogOpCodes.OP_INVALID.getOpCode()) {
        verifyTerminator();
        return HdfsServerConstants.INVALID_TXID;
      }
      // Here, we verify that the Op size makes sense and that the
      // data matches its checksum before attempting to construct an Op.
      // This is important because otherwise we may encounter an
      // OutOfMemoryException which could bring down the NameNode or
      // JournalNode when reading garbage data.
      int opLength =  in.readInt() + OP_ID_LENGTH + CHECKSUM_LENGTH;
      if (opLength > maxOpSize) {
        throw new IOException("Op " + (int)opCodeByte + " has size " +
            opLength + ", but maxOpSize = " + maxOpSize);
      } else  if (opLength < MIN_OP_LENGTH) {
        throw new IOException("Op " + (int)opCodeByte + " has size " +
            opLength + ", but the minimum op size is " + MIN_OP_LENGTH);
      }
      long txid = in.readLong();
      // Verify checksum
      in.reset();
      in.mark(maxOpSize);
      checksum.reset();
      for (int rem = opLength - CHECKSUM_LENGTH; rem > 0;) {
        int toRead = Math.min(temp.length, rem);
        IOUtils.readFully(in, temp, 0, toRead);
        checksum.update(temp, 0, toRead);
        rem -= toRead;
      }
      int expectedChecksum = in.readInt();
      int calculatedChecksum = (int)checksum.getValue();
      if (expectedChecksum != calculatedChecksum) {
        throw new ChecksumException(
            "Transaction is corrupt. Calculated checksum is " +
            calculatedChecksum + " but read checksum " +
            expectedChecksum, txid);
      }
      return txid;
    }
  }

  /**
   * Read edit logs which have a checksum and a transaction ID, but not a
   * length.
   */
  private static class ChecksummedReader extends Reader {
    private final Checksum checksum;

    ChecksummedReader(Checksum checksum, DataInputStream in,
                      StreamLimiter limiter, int logVersion) {
      super(new DataInputStream(
          new CheckedInputStream(in, checksum)), limiter, logVersion);
      this.checksum = checksum;
    }

    @Override
    public FSEditLogOp decodeOp() throws IOException {
      limiter.setLimit(maxOpSize);
      in.mark(maxOpSize);
      // Reset the checksum.  Since we are using a CheckedInputStream, each
      // subsequent read from the  stream will update the checksum.
      checksum.reset();
      byte opCodeByte;
      try {
        opCodeByte = in.readByte();
      } catch (EOFException eof) {
        // EOF at an opcode boundary is expected.
        return null;
      }
      FSEditLogOpCodes opCode = FSEditLogOpCodes.fromByte(opCodeByte);
      if (opCode == OP_INVALID) {
        verifyTerminator();
        return null;
      }
      FSEditLogOp op = cache.get(opCode);
      if (op == null) {
        throw new IOException("Read invalid opcode " + opCode);
      }
      op.setTransactionId(in.readLong());
      op.readFields(in, logVersion);
      // Verify checksum
      int calculatedChecksum = (int)checksum.getValue();
      int expectedChecksum = in.readInt();
      if (expectedChecksum != calculatedChecksum) {
        throw new ChecksumException(
            "Transaction is corrupt. Calculated checksum is " +
                calculatedChecksum + " but read checksum " +
                expectedChecksum, op.txid);
      }
      return op;
    }

    @Override
    public long scanOp() throws IOException {
      // Edit logs of this age don't have any length prefix, so we just have
      // to read the entire Op.
      FSEditLogOp op = decodeOp();
      return op == null ?
          HdfsServerConstants.INVALID_TXID : op.getTransactionId();
    }
  }

  /**
   * Read older edit logs which may or may not have transaction IDs and other
   * features.  This code is used during upgrades and to allow HDFS INotify to
   * read older edit log files.
   */
  private static class LegacyReader extends Reader {
    LegacyReader(DataInputStream in,
                      StreamLimiter limiter, int logVersion) {
      super(in, limiter, logVersion);
    }

    @Override
    public FSEditLogOp decodeOp() throws IOException {
      limiter.setLimit(maxOpSize);
      in.mark(maxOpSize);
      byte opCodeByte;
      try {
        opCodeByte = in.readByte();
      } catch (EOFException eof) {
        // EOF at an opcode boundary is expected.
        return null;
      }
      FSEditLogOpCodes opCode = FSEditLogOpCodes.fromByte(opCodeByte);
      if (opCode == OP_INVALID) {
        verifyTerminator();
        return null;
      }
      FSEditLogOp op = cache.get(opCode);
      if (op == null) {
        throw new IOException("Read invalid opcode " + opCode);
      }
      if (NameNodeLayoutVersion.supports(
            LayoutVersion.Feature.STORED_TXIDS, logVersion)) {
        op.setTransactionId(in.readLong());
      } else {
        op.setTransactionId(HdfsServerConstants.INVALID_TXID);
      }
      op.readFields(in, logVersion);
      return op;
    }

    @Override
    public long scanOp() throws IOException {
      if (!NameNodeLayoutVersion.supports(
          LayoutVersion.Feature.STORED_TXIDS, logVersion)) {
        throw new IOException("Can't scan a pre-transactional edit log.");
      }
      FSEditLogOp op = decodeOp();
      return op == null ?
          HdfsServerConstants.INVALID_TXID : op.getTransactionId();
    }
  }

  public void outputToXml(ContentHandler contentHandler) throws SAXException {
    contentHandler.startElement("", "", "RECORD", new AttributesImpl());
    XMLUtils.addSaxString(contentHandler, "OPCODE", opCode.toString());
    contentHandler.startElement("", "", "DATA", new AttributesImpl());
    XMLUtils.addSaxString(contentHandler, "TXID", "" + txid);
    toXml(contentHandler);
    contentHandler.endElement("", "", "DATA");
    contentHandler.endElement("", "", "RECORD");
  }

  protected abstract void toXml(ContentHandler contentHandler)
      throws SAXException;
  
  abstract void fromXml(Stanza st) throws InvalidXmlException;
  
  public void decodeXml(Stanza st) throws InvalidXmlException {
    this.txid = Long.parseLong(st.getValue("TXID"));
    fromXml(st);
  }
  
  public static void blockToXml(ContentHandler contentHandler, Block block) 
      throws SAXException {
    contentHandler.startElement("", "", "BLOCK", new AttributesImpl());
    XMLUtils.addSaxString(contentHandler, "BLOCK_ID",
        Long.toString(block.getBlockId()));
    XMLUtils.addSaxString(contentHandler, "NUM_BYTES",
        Long.toString(block.getNumBytes()));
    XMLUtils.addSaxString(contentHandler, "GENSTAMP",
        Long.toString(block.getGenerationStamp()));
    contentHandler.endElement("", "", "BLOCK");
  }

  public static Block blockFromXml(Stanza st)
      throws InvalidXmlException {
    long blockId = Long.parseLong(st.getValue("BLOCK_ID"));
    long numBytes = Long.parseLong(st.getValue("NUM_BYTES"));
    long generationStamp = Long.parseLong(st.getValue("GENSTAMP"));
    return new Block(blockId, numBytes, generationStamp);
  }

  public static void delegationTokenToXml(ContentHandler contentHandler,
      DelegationTokenIdentifier token) throws SAXException {
    contentHandler.startElement(
        "", "", "DELEGATION_TOKEN_IDENTIFIER", new AttributesImpl());
    XMLUtils.addSaxString(contentHandler, "KIND", token.getKind().toString());
    XMLUtils.addSaxString(contentHandler, "SEQUENCE_NUMBER",
        Integer.toString(token.getSequenceNumber()));
    XMLUtils.addSaxString(contentHandler, "OWNER",
        token.getOwner().toString());
    XMLUtils.addSaxString(contentHandler, "RENEWER",
        token.getRenewer().toString());
    XMLUtils.addSaxString(contentHandler, "REALUSER",
        token.getRealUser().toString());
    XMLUtils.addSaxString(contentHandler, "ISSUE_DATE",
        Long.toString(token.getIssueDate()));
    XMLUtils.addSaxString(contentHandler, "MAX_DATE",
        Long.toString(token.getMaxDate()));
    XMLUtils.addSaxString(contentHandler, "MASTER_KEY_ID",
        Integer.toString(token.getMasterKeyId()));
    contentHandler.endElement("", "", "DELEGATION_TOKEN_IDENTIFIER");
  }

  public static DelegationTokenIdentifier delegationTokenFromXml(Stanza st)
      throws InvalidXmlException {
    String kind = st.getValue("KIND");
    if (!kind.equals(DelegationTokenIdentifier.
        HDFS_DELEGATION_KIND.toString())) {
      throw new InvalidXmlException("can't understand " +
        "DelegationTokenIdentifier KIND " + kind);
    }
    int seqNum = Integer.parseInt(st.getValue("SEQUENCE_NUMBER"));
    String owner = st.getValue("OWNER");
    String renewer = st.getValue("RENEWER");
    String realuser = st.getValue("REALUSER");
    long issueDate = Long.parseLong(st.getValue("ISSUE_DATE"));
    long maxDate = Long.parseLong(st.getValue("MAX_DATE"));
    int masterKeyId = Integer.parseInt(st.getValue("MASTER_KEY_ID"));
    DelegationTokenIdentifier token =
        new DelegationTokenIdentifier(new Text(owner),
            new Text(renewer), new Text(realuser));
    token.setSequenceNumber(seqNum);
    token.setIssueDate(issueDate);
    token.setMaxDate(maxDate);
    token.setMasterKeyId(masterKeyId);
    return token;
  }

  public static void delegationKeyToXml(ContentHandler contentHandler,
      DelegationKey key) throws SAXException {
    contentHandler.startElement(
        "", "", "DELEGATION_KEY", new AttributesImpl());
    XMLUtils.addSaxString(contentHandler, "KEY_ID",
        Integer.toString(key.getKeyId()));
    XMLUtils.addSaxString(contentHandler, "EXPIRY_DATE",
        Long.toString(key.getExpiryDate()));
    if (key.getEncodedKey() != null) {
      XMLUtils.addSaxString(contentHandler, "KEY",
          Hex.encodeHexString(key.getEncodedKey()));
    }
    contentHandler.endElement("", "", "DELEGATION_KEY");
  }
  
  public static DelegationKey delegationKeyFromXml(Stanza st)
      throws InvalidXmlException {
    int keyId = Integer.parseInt(st.getValue("KEY_ID"));
    long expiryDate = Long.parseLong(st.getValue("EXPIRY_DATE"));
    byte key[] = null;
    try {
      key = Hex.decodeHex(st.getValue("KEY").toCharArray());
    } catch (DecoderException e) {
      throw new InvalidXmlException(e.toString());
    } catch (InvalidXmlException e) {
    }
    return new DelegationKey(keyId, expiryDate, key);
  }

  public static void permissionStatusToXml(ContentHandler contentHandler,
      PermissionStatus perm) throws SAXException {
    contentHandler.startElement(
        "", "", "PERMISSION_STATUS", new AttributesImpl());
    XMLUtils.addSaxString(contentHandler, "USERNAME", perm.getUserName());
    XMLUtils.addSaxString(contentHandler, "GROUPNAME", perm.getGroupName());
    fsPermissionToXml(contentHandler, perm.getPermission());
    contentHandler.endElement("", "", "PERMISSION_STATUS");
  }

  public static PermissionStatus permissionStatusFromXml(Stanza st)
      throws InvalidXmlException {
    Stanza status = st.getChildren("PERMISSION_STATUS").get(0);
    String username = status.getValue("USERNAME");
    String groupname = status.getValue("GROUPNAME");
    FsPermission mode = fsPermissionFromXml(status);
    return new PermissionStatus(username, groupname, mode);
  }

  public static void fsPermissionToXml(ContentHandler contentHandler,
      FsPermission mode) throws SAXException {
    XMLUtils.addSaxString(contentHandler, "MODE",
        Short.toString(mode.toShort()));
  }

  public static FsPermission fsPermissionFromXml(Stanza st)
      throws InvalidXmlException {
    short mode = Short.parseShort(st.getValue("MODE"));
    return new FsPermission(mode);
  }

  private static void fsActionToXml(ContentHandler contentHandler, FsAction v)
      throws SAXException {
    XMLUtils.addSaxString(contentHandler, "PERM", v.SYMBOL);
  }

  private static FsAction fsActionFromXml(Stanza st)
      throws InvalidXmlException {
    FsAction v = FSACTION_SYMBOL_MAP.get(st.getValue("PERM"));
    if (v == null)
      throw new InvalidXmlException("Invalid value for FsAction");
    return v;
  }

  private static void appendAclEntriesToXml(ContentHandler contentHandler,
      List<AclEntry> aclEntries) throws SAXException {
    for (AclEntry e : aclEntries) {
      contentHandler.startElement("", "", "ENTRY", new AttributesImpl());
      XMLUtils.addSaxString(contentHandler, "SCOPE", e.getScope().name());
      XMLUtils.addSaxString(contentHandler, "TYPE", e.getType().name());
      if (e.getName() != null) {
        XMLUtils.addSaxString(contentHandler, "NAME", e.getName());
      }
      fsActionToXml(contentHandler, e.getPermission());
      contentHandler.endElement("", "", "ENTRY");
    }
  }

  private static List<AclEntry> readAclEntriesFromXml(Stanza st) {
    List<AclEntry> aclEntries = Lists.newArrayList();
    if (!st.hasChildren("ENTRY"))
      return null;

    List<Stanza> stanzas = st.getChildren("ENTRY");
    for (Stanza s : stanzas) {
      AclEntry e = new AclEntry.Builder()
        .setScope(AclEntryScope.valueOf(s.getValue("SCOPE")))
        .setType(AclEntryType.valueOf(s.getValue("TYPE")))
        .setName(s.getValueOrNull("NAME"))
        .setPermission(fsActionFromXml(s)).build();
      aclEntries.add(e);
    }
    return aclEntries;
  }

  private static void appendXAttrsToXml(ContentHandler contentHandler,
      List<XAttr> xAttrs) throws SAXException {
    for (XAttr xAttr: xAttrs) {
      contentHandler.startElement("", "", "XATTR", new AttributesImpl());
      XMLUtils.addSaxString(contentHandler, "NAMESPACE",
          xAttr.getNameSpace().toString());
      XMLUtils.addSaxString(contentHandler, "NAME", xAttr.getName());
      if (xAttr.getValue() != null) {
        try {
          XMLUtils.addSaxString(contentHandler, "VALUE",
              XAttrCodec.encodeValue(xAttr.getValue(), XAttrCodec.HEX));
        } catch (IOException e) {
          throw new SAXException(e);
        }
      }
      contentHandler.endElement("", "", "XATTR");
    }
  }

  private static List<XAttr> readXAttrsFromXml(Stanza st)
      throws InvalidXmlException {
    if (!st.hasChildren("XATTR")) {
      return null;
    }

    List<Stanza> stanzas = st.getChildren("XATTR");
    List<XAttr> xattrs = Lists.newArrayListWithCapacity(stanzas.size());
    for (Stanza a: stanzas) {
      XAttr.Builder builder = new XAttr.Builder();
      builder.setNameSpace(XAttr.NameSpace.valueOf(a.getValue("NAMESPACE"))).
          setName(a.getValue("NAME"));
      String v = a.getValueOrNull("VALUE");
      if (v != null) {
        try {
          builder.setValue(XAttrCodec.decodeValue(v));
        } catch (IOException e) {
          throw new InvalidXmlException(e.toString());
        }
      }
      xattrs.add(builder.build());
    }
    return xattrs;
  }
}
