From 6d8aff7b3657332020ef215eb1b2fc16017e4cc8 Mon Sep 17 00:00:00 2001
From: Roberto Sánchez <roberto.sanchez@curisit.net>
Date: Sun, 26 Jan 2014 13:34:03 +0000
Subject: [PATCH] #395 feature - Added license history entity and new angular directive to load file content

---
 securis/src/main/java/net/curisit/securis/db/License.java                   |   47 ++-----
 securis/src/main/resources/static/licenses.html                             |    8 
 securis/src/main/java/net/curisit/securis/db/LicenseHistory.java            |  117 +++++++++++++++++++
 securis/src/main/java/net/curisit/securis/services/LicenseResource.java     |   32 +++++
 securis/src/main/resources/static/js/licenses.js                            |  108 ++++++++++++-----
 securis/src/main/resources/static/js/catalogs.js                            |   14 --
 securis/src/main/resources/static/js/main.js                                |    4 
 securis/src/main/java/net/curisit/securis/security/SecurityInterceptor.java |    8 -
 securis/src/main/resources/db/schema.sql                                    |   16 ++
 9 files changed, 257 insertions(+), 97 deletions(-)

diff --git a/securis/src/main/java/net/curisit/securis/db/License.java b/securis/src/main/java/net/curisit/securis/db/License.java
index f683161..3bd60b0 100644
--- a/securis/src/main/java/net/curisit/securis/db/License.java
+++ b/securis/src/main/java/net/curisit/securis/db/License.java
@@ -2,15 +2,18 @@
 
 import java.io.Serializable;
 import java.util.Date;
+import java.util.List;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import javax.persistence.FetchType;
 import javax.persistence.GeneratedValue;
 import javax.persistence.Id;
 import javax.persistence.JoinColumn;
 import javax.persistence.ManyToOne;
 import javax.persistence.NamedQueries;
 import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
 import javax.persistence.Table;
 
 import org.codehaus.jackson.annotate.JsonAutoDetect;
@@ -77,19 +80,13 @@
 	@Column(name = "modification_timestamp")
 	private Date modificationTimestamp;
 
-	@Column(name = "activation_timestamp")
-	private Date activationTimestamp;
-
-	@Column(name = "cancelation_timestamp")
-	private Date cancelationTimestamp;
-
-	@Column(name = "send_timestamp")
-	private Date sendTimestamp;
-
 	@Column(name = "last_access_timestamp")
 	private Date lastAccessTimestamp;
 
 	private String comments;
+
+	@OneToMany(fetch = FetchType.LAZY, mappedBy = "license")
+	private List<LicenseHistory> history;
 
 	public int getId() {
 		return id;
@@ -209,22 +206,6 @@
 		this.email = email;
 	}
 
-	public Date getActivationTimestamp() {
-		return activationTimestamp;
-	}
-
-	public void setActivationTimestamp(Date activationTimestamp) {
-		this.activationTimestamp = activationTimestamp;
-	}
-
-	public Date getSendTimestamp() {
-		return sendTimestamp;
-	}
-
-	public void setSendTimestamp(Date sendTimestamp) {
-		this.sendTimestamp = sendTimestamp;
-	}
-
 	public void setId(int id) {
 		this.id = id;
 	}
@@ -235,14 +216,6 @@
 
 	public void setCanceledBy(User canceledBy) {
 		this.canceledBy = canceledBy;
-	}
-
-	public Date getCancelationTimestamp() {
-		return cancelationTimestamp;
-	}
-
-	public void setCancelationTimestamp(Date cancelationTimestamp) {
-		this.cancelationTimestamp = cancelationTimestamp;
 	}
 
 	public Date getLastAccessTimestamp() {
@@ -277,6 +250,14 @@
 		this.comments = comments;
 	}
 
