Batch Test improvements

Issue-ID: AAF-776
Change-Id: Ia12106331a1db608955abd353785ab2e765b3c41
Signed-off-by: Instrumental <jonathan.gathman@att.com>
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/Batch.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/Batch.java
index 1c65c05..a588b80 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/Batch.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/Batch.java
@@ -149,7 +149,7 @@
     }
 
     protected abstract void run(AuthzTrans trans);
-    protected abstract void _close(AuthzTrans trans);
+    protected void _close(AuthzTrans trans) {}
     
     public String[] args() {
         return env.get(ssargs);
@@ -363,7 +363,12 @@
 
     public final void close(AuthzTrans trans) {
         _close(trans);
-        cluster.close();
+        if(session!=null) {
+        	session.close();
+        }
+        if(cluster!=null) {
+            cluster.close();
+        }
     }
 
     public static void main(String[] args) {
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/ApprovalSet.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/ApprovalSet.java
index b7176c2..500906d 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/ApprovalSet.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/ApprovalSet.java
@@ -25,7 +25,9 @@
 import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.GregorianCalendar;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.UUID;
 
 import org.onap.aaf.auth.dao.cass.ApprovalDAO;
@@ -93,4 +95,16 @@
 		}
 		return errs==null?Result.ok():Result.err(Result.ERR_Backend,errs.toString());
 	}
+
+	public boolean hasApprovals() {
+		return !ladd.isEmpty();
+	}
+	
+	public Set<String> approvers() {
+		Set<String> rv = new HashSet<>();
+		for(ApprovalDAO.Data app : ladd) {
+			rv.add(app.approver);
+		}
+		return rv;
+	}
 }
\ No newline at end of file
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/Pending.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/Pending.java
index 3072038..5d720c2 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/Pending.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/Pending.java
@@ -35,15 +35,6 @@
 	Date earliest;
 	
 	/**
-	 * Use this Constructor when there is no Last Notified Date
-	 */
-	public Pending() {
-		qty = 1;
-		hasNew = true;
-		earliest = null;
-	}
-
-	/**
 	 * Use this constructor to indicate when last Notified
 	 * @param last_notified
 	 */
@@ -84,6 +75,11 @@
 	
 	public void inc(Pending value) {
 		qty+=value.qty;
+		if(earliest==null) {
+			earliest = value.earliest;
+		} else if(value.earliest!=null && value.earliest.before(earliest)) {
+			earliest = value.earliest;
+		}
 	}
 
 	public void earliest(Date lastnotified) {
@@ -106,4 +102,8 @@
 		return hasNew;
 	}
 
+	public static Pending create() {
+		return new Pending((Date)null);
+	}
+
 }
\ No newline at end of file
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/URApprovalSet.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/URApprovalSet.java
index 2c1ffe6..7f7bff2 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/URApprovalSet.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/approvalsets/URApprovalSet.java
@@ -49,8 +49,12 @@
 		UserRoleDAO.Data urdd = lurdd.load();
 		setConstruct(urdd.bytify());
 		setMemo(getMemo(urdd));
-		setExpires(org.expiration(null, Organization.Expiration.UserInRole));
-		setTargetKey(urdd.role);
+		GregorianCalendar expires = org.expiration(null, Organization.Expiration.UserInRole);
+		if(urdd.expires.before(expires.getTime())) {
+			expires.setTime(urdd.expires);
+		}
+		setExpires(expires);
+		setTargetKey(urdd.user+'|'+urdd.role);
 		setTargetDate(urdd.expires);
 		
 		Result<RoleDAO.Data> r = dv.roleByName(trans, urdd.role);
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/CQLBatch.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/CQLBatch.java
index 738c534..9f685ad 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/CQLBatch.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/CQLBatch.java
@@ -84,4 +84,8 @@
 		}
 		execute(dryRun);
 	}
+	
+	public String toString() {
+		return sb.toString();
+	}
 }
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/ExpireRange.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/ExpireRange.java
index 73bff6e..13d74c8 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/ExpireRange.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/ExpireRange.java
@@ -40,12 +40,11 @@
 	public static final String ONE_WEEK = "OneWeek";
 	private static final String AAF_BATCH_RANGE = "aaf_batch_range.";
 	public Map<String,List<Range>> ranges;
-	public final Date now;
+	public static final Date now = new Date();
 
 	private Range delRange;
 	
 	public ExpireRange(final Access access) {
-		now = new Date();
 		ranges = new HashMap<>();
 		int i=0;
 		String prop = access.getProperty(AAF_BATCH_RANGE + i,null);
@@ -70,6 +69,10 @@
 			}
 	}
 	
