diff --git a/chatto/eclipse-formatter.xml b/chatto/eclipse-formatter.xml
index 0f7be6f..d8ab3c9 100644
--- a/chatto/eclipse-formatter.xml
+++ b/chatto/eclipse-formatter.xml
@@ -308,7 +308,7 @@
-
+
diff --git a/chatto/src/main/java/org/ros/chatto/controller/RegistrationController.java b/chatto/src/main/java/org/ros/chatto/controller/RegistrationController.java
index 3f67c07..a971337 100644
--- a/chatto/src/main/java/org/ros/chatto/controller/RegistrationController.java
+++ b/chatto/src/main/java/org/ros/chatto/controller/RegistrationController.java
@@ -66,7 +66,7 @@ public class RegistrationController {
logger.debug("Captcha text from captcha map = {}", captchaMap.get(userRegistrationDTO.getCaptchaID()));
if (userRegistrationDTO.getCaptchaInput().equals(captchaMap.get(userRegistrationDTO.getCaptchaID()))) {
logger.info("Registration captcha equal success");
- userService.registerUser(userRegistrationDTO);
+ userService.createUser(userRegistrationDTO);
return "redirect:registration?success";
} else {
logger.warn("Registration captcha equal fail");
diff --git a/chatto/src/main/java/org/ros/chatto/model/ApplicationStatus.java b/chatto/src/main/java/org/ros/chatto/model/ApplicationStatus.java
deleted file mode 100644
index 4b20eb2..0000000
--- a/chatto/src/main/java/org/ros/chatto/model/ApplicationStatus.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.ros.chatto.model;
-
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.Table;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Entity
-@Getter
-@Setter
-@Table(name="status")
-public class ApplicationStatus {
- @Id
- private int id;
- private String name;
- @Column(name="value")
- private boolean done;
-}
diff --git a/chatto/src/main/java/org/ros/chatto/model/ChatMessage.java b/chatto/src/main/java/org/ros/chatto/model/ChatMessage.java
index 42b7741..d57ffc6 100644
--- a/chatto/src/main/java/org/ros/chatto/model/ChatMessage.java
+++ b/chatto/src/main/java/org/ros/chatto/model/ChatMessage.java
@@ -9,6 +9,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
@@ -24,15 +25,19 @@ public class ChatMessage {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "m_id")
private Long messageID;
- @OneToOne(fetch = FetchType.LAZY)
+
+ @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "from_user")
private ChatUser fromUser;
- @OneToOne(fetch = FetchType.LAZY)
+
+ @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "to_user")
private ChatUser toUser;
+
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "message")
private MessageCipher messageCipher;
+
@Temporal(TemporalType.TIMESTAMP)
private Date messageTime;
}
diff --git a/chatto/src/main/java/org/ros/chatto/model/ChatUser.java b/chatto/src/main/java/org/ros/chatto/model/ChatUser.java
index 2d9ee31..28ce85a 100644
--- a/chatto/src/main/java/org/ros/chatto/model/ChatUser.java
+++ b/chatto/src/main/java/org/ros/chatto/model/ChatUser.java
@@ -1,6 +1,7 @@
package org.ros.chatto.model;
import java.util.Date;
+import java.util.List;
import java.util.Set;
import javax.persistence.CascadeType;
@@ -22,7 +23,9 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
+import lombok.ToString;
@Entity
@Table(name = "users")
@@ -35,18 +38,26 @@ import lombok.NoArgsConstructor;
public class ChatUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
-// @SequenceGenerator(name="user_generator", sequenceName = "user_seq", allocationSize=50) //mysql does not support sequence id generator
@Column(name = "user_id")
private int userID;
+
@Column(name = "name")
private String userName;
+
String password;
@Temporal(TemporalType.TIMESTAMP)
private Date joinDate;
-// @ManyToMany(cascade = CascadeType.ALL)
-// @JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
- @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
+
+ @ToString.Exclude
+ @EqualsAndHashCode.Exclude
+ @OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH,
+ CascadeType.REFRESH })
@JsonBackReference
-// private Set userRoles = new HashSet();
private Set userRoles;
+
+ // @ToString.Exclude
+ // @EqualsAndHashCode.Exclude
+ // @OneToMany(mappedBy = "fromUser", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH,
+ // CascadeType.REFRESH })
+ // private List chatMessages;
}
diff --git a/chatto/src/main/java/org/ros/chatto/model/Role.java b/chatto/src/main/java/org/ros/chatto/model/Role.java
index d18e285..52ac2be 100644
--- a/chatto/src/main/java/org/ros/chatto/model/Role.java
+++ b/chatto/src/main/java/org/ros/chatto/model/Role.java
@@ -13,6 +13,8 @@ import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
@Entity
@@ -22,10 +24,16 @@ public class Role {
@Id
@Column(name = "role_id")
private int roleID;
+
@Column(name = "role_name")
private String name;
+
private String description;
- @OneToMany(mappedBy = "role", cascade = CascadeType.ALL)
+
+ @OneToMany(mappedBy = "role", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH,
+ CascadeType.REFRESH })
@JsonBackReference
+ @ToString.Exclude
+ @EqualsAndHashCode.Exclude
private Set userRoles = new HashSet<>();
}
diff --git a/chatto/src/main/java/org/ros/chatto/model/UserSession.java b/chatto/src/main/java/org/ros/chatto/model/UserSession.java
index 7ee073b..ccb4a9a 100644
--- a/chatto/src/main/java/org/ros/chatto/model/UserSession.java
+++ b/chatto/src/main/java/org/ros/chatto/model/UserSession.java
@@ -2,7 +2,6 @@ package org.ros.chatto.model;
import java.time.Instant;
-import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
@@ -22,7 +21,7 @@ public class UserSession {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
- @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private ChatUser user;
diff --git a/chatto/src/main/java/org/ros/chatto/model/UserToken.java b/chatto/src/main/java/org/ros/chatto/model/UserToken.java
index d307cc1..072df85 100644
--- a/chatto/src/main/java/org/ros/chatto/model/UserToken.java
+++ b/chatto/src/main/java/org/ros/chatto/model/UserToken.java
@@ -23,7 +23,7 @@ public class UserToken implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "token_id")
- private long tokenID;
+ private int tokenID;
private String userName;
private String tokenContent;
private String role;
diff --git a/chatto/src/main/java/org/ros/chatto/repository/UserRepository.java b/chatto/src/main/java/org/ros/chatto/repository/UserRepository.java
index 9105ae2..742c635 100644
--- a/chatto/src/main/java/org/ros/chatto/repository/UserRepository.java
+++ b/chatto/src/main/java/org/ros/chatto/repository/UserRepository.java
@@ -11,6 +11,9 @@ import org.springframework.stereotype.Repository;
public interface UserRepository extends JpaRepository{
@Query("select cu from ChatUser cu where cu.userName = ?1")
public ChatUser findByUserName(String userName);
+
+ @Query("select cu from ChatUser cu join fetch cu.userRoles where cu.userName = ?1")
+ public ChatUser findByUserNameWithRole(String userName);
@Query("select cu.userName from ChatUser cu where cu.userName != ?1")
public List findAllOtherUserNames(String userName);
diff --git a/chatto/src/main/java/org/ros/chatto/security/MyUserDetailsService.java b/chatto/src/main/java/org/ros/chatto/security/MyUserDetailsService.java
index 0310cea..5be621c 100644
--- a/chatto/src/main/java/org/ros/chatto/security/MyUserDetailsService.java
+++ b/chatto/src/main/java/org/ros/chatto/security/MyUserDetailsService.java
@@ -1,6 +1,6 @@
package org.ros.chatto.security;
-import java.util.List;
+import java.util.Set;
import org.ros.chatto.model.ChatUser;
import org.ros.chatto.model.UserRole;
@@ -18,23 +18,26 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
-
@Autowired
private UserService userService;
@Override
- public UserDetails loadUserByUsername(String username) {
+ public UserDetails loadUserByUsername(final String username) {
log.trace("User Details - loading with username: {}", username);
- List userRoles = userService.getUserWithRole(username);
- if (userRoles.size() == 0) {
+ ChatUser user = userService.getUserWithRole(username);
+
+ if (user == null) {
log.warn("Request for unknown user {}", username);
throw new UsernameNotFoundException(username);
}
- ChatUser user = userRoles.get(0).getUser();
- return User.withUsername(user.getUserName()).password(user.getPassword())
- .roles(userRoles.stream().map(userRole -> {
- log.trace("role = " + userRole.getRole().getName());
- return userRole.getRole().getName();
- }).toArray(size -> new String[size])).build();
+
+ Set userRoles = user.getUserRoles();
+
+ return User.withUsername(user.getUserName())
+ .password(user.getPassword())
+ .roles(userRoles.stream()
+ .map(ur -> ur.getRole().getName())
+ .toArray(size -> new String[size]))
+ .build();
}
-}
\ No newline at end of file
+}
diff --git a/chatto/src/main/java/org/ros/chatto/service/ChatServiceImpl.java b/chatto/src/main/java/org/ros/chatto/service/ChatServiceImpl.java
index 78dfe29..f2a5b9c 100644
--- a/chatto/src/main/java/org/ros/chatto/service/ChatServiceImpl.java
+++ b/chatto/src/main/java/org/ros/chatto/service/ChatServiceImpl.java
@@ -29,10 +29,11 @@ public class ChatServiceImpl implements ChatService {
private final ChatMessageRepository chatMessageRepository;
private final MyConversionService myConversionService;
- public ChatMessageDTO createMessage(final String fromUserName, final String toUserName,
- final MessageCipherDTO messageCipherDTO) {
+ public ChatMessageDTO createMessage(final String fromUserName,
+ final String toUserName, final MessageCipherDTO messageCipherDTO) {
- MessageCipher messageCipher = myConversionService.convertToMessageCipher(messageCipherDTO);
+ MessageCipher messageCipher = myConversionService
+ .convertToMessageCipher(messageCipherDTO);
final ChatUser fromUser = userRepository.findByUserName(fromUserName);
final ChatUser toUser = userRepository.findByUserName(toUserName);
@@ -49,51 +50,53 @@ public class ChatServiceImpl implements ChatService {
@Override
@Transactional(readOnly = true)
- public List getAllMessages(final String fromUser, final String toUser) {
- final List chatMessages = chatMessageRepository.getAllMessages(fromUser, toUser);
- final List chatMessageDTOs = myConversionService.convertToChatMessageDTOs(chatMessages);
+ public List getAllMessages(final String fromUser,
+ final String toUser) {
+ final List chatMessages = chatMessageRepository
+ .getAllMessages(fromUser, toUser);
+ final List chatMessageDTOs = myConversionService
+ .convertToChatMessageDTOs(chatMessages);
return chatMessageDTOs;
}
@Override
@Transactional(readOnly = true)
- public List getAllMessagesForReencryption(final String fromUser, final String toUser) {
- return myConversionService.convertToReencryptionDTOs(chatMessageRepository.getAllMessages(fromUser, toUser));
+ public List getAllMessagesForReencryption(
+ final String fromUser, final String toUser) {
+ return myConversionService.convertToReencryptionDTOs(
+ chatMessageRepository.getAllMessages(fromUser, toUser));
}
@Override
@Transactional(readOnly = true)
- public List getMessagePage(final String fromUser, final String toUser, final int page,
- final int size) {
- // Sort sort = Sort
- // Page chatMessages =
- // chatMessageRepository.getAllMessages(fromUser, toUser,PageRequest.of(page,
- // size));
- // List chatMessageDTOs =
- // myConversionService.convertToChatMessageDTOs(chatMessages);
- // return chatMessageDTOs;
+ public List getMessagePage(final String fromUser,
+ final String toUser, final int page, final int size) {
return myConversionService.convertToChatMessageDTOs(
- chatMessageRepository.getAllMessages(fromUser, toUser, PageRequest.of(page, size)));
+ chatMessageRepository.getAllMessages(fromUser, toUser,
+ PageRequest.of(page, size)));
}
@Override
@Transactional(readOnly = true)
- public List getNewMessages(final String fromUser, final String toUser, final Date lastMessageTime) {
- final List chatMessages = chatMessageRepository.getNewMessages(fromUser, toUser, lastMessageTime);
- // List chatMessageDTOs
+ public List getNewMessages(final String fromUser,
+ final String toUser, final Date lastMessageTime) {
+ final List chatMessages = chatMessageRepository
+ .getNewMessages(fromUser, toUser, lastMessageTime);
return myConversionService.convertToChatMessageDTOs(chatMessages);
}
@Override
- public void reencryptMessages(final List reencryptionDTOs) {
+ public void reencryptMessages(
+ final List reencryptionDTOs) {
final List messageCiphers = reencryptionDTOs.stream()
- .map(reencryptionDTO -> reencryptionDTO.getMessageCipher()).collect(Collectors.toList());
+ .map(reencryptionDTO -> reencryptionDTO.getMessageCipher())
+ .collect(Collectors.toList());
messageCipherRepository.saveAll(messageCiphers);
}
@Override
- public List getAllMessages(final String name, final String userName,
- final PageRequest pageRequest) {
+ public List getAllMessages(final String name,
+ final String userName, final PageRequest pageRequest) {
// TODO Auto-generated method stub
return null;
}
diff --git a/chatto/src/main/java/org/ros/chatto/service/UserService.java b/chatto/src/main/java/org/ros/chatto/service/UserService.java
index 03252fe..5d7941c 100644
--- a/chatto/src/main/java/org/ros/chatto/service/UserService.java
+++ b/chatto/src/main/java/org/ros/chatto/service/UserService.java
@@ -1,22 +1,32 @@
package org.ros.chatto.service;
import java.util.List;
+import java.util.Set;
import org.ros.chatto.dto.ActiveUserDTO;
import org.ros.chatto.dto.UserRegistrationDTO;
import org.ros.chatto.model.ChatUser;
-import org.ros.chatto.model.UserRole;
+import org.ros.chatto.model.Role;
import org.ros.chatto.model.UserSession;
import org.springframework.stereotype.Service;
@Service
public interface UserService {
public List findAllOtherUsers(String userName);
- public UserRole registerUser(UserRegistrationDTO userRegistrationDTO);
+
+ public ChatUser createUser(UserRegistrationDTO userRegistrationDTO);
+
public List getAllRegularUsers();
- public ChatUser findByUserName(String userName);
+
+ public ChatUser getUser(String userName);
+
+ public Set getRoles(ChatUser user);
+
public List getOtherActiveUsers(String userName);
- public List getUserWithRole(String userName);
+
+ public ChatUser getUserWithRole(String userName);
+
public UserSession incrementUserSession(String userName);
+
public UserSession decrementUserSession(String userName);
}
diff --git a/chatto/src/main/java/org/ros/chatto/service/UserServiceImpl.java b/chatto/src/main/java/org/ros/chatto/service/UserServiceImpl.java
index cf50b94..bba4ffa 100644
--- a/chatto/src/main/java/org/ros/chatto/service/UserServiceImpl.java
+++ b/chatto/src/main/java/org/ros/chatto/service/UserServiceImpl.java
@@ -6,6 +6,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.ros.chatto.dto.ActiveUserDTO;
import org.ros.chatto.dto.UserRegistrationDTO;
@@ -44,7 +46,7 @@ public class UserServiceImpl implements UserService {
private final UserTokenService userTokenService;
@Override
- public UserRole registerUser(final UserRegistrationDTO userRegistrationDTO) {
+ public ChatUser createUser(final UserRegistrationDTO userRegistrationDTO) {
final ChatUser user = new ChatUser();
user.setUserName(userRegistrationDTO.getUserName());
user.setPassword(passwordEncoder.encode(userRegistrationDTO.getPassword()));
@@ -53,8 +55,7 @@ public class UserServiceImpl implements UserService {
final Role role = roleRepository.findByName("USER");
userRole.setRole(role);
userRole.setUser(changedUser);
- userRoleRepository.save(userRole);
- return userRole;
+ return userRoleRepository.save(userRole).getUser();
}
@Override
@@ -97,7 +98,7 @@ public class UserServiceImpl implements UserService {
@Transactional(readOnly = true)
@Override
- public ChatUser findByUserName(final String userName) {
+ public ChatUser getUser(final String userName) {
return userRepository.findByUserName(userName);
}
@@ -148,13 +149,14 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(readOnly = true)
- public List getUserWithRole(final String userName) {
- return userRoleRepository.findByUser(userName);
+ public ChatUser getUserWithRole(final String userName) {
+ return userRepository.findByUserNameWithRole(userName);
}
@Override
public UserSession incrementUserSession(String userName) {
- ChatUser chatUser = findByUserName(userName);
+ ChatUser chatUser = getUser(userName);
+ Instant instant = Instant.now();
UserSession userSession = userSessionRepository.findByUserName(userName);
@@ -163,6 +165,7 @@ public class UserServiceImpl implements UserService {
}
userSession.setUser(chatUser);
+ userSession.setTimeStamp(instant);
userSession.setOnline(true);
userSession.setNumSessions(userSession.getNumSessions() + 1);
return userSessionRepository.save(userSession);
@@ -171,6 +174,7 @@ public class UserServiceImpl implements UserService {
@Override
public UserSession decrementUserSession(String userName) {
UserSession userSession = userSessionRepository.findByUserName(userName);
+ Instant instant = Instant.now();
if (userSession == null) {
log.error("User session is somehow null for user: " + userName);
@@ -190,6 +194,12 @@ public class UserServiceImpl implements UserService {
}
userSession.setNumSessions(numSessions);
+ userSession.setTimeStamp(instant);
return userSessionRepository.save(userSession);
}
+
+ @Override
+ public Set getRoles(ChatUser user) {
+ return user.getUserRoles().stream().map(ur -> ur.getRole()).collect(Collectors.toSet());
+ }
}
diff --git a/chatto/src/main/resources/application.properties b/chatto/src/main/resources/application.properties
index c7282ff..5ba5739 100644
--- a/chatto/src/main/resources/application.properties
+++ b/chatto/src/main/resources/application.properties
@@ -11,7 +11,7 @@ spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDial
# Hibernate ddl auto (create, create-drop, validate, update)
-spring.jpa.hibernate.ddl-auto = none
+spring.jpa.hibernate.ddl-auto = validate
spring.jpa.open-in-view=false
logging.level.org.hibernate.stat=debug
diff --git a/chatto/src/main/resources/db/migration/V1__ddl.sql b/chatto/src/main/resources/db/migration/V1__ddl.sql
index cbc5ced..911de22 100644
--- a/chatto/src/main/resources/db/migration/V1__ddl.sql
+++ b/chatto/src/main/resources/db/migration/V1__ddl.sql
@@ -47,7 +47,7 @@ CREATE TABLE IF NOT EXISTS `users_roles` (
KEY `user` (`user_id`),
KEY `role` (`role_id`),
CONSTRAINT `FOREIGN KEY USER IN USERS-ROLES TABLE` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- CONSTRAINT `fk_roles_roleAssignments` FOREIGN KEY (`role_id`) REFERENCES `roles` (`role_id`) ON DELETE CASCADE ON UPDATE CASCADE
+ CONSTRAINT `fk_roles_roleAssignments` FOREIGN KEY (`role_id`) REFERENCES `roles` (`role_id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `chat_messages` (
@@ -64,17 +64,10 @@ CREATE TABLE IF NOT EXISTS `chat_messages` (
KEY `FOREIGN KEY TO USER IN MESSAGES TABLE` (`to_user`),
CONSTRAINT `FOREIGN KEY ENC MESSAGE TABLE` FOREIGN KEY (`message`) REFERENCES `message_ciphers` (`id`) ON UPDATE CASCADE,
CONSTRAINT `FOREIGN KEY FROM USER IN MESSAGES TABLE` FOREIGN KEY (`from_user`) REFERENCES `users` (`user_id`) ON UPDATE CASCADE,
- CONSTRAINT `FOREIGN KEY TO USER IN MESSAGES TABLE` FOREIGN KEY (`to_user`) REFERENCES `users` (`user_id`) ON UPDATE CASCADE
+ CONSTRAINT `FOREIGN KEY TO USER IN MESSAGES TABLE` FOREIGN KEY (`to_user`) REFERENCES `users` (`user_id`) ON UPDATE CASCADE,
+ UNIQUE KEY `UNIQUE MESSAGE CIPHER` (`message`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-CREATE TABLE IF NOT EXISTS `status` (
- `id` int(2) NOT NULL AUTO_INCREMENT,
- `name` varchar(15) NOT NULL,
- `value` tinyint(1) NOT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `name` (`name`)
-) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-
CREATE TABLE IF NOT EXISTS `user_sessions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(15) NOT NULL,
@@ -83,7 +76,8 @@ CREATE TABLE IF NOT EXISTS `user_sessions` (
`time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_name` (`user_id`),
- CONSTRAINT `FOREIGN KEY USER ID` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+ CONSTRAINT `FOREIGN KEY USER ID` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ UNIQUE KEY `UNIQUE USER CONSTRAINT` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `tokens` (