+	public List<LicenseHistory> getHistory() {
+		return history;
+	}
+
+	public void setHistory(List<LicenseHistory> history) {
+		this.history = history;
+	}
+
 	public static class Status {
 		public static final int CREATED = 0;
 		public static final int SENT = 1;
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java b/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
new file mode 100644
index 0000000..82d2537
--- /dev/null
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
@@ -0,0 +1,117 @@
+package net.curisit.securis.db;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+import org.codehaus.jackson.annotate.JsonAutoDetect;
+import org.codehaus.jackson.annotate.JsonIgnore;
+import org.codehaus.jackson.annotate.JsonIgnoreProperties;
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+
+/**
+ * Entity implementation class for Entity: license
+ * 
+ */
+@JsonAutoDetect
+@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
+@Entity
+@Table(name = "license_history")
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NamedQueries(
+	{ @NamedQuery(name = "list-license-history", query = "SELECT lh FROM LicenseHistory lh where lh.license.id = :licId") })
+public class LicenseHistory implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@Id
+	@GeneratedValue
+	private int id;
+
+	@JsonIgnore
+	@ManyToOne
+	@JoinColumn(name = "license_id")
+	private License license;
+
+	@JsonIgnore
+	@ManyToOne
+	@JoinColumn(name = "username")
+	private User user;
+
+	private String action;
+	private String comments;
+
+	private Date timestamp;
+
+	public int getId() {
+		return id;
+	}
+
+	public License getLicense() {
+		return license;
+	}
+
+	public void setLicense(License license) {
+		this.license = license;
+	}
+
+	public User getUser() {
+		return user;
+	}
+
+	@JsonProperty("username")
+	public String getUsername() {
+		return user == null ? null : user.getUsername();
+	}
+
+	public void setUser(User user) {
+		this.user = user;
+	}
+
+	public String getAction() {
+		return action;
+	}
+
+	public void setAction(String action) {
+		this.action = action;
+	}
+
+	public String getComments() {
+		return comments;
+	}
+
+	public void setComments(String comments) {
+		this.comments = comments;
+	}
+
+	public Date getTimestamp() {
+		return timestamp;
+	}
+
+	public void setTimestamp(Date timestamp) {
+		this.timestamp = timestamp;
+	}
+
+	public void setId(int id) {
+		this.id = id;
+	}
+
+	public static class Actions {
+		public static final String CREATE = "creation";
+		public static final String ADD_REQUEST = "request";
+		public static final String SEND = "send";
+		public static final String MODIFY = "modify";
+		public static final String ACTIVATE = "activate";
+		public static final String CANCEL = "cancel";
+		public static final String DELETE = "delete";
+	}
+}
diff --git a/securis/src/main/java/net/curisit/securis/security/SecurityInterceptor.java b/securis/src/main/java/net/curisit/securis/security/SecurityInterceptor.java
index 4f02169..9150be8 100644
--- a/securis/src/main/java/net/curisit/securis/security/SecurityInterceptor.java
+++ b/securis/src/main/java/net/curisit/securis/security/SecurityInterceptor.java
@@ -52,9 +52,6 @@
 	com.google.inject.Provider<EntityManager> emProvider;
 
 	public void filter(ContainerRequestContext containerRequestContext) throws IOException {
-		// log.info("scw {}, {}", scw, scw.getClass());
-		log.info("MACHED res: {}", containerRequestContext.getUriInfo().getMatchedResources());
-		// dispatcher.getDefaultContextObjects().remove(SecurityContextWrapper.class);
 		ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
 		Method method = methodInvoker.getMethod();
 
@@ -82,11 +79,8 @@
 			scw.setOrganizationsIds(orgs);
 			containerRequestContext.setSecurityContext(scw);
 			// Next line provide injection in resource methods
-			log.info("TEST context {}", ResteasyProviderFactory.getContextData(BasicSecurityContext.class));
 			ResteasyProviderFactory.pushContext(BasicSecurityContext.class, scw);
-			// log.info("{}", dispatcher.getDefaultContextObjects());
-			// dispatcher.getDefaultContextObjects().put(SecurityContextWrapper.class, secContext);
-			log.info("Added custom SecurityContext for user {}", username);
+			log.debug("Added custom SecurityContext for user {}, orgs: {}", username, orgs);
 		}
 	}
 