+	public static Range newFutureRange() {
+		return new Range("Approval",1,1,0,0,GregorianCalendar.MONTH,1);
+	}
+	
 	public Set<String> names() {
 		Set<String> names = new HashSet<>();
         for(List<Range> lr : ranges.values()) {
@@ -90,7 +93,7 @@
 		return rv;
 	}
 	
-	public class Range {
+	public static class Range {
 		private final String name;
 		private final int reportingLevel;
 		private final int interval; // in Days
@@ -138,7 +141,7 @@
 			return end;
 		}
 		
-		private boolean inRange(final Date date) {
+		public boolean inRange(final Date date) {
 			if(date==null) {
 				return false;
 			} else {
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/Future.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/Future.java
index 13f8193..4f87e33 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/Future.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/Future.java
@@ -23,6 +23,7 @@
 
 package org.onap.aaf.auth.batch.helpers;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Date;
@@ -32,6 +33,7 @@
 import java.util.UUID;
 
 import org.onap.aaf.auth.dao.cass.FutureDAO;
+import org.onap.aaf.auth.dao.cass.UserRoleDAO;
 import org.onap.aaf.auth.env.AuthzTrans;
 import org.onap.aaf.auth.layer.Result;
 import org.onap.aaf.cadi.util.CSV;
@@ -90,7 +92,21 @@
         fdd.start = start;
         fdd.expires = expires;
         fdd.construct = construct;
-        role = Approval.roleFromMemo(memo);
+        String role = null;
+        switch(target) {
+        	case "user_role":
+    			UserRoleDAO.Data urdd = new UserRoleDAO.Data();
+    			try {
+    				urdd.reconstitute(construct);
+    				fdd.target_key = urdd.user + '|' + urdd.role;
+    				fdd.target_date=urdd.expires;
+    				role=urdd.role;
+    			} catch (IOException e) {
+    				e.printStackTrace(System.err);
+    			}
+    			break;
+        }
+    	this.role = role;
     }
     
     public final UUID id() {
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/LastNotified.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/LastNotified.java
index 22231f3..0539fcd 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/LastNotified.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/LastNotified.java
@@ -28,7 +28,7 @@
 import java.util.Set;
 import java.util.TreeMap;
 
-import org.onap.aaf.auth.batch.helpers.Notification.TYPE;
+import org.onap.aaf.auth.dao.cass.UserRoleDAO;
 
 import com.datastax.driver.core.ResultSet;
 import com.datastax.driver.core.Row;
@@ -37,6 +37,7 @@
 public class LastNotified {
 	private Map<String,Date> lastNotified = new TreeMap<>();
 	private Session session;
+	public static final Date never = new Date(0);
 	
 	public LastNotified(Session session) {
 		this.session = session;
@@ -44,7 +45,7 @@
 	
 	public void add(Set<String> users) {
 		StringBuilder query = new StringBuilder();
-		startNotifyQuery(query);
+		startQuery(query);
 		int cnt = 0;
     	for(String user : users) {
     		if(++cnt>1) {
@@ -54,37 +55,74 @@
     		query.append(user);
     		query.append('\'');
     		if(cnt>=30) {
-    			endNotifyQuery(query, Notification.TYPE.OA);
+    			endQuery(query);
     			add(session.execute(query.toString()),lastNotified);
     			query.setLength(0);
-    			startNotifyQuery(query);
+    			startQuery(query);
     			cnt=0;
     		}
     	}
     	if(cnt>0) {
-    		endNotifyQuery(query, Notification.TYPE.OA);
+    		endQuery(query);
 			add(session.execute(query.toString()),lastNotified);
     	}
 	}
 
-	public Date lastNotified(String user) {
-		return lastNotified.get(user);
+	/**
+	 * Note: target_key CAN also contain a Pipe.
+	 * 
+	 * @param user
+	 * @param target
+	 * @param target_key
+	 * @return
+	 */
+	public Date lastNotified(String user, String target, String target_key) {
+		String key = user + '|' + target + '|' + target_key;
+		return lastNotified(key);
 	}
 	
-	private void add(ResultSet result, Map<String, Date> lastNotified) {
+	public Date lastNotified(String key) {
+		Date rv = lastNotified.get(key);
+		if(rv==null) {
+			rv = never;
+			lastNotified.put(key, rv);
+		}
+		return rv;
+	}
+	
+	private Date add(ResultSet result, Map<String, Date> lastNotified) {
+		Date last = null;
     	for(Iterator<Row> iter = result.iterator(); iter.hasNext();) {
     		Row r = iter.next();
-    		lastNotified.put(r.getString(0), r.getTimestamp(1));
+    		String key = r.getString(0) + '|' +
+    				     r.getString(1) + '|' +
+    				     r.getString(2);
+    		
+    		lastNotified.put(key, last=r.getTimestamp(3));
     	}
+    	return last;
 	}
 
-	private void startNotifyQuery(StringBuilder query) {
-		query.append("SELECT user,last FROM authz.notify WHERE user in (");
+	private void startQuery(StringBuilder query) {
+		query.append("SELECT user,target,key,last FROM authz.notified WHERE user in (");
 	}
-    
-    private void endNotifyQuery(StringBuilder query, TYPE oa) {
-    	query.append(") AND type=");
-    	query.append(oa.idx());
-    	query.append(';');
-    }
+
+	private void endQuery(StringBuilder query) {
+		query.append(");");
+	}
+
+	public void update(StringBuilder query,String user, String target, String key) {
+		query.append("UPDATE authz.notified SET last=dateof(now()) WHERE user='");
+		query.append(user);
+		query.append("' AND target='");
+		query.append(target);
+		query.append("' AND key='");
+		query.append(key);
+		query.append("';");
+	}
+
+	public static String newKey(UserRoleDAO.Data urdd) {
+		return urdd.user + "|ur|" + urdd.role;
+	}
+
 }
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/Notification.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/Notification.java
deleted file mode 100644
index ae0d37b..0000000
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/Notification.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * ============LICENSE_START====================================================
- * org.onap.aaf
- * ===========================================================================
- * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
- * ===========================================================================
- * Modifications Copyright (C) 2018 IBM.
- * ===========================================================================
- * Licensed 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.
- * ============LICENSE_END====================================================
- *
- */
-
-package org.onap.aaf.auth.batch.helpers;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.TreeMap;
-
-import org.onap.aaf.auth.batch.actions.Message;
-import org.onap.aaf.auth.env.AuthzTrans;
-import org.onap.aaf.auth.org.Organization;
-import org.onap.aaf.misc.env.Env;
-import org.onap.aaf.misc.env.TimeTaken;
-import org.onap.aaf.misc.env.Trans;
-import org.onap.aaf.misc.env.util.Chrono;
-
-import com.datastax.driver.core.ResultSet;
-import com.datastax.driver.core.Row;
-import com.datastax.driver.core.Session;
-import com.datastax.driver.core.SimpleStatement;
-import com.datastax.driver.core.Statement;
-
-public class Notification {
-    public enum TYPE {
-        OA("Owner Approval",1),SA("Supervisor Approval",2),CN("Credential Expiration",20);
-        
-        private String desc;
-        private int type;
-    
-        private TYPE(String desc,int type) {
-            this.desc = desc;
-            this.type = type;
-        }
-        
-        public String desc() {
-            return desc;
-        }
-        
-        public int idx() {
-            return type;
-        }
-
-        public static TYPE get(int idx) {
-            for (TYPE nt : TYPE.values()) {
-                if (idx==nt.type) {
-                    return nt;
-                }
-            }
-            return null;
-        }
-    }
-
-
-    public static final TreeMap<String,List<Notification>> data = new TreeMap<>();
-    public static final Date now = new Date();
-    
-    public final String user;
-    public final TYPE type;
-    public Date last;
-    public int checkSum;
-    public Message msg;
-    private int current;
-    public Organization org;
-    public int count;
-
-    public static Creator<Notification> v2_0_18 = new Creator<Notification>() {
-        @Override
-        public Notification create(Row row) {
-            int idx =row.getInt(1);
-            TYPE typeCreator = TYPE.get(idx);
-            if (typeCreator==null) {
-                return null;
-            }
-            return new Notification(row.getString(0), typeCreator, row.getTimestamp(2), row.getInt(3));
-        }
-
-        @Override
-        public String select() {
-            return "SELECT user,type,last,checksum FROM authz.notify LIMIT 100000";
-        }
-    };
-    
-    private Notification(String user, TYPE nt, Date last, int checksum) {
-        this.user = user;
-        this.type = nt;
-        this.last = last;
-        this.checkSum = checksum;
-        current = 0;
-        count = 0;
-    }
-    
-    public static void load(Trans trans, Session session, Creator<Notification> creator ) {
-        trans.info().log( "query: " + creator.select() );
-        TimeTaken tt = trans.start("Load Notify", Env.REMOTE);
-       
-        ResultSet results;
-        try {
-            Statement stmt = new SimpleStatement(creator.select());
-            results = session.execute(stmt);
-        } finally {
-            tt.done();
-        }
-        int count = 0;
-        tt = trans.start("Process Notify", Env.SUB);
-
-        try {
-            for (Row row : results.all()) {
-                ++count;
-                try {
-                    Notification not = creator.create(row);
-                    List<Notification> ln = data.get(not.user);
-                    if (ln==null) {
-                        ln = new ArrayList<>();
-                        data.put(not.user, ln);
-                    }
-                    ln.add(not);
-                } finally {
-                    tt.done();
-                }
-            }
-        } finally {
-            tt.done();
-            trans.info().log("Found",count,"Notify Records");
-        }
-    }
-    
-    public static Notification get(String user, TYPE type) {
-        List<Notification> ln = data.get(user);
-        if (ln!=null) {
-            for (Notification n : ln) {
-                if (type.equals(n.type)) {
-                    return n;
-                }
-            }
-        }
-        return null;
-    }
-
-    public static Notification create(String user, TYPE type) {
-        return new Notification(user,type,null,0);
-    }
-
-    
-    public void set(Message msg) {
-        this.msg = msg; 
-    }
-
-    public int checksum() {
-        if (msg==null) {
-            current=0;
-        } else if (current==0) {
-            for (String l : msg.lines) {
-                for (byte b : l.getBytes()) {
-                    current+=b;
-                }
-            }
-        }
-        return current;
-    }
-    
-    public boolean update(AuthzTrans trans, Session session, boolean dryRun) {
-        checksum();
-        if (last==null || current==0 || current!=checkSum) {
-            last = now;
-            current = checksum();
-            String update = "UPDATE authz.notify SET " +
-                    "last = '" + Chrono.utcStamp(last) +
-                    "', checksum=" +
-                    current +
-                    " WHERE user='" +
-                    user + 
-                    "' AND type=" +
-                    type.idx() +
-                    ";";
-            if (dryRun) {
-                trans.info().log("Would",update);
-            } else {
-                session.execute(update);
-            }
-            return true;
-        }
-        return false;
-    }
-
-    public String toString() {
-        return "\"" + user + "\",\"" + type.name() + "\",\"" 
-                + Chrono.dateTime(last)+ "\", "  + checkSum;
-    }
-}
\ No newline at end of file
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/UserRole.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/UserRole.java
index 55dd1e7..343a0e2 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/UserRole.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/helpers/UserRole.java
@@ -122,12 +122,12 @@
     }
     
     public static void loadOneUser(Trans trans, Session session, Creator<UserRole> creator, String user, Visitor<UserRole> visitor ) {
-        load(trans,session,creator,"role='"+ user +"';",visitor);
+        load(trans,session,creator,"user='"+ user +'\'',visitor);
     }
 
     private static void load(Trans trans, Session session, Creator<UserRole> creator, String where, Visitor<UserRole> visitor) {
         String query = creator.query(where);
-        trans.info().log( "query: " + query );
+        trans.debug().log( "query: " + query );
         TimeTaken tt = trans.start("Read UserRoles", Env.REMOTE);
 
         ResultSet results;
@@ -145,7 +145,7 @@
                 tt.done();
             }
         } finally {
-            trans.info().log("Loaded",totalLoaded,"UserRoles");
+            trans.debug().log("Loaded",totalLoaded,"UserRoles");
         }
     }
 
@@ -337,16 +337,26 @@
     	sb.append("';\n");
     }
 
-    public static void batchExtend(StringBuilder sb, List<String> row, String newDate ) {
+    public static void batchExtend(StringBuilder sb, List<String> row, Date newDate ) {
     	sb.append("UPDATE authz.user_role SET expires='");
-    	sb.append(newDate);
+    	sb.append(Chrono.dateTime(newDate));
     	sb.append("' WHERE user='");
     	sb.append(row.get(1));
     	sb.append("' AND role='");
     	sb.append(row.get(2));
     	sb.append("';\n");
     }
-    
+
+    public void batchUpdateExpires(StringBuilder sb) {
+    	sb.append("UPDATE authz.user_role SET expires='");
+    	sb.append(Chrono.dateTime(expires()));
+    	sb.append("' WHERE user='");
+    	sb.append(user());
+    	sb.append("' AND role='");
+    	sb.append(role());
+    	sb.append("';\n");
+    }
+
 	public static String histMemo(String fmt, List<String> row) {
 		String reason;
 		if(row.size()>7) { // Reason included
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Analyze.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Analyze.java
index a0dce74..70c950e 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Analyze.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Analyze.java
@@ -38,6 +38,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.UUID;
 
 import org.onap.aaf.auth.batch.Batch;
@@ -49,6 +50,7 @@
 import org.onap.aaf.auth.batch.helpers.ExpireRange;
 import org.onap.aaf.auth.batch.helpers.ExpireRange.Range;
 import org.onap.aaf.auth.batch.helpers.Future;
+import org.onap.aaf.auth.batch.helpers.LastNotified;
 import org.onap.aaf.auth.batch.helpers.Role;
 import org.onap.aaf.auth.batch.helpers.UserRole;
 import org.onap.aaf.auth.batch.helpers.X509;
@@ -87,6 +89,8 @@
 	private CSV.Writer deleteCW;
 	private CSV.Writer approveCW;
 	private CSV.Writer extendCW;
+	private Range futureRange;
+	private final String sdate;
 	
 	public Analyze(AuthzTrans trans) throws APIException, IOException, OrganizationException {
         super(trans.env());
@@ -110,14 +114,14 @@
             writerList = new HashMap<>();
             
             expireRange = new ExpireRange(trans.env().access());
-            String sdate = Chrono.dateOnlyStamp(expireRange.now);
+            sdate = Chrono.dateOnlyStamp(ExpireRange.now);
             for( List<Range> lr : expireRange.ranges.values()) {
             	for(Range r : lr ) {
             		if(writerList.get(r.name())==null) {
                     	File file = new File(logDir(),r.name() + sdate +CSV);
                     	CSV csv = new CSV(env.access(),file);
                     	CSV.Writer cw = csv.writer(false);
-                    	cw.row(INFO,r.name(),Chrono.dateOnlyStamp(expireRange.now),r.reportingLevel());
+                    	cw.row(INFO,r.name(),sdate,r.reportingLevel());
                     	writerList.put(r.name(),cw);
                     	if("Delete".equals(r.name())) {
                     		deleteDate = r.getEnd();
@@ -129,17 +133,18 @@
             }
             
             // Setup New Approvals file
+            futureRange = ExpireRange.newFutureRange();
             File file = new File(logDir(),APPROVALS + sdate +CSV);
             CSV approveCSV = new CSV(env.access(),file);
             approveCW = approveCSV.writer();
-            approveCW.row(INFO,APPROVALS,Chrono.dateOnlyStamp(expireRange.now),1);
+            approveCW.row(INFO,APPROVALS,sdate,1);
             writerList.put(APPROVALS,approveCW);
             
             // Setup Extend Approvals file
             file = new File(logDir(),EXTEND + sdate +CSV);
             CSV extendCSV = new CSV(env.access(),file);
             extendCW = extendCSV.writer();
-            extendCW.row(INFO,EXTEND,Chrono.dateOnlyStamp(expireRange.now),1);
+            extendCW.row(INFO,EXTEND,sdate,1);
             writerList.put(EXTEND,extendCW);
             
             // Load full data of the following
@@ -160,7 +165,7 @@
     	try {
 			Future.load(noAvg, session, Future.withConstruct, fut -> {
 				List<Approval> appls = Approval.byTicket.get(fut.id());
-				if(fut.expires().before(expireRange.now)) {
+				if(!futureRange.inRange(fut.expires())) {
 					deleteCW.comment("Future %s expired", fut.id());
 					Future.row(deleteCW,fut);
 					if(appls!=null) {
@@ -179,6 +184,7 @@
     		tt.done();
     	}
 		
+    	Set<String> approvers = new TreeSet<>();
     	tt = trans.start("Connect Approvals with Futures",Trans.SUB);
     	try {
 			for(Approval appr : Approval.list) {
@@ -192,6 +198,7 @@
 					Approval.row(deleteCW, appr);
 				} else {
 					ticket.approvals.add(appr); // add to found Ticket
+					approvers.add(appr.getApprover());
 				}
 			}
     	} finally {
@@ -205,78 +212,98 @@
 		Map<String,Pending> pendingApprs = new HashMap<>();
 		Map<String,Pending> pendingTemp = new HashMap<>();
 
+		// Convert Good Tickets to keyed User/Role for UserRole Step
+		Map<String,Ticket> mur = new TreeMap<>();
+		LastNotified ln = new LastNotified(session);
+		ln.add(approvers);
+		String approver;
+		
 		tt = trans.start("Analyze Good Tickets",Trans.SUB);
 		try {
 			for(Ticket ticket : goodTickets.values()) {
-				pendingTemp.clear();
-				switch(ticket.f.target()) {
-					case "user_role":
-						int state[][] = new int[3][3];
-						int type;
-								
-						for(Approval appr : ticket.approvals) {
-							switch(appr.getType()) {
-								case "owner":
-									type=owner;
-									break;
-								case "supervisor":
-									type=supervisor;
-									break;
-								default:
-									type=0;
+				try {
+					pendingTemp.clear();
+					switch(ticket.f.target()) {
+						case "user_role":
+							int state[][] = new int[3][3];
+							int type;
+									
+							for(Approval appr : ticket.approvals) {
+								switch(appr.getType()) {
+									case "owner":
+										type=owner;
+										break;
+									case "supervisor":
+										type=supervisor;
+										break;
+									default:
+										type=0;
+								}
+								++state[type][total]; // count per type
+								switch(appr.getStatus()) {
+									case "pending":
+										++state[type][pending];
+										approver = appr.getApprover();
+										Pending n = pendingTemp.get(approver);
+										if(n==null) {
+											Date lastNotified = ln.lastNotified(approver,"ur",ticket.f.fdd.target_key);
+											pendingTemp.put(approver,new Pending(lastNotified));
+										} else {
+											n.inc();
+										}
+										break;
+									case "approved":
+										++state[type][approved];
+										break;
+									default:
+										++state[type][unknown];
+								}
 							}
-							++state[type][total]; // count per type
-							switch(appr.getStatus()) {
-								case "pending":
-									++state[type][pending];
-									Pending n = pendingTemp.get(appr.getApprover());
-									if(n==null) {
-										pendingTemp.put(appr.getApprover(),new Pending());
+							
+							// To Approve:
+							// Always must have at least 1 owner
+							if((state[owner][total]>0 && state[owner][approved]>0) &&
+								// If there are no Supervisors, that's ok
+							    (state[supervisor][total]==0 || 
+							    // But if there is a Supervisor, they must have approved 
+							    (state[supervisor][approved]>0))) {
+									UserRoleDAO.Data urdd = new UserRoleDAO.Data();
+									try {
+										urdd.reconstitute(ticket.f.fdd.construct);
+										if(urdd.expires.before(ticket.f.expires())) {
+											extendCW.row("extend_ur",urdd.user,urdd.role,ticket.f.expires());
+										}
+									} catch (IOException e) {
+										trans.error().log("Could not reconstitute UserRole");
+									}
+							} else { // Load all the Pending.
+								for(Entry<String, Pending> es : pendingTemp.entrySet()) {
+									Pending p = pendingApprs.get(es.getKey());
+									if(p==null) {
+										pendingApprs.put(es.getKey(), es.getValue());
 									} else {
-										n.inc();
+										p.inc(es.getValue());
 									}
-									break;
-								case "approved":
-									++state[type][approved];
-									break;
-								default:
-									++state[type][unknown];
-							}
-						}
-						
-						// To Approve:
-						// Always must have at least 1 owner
-						if((state[owner][total]>0 && state[owner][approved]>0) &&
-							// If there are no Supervisors, that's ok
-						    (state[supervisor][total]==0 || 
-						    // But if there is a Supervisor, they must have approved 
-						    (state[supervisor][approved]>0))) {
-								UserRoleDAO.Data urdd = new UserRoleDAO.Data();
-								try {
-									urdd.reconstitute(ticket.f.fdd.construct);
-									if(urdd.expires.before(ticket.f.expires())) {
-										extendCW.row("extend_ur",urdd.user,urdd.role,ticket.f.expires());
-									}
-								} catch (IOException e) {
-									trans.error().log("Could not reconstitute UserRole");
-								}
-						} else { // Load all the Pending.
-							for(Entry<String, Pending> es : pendingTemp.entrySet()) {
-								Pending p = pendingApprs.get(es.getKey());
-								if(p==null) {
-									pendingApprs.put(es.getKey(), es.getValue());
-								} else {
-									p.inc(es.getValue());
 								}
 							}
+							break;
+					}
+				} finally {
+					if("user_role".equals(ticket.f.fdd.target)) {
+						String key = ticket.f.fdd.target_key; 
+						if(key!=null) {
+							mur.put(key, ticket);
 						}
-						break;
+					}
 				}
 			}
 		} finally {
 			tt.done();
 		}
-		
+
+		// Good Tickets no longer needed
+		goodTickets.clear();
+
 		/**
 		 * Decide to Notify about Approvals, based on activity/last Notified
 		 */
@@ -288,7 +315,9 @@
 			
 			for(Entry<String, Pending> es : pendingApprs.entrySet()) {
 				Pending p = es.getValue();
-				if(p.newApprovals() || p.earliest() == null || p.earliest().after(remind)) {
+				if(p.newApprovals() 
+						|| p.earliest() == null 
+						|| p.earliest().after(remind)) {
 					p.row(approveCW,es.getKey());
 				}
 			}
@@ -297,7 +326,6 @@
 		}
 		
 		// clear out Approval Intermediates
-		goodTickets.clear();
 		pendingTemp = null;
 		pendingApprs = null;
 		
@@ -309,7 +337,7 @@
 		try {
 			tt = trans.start("Analyze UserRoles, storing Owners",Trans.SUB);
 			Set<String> specialCommented = new HashSet<>();
-			Map<String, Set<UserRole>> owners = new TreeMap<String, Set<UserRole>>();
+			Map<String, Set<UserRole>> owners = new TreeMap<>();
  			try {
 				UserRole.load(noAvg, session, UserRole.v2_0_11, ur -> {
 					Identity identity;
@@ -340,20 +368,25 @@
 							ur.row(deleteCW, UserRole.UR,String.format("Role %s does not exist", ur.role()));
 							return;
 						}
-						// Cannot just delete owners, unless there is at least one left. Process later
-						if ("owner".equals(ur.rname())) {
-							Set<UserRole> urs = owners.get(ur.role());
-							if (urs == null) {
-								urs = new HashSet<UserRole>();
-								owners.put(ur.role(), urs);
-							}
-							urs.add(ur);
-						} else {
-							Range r = writeAnalysis(noAvg,ur);
-							if(r!=null) {
-								Approval existing = findApproval(ur);
-								if(existing==null) {
-									ur.row(approveCW,UserRole.APPROVE_UR);
+						// Just let expired UserRoles sit until deleted
+						if(futureRange.inRange(ur.expires())) {
+							if(!mur.containsKey(ur.user() + '|' + ur.role())) {
+								// Cannot just delete owners, unless there is at least one left. Process later
+								if ("owner".equals(ur.rname())) {
+									Set<UserRole> urs = owners.get(ur.role());
+									if (urs == null) {
+										urs = new HashSet<UserRole>();
+										owners.put(ur.role(), urs);
+									}
+									urs.add(ur);
+								} else {
+									Range r = writeAnalysis(noAvg,ur);
+									if(r!=null) {
+										Approval existing = findApproval(ur);
+										if(existing==null) {
+											ur.row(approveCW,UserRole.APPROVE_UR);
+										}
+									}
 								}
 							}
 						}
@@ -374,16 +407,16 @@
  			tt = trans.start("Analyze Owners Separately",Trans.SUB);
  			try {
 				if (!owners.values().isEmpty()) {
-					File file = new File(logDir(), EXPIRED_OWNERS + Chrono.dateOnlyStamp(expireRange.now) + CSV);
+					File file = new File(logDir(), EXPIRED_OWNERS + sdate + CSV);
 					final CSV ownerCSV = new CSV(env.access(),file);
 					CSV.Writer expOwner = ownerCSV.writer();
-					expOwner.row(INFO,EXPIRED_OWNERS,Chrono.dateOnlyStamp(expireRange.now),2);
+					expOwner.row(INFO,EXPIRED_OWNERS,sdate,2);
 
 					try {
 						for (Set<UserRole> sur : owners.values()) {
 							int goodOwners = 0;
 							for (UserRole ur : sur) {
-								if (ur.expires().after(expireRange.now)) {
+								if (ur.expires().after(ExpireRange.now)) {
 									++goodOwners;
 								}
 							}
@@ -462,7 +495,6 @@
 					} catch (CertificateException | IOException e) {
 						noAvg.error().log(e, "Error Decrypting X509");
 					}
-	
 				});
 			} finally {
 				tt.done();
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Notify.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Notify.java
index 1c1f660..7cddea2 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Notify.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/Notify.java
@@ -25,22 +25,36 @@
 import java.io.IOException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
+import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
+import java.util.TreeMap;
 
 import org.onap.aaf.auth.batch.Batch;
+import org.onap.aaf.auth.batch.approvalsets.Pending;
+import org.onap.aaf.auth.batch.helpers.CQLBatch;
+import org.onap.aaf.auth.batch.helpers.CQLBatchLoop;
+import org.onap.aaf.auth.batch.helpers.LastNotified;
 import org.onap.aaf.auth.batch.reports.bodies.NotifyBody;
+import org.onap.aaf.auth.batch.reports.bodies.NotifyPendingApprBody;
 import org.onap.aaf.auth.env.AuthzTrans;
 import org.onap.aaf.auth.org.Mailer;
 import org.onap.aaf.auth.org.Organization.Identity;
 import org.onap.aaf.auth.org.OrganizationException;
 import org.onap.aaf.cadi.Access;
 import org.onap.aaf.cadi.CadiException;
+import org.onap.aaf.cadi.PropAccess;
 import org.onap.aaf.cadi.client.Holder;
 import org.onap.aaf.cadi.util.CSV;
 import org.onap.aaf.misc.env.APIException;
+import org.onap.aaf.misc.env.TimeTaken;
+import org.onap.aaf.misc.env.Trans;
 import org.onap.aaf.misc.env.util.Chrono;
 
  public class Notify extends Batch {
@@ -52,9 +66,15 @@
 	 private final int indent;
 	 private final boolean urgent;
 	 public final String guiURL;
+	 private PropAccess access;
+	 private AuthzTrans noAvg;
+	 private CQLBatch cqlBatch;
 
 	 public Notify(AuthzTrans trans) throws APIException, IOException, OrganizationException {
 		 super(trans.env());
+		 access = env.access();
+		 session = super.cluster.connect();
+
 		 String mailerCls = env.getProperty("MAILER");
 		 String mailFrom = env.getProperty("MAIL_FROM");
 		 String header_html = env.getProperty("HEADER_HTML");
@@ -82,12 +102,14 @@
 				 sb.append('\n');
 			 }
 			 String html_css = env.getProperty(HTML_CSS);
+			 String temp;
 			 int hc = sb.indexOf(HTML_CSS);
 			 if(hc!=0 && html_css!=null) {
-				 header = sb.replace(hc,hc+HTML_CSS.length(), html_css).toString();
+				 temp = sb.replace(hc,hc+HTML_CSS.length(), html_css).toString();
 			 } else {
-				 header = sb.toString();
+				 temp = sb.toString();
 			 }
+			 header = temp.replace("AAF:ENV", batchEnv);
 		 } finally {
 			 br.close();
 		 }
@@ -119,6 +141,8 @@
 			 br.close();
 		 }
 
+		 noAvg = trans.env().newTransNoAvg();
+		 cqlBatch = new CQLBatch(noAvg.info(),session); 
 	 }
 
 	 /*
@@ -127,10 +151,10 @@
 	  */
 	 @Override
 	 protected void run(AuthzTrans trans) {
-		 AuthzTrans noAvg = trans.env().newTransNoAvg();
 
 		 final Holder<List<String>> info = new Holder<>(null);
 		 final Set<String> errorSet = new HashSet<>();
+		 String fmt = "%s"+Chrono.dateOnlyStamp()+".csv";
 
 		 try {
 			 // Class Load possible data
@@ -145,7 +169,6 @@
 					 notifyFile.add(new File(logDir, args()[i]));
 				 }
 			 } else {
-				 String fmt = "%s"+Chrono.dateOnlyStamp()+".csv";
 				 File file;
 				 for(NotifyBody nb : NotifyBody.getAll()) {
 					 file = new File(logDir,String.format(fmt, nb.name()));
@@ -188,6 +211,100 @@
 			 for(NotifyBody nb : NotifyBody.getAll()) {
 				 notify(noAvg, nb);
 			 }
+			 
+			 //
+			 // Do Pending Approval Notifies. We do this separately, because we are consolidating
+			 // all the new entries, etc.
+			 //
+			 List<CSV> csvList = new ArrayList<>();
+			 for(String s : new String[] {"Approvals","ApprovalsNew"}) {
+				 File f = new File(logDir(),String.format(fmt, s));
+				 if(f.exists()) {
+					 csvList.add(new CSV(access,f));
+				 }
+			 }
+			 
+			 Map<String,Pending> mpending = new TreeMap<>();
+			 Holder<Integer> count = new Holder<>(0);
+			 for(CSV approveCSV : csvList) {
+	        	TimeTaken tt = trans.start("Load Analyzed Reminders",Trans.SUB,approveCSV.name());
+	        	try {
+					approveCSV.visit(row -> {
+						switch(row.get(0)) {
+//							case "info":
+//								break;
+							case Pending.REMIND:
+								try {
+									String user = row.get(1);
+									Pending p = new Pending(row);
+									Pending mp = mpending.get(user);
+									if(mp==null) {
+										mpending.put(user, p);
+									} else {
+										mp.inc(p); // FYI, unlikely
+									}
+									count.set(count.get()+1);
+								} catch (ParseException e) {
+									trans.error().log(e);
+								} 
+							break;
+						}
+					});
+				} catch (IOException | CadiException e) {
+					e.printStackTrace();
+					// .... but continue with next row
+	        	} finally {
+	        		tt.done();
+	        	}
+	        }
+	        trans.info().printf("Read %d Reminder Rows", count.get());
+	        
+        	NotifyPendingApprBody npab = new NotifyPendingApprBody(access);
+
+        	GregorianCalendar gc = new GregorianCalendar();
+        	gc.add(GregorianCalendar.DAY_OF_MONTH, 7); // Get from INFO?
+        	Date oneWeek = gc.getTime();
+        	CSV.Saver rs = new CSV.Saver();
+        	
+        	TimeTaken tt = trans.start("Obtain Last Notifications for Approvers", Trans.SUB);
+        	LastNotified lastN;
+        	try {
+        		lastN = new LastNotified(session);
+        		lastN.add(mpending.keySet());
+        	} finally {
+        		tt.done();
+        	}
+        	
+        	Pending p;
+    		final CQLBatchLoop cbl = new CQLBatchLoop(cqlBatch,50,dryRun);
+        	tt = trans.start("Notify for Pending", Trans.SUB);
+        	try {
+        		for(Entry<String, Pending> es : mpending.entrySet()) {
+        			p = es.getValue();
+        			boolean nap = p.newApprovals();
+        			if(!nap) {
+            			Date dateLastNotified = lastN.lastNotified(es.getKey(),"pending","");
+            			if(dateLastNotified==null || dateLastNotified.after(oneWeek) ) {
+            				nap=true;
+            			}
+        			}
+        			if(nap) {
+        				rs.row("appr", es.getKey(),p.qty(),batchEnv);
+        				npab.store(rs.asList());
+        				if(notify(noAvg, npab)>0) {
+        					// Update
+        					cbl.preLoop();
+        					lastN.update(cbl.inc(),es.getKey(),"pending","");
+        				}
+        			}
+        		}
+        	} finally {
+        		cbl.flush();
+        		tt.done();
+        	}
+            trans.info().printf("Created %d Notifications", count.get());
+
+
 
 		} catch (APIException | IOException e1) {
 			trans.error().log(e1);
@@ -272,8 +389,4 @@
 		 return nb.count();
 	 }
 
-	@Override
-	 protected void _close(AuthzTrans trans) {
-	 }
-
  }
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/NotifyCredBody.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/NotifyCredBody.java
index 94502d9..15a104d 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/NotifyCredBody.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/NotifyCredBody.java
@@ -32,15 +32,17 @@
 public abstract class NotifyCredBody extends NotifyBody {
 
 	private final String explanation;
+	private final String instruction;
+	 
 	public NotifyCredBody(Access access, String name) throws IOException {
 		super(access,"cred",name);
 		
 		// Default
 		explanation = "The following Credentials that you are responsible for "
-				+ "are expiring on the dates shown. "
-				+ "Failure to act before the expiration date will cause your App's "
-				+ "Authentications to fail."
-				+ "<h3>Instructions for 'Password':</h3><ul>" 
+				+ "are expiring on the dates shown. <br><br>"
+				;
+				
+        instruction = "<br><h3>Instructions for 'Password':</h3><ul>" 
 				+ "<li><b><i>Click</i></b> on the Fully Qualified ID to ADD a new Password</li>"
 				+ "<li><b>REMEMBER!</b> You are not finished until you <ol>"
 				+ "<li><b>CHANGE <i>ALL</i></b> the configurations on <b><i>ALL</i></b> your processes!!</li>"
@@ -48,10 +50,20 @@
 				+ "<li>IF there is a WARNING, click the link for more information</li>"
 				+ "</ul>";
 	}
+	
+	/**
+	 * Default Dynamic Text.  Override is expected
+	 * @return
+	 */
+	protected String dynamic() {
+		return "Failure to act before the expiration date will cause your App's Authentications to fail.";
+	}
 
 	@Override
 	public boolean body(AuthzTrans trans, StringBuilder sb, int indent, Notify n, String id) {
-		println(sb,indent,explanation);
+		print(sb,indent,explanation);
+		print(sb,indent,dynamic());
+		println(sb,indent,instruction);
 		println(sb,indent,"<table>");
 		indent+=2;
 		println(sb,indent,"<tr>");
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/OneMonthNotifyCredBody.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/OneMonthNotifyCredBody.java
index a288f49..866dc23 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/OneMonthNotifyCredBody.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/OneMonthNotifyCredBody.java
@@ -34,4 +34,12 @@
 	public String subject() {
 		return String.format("AAF One Month Credential Notification (ENV: %s)",env);
 	}
+	
+	/* (non-Javadoc)
+	 * @see org.onap.aaf.auth.batch.reports.bodies.NotifyCredBody#dynamic()
+	 */
+	@Override
+	protected String dynamic() {
+		return "This is your <b>one month</b> notification. " + super.dynamic();
+	}
 }
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoMonthNotifyCredBody.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoMonthNotifyCredBody.java
index eb52c6d..98ee47d 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoMonthNotifyCredBody.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoMonthNotifyCredBody.java
@@ -34,4 +34,14 @@
 	public String subject() {
 		return String.format("AAF Two Month Credential Notification (ENV: %s)",env);
 	}
+
+	/* (non-Javadoc)
+	 * @see org.onap.aaf.auth.batch.reports.bodies.NotifyCredBody#dynamic()
+	 */
+	@Override
+	protected String dynamic() {
+		return "This is a friendly, <b>2 month reminder</b> to schedule appropriate creation and deployment "
+				+ "of your credentials, and modification of your configurations on a per instance basis. "
+				+ " Use the following text to help create your Ticket.";
+	}
 }
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoWeeksNotifyCredBody.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoWeeksNotifyCredBody.java
index 818556c..4618856 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoWeeksNotifyCredBody.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/reports/bodies/TwoWeeksNotifyCredBody.java
@@ -34,4 +34,12 @@
 	public String subject() {
 		return String.format("AAF Two Week Credential Notification (ENV: %s)",env);
 	}
+	
+	/* (non-Javadoc)
+	 * @see org.onap.aaf.auth.batch.reports.bodies.NotifyCredBody#dynamic()
+	 */
+	@Override
+	protected String dynamic() {
+		return "You have now reached critical stage. This email is escalated to your superiors. " + super.dynamic();
+	}
 }
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Approvals.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Approvals.java
index 03c812a..0df4934 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Approvals.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Approvals.java
@@ -23,13 +23,12 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.TreeMap;
 
 import org.onap.aaf.auth.batch.Batch;
@@ -38,42 +37,34 @@
 import org.onap.aaf.auth.batch.approvalsets.Pending;
 import org.onap.aaf.auth.batch.approvalsets.URApprovalSet;
 import org.onap.aaf.auth.batch.helpers.BatchDataView;
-import org.onap.aaf.auth.batch.helpers.CQLBatch;
-import org.onap.aaf.auth.batch.helpers.CQLBatchLoop;
-import org.onap.aaf.auth.batch.helpers.LastNotified;
 import org.onap.aaf.auth.batch.helpers.NS;
-import org.onap.aaf.auth.batch.helpers.Notification;
-import org.onap.aaf.auth.batch.helpers.Notification.TYPE;
 import org.onap.aaf.auth.batch.helpers.Role;
 import org.onap.aaf.auth.batch.helpers.UserRole;
-import org.onap.aaf.auth.batch.reports.Notify;
-import org.onap.aaf.auth.batch.reports.bodies.NotifyPendingApprBody;
 import org.onap.aaf.auth.dao.cass.UserRoleDAO;
 import org.onap.aaf.auth.env.AuthzTrans;
 import org.onap.aaf.auth.layer.Result;
 import org.onap.aaf.auth.org.OrganizationException;
-import org.onap.aaf.cadi.Access;
 import org.onap.aaf.cadi.CadiException;
 import org.onap.aaf.cadi.client.Holder;
 import org.onap.aaf.cadi.util.CSV;
+import org.onap.aaf.cadi.util.CSV.Writer;
 import org.onap.aaf.misc.env.APIException;
 import org.onap.aaf.misc.env.TimeTaken;
 import org.onap.aaf.misc.env.Trans;
 import org.onap.aaf.misc.env.util.Chrono;
 
 public class Approvals extends Batch {
-    private final Access access;
 	private final AuthzTrans noAvg;
 	private BatchDataView dataview;
 	private List<CSV> csvList;
-	private GregorianCalendar now;
-	private final Notify notify;
+	private Writer napproveCW;
+	private static final GregorianCalendar now = new GregorianCalendar();
+	private static final String sdate = Chrono.dateOnlyStamp(now);
+	private static final String CSV = ".csv";
+	private static final String APPROVALS_NEW = "ApprovalsNew";
 	
-
     public Approvals(AuthzTrans trans) throws APIException, IOException, OrganizationException {
         super(trans.env());
-        notify = new Notify(trans);
-        access = env.access();
         noAvg = env.newTransNoAvg();
         noAvg.setUser(new BatchPrincipal("batch:Approvals"));
         session = cluster.connect();
@@ -82,8 +73,6 @@
         Role.load(trans, session);
         UserRole.load(trans, session, UserRole.v2_0_11);
 
-        now = new GregorianCalendar();
-        
         csvList = new ArrayList<>();
         File f;
         if(args().length>0) {
@@ -103,45 +92,21 @@
 	        	trans.error().printf("CSV File %s does not exist",f.getAbsolutePath());
 			}
         }
+        
+
+        File file = new File(logDir(),APPROVALS_NEW + sdate +CSV);
+        CSV approveCSV = new CSV(env.access(),file);
+        napproveCW = approveCSV.writer();
+        napproveCW.row("info",APPROVALS_NEW,sdate,1);
+        
     }
 
     @Override
     protected void run(AuthzTrans trans) {
     	Map<String,Pending> mpending = new TreeMap<>();
-		Holder<Integer> count = new Holder<>(0);
-		final CQLBatchLoop cbl = new CQLBatchLoop(new CQLBatch(noAvg.info(),session),100,dryRun);
-        for(CSV approveCSV : csvList) {
-        	TimeTaken tt = trans.start("Load Analyzed Reminders",Trans.SUB,approveCSV.name());
-        	try {
-				approveCSV.visit(row -> {
-					switch(row.get(0)) {
-						case Pending.REMIND:
-							try {
-								String user = row.get(1);
-								Pending p = new Pending(row);
-								Pending mp = mpending.get(user);
-								if(mp==null) {
-									mpending.put(user, p);
-								} else {
-									mp.inc(p); // FYI, unlikely
-								}
-								count.set(count.get()+1);
-							} catch (ParseException e) {
-								trans.error().log(e);
-							} 
-						break;
-					}
-				});
-			} catch (IOException | CadiException e) {
-				e.printStackTrace();
-				// .... but continue with next row
-        	} finally {
-        		tt.done();
-        	}
-        }
-        trans.info().printf("Processed %d Reminder Rows", count.get());
+		Pending p = Pending.create();
 
-        count.set(0);
+		Holder<Integer> count = new Holder<>(0);
         for(CSV approveCSV : csvList) {
         	TimeTaken tt = trans.start("Processing %s's UserRoles",Trans.SUB,approveCSV.name());
         	try {
@@ -155,14 +120,20 @@
 							});
 							Result<Void> rw = uras.write(noAvg);
 							if(rw.isOK()) {
-								Pending p = new Pending();
-								Pending mp = mpending.get(urdd.user);
-								if(mp==null) {
-									mpending.put(urdd.user, p);
+								Set<String> approvers = uras.approvers();
+								if(approvers.isEmpty()) {
+									trans.error().printf("No Approvers found for %s-%s (probably no owner)",urdd.user,urdd.role);
 								} else {
-									mp.inc(p);
+									for(String approver : approvers) {
+										Pending mp = mpending.get(approver);
+										if(mp==null) {
+											mpending.put(approver, Pending.create());
+										} else {
+											mp.inc(p); // FYI, unlikely
+										}
+									}
+									count.set(count.get()+1);
 								}
-								count.set(count.get()+1);
 							} else {
 								trans.error().log(rw.errorString());
 							}
@@ -178,60 +149,25 @@
 	    	}
             trans.info().printf("Processed %d UserRoles", count.get());
 
-            count.set(0);
-        	NotifyPendingApprBody npab = new NotifyPendingApprBody(access);
-
-        	GregorianCalendar gc = new GregorianCalendar();
-        	gc.add(GregorianCalendar.DAY_OF_MONTH, 7);
-        	Date oneWeek = gc.getTime();
-        	CSV.Saver rs = new CSV.Saver();
-        	
-        	tt = trans.start("Obtain Last Notifications", Trans.SUB);
-        	LastNotified lastN;
+        	tt = trans.start("Processing %s's UserRoles",Trans.SUB,approveCSV.name());
+        	int cnt = 0;
         	try {
-        		lastN = new LastNotified(session);
-        		lastN.add(mpending.keySet());
-        	} finally {
-        		tt.done();
-        	}
-        	
-        	Pending p;
-        	tt = trans.start("Notify for Pending", Trans.SUB);
-        	try {
-        		for(Entry<String, Pending> es : mpending.entrySet()) {
-        			p = es.getValue();
-        			Date dateLastNotified = lastN.lastNotified(es.getKey());
-        			if(p.newApprovals() || dateLastNotified==null || dateLastNotified.after(oneWeek) ) {
-        				rs.row("appr", es.getKey(),p.qty(),batchEnv);
-        				npab.store(rs.asList());
-        				if(notify.notify(noAvg, npab)>0) {
-        					// Update
-        					cbl.preLoop();
-        					update(cbl.inc(),es.getKey(),Notification.TYPE.OA);
-        				}
-        			}
-        		}
-        	} finally {
-        		tt.done();
-        	}
-            trans.info().printf("Created %d Notifications", count.get());
-	    }
+	            for(Entry<String, Pending> es : mpending.entrySet()) {
+	            	p.row(napproveCW,es.getKey());
+	            	++cnt;
+	            }
+            } finally {
+            	tt.done();
+            	trans.info().printf("Processed %d Reminders", cnt);
+            }
+ 	    }
     }
-    
-    private void update(StringBuilder sb, String user, TYPE oa) {
-    	sb.append("UPDATE authz.notify SET last=dateof(now()) WHERE user='");
-    	sb.append(user);
-    	sb.append("' AND type=");
-    	sb.append(oa.idx());
-    	sb.append(';');
-		
-	}
 
 	@Override
     protected void _close(AuthzTrans trans) {
-    	if(session!=null) {
-    		session.close();
-    		session = null;
-    	}
+		if(napproveCW!=null) {
+			napproveCW.flush();
+			napproveCW.close();
+		}
     }
 }
diff --git a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Extend.java b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Extend.java
index 870dc1e..3a0f7b9 100644
--- a/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Extend.java
+++ b/auth/auth-batch/src/main/java/org/onap/aaf/auth/batch/update/Extend.java
@@ -151,7 +151,7 @@
 									gc.setTime(now.getTime());
 								}
 								gc.add(gcType, extendBy);
-								UserRole.batchExtend(sb,row,Chrono.dateTime(gc));
+								UserRole.batchExtend(sb,row,gc.getTime());
 								break;
 							case "cred":
 								int ctype = Integer.parseInt(row.get(3));
diff --git a/auth/auth-batch/src/test/java/org/onap/aaf/auth/batch/test/JU_NotificationTest.java b/auth/auth-batch/src/test/java/org/onap/aaf/auth/batch/test/JU_NotificationTest.java
deleted file mode 100644
index f1cba0b..0000000
--- a/auth/auth-batch/src/test/java/org/onap/aaf/auth/batch/test/JU_NotificationTest.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * ============LICENSE_START====================================================
- * org.onap.aaf
- * ===========================================================================
- * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
- * ===========================================================================
- * Licensed 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.
- * ============LICENSE_END====================================================
- *
- */
-package org.onap.aaf.auth.batch.test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.onap.aaf.auth.batch.actions.Message;
-import org.onap.aaf.auth.batch.helpers.Creator;
-import org.onap.aaf.auth.batch.helpers.Notification;
-import org.onap.aaf.auth.batch.helpers.Notification.TYPE;
-import org.onap.aaf.auth.env.AuthzTrans;
-import org.onap.aaf.misc.env.Env;
-import org.onap.aaf.misc.env.LogTarget;
-import org.onap.aaf.misc.env.TimeTaken;
-
-public class JU_NotificationTest {
-
-	@Mock
-	private AuthzTrans trans;
-	@Mock
-	private Creator<Notification> creator;
-	@Mock
-	private TimeTaken tt;
-
-	@Mock
-	private LogTarget logTarget;
-	private Message msg;
-
-	@Before
-	public void setUp() throws Exception {
-		initMocks(this);
-
-		msg = new Message();
-		msg.line("%n", "Message");
-
-		when(trans.info()).thenReturn(logTarget);
-		when(trans.start("Load Notify", Env.REMOTE)).thenReturn(tt);
-	}
-
-	@Test
-	public void test() {
-		Notification notification = Notification.create("user", TYPE.CN);
-		assertEquals(notification.checksum(), 0);
-		notification.set(msg);
-		assertEquals(notification.checksum(), 10);
-		assertNull(Notification.get("user", TYPE.CN));
-		assertTrue(notification.update(trans, null, true));
-		assertTrue(notification.toString().contains("\"user\",\"CN\","));
-
-
-	}
-}
\ No newline at end of file
diff --git a/auth/auth-cass/cass_init/init.cql b/auth/auth-cass/cass_init/init.cql
index 75b02c5..f280de0 100644
--- a/auth/auth-cass/cass_init/init.cql
+++ b/auth/auth-cass/cass_init/init.cql
@@ -90,12 +90,12 @@
 CREATE INDEX cert_id ON cert(id);
 CREATE INDEX cert_x500 ON cert(x500);
 
-CREATE TABLE notify (
+CREATE TABLE notified (
   user 		text,
-  type 		int,
+  target        text,
+  key		text,
   last 		timestamp,
-  checksum 	int,
-  PRIMARY KEY (user,type)
+  PRIMARY KEY (user,target,key)
 );
 
 CREATE TABLE x509 (
@@ -175,6 +175,7 @@
 );
 CREATE INDEX future_idx ON future(target);
 CREATE INDEX future_start_idx ON future(start);
+CREATE INDEX future_target_key ON authz.future (target_key);
 
 
 CREATE TABLE approval (
diff --git a/auth/auth-cass/cass_init/init2_10.cql b/auth/auth-cass/cass_init/init2_10.cql
index b719507..21bd4b6 100644
--- a/auth/auth-cass/cass_init/init2_10.cql
+++ b/auth/auth-cass/cass_init/init2_10.cql
@@ -2,3 +2,13 @@
 alter TABLE cred ADD tag varchar;
 alter TABLE future ADD target_key varchar;
 alter TABLE future ADD target_date timestamp;
+CREATE INDEX future_target_key ON authz.future (target_key);
+
+CREATE TABLE notified (
+  user          text,
+  target        text,
+  key           text,
+  last          timestamp,
+  PRIMARY KEY (user,target,key)
+);
+
diff --git a/auth/auth-certman/src/main/java/org/onap/aaf/auth/cm/ca/LocalCA.java b/auth/auth-certman/src/main/java/org/onap/aaf/auth/cm/ca/LocalCA.java
index 08c9685..c51ddbd 100644
--- a/auth/auth-certman/src/main/java/org/onap/aaf/auth/cm/ca/LocalCA.java
+++ b/auth/auth-certman/src/main/java/org/onap/aaf/auth/cm/ca/LocalCA.java
@@ -203,7 +203,7 @@
     public X509andChain sign(Trans trans, CSRMeta csrmeta) throws IOException, CertException {
         GregorianCalendar gc = new GregorianCalendar();
         Date start = gc.getTime();
-        gc.add(GregorianCalendar.MONTH, 6);
+        gc.add(GregorianCalendar.MONTH, 12);
         Date end = gc.getTime();
         X509Certificate x509;
         TimeTaken tt = trans.start("Create/Sign Cert",Env.SUB);
diff --git a/auth/auth-gui/src/main/java/org/onap/aaf/auth/gui/pages/ApprovalForm.java b/auth/auth-gui/src/main/java/org/onap/aaf/auth/gui/pages/ApprovalForm.java
index f173038..0c984e4 100644
--- a/auth/auth-gui/src/main/java/org/onap/aaf/auth/gui/pages/ApprovalForm.java
+++ b/auth/auth-gui/src/main/java/org/onap/aaf/auth/gui/pages/ApprovalForm.java
@@ -40,13 +40,12 @@
 import org.onap.aaf.auth.gui.table.AbsCell;
 import org.onap.aaf.auth.gui.table.ButtonCell;
 import org.onap.aaf.auth.gui.table.RadioCell;
-import org.onap.aaf.auth.gui.table.RefCell;
 import org.onap.aaf.auth.gui.table.TableData;
-import org.onap.aaf.auth.gui.table.TextAndRefCell;
 import org.onap.aaf.auth.gui.table.TextCell;
+import org.onap.aaf.auth.gui.table.TextToolTipCell;
 import org.onap.aaf.auth.org.Organization;
-import org.onap.aaf.auth.org.OrganizationFactory;
 import org.onap.aaf.auth.org.Organization.Identity;
+import org.onap.aaf.auth.org.OrganizationFactory;
 import org.onap.aaf.cadi.CadiException;
 import org.onap.aaf.cadi.client.Future;
 import org.onap.aaf.cadi.client.Rcli;
@@ -91,7 +90,7 @@
                 }
             },
             new Form(true,new Table<AAF_GUI,AuthzTrans>("Approval Requests", gui.env.newTransNoAvg(),new Model(gui.env),"class=stdform"))
-                .preamble("The following requires your Approval to proceed in the AAF System.</p><p class=subtext>Hover on Identity for Name; click for WebPhone; If Deny is the only option, User is no longer valid."),
+                .preamble("The following requires your Approval to proceed in the AAF System.</p><p class=subtext>Hover on Name for Identity; If Deny is the only option, User is no longer valid."),
             new NamedCode(false, "selectAlljs") {
                 @Override
                 public void code(final Cache<HTMLGen> cache, final HTMLGen hgen) throws APIException, IOException {
@@ -116,7 +115,7 @@
      */
     private static class Model extends TableData<AAF_GUI,AuthzTrans> {
         //TODO come up with a generic way to do ILM Info (people page)
-        private static final String TODO_ILM_INFO = "TODO: ILM Info";
+//        private static final String TODO_ILM_INFO = "TODO: ILM Info";
         
         
         private static final String[] headers = new String[] {"Identity","Request","Approve","Deny"};
@@ -216,7 +215,7 @@
 	//                        } else {
 	                            approverHeader = new AbsCell[] { 
 	                                    new TextCell("Approvals Delegated to Me by " + iapprover.fullName() 
-	                                       + '(' + iapprover.id() +')',
+	                                        + '(' + iapprover.id() + ')',
 	                                            new String[] {"colspan=4", "class=head"})
 	                            };
 	//                        }
@@ -242,34 +241,29 @@
 	                                userCell = AbsCell.Null; 
 	                            } else if (user.endsWith(trans.org().getRealm())){
 	                                userOK=true;
-//	                                String title;
+	                                String title;
 	                                Organization org = OrganizationFactory.obtain(trans.env(), user);
 	                                if (org==null) {
-//	                                    title="";
+	                                    title="";
 		                                userCell = new TextCell(user);
 	                                } else {
 	                                    Identity au = org.getIdentity(trans, user);
 	                                    if (au!=null) {
 	                                    	if(au.isPerson()) {
-	                                    		userCell = new TextCell(au.fullName() + "\n(" + au.id() + ')');
+	                                    		userCell = new TextToolTipCell(au.fullName(),"Identity: " + au.id());
 	                                    	} else {
-	                                    		userCell = new TextCell(au.fullID());
+	                                            Identity managedBy = au.responsibleTo();
+	                                            if (managedBy==null) {
+	                                                title ="Identity: " + au.type();
+	                                            } else {
+	                                                title="Sponsor: " + managedBy.fullName();                                                
+	                                            }
+	                                    		userCell = new TextToolTipCell(au.fullID(),title);
 	                                    	}
-//	                                    	
-//	                                        if ("MECHID".equals(au.type())) {
-//	                                            Identity managedBy = au.responsibleTo();
-//	                                            if (managedBy==null) {
-//	                                                title ="title=" + au.type();
-//	                                            } else {
-//	                                                title="title=Sponsor is " + managedBy.fullName();                                                
-//	                                            }
-//	                                        } else {
-//	                                            title="title=" + au.fullName();
-//	                                        }
 	                                    } else {
 	                                        userOK=false;
-//	                                        title="title=Not a User at " + org.getName();
-			                                userCell = new TextCell(user);
+	                                        title="Not a User at " + org.getName();
+			                                userCell = new TextToolTipCell(user,title);
 	                                    }
 	                                }
 	                                prevUser=user;
diff --git a/auth/auth-gui/src/main/java/org/onap/aaf/auth/gui/table/TextToolTipCell.java b/auth/auth-gui/src/main/java/org/onap/aaf/auth/gui/table/TextToolTipCell.java
new file mode 100644
index 0000000..986b824
--- /dev/null
+++ b/auth/auth-gui/src/main/java/org/onap/aaf/auth/gui/table/TextToolTipCell.java
@@ -0,0 +1,51 @@
+/**
+ * ============LICENSE_START====================================================
+ * org.onap.aaf
+ * ===========================================================================
+ * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
+ * ===========================================================================
+ * Licensed 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.
+ * ============LICENSE_END====================================================
+ *
+ */
+
+package org.onap.aaf.auth.gui.table;
+
+import org.onap.aaf.misc.xgen.html.HTMLGen;
+
+/**
+ * Write Simple Text into a Cell
+ * @author Jonathan
+ *
+ */
+public class TextToolTipCell extends AbsCell {
+    public final String name;
+    private final String[] attrs;
+	private final String tooltip;
+    
+    public TextToolTipCell(String name, String tooltip, String... attributes) {
+        attrs = attributes;
+        this.name = name;
+        this.tooltip = "<abbr title=\"" + tooltip + "\">";
+    }
+    
+    @Override
+    public void write(HTMLGen hgen) {
+        hgen.text(tooltip + name + "</abbr>");
+    }
+    
+    @Override
+    public String[] attrs() {
+        return attrs;
+    }
+}
diff --git a/cadi/core/src/main/java/org/onap/aaf/cadi/filter/CadiFilter.java b/cadi/core/src/main/java/org/onap/aaf/cadi/filter/CadiFilter.java
index cd8eff4..01bf6f2 100644
--- a/cadi/core/src/main/java/org/onap/aaf/cadi/filter/CadiFilter.java
+++ b/cadi/core/src/main/java/org/onap/aaf/cadi/filter/CadiFilter.java
@@ -303,7 +303,14 @@
     private boolean noAuthn(HttpServletRequest hreq) {
         if (pathExceptions!=null) {
             String pi = hreq.getPathInfo();
-            if (pi==null) return false; // JBoss sometimes leaves null
+            if (pi==null) {
+            	// Attempt to get from URI only  (Daniel Rose)
+                pi = hreq.getRequestURI().substring(hreq.getContextPath().length());
+                if(pi==null) {
+                	// Nothing works.
+                	return false; // JBoss sometimes leaves null
+                }
+            }
             for (String pe : pathExceptions) {
                 if (pi.startsWith(pe))return true;
             }
diff --git a/cadi/core/src/main/java/org/onap/aaf/cadi/util/CSV.java b/cadi/core/src/main/java/org/onap/aaf/cadi/util/CSV.java
index 1d60ae5..47de84e 100644
--- a/cadi/core/src/main/java/org/onap/aaf/cadi/util/CSV.java
+++ b/cadi/core/src/main/java/org/onap/aaf/cadi/util/CSV.java
@@ -44,6 +44,7 @@
 	private File csv;
 	private Access access;
 	private boolean processAll;
+	private char delimiter = ',';
 	
 	public CSV(Access access, File file) {
 		this.access = access;
@@ -57,6 +58,11 @@
 		processAll = false;
 	}
 	
+	public CSV setDelimiter(char delimiter) {
+		this.delimiter = delimiter;
+		return this;
+	}
+	
 	public String name() {
 		return csv.getName();
 	}
@@ -116,16 +122,17 @@
 									escape = true;
 								}
 								break;
-							case ',':
-								if(quotes) {
-									sb.append(c);
-								} else {
-									row.add(sb.toString());
-									sb.setLength(0);
-								}
-								break;
 							default:
-								sb.append(c);
+								if(delimiter==c) {
+									if(quotes) {
+										sb.append(c);
+									} else {
+										row.add(sb.toString());
+										sb.setLength(0);
+									}
+								} else {
+									sb.append(c);
+								}
 						}
 					}
 					if(sb.length()>0 || c==',') {