From f8ef9adbec02e6dfba7a170333d2094d488da6ab Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata@redhat.com>
Date: Mon, 7 Apr 2014 16:39:29 -0400
Subject: [PATCH] Added change token status dialog.

A new dialog has been added to change the token status. The status
can be changed by clicking the Status value in the tokens table.
Initially the status is Uninitialized. The status can be changed
according to the allowed status transitions defined in the CS.cfg.

The status and reason fields in TokenRecord is now translated into
a single status field in TokenData. This way the UI only needs to
handle a single status field.

A new field has also been added to the database for token type.
Some issues displaying and updating some token attributes have been
fixed as well.

Ticket #654
---
 .../com/netscape/certsrv/tps/token/TokenData.java  |  47 +++---
 .../netscape/certsrv/tps/token/TokenResource.java  |   8 +
 .../netscape/certsrv/tps/token/TokenStatus.java    |  54 +++++++
 .../com/netscape/cmstools/tps/token/TokenCLI.java  |   2 +-
 base/server/share/webapps/pki/js/pki-ui.js         |  14 ++
 base/tps-tomcat/shared/webapps/tps/js/token.js     |  94 ++++++++++--
 base/tps-tomcat/shared/webapps/tps/ui/tokens.html  |  45 +++++-
 .../dogtagpki/server/tps/dbs/TokenDatabase.java    |   1 -
 .../org/dogtagpki/server/tps/dbs/TokenRecord.java  |  16 ++
 .../dogtagpki/server/tps/rest/TokenService.java    | 162 ++++++++++++++++++++-
 10 files changed, 394 insertions(+), 49 deletions(-)
 create mode 100644 base/common/src/com/netscape/certsrv/tps/token/TokenStatus.java