diff --git a/securis/src/main/java/net/curisit/securis/services/LicenseResource.java b/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
index 005ef97..f9cb7c3 100644
--- a/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
@@ -1,5 +1,6 @@
 package net.curisit.securis.services;
 
+import java.io.IOException;
 import java.util.Date;
 import java.util.List;
 
@@ -21,6 +22,7 @@
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
+import net.curisit.integrity.commons.JsonUtils;
 import net.curisit.integrity.commons.Utils;
 import net.curisit.integrity.exception.CurisException;
 import net.curisit.securis.DefaultExceptionHandler;
@@ -31,6 +33,7 @@
 import net.curisit.securis.security.Securable;
 import net.curisit.securis.utils.TokenHelper;
 
+import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -124,7 +127,7 @@
 		{ MediaType.APPLICATION_JSON })
 	@Transactional
 	public Response create(License lic, @Context BasicSecurityContext bsc) {
-		log.info("Creating new organization");
+		log.info("Creating new license from create()");
 		EntityManager em = emProvider.get();
 		Pack pack = null;
 		if (lic.getPackId() != null) {
@@ -143,7 +146,7 @@
 		}
 
 		try {
-			User createdBy = getUser(lic.getCreatedById(), em);
+			User createdBy = getUser(bsc.getUserPrincipal().getName(), em);
 			lic.setCreatedBy(createdBy);
 		} catch (CurisException ex) {
 			String createdByUsername = lic.getCreatedById();
@@ -161,6 +164,31 @@
 		return Response.ok(lic).build();
 	}
 
+	@POST
+	@Path("/")
+	@Consumes(MediaType.MULTIPART_FORM_DATA)
+	@Securable
+	@Produces(
+		{ MediaType.APPLICATION_JSON })
+	@Transactional
+	public Response createWithFile(MultipartFormDataInput mpfdi, @Context BasicSecurityContext bsc) throws IOException {
+		License lic = new License();
+		lic.setCode(mpfdi.getFormDataPart("code", String.class, null));
+		lic.setRequestData(mpfdi.getFormDataPart("request_data", String.class, null));
+		lic.setPackId(mpfdi.getFormDataPart("pack_id", Integer.class, null));
+		lic.setFullName(mpfdi.getFormDataPart("full_name", String.class, null));
+		lic.setEmail(mpfdi.getFormDataPart("email", String.class, null));
+		lic.setComments(mpfdi.getFormDataPart("comments", String.class, null));
+		try {
+			log.info("File content: {}", lic.getRequestData());
+			log.info("License read from multipart: {}", JsonUtils.toJSON(lic));
+		} catch (CurisException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		return create(lic, bsc);
+	}
+
 	private User getUser(String username, EntityManager em) throws CurisException {
 		User user = null;
 		if (username != null) {
diff --git a/securis/src/main/resources/db/schema.sql b/securis/src/main/resources/db/schema.sql
index 813b45a..4860b61 100644
--- a/securis/src/main/resources/db/schema.sql
+++ b/securis/src/main/resources/db/schema.sql
@@ -76,14 +76,22 @@
   email VARCHAR(100)  NOT NULL,  
   comments VARCHAR(1024) NULL ,
   creation_timestamp DATETIME NOT NULL ,  
-  send_timestamp DATETIME NULL ,  
   modification_timestamp DATETIME NULL ,  
-  activation_timestamp DATETIME NULL ,  
-  cancelation_timestamp DATETIME NULL ,  
   last_access_timestamp DATETIME NULL ,  
   canceled_by varchar(45) NULL ,  
   created_by varchar(45) NULL ,  
-  status VARCHAR(3) NOT NULL default 0,  
+  status INT NOT NULL default 0,  
+  PRIMARY KEY (id));
+  
+  
+drop table IF EXISTS license_history;
+CREATE TABLE IF NOT EXISTS license_history (
+  id INT NOT NULL auto_increment,
+  license_id INT NOT NULL,
+  username VARCHAR(45) NOT NULL,
+  timestamp DATETIME NOT NULL ,
+  action VARCHAR(40) , 
+  comments VARCHAR(512) ,
   PRIMARY KEY (id));
   
   
diff --git a/securis/src/main/resources/static/js/catalogs.js b/securis/src/main/resources/static/js/catalogs.js
index 5f244ac..66c49f7 100644
--- a/securis/src/main/resources/static/js/catalogs.js
+++ b/securis/src/main/resources/static/js/catalogs.js
@@ -159,8 +159,7 @@
                                  **********************************************/
 
 								function _success(response) {
-									console.log('$resource')
-									console.log(response)
+									console.debug('$resource action success')
 								}
 								function _fail(response) {
 									console
@@ -244,9 +243,6 @@
 										promises.push(refs[f.name].$promise);
 									});
 
-									console.log('promises: ' + promises.length
-											+ ' ')
-									console.log(promises)
 									$q.all(promises)
 									  .then(function() {
 											    for(var i in refsFields) {
@@ -254,10 +250,7 @@
 											        var rf = refsFields[i];
 													var cat = that.getResource(rf.resource);
 													var pk = that.getPk(that.getMetadata(rf.resource))
-													console.log('PK field for '
-																	+ rf.name
-																	+ ' is '
-																	+ pk)
+													//console.log('PK field for ' + rf.name + ' is ' + pk)
 													var comboData = []
 													refs[rf.name].forEach(function(row) {
 																comboData.push({
@@ -271,8 +264,6 @@
 																		});
 															})
 													refs[rf.name] = comboData;
-													console.log('Ready for combo for ' + rf.name)
-													console.log(comboData);
 												}
 											    _current && _current.fields.forEach(function(f) {
 															if (f.values)
@@ -280,7 +271,6 @@
 														});
 											})
 
-									console.log(refs);
 									return refs;
 								}
 
diff --git a/securis/src/main/resources/static/js/licenses.js b/securis/src/main/resources/static/js/licenses.js
index 67bf7b4..fb5dd85 100644
--- a/securis/src/main/resources/static/js/licenses.js
+++ b/securis/src/main/resources/static/js/licenses.js
@@ -9,6 +9,44 @@
 	    }
 
 	var app = angular.module('securis');
