001/** 002 * Copyright 2014 Tampere University of Technology, Pori Department 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package service.tut.pori.users.twitter; 017 018import java.util.Date; 019import java.util.List; 020import java.util.Map; 021import java.util.Map.Entry; 022 023import org.apache.commons.lang3.StringUtils; 024import org.apache.log4j.Logger; 025import org.springframework.beans.factory.annotation.Autowired; 026import org.springframework.jdbc.core.JdbcTemplate; 027import org.springframework.transaction.TransactionStatus; 028import org.springframework.transaction.support.TransactionCallback; 029 030import service.tut.pori.users.UserDAO; 031import twitter4j.auth.AccessToken; 032import core.tut.pori.dao.clause.SQLClause.SQLType; 033import core.tut.pori.dao.SQLDAO; 034import core.tut.pori.users.ExternalAccountConnection; 035import core.tut.pori.users.UserIdentity; 036import core.tut.pori.users.ExternalAccountConnection.UserServiceType; 037 038/** 039 * DAO for handling authorization tokens for Twitter User Service. 040 */ 041public class TwitterUserDAO extends SQLDAO { 042 private static final Logger LOGGER = Logger.getLogger(TwitterUserDAO.class); 043 /* tables */ 044 private static final String TABLE_USERS_TWITTER = DATABASE+".users_twitter"; 045 private static final String TABLE_USERS_TWITTER_REQUEST_TOKENS = DATABASE+".users_twitter_request_tokens"; 046 /* columns */ 047 private static final String COLUMN_ACCESS_TOKEN = "access_token"; 048 private static final String COLUMN_ACCESS_TOKEN_SECRET = "access_token_secret"; 049 private static final String COLUMN_REDIRECT_URI = "redirect_uri"; 050 private static final String COLUMN_REQUEST_TOKEN = "request_token"; 051 private static final String COLUMN_REQUEST_TOKEN_SECRET = "request_token_secret"; 052 /* sql scripts */ 053 private static final String SQL_GET_ACCESS_TOKEN = "SELECT "+COLUMN_ACCESS_TOKEN+", "+COLUMN_ACCESS_TOKEN_SECRET+" FROM "+TABLE_USERS_TWITTER+" WHERE "+COLUMN_USER_ID+"=?"; 054 055 private static final String SQL_INSERT_REQUEST_TOKEN = "INSERT INTO "+TABLE_USERS_TWITTER_REQUEST_TOKENS+" ("+COLUMN_USER_ID+", "+COLUMN_REQUEST_TOKEN+", "+COLUMN_REQUEST_TOKEN_SECRET+", "+COLUMN_REDIRECT_URI+", "+COLUMN_ROW_CREATED+") VALUES (?,?,?,?,NOW())"; 056 private static final int[] SQL_INSERT_REQUEST_TOKEN_SQL_TYPES = {SQLType.LONG.toInt(), SQLType.STRING.toInt(), SQLType.STRING.toInt(), SQLType.STRING.toInt()}; 057 058 private static final String SQL_GET_REQUEST_TOKEN = "SELECT "+COLUMN_USER_ID+", "+COLUMN_REQUEST_TOKEN_SECRET+", "+COLUMN_REDIRECT_URI+", "+COLUMN_ROW_UPDATED+" FROM "+TABLE_USERS_TWITTER_REQUEST_TOKENS+" WHERE "+COLUMN_REQUEST_TOKEN+"=?"; 059 060 private static final String SQL_REMOVE_ACCESS_TOKEN_BY_USER = "DELETE FROM "+TABLE_USERS_TWITTER+" WHERE "+COLUMN_USER_ID+"=?"; 061 062 private static final String SQL_REMOVE_REQUEST_TOKEN = "DELETE FROM "+TABLE_USERS_TWITTER_REQUEST_TOKENS+" WHERE "+COLUMN_REQUEST_TOKEN+"=?"; 063 064 private static final String SQL_REMOVE_REQUEST_TOKEN_BY_USER = "DELETE FROM "+TABLE_USERS_TWITTER_REQUEST_TOKENS+" WHERE "+COLUMN_USER_ID+"=?"; 065 066 private static final int[] SQL_REQUEST_TOKEN_SQL_TYPE = {SQLType.STRING.toInt()}; 067 068 /** tokens generated by twitter should be unique, so the unique column constraint on access token should never match on ON DUPLICATE KEY UPDATE for two different users, the match will be solely done on user id */ 069 private static final String SQL_SET_ACCESS_TOKEN = "INSERT INTO "+TABLE_USERS_TWITTER+" ("+COLUMN_USER_ID+","+COLUMN_ACCESS_TOKEN+","+COLUMN_ACCESS_TOKEN_SECRET+","+COLUMN_ROW_CREATED+") VALUES (?,?,?,NOW()) ON DUPLICATE KEY UPDATE "+COLUMN_ACCESS_TOKEN+"=VALUES("+COLUMN_ACCESS_TOKEN+"), "+COLUMN_ACCESS_TOKEN_SECRET+"=VALUES("+COLUMN_ACCESS_TOKEN_SECRET+")"; 070 private static final int[] SQL_SET_ACCESS_TOKEN_SQL_TYPES = {SQLType.LONG.toInt(),SQLType.STRING.toInt(),SQLType.STRING.toInt()}; 071 072 private static final String SQL_SET_REQUEST_TOKEN = "INSERT INTO "+TABLE_USERS_TWITTER_REQUEST_TOKENS+" ("+COLUMN_USER_ID+", "+COLUMN_REQUEST_TOKEN+", "+COLUMN_REQUEST_TOKEN_SECRET+", "+COLUMN_REDIRECT_URI+", "+COLUMN_ROW_CREATED+") VALUES (?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE "+COLUMN_REQUEST_TOKEN+"=VALUES("+COLUMN_REQUEST_TOKEN+"), "+COLUMN_REQUEST_TOKEN_SECRET+"=VALUES("+COLUMN_REQUEST_TOKEN_SECRET+"), "+COLUMN_REDIRECT_URI+"=VALUES("+COLUMN_REDIRECT_URI+")"; 073 private static final int[] SQL_SET_REQUEST_TOKEN_SQL_TYPES = {SQLType.LONG.toInt(), SQLType.STRING.toInt(), SQLType.STRING.toInt(), SQLType.STRING.toInt()}; 074 075 private static final int[] SQL_USER_IDENTITY_SQL_TYPE = {SQLType.LONG.toInt()}; 076 077 @Autowired 078 private UserDAO _userDAO = null; 079 080 /** 081 * 082 * @param token 083 */ 084 public void setRequestToken(RequestToken token) { 085 Long userId = token.getUserIdValue(); 086 if(userId == null){ // do not use on duplicate update for null, so that a new row will always be added 087 getJdbcTemplate().update(SQL_INSERT_REQUEST_TOKEN, new Object[]{userId, token.getToken(), token.getSecret(), token.getRedirectUri()}, SQL_INSERT_REQUEST_TOKEN_SQL_TYPES); 088 }else{ 089 getJdbcTemplate().update(SQL_SET_REQUEST_TOKEN, new Object[]{userId, token.getToken(), token.getSecret(), token.getRedirectUri()}, SQL_SET_REQUEST_TOKEN_SQL_TYPES); 090 } 091 } 092 093 /** 094 * This will remove both the access token and the request token associated with the given user (if any) 095 * 096 * @param userIdentity valid user identity 097 */ 098 public void removeTokens(UserIdentity userIdentity) { 099 Object[] ob = {userIdentity.getUserId()}; 100 JdbcTemplate t = getJdbcTemplate(); 101 if(t.update(SQL_REMOVE_ACCESS_TOKEN_BY_USER, ob, SQL_USER_IDENTITY_SQL_TYPE) < 1){ 102 LOGGER.debug("No access tokens removed for user, id: "+ob[0]); 103 } 104 if(t.update(SQL_REMOVE_REQUEST_TOKEN_BY_USER, ob, SQL_USER_IDENTITY_SQL_TYPE) < 1){ 105 LOGGER.debug("No request tokens removed for user, id: "+ob[0]); 106 } 107 } 108 109 /** 110 * 111 * @param token the request token value 112 * @return the fully populated request token or null if not found 113 */ 114 public RequestToken getRequestToken(String token) { 115 List<Map<String, Object>> rows = getJdbcTemplate().queryForList(SQL_GET_REQUEST_TOKEN, new Object[]{token}, SQL_REQUEST_TOKEN_SQL_TYPE); 116 if(rows.isEmpty()){ 117 LOGGER.debug("The request token was not found."); 118 return null; 119 } 120 RequestToken requestToken = extractRequestToken(rows.get(0)); 121 requestToken.setToken(token); 122 return requestToken; 123 } 124 125 /** 126 * 127 * @param row 128 * @return the token or null on failure 129 */ 130 private RequestToken extractRequestToken(Map<String, Object> row){ 131 RequestToken token = new RequestToken(); 132 for(Entry<String, Object> e : row.entrySet()){ 133 String column = e.getKey(); 134 switch(column){ 135 case COLUMN_USER_ID: 136 token.setUserId(new UserIdentity((Long) e.getValue())); 137 break; 138 case COLUMN_REQUEST_TOKEN: 139 token.setToken((String) e.getValue()); 140 break; 141 case COLUMN_REQUEST_TOKEN_SECRET: 142 token.setSecret((String) e.getValue()); 143 break; 144 case COLUMN_REDIRECT_URI: 145 token.setRedirectUri((String) e.getValue()); 146 break; 147 case COLUMN_ROW_UPDATED: 148 token.setUpdated((Date) e.getValue()); 149 break; 150 default: 151 LOGGER.warn("Ignored unknown column: "+column); 152 break; 153 } 154 } 155 return token; 156 } 157 158 /** 159 * 160 * @param token 161 */ 162 public void removeRequestToken(RequestToken token) { 163 removeRequestToken(token.getToken()); 164 } 165 166 /** 167 * 168 * @param requestToken 169 */ 170 private void removeRequestToken(String requestToken) { 171 if(getJdbcTemplate().update(SQL_REMOVE_REQUEST_TOKEN, new Object[]{requestToken}, SQL_REQUEST_TOKEN_SQL_TYPE) < 1){ 172 LOGGER.debug("No token removed, request token: "+requestToken); 173 } 174 } 175 176 /** 177 * 178 * @param authorizedUser a valid user 179 * @return access token for the given user or null if none was found 180 */ 181 public AccessToken getAccessToken(UserIdentity authorizedUser) { 182 List<Map<String, Object>> rows = getJdbcTemplate().queryForList(SQL_GET_ACCESS_TOKEN, new Object[]{authorizedUser.getUserId()}, SQL_USER_IDENTITY_SQL_TYPE); 183 if(rows.isEmpty()){ 184 LOGGER.debug("No access token found for user, id: "+authorizedUser.getUserId()); 185 return null; 186 } 187 188 Map<String, Object> row = rows.get(0); 189 return new AccessToken((String)row.get(COLUMN_ACCESS_TOKEN), (String) row.get(COLUMN_ACCESS_TOKEN_SECRET)); 190 } 191 192 /** 193 * 194 * @param twitterUserId 195 * @param token 196 * @param userId 197 * @return true on success 198 */ 199 public boolean setAccessToken(final String twitterUserId, final AccessToken token, final UserIdentity userId) { 200 if(!UserIdentity.isValid(userId) || token == null || StringUtils.isBlank(twitterUserId)){ 201 LOGGER.debug("Invalid userId or token."); 202 return false; 203 } 204 205 return getTransactionTemplate().execute(new TransactionCallback<Boolean>() { 206 207 @Override 208 public Boolean doInTransaction(TransactionStatus status) { 209 JdbcTemplate t = getJdbcTemplate(); 210 211 String oldTwitterUserId = getTwitterUserId(userId); 212 if(oldTwitterUserId == null){ 213 LOGGER.debug("Creating a new external account connection for user, id: "+userId.getUserId()+", google user id: "+twitterUserId); 214 _userDAO.insertExternalAccountConnection(new ExternalAccountConnection(twitterUserId, UserServiceType.TWITTER), userId); 215 }else if(!oldTwitterUserId.equals(twitterUserId)){ // do not allow changing the twitter user, this may cause undefined behavior on other parts of the system 216 LOGGER.warn("The given twitter user Id "+twitterUserId+" did not match the old twitter user id: "+oldTwitterUserId); 217 return false; 218 } 219 220 t.update(SQL_SET_ACCESS_TOKEN, new Object[]{userId.getUserId(),token.getToken(),token.getTokenSecret()}, SQL_SET_ACCESS_TOKEN_SQL_TYPES); 221 222 return true; 223 } 224 }); 225 } 226 227 /** 228 * 229 * @param userId non-null, valid userId 230 * @return twitter user id for the given userId, if known by the system, otherwise returns null 231 */ 232 public String getTwitterUserId(UserIdentity userId){ 233 ExternalAccountConnection connection = _userDAO.getExternalAccountConnection(userId, UserServiceType.TWITTER); 234 return (connection == null ? null : connection.getExternalId()); 235 } 236}