diff --git a/base/common/src/com/netscape/certsrv/tps/token/TokenData.java b/base/common/src/com/netscape/certsrv/tps/token/TokenData.java
index 3c6b2d7baff23940cc2cbdb235f241fbe5313ae9..27ee1fa203f083b41843af9405914121a2571c13 100644
--- a/base/common/src/com/netscape/certsrv/tps/token/TokenData.java
+++ b/base/common/src/com/netscape/certsrv/tps/token/TokenData.java
@@ -53,8 +53,8 @@ public class TokenData {
     String id;
     String tokenID;
     String userID;
-    String status;
-    String reason;
+    String type;
+    TokenStatus status;
     String appletID;
     String keyInfo;
     Date createTimestamp;
@@ -89,24 +89,24 @@ public class TokenData {
         this.userID = userID;
     }
 
+    @XmlElement(name="Type")
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
     @XmlElement(name="Status")
-    public String getStatus() {
+    public TokenStatus getStatus() {
         return status;
     }
 
-    public void setStatus(String status) {
+    public void setStatus(TokenStatus status) {
         this.status = status;
     }
 
-    @XmlElement(name="Reason")
-    public String getReason() {
-        return reason;
-    }
-
-    public void setReason(String reason) {
-        this.reason = reason;
-    }
-
     @XmlElement(name="AppletID")
     public String getAppletID() {
         return appletID;
@@ -162,9 +162,9 @@ public class TokenData {
         result = prime * result + ((keyInfo == null) ? 0 : keyInfo.hashCode());
         result = prime * result + ((link == null) ? 0 : link.hashCode());
         result = prime * result + ((modifyTimestamp == null) ? 0 : modifyTimestamp.hashCode());
-        result = prime * result + ((reason == null) ? 0 : reason.hashCode());
         result = prime * result + ((status == null) ? 0 : status.hashCode());
         result = prime * result + ((tokenID == null) ? 0 : tokenID.hashCode());
+        result = prime * result + ((type == null) ? 0 : type.hashCode());
         result = prime * result + ((userID == null) ? 0 : userID.hashCode());
         return result;
     }
@@ -208,21 +208,18 @@ public class TokenData {
                 return false;
         } else if (!modifyTimestamp.equals(other.modifyTimestamp))
             return false;
-        if (reason == null) {
-            if (other.reason != null)
-                return false;
-        } else if (!reason.equals(other.reason))
-            return false;
-        if (status == null) {
-            if (other.status != null)
-                return false;
-        } else if (!status.equals(other.status))
+        if (status != other.status)
             return false;
         if (tokenID == null) {
             if (other.tokenID != null)
                 return false;
         } else if (!tokenID.equals(other.tokenID))
             return false;
+        if (type == null) {
+            if (other.type != null)
+                return false;
+        } else if (!type.equals(other.type))
+            return false;
         if (userID == null) {
             if (other.userID != null)
                 return false;
@@ -255,8 +252,8 @@ public class TokenData {
         TokenData before = new TokenData();
         before.setID("token1");
         before.setUserID("user1");
-        before.setStatus("revoked");
-        before.setReason("lost");
+        before.setType("userKey");
+        before.setStatus(TokenStatus.ACTIVE);
         before.setAppletID("APPLET1234");
         before.setKeyInfo("key info");
         before.setCreateTimestamp(new Date());
diff --git a/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java b/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java
index 3f5f305ef9ee22284e4edbbc265a5e9f185bf404..e530a852758f6589f4abb0f1dfae707c349d3931 100644
--- a/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java
+++ b/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java
@@ -74,6 +74,14 @@ public interface TokenResource {
             @PathParam("tokenID") String tokenID,
             TokenData tokenData);
 
+    @POST
+    @Path("{tokenID}")
+    @ClientResponseType(entityType=TokenData.class)
+    @ACLMapping("tokens.modify")
+    public Response changeTokenStatus(
+            @PathParam("tokenID") String tokenID,
+            @QueryParam("status") TokenStatus tokenStatus);
+
     @DELETE
     @Path("{tokenID}")
     @ClientResponseType(entityType=Void.class)
diff --git a/base/common/src/com/netscape/certsrv/tps/token/TokenStatus.java b/base/common/src/com/netscape/certsrv/tps/token/TokenStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c6d349b3eeff65ebc7493ad50c4e7a0cbadb2f7
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/tps/token/TokenStatus.java
@@ -0,0 +1,54 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2014 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.tps.token;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Endi S. Dewata
+ */
+public enum TokenStatus {
+
+    UNINITIALIZED(0),
+    DAMAGED(1),
+    PERM_LOST(2),
+    TEMP_LOST(3),
+    ACTIVE(4),
+    TEMP_LOST_PERM_LOST(5),
+    TERMINATED(6);
+
+    static Map<Integer, TokenStatus> map = new HashMap<Integer, TokenStatus>();
+
+    Integer value;
+
+    static {
+        for (TokenStatus state : TokenStatus.values()) {
+            map.put(state.value, state);
+        }
+    }
+
+    TokenStatus(Integer value) {
+        this.value = value;
+    }
+
+    public static TokenStatus fromInt(Integer value) {
+        return map.get(value);
+    }
+}
diff --git a/base/java-tools/src/com/netscape/cmstools/tps/token/TokenCLI.java b/base/java-tools/src/com/netscape/cmstools/tps/token/TokenCLI.java
index a26c07f9ae169a39f48812909c5304d6252254c2..df3ab289029ccafa73337fd62de372af4b863c19 100644
--- a/base/java-tools/src/com/netscape/cmstools/tps/token/TokenCLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/tps/token/TokenCLI.java
@@ -52,8 +52,8 @@ public class TokenCLI extends CLI {
     public static void printToken(TokenData token) {
         System.out.println("  Token ID: " + token.getID());
         if (token.getUserID() != null) System.out.println("  User ID: " + token.getUserID());
+        if (token.getType() != null) System.out.println("  Status: " + token.getType());
         if (token.getStatus() != null) System.out.println("  Status: " + token.getStatus());
-        if (token.getReason() != null) System.out.println("  Reason: " + token.getReason());
         if (token.getAppletID() != null) System.out.println("  Applet ID: " + token.getAppletID());
         if (token.getKeyInfo() != null) System.out.println("  Key Info: " + token.getKeyInfo());
         if (token.getCreateTimestamp() != null) System.out.println("  Date Created: " + token.getCreateTimestamp());
diff --git a/base/server/share/webapps/pki/js/pki-ui.js b/base/server/share/webapps/pki/js/pki-ui.js
index 8314ca22f5408a8512b02975f735bd55c3f97cc3..c4ff6157b8681172d2b458c13cbb6b1680c7e075 100644
--- a/base/server/share/webapps/pki/js/pki-ui.js
+++ b/base/server/share/webapps/pki/js/pki-ui.js
@@ -291,10 +291,17 @@ var Dialog = Backbone.View.extend({
     load: function() {
         var self = this;
 
+        // load input fields
         $("input", self.$el).each(function(index) {
             var input = $(this);
             self.loadField(input);
         });
+
+        // load drop-down lists
+        $("select", self.$el).each(function(index) {
+            var input = $(this);
+            self.loadField(input);
+        });
     },
     loadField: function(input) {
         var self = this;
@@ -306,10 +313,17 @@ var Dialog = Backbone.View.extend({
     save: function() {
         var self = this;
 
+        // save input fields
         $("input", self.$el).each(function(index) {
             var input = $(this);
             self.saveField(input);
         });
+
+        // save drop-down lists
+        $("select", self.$el).each(function(index) {
+            var input = $(this);
+            self.saveField(input);
+        });
     },
     saveField: function(input) {
         var self = this;
diff --git a/base/tps-tomcat/shared/webapps/tps/js/token.js b/base/tps-tomcat/shared/webapps/tps/js/token.js
index aa51c73b207deeb07feb0c77de4e1e0e8f96ab6e..5e793257ba0339bcab8e187baf0dc6d24a71d979 100644
--- a/base/tps-tomcat/shared/webapps/tps/js/token.js
+++ b/base/tps-tomcat/shared/webapps/tps/js/token.js
@@ -19,6 +19,15 @@
  * @author Endi S. Dewata
  */
 
+var TokenStatus = {
+    UNINITIALIZED: "Uninitialized",
+    ACTIVE: "Active",
+    TEMP_LOST: "Temporarily lost",
+    PERM_LOST: "Permanently lost",
+    DAMAGED: "Physically damaged",
+    TERMINATED: "Terminated"
+};
+
 var TokenModel = Model.extend({
     urlRoot: "/tps/rest/tokens",
     parseResponse: function(response) {
@@ -26,8 +35,9 @@ var TokenModel = Model.extend({
             id: response.id,
             tokenID: response.TokenID,
             userID: response.UserID,
+            type: response.Type,
             status: response.Status,
-            reason: response.Reason,
+            statusLabel: TokenStatus[response.Status],
             appletID: response.AppletID,
             keyInfo: response.KeyInfo,
             createTimestamp: response.CreateTimestamp,
@@ -39,13 +49,26 @@ var TokenModel = Model.extend({
             id: this.id,
             TokenID: attributes.tokenID,
             UserID: attributes.userID,
+            Type: attributes.type,
             Status: attributes.status,
-            Reason: attributes.reason,
             AppletID: attributes.appletID,
             KeyInfo: attributes.keyInfo,
             CreateTimestamp: attributes.createTimestamp,
             ModifyTimestamp: attributes.modifyTimestamp
         };
+    },
+    changeStatus: function(options) {
+        var self = this;
+        $.ajax({
+            type: "POST",
+            url: self.url() + "?status=" + options.status,
+            dataType: "json"
+        }).done(function(data, textStatus, jqXHR) {
+            self.set(self.parseResponse(data));
+            if (options.success) options.success.call(self, data, textStatus, jqXHR);
+        }).fail(function(jqXHR, textStatus, errorThrown) {
+            if (options.error) options.error.call(self, jqXHR, textStatus, errorThrown);
+        });
     }
 });
 
@@ -63,16 +86,68 @@ var TokenCollection = Collection.extend({
             id: entry.id,
             tokenID: entry.TokenID,
             userID: entry.UserID,
+            type: entry.Type,
             status: entry.Status,
-            reason: entry.Reason,
+            statusLabel: TokenStatus[entry.Status],
             appletID: entry.AppletID,
             keyInfo: entry.KeyInfo,
-            created: entry.CreateTimestamp,
-            modified: entry.ModifyTimestamp
+            createTimestamp: entry.CreateTimestamp,
+            modifyTimestamp: entry.ModifyTimestamp
         });
     }
 });
 
+var TokenTableItem = TableItem.extend({
+    initialize: function(options) {
+        var self = this;
+        PropertiesTableItem.__super__.initialize.call(self, options);
+    },
+    open: function(td) {
+        var self = this;
+
+        var name = td.attr("name");
+        if (name != "status") {
+            TokenTableItem.__super__.open.call(self, td);
+            return;
+        }
+
+        var dialog = new Dialog({
+            el: $("#token-state-dialog"),
+            title: "Change Token State",
+            readonly: ["tokenID", "userID", "type",
+                "appletID", "keyInfo", "createTimestamp", "modifyTimestamp"],
+            actions: ["cancel", "save"]
+        });
+
+        dialog.entry = _.clone(self.entry);
+
+        dialog.handler("save", function() {
+
+            // save changes
+            dialog.save();
+
+            // check if the status was changed
+            if (self.entry.status != dialog.entry.status) {
+
+                var model = self.table.collection.get(self.entry.id);
+                model.changeStatus({
+                    status: dialog.entry.status,
+                    success: function(data, textStatus, jqXHR) {
+                        self.table.render();
+                    },
+                    error: function(jqXHR, textStatus, errorThrow) {
+                        alert("ERROR: " + jqXHR.responseText);
+                    }
+                });
+            }
+
+            dialog.close();
+        });
+
+        dialog.open();
+    }
+});
+
 var TokenPage = Page.extend({
     load: function() {
         var self = this;
@@ -80,16 +155,14 @@ var TokenPage = Page.extend({
         var addDialog = new Dialog({
             el: $("#token-dialog"),
             title: "Add Token",
-            readonly: ["status", "reason", "appletID", "keyInfo",
-                "createTimestamp", "modifyTimestamp"],
+            readonly: ["statusLabel", "createTimestamp", "modifyTimestamp"],
             actions: ["cancel", "add"]
         });
 
         var editDialog = new Dialog({
             el: $("#token-dialog"),
             title: "Edit Token",
-            readonly: ["tokenID", "status", "reason", "appletID", "keyInfo",
-                "createTimestamp", "modifyTimestamp"],
+            readonly: ["tokenID", "statusLabel", "createTimestamp", "modifyTimestamp"],
             actions: ["cancel", "save"]
         });
 
@@ -97,7 +170,8 @@ var TokenPage = Page.extend({
             el: $("table[name='tokens']"),
             collection: new TokenCollection(),
             addDialog: addDialog,
-            editDialog: editDialog
+            editDialog: editDialog,
+            tableItem: TokenTableItem
         });
 
         table.render();
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/tokens.html b/base/tps-tomcat/shared/webapps/tps/ui/tokens.html
index 7a5c168350aa05716be2e021e294ece13d04aedb..ea3c4a5c25d301b6a7d494868de7f23f57e74932 100644
--- a/base/tps-tomcat/shared/webapps/tps/ui/tokens.html
+++ b/base/tps-tomcat/shared/webapps/tps/ui/tokens.html
@@ -36,8 +36,8 @@
         <th class="pki-select-column"><input id="token_selectall" type="checkbox"><label for="token_selectall">&nbsp;</label></th>
         <th>Token ID</th>
         <th>User ID</th>
+        <th>Type</th>
         <th>Status</th>
-        <th>Reason</th>
         <th>Applet ID</th>
         <th>Key Info</th>
         <th>Created</th>
@@ -49,12 +49,12 @@
         <td class="pki-select-column"><input id="token_select" type="checkbox"><label for="token_select">&nbsp;</label></td>
         <td name="id"><a href="/tps/ui/tokens/${id}">${id}</a></td>
         <td name="userID">${userID}</td>
-        <td name="status"><a href="/tps/ui/tokens/${id}/status">${status}</a></td>
-        <td name="reason">${reason}</td>
+        <td name="type">${type}</td>
+        <td name="status"><a href="/tps/ui/tokens/${id}/status">${statusLabel}</a></td>
         <td name="appletID">${appletID}</td>
         <td name="keyInfo">${keyInfo}</td>
-        <td name="created">${created}</td>
-        <td name="modified">${modified}</td>
+        <td name="created">${createTimestamp}</td>
+        <td name="modified">${modifyTimestamp}</td>
     </tr>
 </tbody>
 <tfoot>
@@ -90,8 +90,8 @@
         <fieldset>
             <label>Token ID</label><input name="tokenID" type="text"><br>
             <label>User ID</label><input name="userID" type="text"><br>
-            <label>Status</label><input name="status" type="text"><br>
-            <label>Reason</label><input name="reason" type="text"><br>
+            <label>Type</label><input name="type" type="text"><br>
+            <label>Status</label><input name="statusLabel" type="text"><br>
             <label>Applet ID</label><input name="appletID" type="text"><br>
             <label>Key Info</label><input name="keyInfo" type="text"><br>
             <label>Created</label><input name="createTimestamp" type="text"><br>
@@ -105,3 +105,34 @@
         </footer>
     </div>
 </div>
+
+<div id="token-state-dialog" class="rcue-dialog-background">
+    <div class="rcue-dialog">
+        <header>
+            <h1>Change Token Status</h1>
+            <a class="rcue-button-close" href="#"></a>
+        </header>
+        <fieldset>
+            <label>Token ID</label><input name="tokenID" type="text"><br>
+            <label>User ID</label><input name="userID" type="text"><br>
+            <label>Type</label><input name="type" type="text"><br>
+            <label>Status</label>
+            <select name="status">
+            <option value="UNINITIALIZED">Uninitialized</option>
+            <option value="ACTIVE">Active</option>
+            <option value="TEMP_LOST">Temporarily lost</option>
+            <option value="PERM_LOST">Permanently lost</option>
+            <option value="DAMAGED">Physically damaged</option>
+            <option value="TERMINATED">Terminated</option>
+            </select><br>
+            <label>Applet ID</label><input name="appletID" type="text"><br>
+            <label>Key Info</label><input name="keyInfo" type="text"><br>
+            <label>Created</label><input name="createTimestamp" type="text"><br>
+            <label>Modified</label><input name="modifyTimestamp" type="text"><br>
+        </fieldset>
+        <footer>
+            <button name="save" class="primary">Save</button>
+            <button name="cancel">Cancel</button>
+        </footer>
+    </div>
+</div>
diff --git a/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenDatabase.java b/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenDatabase.java
index 6f5a790942b9c471cecfbd8793b0b4effa84f795..f95e10bf7813dcd2878adc0ce5feb6946cec93fa 100644
--- a/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenDatabase.java
+++ b/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenDatabase.java
@@ -38,7 +38,6 @@ public class TokenDatabase extends LDAPDatabase<TokenRecord> {
 
     @Override
     public void addRecord(String id, TokenRecord tokenRecord) throws Exception {
-        tokenRecord.setStatus("ENABLED");
         tokenRecord.setCreateTimestamp(new Date());
 
         super.addRecord(id, tokenRecord);
diff --git a/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenRecord.java b/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenRecord.java
index bab61ac199e619fb55b00eee952f83868b739812..27532bbf2154b829eae6cf68c577c875e4cd5682 100644
--- a/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenRecord.java
+++ b/base/tps-tomcat/src/org/dogtagpki/server/tps/dbs/TokenRecord.java
@@ -34,6 +34,7 @@ public class TokenRecord extends DBRecord {
 
     String id;
     String userID;
+    String type;
     String status;
     String reason;
     String appletID;
@@ -59,6 +60,15 @@ public class TokenRecord extends DBRecord {
         this.userID = userID;
     }
 
+    @DBAttribute("tokenType")
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
     @DBAttribute("tokenStatus")
     public String getStatus() {
         return status;
@@ -124,6 +134,7 @@ public class TokenRecord extends DBRecord {
         result = prime * result + ((modifyTimestamp == null) ? 0 : modifyTimestamp.hashCode());
         result = prime * result + ((reason == null) ? 0 : reason.hashCode());
         result = prime * result + ((status == null) ? 0 : status.hashCode());
+        result = prime * result + ((type == null) ? 0 : type.hashCode());
         result = prime * result + ((userID == null) ? 0 : userID.hashCode());
         return result;
     }
@@ -172,6 +183,11 @@ public class TokenRecord extends DBRecord {
                 return false;
         } else if (!status.equals(other.status))
             return false;
+        if (type == null) {
+            if (other.type != null)
+                return false;
+        } else if (!type.equals(other.type))
+            return false;
         if (userID == null) {
             if (other.userID != null)
                 return false;
diff --git a/base/tps-tomcat/src/org/dogtagpki/server/tps/rest/TokenService.java b/base/tps-tomcat/src/org/dogtagpki/server/tps/rest/TokenService.java
index 110abe7db70f06798fdfc95e84b12389df2722d2..dd3fc72c123e3ee84cb2a86cd27ce254fb94e91d 100644
--- a/base/tps-tomcat/src/org/dogtagpki/server/tps/rest/TokenService.java
+++ b/base/tps-tomcat/src/org/dogtagpki/server/tps/rest/TokenService.java
@@ -21,7 +21,11 @@ package org.dogtagpki.server.tps.rest;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URLEncoder;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.Context;
@@ -37,10 +41,12 @@ import org.jboss.resteasy.plugins.providers.atom.Link;
 
 import com.netscape.certsrv.apps.CMS;
 import com.netscape.certsrv.base.BadRequestException;
+import com.netscape.certsrv.base.IConfigStore;
 import com.netscape.certsrv.base.PKIException;
 import com.netscape.certsrv.tps.token.TokenCollection;
 import com.netscape.certsrv.tps.token.TokenData;
 import com.netscape.certsrv.tps.token.TokenResource;
+import com.netscape.certsrv.tps.token.TokenStatus;
 import com.netscape.cms.servlet.base.PKIService;
 
 /**
@@ -62,8 +68,91 @@ public class TokenService extends PKIService implements TokenResource {
 
     public final static int DEFAULT_SIZE = 20;
 
-    public TokenService() {
+    public Map<TokenStatus, Collection<TokenStatus>> transitions = new HashMap<TokenStatus, Collection<TokenStatus>>();
+
+    public TokenService() throws Exception {
         CMS.debug("TokenService.<init>()");
+        IConfigStore configStore = CMS.getConfigStore();
+
+        // load allowed token state transitions
+        CMS.debug("TokenService: allowed transitions:");
+
+        for (String transition : configStore.getString("tokendb.allowedTransitions").split(",")) {
+            String states[] = transition.split(":");
+            TokenStatus fromState = TokenStatus.fromInt(Integer.valueOf(states[0]));
+            TokenStatus toState = TokenStatus.fromInt(Integer.valueOf(states[1]));
+            CMS.debug("TokenService:  - " + fromState + " to " + toState);
+
+            Collection<TokenStatus> nextStates = transitions.get(fromState);
+            if (nextStates == null) {
+                nextStates = new HashSet<TokenStatus>();
+                transitions.put(fromState, nextStates);
+            }
+            nextStates.add(toState);
+        }
+
+    }
+
+    public TokenStatus getTokenStatus(TokenRecord tokenRecord) {
+        String status = tokenRecord.getStatus();
+
+        if ("uninitialized".equals(status)) {
+            return TokenStatus.UNINITIALIZED;
+
+        } else if ("active".equals(status)) {
+            return TokenStatus.ACTIVE;
+
+        } else if ("lost".equals(status)) {
+            String reason = tokenRecord.getReason();
+
+            if ("keyCompromise".equals(reason)) {
+                return TokenStatus.PERM_LOST;
+
+            } else if ("destroyed".equals(reason)) {
+                return TokenStatus.DAMAGED;
+
+            } else if ("onHold".equals(reason)) {
+                return TokenStatus.TEMP_LOST;
+            }
+
+        } else if ("terminated".equals(status)) {
+            return TokenStatus.TERMINATED;
+        }
+
+        return TokenStatus.PERM_LOST;
+    }
+
+    public void setTokenStatus(TokenRecord tokenRecord, TokenStatus tokenState) {
+        switch (tokenState) {
+        case UNINITIALIZED:
+            tokenRecord.setStatus("uninitialized");
+            tokenRecord.setReason(null);
+            break;
+        case ACTIVE:
+            tokenRecord.setStatus("active");
+            tokenRecord.setReason(null);
+            break;
+        case PERM_LOST:
+        case TEMP_LOST_PERM_LOST:
+            tokenRecord.setStatus("lost");
+            tokenRecord.setReason("keyCompromise");
+            break;
+        case DAMAGED:
+            tokenRecord.setStatus("lost");
+            tokenRecord.setReason("destroyed");
+            break;
+        case TEMP_LOST:
+            tokenRecord.setStatus("lost");
+            tokenRecord.setReason("onHold");
+            break;
+        case TERMINATED:
+            tokenRecord.setStatus("terminated");
+            tokenRecord.setReason(null);
+            break;
+        default:
+            throw new PKIException("Unsupported token state: " + tokenState);
+        }
+
     }
 
     public TokenData createTokenData(TokenRecord tokenRecord) {
@@ -72,8 +161,8 @@ public class TokenService extends PKIService implements TokenResource {
         tokenData.setID(tokenRecord.getId());
         tokenData.setTokenID(tokenRecord.getId());
         tokenData.setUserID(tokenRecord.getUserID());
-        tokenData.setStatus(tokenRecord.getStatus());
-        tokenData.setReason(tokenRecord.getReason());
+        tokenData.setType(tokenRecord.getType());
+        tokenData.setStatus(getTokenStatus(tokenRecord));
         tokenData.setAppletID(tokenRecord.getAppletID());
         tokenData.setKeyInfo(tokenRecord.getKeyInfo());
         tokenData.setCreateTimestamp(tokenRecord.getCreateTimestamp());
@@ -98,8 +187,8 @@ public class TokenService extends PKIService implements TokenResource {
         TokenRecord tokenRecord = new TokenRecord();
         tokenRecord.setId(tokenData.getID());
         tokenRecord.setUserID(tokenData.getUserID());
-        tokenRecord.setStatus(tokenData.getStatus());
-        tokenRecord.setReason(tokenData.getReason());
+        tokenRecord.setType(tokenData.getType());
+        setTokenStatus(tokenRecord, tokenData.getStatus());
         tokenRecord.setAppletID(tokenData.getAppletID());
         tokenRecord.setKeyInfo(tokenData.getKeyInfo());
         tokenRecord.setCreateTimestamp(tokenData.getCreateTimestamp());
@@ -186,6 +275,9 @@ public class TokenService extends PKIService implements TokenResource {
             TPSSubsystem subsystem = (TPSSubsystem)CMS.getSubsystem(TPSSubsystem.ID);
             TokenDatabase database = subsystem.getTokenDatabase();
 
+            // new tokens are uninitialized when created
+            tokenData.setStatus(TokenStatus.UNINITIALIZED);
+
             database.addRecord(tokenID, createTokenRecord(tokenData));
             tokenData = createTokenData(database.getRecord(tokenID));
 
@@ -211,6 +303,9 @@ public class TokenService extends PKIService implements TokenResource {
 
             TokenRecord tokenRecord = database.getRecord(tokenID);
             tokenRecord.setUserID(tokenData.getUserID());
+            tokenRecord.setType(tokenData.getType());
+            tokenRecord.setAppletID(tokenData.getAppletID());
+            tokenRecord.setKeyInfo(tokenData.getKeyInfo());
             database.updateRecord(tokenID, tokenRecord);
 
             tokenData = createTokenData(database.getRecord(tokenID));
@@ -237,11 +332,30 @@ public class TokenService extends PKIService implements TokenResource {
 
             TokenRecord tokenRecord = database.getRecord(tokenID);
 
+            // update user ID if specified
             String userID = tokenData.getUserID();
             if (userID != null) {
                 tokenRecord.setUserID(userID);
             }
 
+            // update type if specified
+            String type = tokenData.getType();
+            if (type != null) {
+                tokenRecord.setType(type);
+            }
+
+            // update applet ID if specified
+            String appletID = tokenData.getAppletID();
+            if (appletID != null) {
+                tokenRecord.setAppletID(appletID);
+            }
+
+            // update key info if specified
+            String keyInfo = tokenData.getKeyInfo();
+            if (keyInfo != null) {
+                tokenRecord.setKeyInfo(keyInfo);
+            }
+
             database.updateRecord(tokenID, tokenRecord);
 
             tokenData = createTokenData(database.getRecord(tokenID));
@@ -255,6 +369,44 @@ public class TokenService extends PKIService implements TokenResource {
     }
 
     @Override
+    public Response changeTokenStatus(String tokenID, TokenStatus tokenStatus) {
+
+        if (tokenID == null) throw new BadRequestException("Token ID is null.");
+        if (tokenStatus == null) throw new BadRequestException("Token state is null.");
+
+        CMS.debug("TokenService.changeTokenStatus(\"" + tokenID + "\", \"" + tokenStatus + "\")");
+
+        try {
+            TPSSubsystem subsystem = (TPSSubsystem)CMS.getSubsystem(TPSSubsystem.ID);
+            TokenDatabase database = subsystem.getTokenDatabase();
+
+            TokenRecord tokenRecord = database.getRecord(tokenID);
+            TokenStatus currentTokenStatus = getTokenStatus(tokenRecord);
+            CMS.debug("TokenService.changeTokenStatus(): current status: " + currentTokenStatus);
+
+            // make sure transition is allowed
+            Collection<TokenStatus> nextStatuses = transitions.get(currentTokenStatus);
+            CMS.debug("TokenService.changeTokenStatus(): allowed next statuses: " + nextStatuses);
+            if (nextStatuses == null || !nextStatuses.contains(tokenStatus)) {
+                CMS.debug("TokenService.changeTokenStatus(): next status not allowed: " + tokenStatus);
+                throw new BadRequestException("Invalid token status transition: " + currentTokenStatus + " to " + tokenStatus);
+            }
+
+            CMS.debug("TokenService.changeTokenStatus(): next status allowed: " + tokenStatus);
+            setTokenStatus(tokenRecord, tokenStatus);
+            database.updateRecord(tokenID, tokenRecord);
+
+            TokenData tokenData = createTokenData(database.getRecord(tokenID));
+
+            return createOKResponse(tokenData);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new PKIException(e.getMessage());
+        }
+    }
+
+    @Override
     public Response removeToken(String tokenID) {
 
         if (tokenID == null) throw new BadRequestException("Token ID is null.");
-- 
1.8.4.2

