Announcement Announcement Module
Collapse
No announcement yet.
History tables and error logging Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • History tables and error logging

    Hi,
    [I hope this is the right forum]
    I need to track all updates to the database in my application. I mean, all versions of a record must be kept.
    For example, in my DAO, if I have a method like
    Code:
    public User update_updateUser(User user) {
      getHibernateTemplate().saveOrUpdate(user);
    }
    I want to make this run as:
    Code:
    public User update_updateUser(User user) {
      BackupUser backupUser=new BackupUser(User);
      getHibernateTemplate().save(backupUser);
      getHibernateTemplate().saveOrUpdate(user);
    }
    For each entity, I will need to create another entity (Like BackupUser), which maps to another database table (Like BACKUP_USER).

    I would like to learn if it is possible, by use of AOP, to have the functionality I mentioned above - without handcoding manually for each Hibernate call.

    Another similar requirement is error logging. Whenever Spring catches an Exception, I would like to run this code:
    Code:
    ApplicationErrorLog appErrorLog = new(ApplicationErrorLog);
    getHibernateTemplate().save(applicationErrorLog );
    I know that I have to read a lot about AOP, but until then, your help, simple code examples and appcontext.xml will be will be very much appreciated.

    Regards,
    Turgay Zengin

  • #2
    did you consider using database triggers?

    Comment


    • #3
      Right, the natural way to implement this is using an update trigger, but I do not want to go into database triggers, because I want the database to be portable.

      Regards,
      Turgay Zengin

      Comment


      • #4
        This is what I could do, after studying AOP:

        SaveDatabaseInterceptor is for catching save operations, and then saving a backup object to the database. It works if the object implements Auditable --if(args0 instanceof Auditable)--, and if this is not a "insert into" but a "update" operation --if(oldId!=null)--.

        Code:
        public class SaveDatabaseInterceptor implements MethodInterceptor {
        	private SystemDAO systemDAO = null;
        
        	public void setSystemDAO(SystemDAO systemDao) {
                this.systemDAO = systemDao;
            }
        
        	public Object invoke(MethodInvocation invocation) throws Throwable {
        		Object[] args=invocation.getArguments();
        		Object args0=args[0];
        		if(args0 instanceof Auditable) {
        			BasePersistentObject bpo=(BasePersistentObject)args[0];
        			Long oldId=bpo.getId();
        			if(oldId!=null){				
        				BasePersistentObject backupBPO=bpo.getBackupObject();
        				systemDAO.saveBPO(backupBPO);
        				System.out.println("save advice worked!:" + invocation);
        			}		
        		}
        		Object rval = invocation.proceed();	
        		return rval;
        	}
        }
        Each Auditable entity class defines a method to get a "Backup" object. For example, in User.java:

        Code:
        	public BasePersistentObject getBackupObject() {
        		return new Backup_User(this);
        	}
        Backup_User is a seperate class, which also has a mapping file for hibernate.

        For "delete" operations, I have the following.

        Code:
        public class RemoveDatabaseInterceptor implements MethodInterceptor {
        	private SystemDAO systemDAO = null;
        
        	public void setSystemDAO(SystemDAO systemDao) {
                this.systemDAO = systemDao;
            }
        	
        	public Object invoke(MethodInvocation invocation) throws Throwable {	
        		Object[] args=invocation.getArguments();
        		Object args0=args[0];
        		if(args0 instanceof Auditable) {
        			BasePersistentObject bpo=(BasePersistentObject)args[0];
        			
        			if(bpo!=null){				
        				BasePersistentObject deletedBPO=bpo.getDeletedObject();
        				systemDAO.saveBPO(deletedBPO);
        				System.out.println("remove advice worked!:" + invocation);
        			}		
        		}
        
        		Object rval = invocation.proceed();	
        		return rval;
        		
        	}
        	
        }
        Likewise, each Auditable class has a method to get a "Deleted" object:
        Code:
        	public BasePersistentObject getDeletedObject() {
        		return new Deleted_User(this);
        	}
        Relevant parts from the applicationContext.xml:
        Code:
        	<bean id="DAOTemplate" lazy-init="true"
        		class="org.springframework.aop.framework.ProxyFactoryBean">
        		<property name="interceptorNames">
        			<list>
        				<value>saveDatabaseAdvisor</value>
        				<value>removeDatabaseAdvisor</value>
        			</list>
        		</property>
        	</bean>
        
            <bean id="userDAO" parent="DAOTemplate" >
        		<property name="proxyInterfaces"><value>dao.UserDAO</value></property>
        		<property name="target"><ref local="userDAOTarget"/></property>
            </bean>    
        
        	<bean id="userDAOTarget" class="dao.hibernate.UserDAOHibernate"> 
                <property name="sessionFactory"><ref local="sessionFactory"/></property>		
        	</bean>		
        
            <bean id="systemDAO" parent="DAOTemplate" >
        		<property name="proxyInterfaces"><value>dao.SystemDAO</value></property>
        		<property name="target"><ref local="systemDAOTarget"/></property>
            </bean>    
        
        	<bean id="systemDAOTarget" class="dao.hibernate.SystemDAOHibernate"> 
                <property name="sessionFactory"><ref local="sessionFactory"/></property>		
        	</bean>		
        
        	<bean id="saveDatabaseInterceptor" class="util.SaveDatabaseInterceptor">
        		<property name="systemDAO"><ref local="systemDAO"/></property>
        	</bean>
        
        	<bean id="saveDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        		<property name="advice"><ref local="saveDatabaseInterceptor"/></property>
        		<property name="patterns">
        			<list>
        				<value>.*save.*</value>				
        			</list>			
        		</property>
        	</bean>
        
        	<bean id="removeDatabaseInterceptor" class="util.RemoveDatabaseInterceptor" >
        		<property name="systemDAO"><ref local="systemDAO"/></property>
        	</bean>
        	
        	<bean id="removeDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        		<property name="advice"><ref local="removeDatabaseInterceptor"/></property>
        		<property name="patterns">
        			<list>
        				<value>.*remove.*</value>
        			</list>			
        		</property>
        	</bean>
        I now have only one problem with the remove operation. When the entity itself is deleted from the master table(User), a record will be inserted into the "Deleted_" table (Deleted_User). But I cannot put a foreign key to the "Deleted" table to point to the "deletedUser", because the record will be deleted from the master table...

        I am thinking about not deleting from the master table actually, but marking it as deleted, using a field called DATABASE_STATUS, and setting it to "DELETED". ("ACTIVE" if not deleted). But this also introduces another problem: If I have unique constraints on the table (USERNAME UNIQUE), than I cannot insert a new record violating that unique constraint. Any ideas?

        Regards,
        Turgay Zengin

        Comment


        • #5
          Here is the design I came up with, hoping that it could be useful for others as well. Any comments, critiques, improvement suggestions are welcome.

          SaveDatabaseInterceptor is for catching save operations, and then saving a backup object to the database. It works if the object implements Auditable --if(args0 instanceof Auditable)--, and if this is not a "insert into" but a "update" operation --if(oldId!=null)--.
          Code:
          public class SaveDatabaseInterceptor implements MethodInterceptor &#123;
          	private SystemDAO systemDAO = null;
          	private static final Log log = LogFactory.getLog&#40;SaveDatabaseInterceptor.class&#41;;
          	
          	public void setSystemDAO&#40;SystemDAO systemDao&#41; &#123;
                  this.systemDAO = systemDao;
              &#125;
          
          	public Object invoke&#40;MethodInvocation invocation&#41; throws Throwable &#123;
          		log.debug&#40;"save advice will work&#58;" + invocation&#41;;
          		BasePersistentObject bpo=null;
          		Object&#91;&#93; args=invocation.getArguments&#40;&#41;;
          		Object args0=args&#91;0&#93;;
          		if&#40;args0 instanceof Auditable&#41; &#123;
          			bpo=&#40;BasePersistentObject&#41;args0;
          			Long oldId=bpo.getId&#40;&#41;;
          			if&#40;oldId!=null&#41;&#123;												
          				BasePersistentObject backupBPO=bpo.getBackupObject&#40;&#41;;
          				backupBPO.setDatabaseStatus&#40;BasePersistentObject.RECORD_UPDATED+"_"+bpo.getBaseRecord&#40;&#41;.getId&#40;&#41;+"_"+&#40;bpo.getDatabaseVersion&#40;&#41;+1&#41;&#41;;
          				systemDAO.saveBackupBPO&#40;backupBPO&#41;;				
          				log.debug&#40;"save advice worked&#58;" + invocation&#41;;
          			&#125;
          			else &#123;
          				//new record
          				bpo.setBaseRecord&#40;bpo&#41;;
          			&#125;
          		&#125;
          		Object rval = invocation.proceed&#40;&#41;;
          		return rval;
          	&#125;
          Here is the BAsePersistentObject from which all my persistent classes inherit from:
          Code:
          public abstract class BasePersistentObject implements Serializable &#123;
          	public static String RECORD_ACTIVE="ACTIVE";
          	public static String RECORD_UPDATED="UPDATED";
          	public static String RECORD_DELETED="DELETED";
          	
              protected Long id;
              protected BasePersistentObject baseRecord;
              protected int databaseVersion;
              protected String databaseStatus=RECORD_ACTIVE;
              protected User createdUser;
              protected Timestamp createdTime;
              protected User lastModifiedUser;
              protected Timestamp lastModifiedTime;
              protected Timestamp systemModifiedTime;
              protected User ownerUser;
                  
          // getters, setters omitted
              
              public boolean isActive&#40;&#41; &#123; return getDatabaseStatus&#40;&#41;.equals&#40;"ACTIVE"&#41;; &#125;
          
              public boolean isDeleted&#40;&#41; &#123; return getDatabaseStatus&#40;&#41;.equals&#40;"DELETED"&#41;; &#125;
              
          	public  BasePersistentObject getBackupObject&#40;&#41; &#123;
          		BasePersistentObject bpo=null;
          		try&#123;
          			bpo=&#40;BasePersistentObject&#41;BeanUtils.cloneBean&#40;this&#41;;
          			bpo.setId&#40;null&#41;;
          		&#125;catch&#40;Exception e&#41; &#123;
          			System.out.println&#40;e&#41;;
          		&#125;
          		return bpo;
          	&#125;
          The interesting method here is getBAckupObject(), which returns a copy of this entity for auditing purposes. The classes to be audited will have to implement a marker interface called Auditable.
          Remove advice will simply modify the database status as "DELETED".

          Code:
          public class RemoveDatabaseInterceptor implements MethodInterceptor &#123;
          	private static final Log log = LogFactory.getLog&#40;RemoveDatabaseInterceptor.class&#41;;
          	private SystemDAO systemDAO = null;
          
          	public void setSystemDAO&#40;SystemDAO systemDao&#41; &#123;
                  this.systemDAO = systemDao;
              &#125;
          	
          	public Object invoke&#40;MethodInvocation invocation&#41; throws Throwable &#123;
          		log.debug&#40;"remove advice will work&#58;" + invocation&#41;;
          		Object&#91;&#93; args=invocation.getArguments&#40;&#41;;
          		Object args0=args&#91;0&#93;;
          		if&#40;args0 instanceof Auditable&#41; &#123;
          			BasePersistentObject bpo=&#40;BasePersistentObject&#41;args&#91;0&#93;;			
          			if&#40;!bpo.getDatabaseStatus&#40;&#41;.equals&#40;BasePersistentObject.RECORD_ACTIVE&#41;&#41;
          				throw new Exception&#40;"Status of the entity object was not ACTIVE"&#41;;
          			else &#123;
          
          				bpo.setDatabaseStatus&#40;BasePersistentObject.RECORD_DELETED+"_"+bpo.getBaseRecord&#40;&#41;.getId&#40;&#41;&#41;;
          				log.debug&#40;"remove advice worked&#58;" + invocation&#41;;
          			&#125;
          		&#125;
          
          		Object rval = invocation.proceed&#40;&#41;;	
          		return rval;
          		
          	&#125;
          SystemDAO, which handles application wide DAO, looks like:
          Code:
          public interface SystemDAO extends DAO &#123;
          	/**
          	 * When an exception occurs, we want to record it in the database,
          	 * with use of AOP&#40;ExceptionAdvice&#41;. Not to be used directly, the system calls when needed.
          	 * @param ael the ApplicationErrorLog object to be logged in the database.
          	 * @return the ApplicationErrorLog object that was logged in the database.
          	 * @see ExceptionAdvice 
          	 */
              public ApplicationErrorLog saveApplicationErrorLog&#40;ApplicationErrorLog ael&#41;;
              
              /**
          	 * All database create or update operations can use this interface.
          	 * @param bpo the BasePersistentObject to be created or updated in the database.
          	 * Subject to the AOP SaveDatabaseInterceptor if implements Auditable.
          	 * @return the BasePersistentObject that was created or updated in the database.
          	 * @see SaveDatabaseInterceptor
          	 * @see Auditable
          	 */
              public BasePersistentObject saveBPO&#40;BasePersistentObject bpo&#41;;
              
              /**
          	 * Backup records&#40;auditing&#41; are saved through this interface.
          	 * Created to let SaveDatabaseInterceptor bypass this database update.
          	 * @param bpo the BasePersistentObject to be created in the database for auditing purposes.
          	 * @return the BasePersistentObject that was created in the database for auditing purposes..
          	 * @see SaveDatabaseInterceptor
          	 */
              public BasePersistentObject saveBackupBPO&#40;BasePersistentObject bpo&#41;;
              
              /**
          	 * All database "delete" operations use this interface.
          	 * @param bpo the BasePersistentObject to be deleted in the database.
          	 * Subject to the AOP RemoveDatabaseInterceptor if implements Auditable.
          	 * @see RemoveDatabaseInterceptor
          	 * @see Auditable
          	 */
              public void removeBPO&#40;BasePersistentObject bpo&#41;;
              
              /**
          	 * All database "select * from ATABLE where id=?" operations use this interface.
          	 * @param theClass The class name of the entity to be retrieved. 
          	 * @param id The Long representation of the identifier field
          	 * @return the BasePersistentObject that was retrieved from the database.
          	 */
              public BasePersistentObject retrieveBPO&#40;Class theClass,Long id&#41;;
          And the Hibernate implementation:
          Code:
          public class SystemDAOHibernate extends HibernateDaoSupport implements SystemDAO &#123;
              private static final Log log = LogFactory.getLog&#40;SystemDAOHibernate.class&#41;;
          
          	public ApplicationErrorLog saveApplicationErrorLog&#40;ApplicationErrorLog ael&#41; &#123;
          		getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;ael&#41;;
          		return ael;
          	&#125;
          
              public BasePersistentObject saveBPO&#40;BasePersistentObject bpo&#41; &#123;
                  getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;bpo&#41;;
                  return bpo;
              &#125;
          
              public BasePersistentObject saveBackupBPO&#40;BasePersistentObject bpo&#41; &#123;
                  getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;bpo&#41;;
                  return bpo;
              &#125;
              
          	public void removeBPO&#40;BasePersistentObject bpo&#41; &#123;
          		getHibernateTemplate&#40;&#41;.saveOrUpdate&#40;bpo&#41;;		
          	&#125;
          
              public BasePersistentObject retrieveBPO&#40;Class theClass,Long id&#41;&#123;
              	return &#40;BasePersistentObject&#41;getHibernateTemplate&#40;&#41;.get&#40;theClass,id&#41;;
              &#125;
          The ExceptionAdvice mentioned above looks like (inserts a record into the APPLICATIONERRORLOG table):

          Code:
          public class ExceptionAdvice implements ThrowsAdvice&#123;
          	private SystemDAO systemDAO = null;
          	private UserDAO userDAO = null;
          	private static final Log log = LogFactory.getLog&#40;ExceptionAdvice.class&#41;;
          	
          	public void setUserDAO&#40;UserDAO userDao&#41; &#123;
                  this.userDAO = userDao;
              &#125;
          	public void setSystemDAO&#40;SystemDAO systemDao&#41; &#123;
                  this.systemDAO = systemDao;
              &#125;
          
          	public void afterThrowing&#40;Method m,Object&#91;&#93; args,Object target,Exception ex&#41;&#123;
          		log.debug&#40;"Exception advice will work!&#58;"+ex&#41;;
          		ApplicationErrorLog ael=new ApplicationErrorLog&#40;&#41;;
          		Timestamp now = new Timestamp&#40;System.currentTimeMillis&#40;&#41;&#41;;
          		ael.setCreatedTime&#40;now&#41;;
          		
          		ael.setCreatedUser&#40;userDAO.retrieveUserByUserName&#40;"admin"&#41;&#41;;
          		ael.setDescription&#40;"Method&#58;"+m+" Exception&#58;"+ex&#41;;
          		ael.setErrorNumber&#40;"111"&#41;; //TODO&#58; To be implemented by looking at the actual error...		
          		systemDAO.saveApplicationErrorLog&#40;ael&#41;;
          		log.debug&#40;"Exception advice worked!&#58;"+ex&#41;;
          	&#125;
          The database schema for an Auditable class looks like:
          Code:
          create table DEPARTMENT&#40;
          ID NUMERIC NOT NULL primary key,
          DATABASEVERSION NUMERIC NOT NULL,
          BASERECORDID NUMERIC NOT NULL,
          DATABASESTATUS VARCHAR&#40;15&#41; DEFAULT 'ACTIVE' NOT NULL,
          CREATEDUSERID NUMERIC NOT NULL,
          CREATEDTIME TIMESTAMP NOT NULL,
          LASTMODIFIEDUSERID NUMERIC NOT NULL,
          LASTMODIFIEDTIME TIMESTAMP NOT NULL,
          SYSTEMMODIFIEDTIME TIMESTAMP NOT NULL,
          OWNERUSERID NUMERIC NOT NULL,
          NAME VARCHAR&#40;20&#41; NOT NULL,
          DESCRIPTION VARCHAR&#40;100&#41; NOT NULL&#41;;
          And finally the applicationcontext.xml (I know this has been long already )
          Code:
          <beans>
          <!-- BASICDATASOURCE -->
              <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
                  <!-- datasource properties omitted -->
              </bean>
          
              <!-- Hibernate SessionFactory -->
              <bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
                  <property name="dataSource"><ref local="dataSource"/></property>
                  <!-- Hibernate definitions omitted -->
              </bean>
          
              <!-- Transaction manager for a single Hibernate SessionFactory &#40;alternative to JTA&#41; -->
              <bean id="transactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager">
                  <property name="sessionFactory"><ref local="sessionFactory"/></property>
              </bean>
          
          	<bean id="DAOTemplate" lazy-init="true"
          		class="org.springframework.aop.framework.ProxyFactoryBean">
          		<property name="interceptorNames">
          			<list>
          				<value>saveDatabaseAdvisor</value>
          				<value>removeDatabaseAdvisor</value>
          			</list>
          		</property>
          	</bean>
          
              <bean id="userDAO" parent="DAOTemplate" >
          		<property name="proxyInterfaces"><value>dao.UserDAO</value></property>
          		<property name="target">
          			<bean id="userDAOTarget" class="dao.hibernate.UserDAOHibernate"> 
          				<property name="sessionFactory"><ref local="sessionFactory"/></property>
          			</bean> 
          		</property>
              </bean>    
          
              <bean id="systemDAO" parent="DAOTemplate" >
          		<property name="proxyInterfaces"><value>dao.SystemDAO</value></property>
          		<property name="target">
          			<bean id="systemDAOTarget" class="dao.hibernate.SystemDAOHibernate"> 
          				<property name="sessionFactory"><ref local="sessionFactory"/></property>		
          			</bean>	
          		</property>
              </bean>    		
          
          	<bean id="TransactionalFacadeTemplate" lazy-init="true"
          		class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
          		<property name="transactionManager"><ref local="transactionManager"/></property> 
          		<property name="transactionAttributes"> 
          			<props> 
          				<prop key="update_*">PROPAGATION_REQUIRED</prop> 
          				<prop key="*">PROPAGATION_SUPPORTS,readOnly</prop> 
          			</props> 
          		</property>	
          	</bean>
          
           	<bean id="SecureFacadeTemplate" lazy-init="true" parent="TransactionalFacadeTemplate"
          		class="org.springframework.aop.framework.ProxyFactoryBean">
          		<property name="interceptorNames">
          			<list>
          				<value>securityAdvisor</value>
          			</list>
          		</property>
          	</bean>
          	
              <bean id="userManagerFacade" parent="SecureFacadeTemplate" >
          		<property name="proxyInterfaces"><value>service.UserManagerFacade</value></property>
          		<property name="target">
          			<bean id="userManagerFacadeTarget" class="service.impl.UserManagerFacadeImpl"> 
          				<property name="userDAO"><ref local="userDAO"/></property> 
          				<property name="systemDAO"><ref local="systemDAO"/></property> 
          			</bean> 
          		</property>
              </bean>    
          
          	<bean id="saveDatabaseInterceptor" class="util.SaveDatabaseInterceptor">
          		<property name="systemDAO"><ref local="systemDAO"/></property>
          	</bean>
          
          	<bean id="saveDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
          		<property name="advice"><ref local="saveDatabaseInterceptor"/></property>
          		<property name="patterns">
          			<list>
          				<!--<value>.*save.*</value>-->
          				<value>.*save&#40;?!Backup&#41;\w+</value>		
          			</list>			
          		</property>
          	</bean>
          
          	<bean id="removeDatabaseInterceptor" class="util.RemoveDatabaseInterceptor" >
          		<property name="systemDAO"><ref local="systemDAO"/></property>
          	</bean>
          	
          	<bean id="removeDatabaseAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
          		<property name="advice"><ref local="removeDatabaseInterceptor"/></property>
          		<property name="patterns">
          			<list>
          				<value>.*remove.*</value>
          			</list>			
          		</property>
          	</bean>
          
          	<bean id="securityInterceptor" class="util.SecurityInterceptor" />
          
          	<bean id="securityAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
          		<property name="advice"><ref local="securityInterceptor"/></property>
          		<property name="patterns">
          			<list>
          				<value>.*retrieve.*</value>
          				<value>.*update_.*</value>
          			</list>			
          		</property>
          	</bean>
          
          	<bean id="exceptionAdvice" class="util.ExceptionAdvice" >
          		<property name="userDAO"><ref local="userDAO"/></property>
          		<property name="systemDAO"><ref local="systemDAO"/></property>
          	</bean>
          
          	<bean id="exceptionAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
          		<property name="advice"><ref local="exceptionAdvice"/></property>
          		<property name="patterns">
          			<list>
          				<value>.*</value>
          			</list>			
          		</property>
          	</bean>
          	
          	<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">		
          		<property name="advisorBeanNamePrefix">
          				<value>exceptionAdv</value>			
          		</property>
          		<property name="usePrefix">
          				<value>true</value>			
          		</property>
          	</bean>
          
          </beans>
          And (really finally) for the sake of completeness, here is the SecurityInterceptor:
          Code:
          public class SecurityInterceptor implements MethodInterceptor &#123;
          	private static final Log log = LogFactory.getLog&#40;SecurityInterceptor.class&#41;;
          	
          	public Object invoke&#40;MethodInvocation invocation&#41; throws Throwable &#123;
          //	 Apply crosscutting code
          	doSecurityCheck&#40;invocation&#41;;
          	
          //	 Call next interceptor
          	return invocation.proceed&#40;&#41;;
          	&#125;
          	
          	protected void doSecurityCheck&#40;MethodInvocation invocation&#41; throws UnAuthorizedException &#123;
          		log.debug&#40;"security check performed&#58;"+invocation&#41;; //TODO&#58; implement security check...
          	&#125;
          You can follow the progress of this at http://sourceforge.net/projects/openhelpdesk if you want to.

          Regards,
          Turgay Zengin

          Comment

          Working...
          X