+	
+	app.directive('fileLoader',
+	            function($timeout, $parse) {
+	                return {
+	                    restrict : 'A', // only activate on element attribute
+	                    require : '',
+	                    link : function(scope, element, attrs) {
+                            console.log('scope.license: ' + scope.$parent.license);
+                            var setter = $parse(attrs.fileLoader).assign;
+	                        element.bind('change', function(evt) {
+	                            console.log('scope.license: ' + scope.$parent.license);
+	                            var field = $parse(attrs.fileLoader);
+	                            console.log('field: ' + field);
+	                            var fileList = evt.target.files;
+	                            if (fileList != null && fileList[0]) {
+	                                var reader = new FileReader();
+                                    reader.onerror = function(data) {
+                                        setter(scope.$parent, 'ERROR');
+                                        scope.$apply();
+                                    }
+                                    reader.onload = function(data) {
+                                        setter(scope.$parent, reader.result);
+                                        scope.$apply();
+                                    }
+                                    
+                                    reader.readAsText(fileList[0]);       
+	                            } else {
+                                    console.log('NO FILE: ');
+                                    field = '';
+                                    scope.$apply();
+	                            }
+	                        });
+	                        
+	                    }
+	                };
+	            });
+
+	console.log(' OK ????? ');
 
 	app.controller('PackAndLicensesCtrl', [
 	                    			'$scope',
@@ -17,38 +55,6 @@
 	                    			'$store',
 	                    			'$L',
 	   			function($scope, $http, toaster, $store, $L) {
-	                    				$scope.licenses = [
-	                    					                {id: 1,
-	                    					                	"code": "BP-SA-001-AKSJMS234",
-	                    					                	"user_fullname": "Johnny Belmonte",
-	                    					                	"user_email": "jb@curisit.net",
-	                    					                	"status": 3},
-	                    						                {id: 2,
-	                    						                	"code": "BP-SA-001-KAJSDHAJS",
-	                    						                	"user_fullname": "Walter Simons",
-	                    						                	"user_email": "ws@curisit.net",
-	                    						                	"status": 1},
-	                    						                {id: 3,
-	                    						                	"code": "BP-SA-001-ASKDGHKA",
-	                    						                	"user_fullname": "Frank Belmonte",
-	                    						                	"user_email": "fb@curisit.net",
-	                    						                	"status": 2},
-	                    							                {id: 4,
-	                    							                	"code": "BP-SA-001-BBBGGGG",
-	                    							                	"user_fullname": "John Dalton",
-	                    							                	"user_email": "jd@curisit.net",
-	                    							                	"status": 3},
-	                    							                {id: 5,
-	                    							                	"code": "BP-SA-001-AKADNAJANA",
-	                    							                	"user_fullname": "Walter Martins",
-	                    							                	"user_email": "wm@curisit.net",
-	                    							                	"status": 3},
-	                    							                {id: 6,
-	                    							                	"code": "BP-SA-001-AKANDAKS",
-	                    							                	"user_fullname": "Joe Bolton",
-	                    							                	"user_email": "jbol@curisit.net",
-	                    							                	"status": 2}
-	                    					                ];
 	                    					
         				$scope.maxLengthErrorMsg = function(displayname, fieldMaxlength) {
         					return $L.get("{0} length is too long (max: {1}).", $L.get(displayname), fieldMaxlength);
@@ -171,7 +177,6 @@
 	                                   function($scope, $http, $resource, toaster, $store, $L) {
 	                                       $scope.$on('pack_changed', function(evt, message) {
                                                $scope.licenses = licenseResource.query({packId: $scope.currentPack.id});
-	                                           console.log('on pack_changed');
 	                                           if ($scope.showForm) {
 	                                               if ($scope.isNew) {
 	                                                   $scope.license.pack_id = $scope.currentPack.id
@@ -183,6 +188,25 @@
 	                                           
 	                                       var licenseResource = $resource('/license/:licenseId', {
 	                                           licenseId : '@id'
+	                                       }, {
+	                                           save_w_upload: {
+	                                               method: "POST",
+	                                               transformRequest: function(data, headersGetter) {
+	                                                   // To use an object without FormData, follow: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest?redirectlocale=en-US&redirectslug=DOM%2FXMLHttpRequest%2FUsing_XMLHttpRequest#Submitting_forms_and_uploading_files
+	                                                   var formData = new FormData();
+	                                                   angular.forEach(data, function(value, key) {
+	                                                       if (key !== 'request_data')
+	                                                           formData.append(key, value)
+                                                           else
+                                                               formData.append(key, $('input#request_data_file').get(0).files[0]);
+	                                                   })
+	                                                   
+	                                                   return formData;
+	                                               },
+	                                               headers: {
+	                                                   'Content-Type': undefined
+	                                               }
+	                                           }
 	                                       });
 	                                       $scope.mandatory = {
 	                                               code: true
@@ -202,12 +226,28 @@
 	                                       if ($scope.currentPack)
 	                                           $scope.licenses = licenseResource.query({packId: $scope.currentPack.id});
 	                                       
+//	                                       $(document).on('change', '#request_data_file', function(newValue, oldValue) {
+//	                                          console.log('File changed!!!!'); 
+//	                                           var reader = new FileReader();
+//	                                           reader.onload = function(data) {
+//                                                   console.log('LOAD complete: ' + data);
+//                                                   console.log('LOAD reader.result: ' + reader.result);
+//                                                   $('input#request_data').val(reader.result)
+//	                                           }
+//                                               console.log('file: ' + $('input#request_data_file').get(0).files[0]);
+//                                               console.log('file2: ' + $scope.request_data_file);
+//	                                           reader.readAsText($('input#request_data_file').get(0).files[0]);	                                            
+//	                                       });
+	                                       
 	                                       $scope.save = function() {
+	                                           $( "form#licenseForm" )
+	                                           .attr( "enctype", "multipart/form-data" )
+	                                           .attr( "encoding", "multipart/form-data" );
 	                                           var _success = function() {
 	                                               if (!$scope.isNew) $scope.showForm = false;
 	                                               $scope.licenses = licenseResource.query({packId: $scope.currentPack.id});
 	                                           }
-	                                           licenseResource.save($scope.license, _success)
+	                                           licenseResource.save_w_upload($scope.license, _success)
 	                                       }
 	                                       
 	                                       $scope.newLicense = function() {
diff --git a/securis/src/main/resources/static/js/main.js b/securis/src/main/resources/static/js/main.js
index 0bf562d..1406d54 100644
--- a/securis/src/main/resources/static/js/main.js
+++ b/securis/src/main/resources/static/js/main.js
@@ -37,7 +37,7 @@
                               $location.path('/login');
                               toaster.pop('warning', 'Session has expired', null, 4000);
     		              } else {
-    		                  console.log('Last access recent');
+    		                  console.debug('Last access recent');
     		              }
     		          }
     		          $store.set('last_access', now);
@@ -61,7 +61,7 @@
 		});
 
 	m.config(function($routeProvider, $locationProvider, $httpProvider) {
-		console.log('Configuring routes...');
+		console.debug('Configuring routes...');
 		    $routeProvider.when('/login', {
 		      templateUrl: 'login.html',
 		      controller: 'LoginCtrl'
diff --git a/securis/src/main/resources/static/licenses.html b/securis/src/main/resources/static/licenses.html
index 79de58e..6003901 100644
--- a/securis/src/main/resources/static/licenses.html
+++ b/securis/src/main/resources/static/licenses.html
@@ -163,7 +163,7 @@
 			</div>
 			
 		</div>
-
+{{license | json}}
 		<div id="licenses_section" class="col-md-6"  ng-controller="LicensesCtrl">
 				<nav class="navbar navbar-default navbar-static-top" ng-disabled="!currentPack">
 					<!-- Brand and toggle get grouped for better mobile display -->
@@ -253,12 +253,13 @@
 							</div>
 							</div>
 						</div>
-
+{{request_data}}
 						<div class="form-group" ng-if="isNew || !license.request_data" >
 							<label class="col-md-3 control-label" for="request_data" i18n>Request data</label>
 							<div class="col-md-8">
-								<textarea type="string" id="request_data" name="request_data" placeholder=""
+								<textarea id="request_data" name="request_data" placeholder=""
 										class="form-control" ng-model="license.request_data" rows="2" ng-required="mandatory.request_data" ng-maxlength="{{maxlength.request_data}}"></textarea>
+								<input file-loader="license.request_data" type="file" title="" >
 							<div class="alert inline-alert alert-warning" ng-show="licenseForm.request_data.$invalid">
 							    <span class="glyphicon glyphicon-warning-sign"></span>
 							    <span ng-show="licenseForm.request_data.$error.maxlength" ng-bind="maxlengthErrorMsg('Request data', maxlength.request_data)"></span>
@@ -272,6 +273,7 @@
 							<div class="col-md-8">
 								<textarea type="string" id="comments" name="comments" placeholder=""
 										class="form-control" ng-model="license.comments" rows="2" ng-required="mandatory.comments" ng-maxlength="{{maxlength.comments}}"></textarea>
+										
 							<div class="alert inline-alert alert-warning" ng-show="licenseForm.comments.$invalid">
 							    <span class="glyphicon glyphicon-warning-sign"></span>
 							    <span ng-show="licenseForm.comments.$error.maxlength" ng-bind="maxlengthErrorMsg('Comments', maxlength.comments)"></span>

--
Gitblit v1.3.2