/*
* Symphony - A modern community (forum/SNS/blog) platform written in Java.
* Copyright (C) 2012-2017, b3log.org & hacpai.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.b3log.symphony.service;
import org.apache.commons.lang.StringUtils;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.ioc.inject.Inject;
import org.b3log.latke.logging.Level;
import org.b3log.latke.logging.Logger;
import org.b3log.latke.model.Pagination;
import org.b3log.latke.model.User;
import org.b3log.latke.repository.*;
import org.b3log.latke.service.ServiceException;
import org.b3log.latke.service.annotation.Service;
import org.b3log.latke.util.CollectionUtils;
import org.b3log.latke.util.Paginator;
import org.b3log.symphony.model.*;
import org.b3log.symphony.repository.FollowRepository;
import org.b3log.symphony.repository.PointtransferRepository;
import org.b3log.symphony.repository.UserRepository;
import org.b3log.symphony.util.Sessions;
import org.b3log.symphony.util.Times;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* User query service.
*
* @author <a href="http://88250.b3log.org">Liang Ding</a>
* @author <a href="http://zephyr.b3log.org">Zephyr</a>
* @version 1.8.6.13, Apr 23, 2017
* @since 0.2.0
*/
@Service
public class UserQueryService {
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(UserQueryService.class);
/**
* All usernames.
*/
public static final List<JSONObject> USER_NAMES = Collections.synchronizedList(new ArrayList<JSONObject>());
/**
* User repository.
*/
@Inject
private UserRepository userRepository;
/**
* Follow repository.
*/
@Inject
private FollowRepository followRepository;
/**
* Avatar query service.
*/
@Inject
private AvatarQueryService avatarQueryService;
/**
* Pointtransfer repository.
*/
@Inject
private PointtransferRepository pointtransferRepository;
/**
* Role query service.
*/
@Inject
private RoleQueryService roleQueryService;
/**
* Get nice users with the specified fetch size.
*
* @param fetchSize the specified fetch size
* @return a list of users
*/
public List<JSONObject> getNiceUsers(int fetchSize) {
final List<JSONObject> ret = new ArrayList<>();
final int RANGE_SIZE = 64;
try {
final Query userQuery = new Query();
userQuery.setCurrentPageNum(1).setPageCount(1).setPageSize(RANGE_SIZE).
setFilter(new PropertyFilter(UserExt.USER_STATUS, FilterOperator.EQUAL, UserExt.USER_STATUS_C_VALID)).
addSort(UserExt.USER_ARTICLE_COUNT, SortDirection.DESCENDING).
addSort(UserExt.USER_COMMENT_COUNT, SortDirection.DESCENDING);
final JSONArray rangeUsers = userRepository.get(userQuery).optJSONArray(Keys.RESULTS);
final int realLen = rangeUsers.length();
if (realLen < fetchSize) {
fetchSize = realLen;
}
final List<Integer> indices = CollectionUtils.getRandomIntegers(0, realLen, fetchSize);
for (final Integer index : indices) {
ret.add(rangeUsers.getJSONObject(index));
}
for (final JSONObject selectedUser : ret) {
avatarQueryService.fillUserAvatarURL(UserExt.USER_AVATAR_VIEW_MODE_C_STATIC, selectedUser);
}
} catch (final Exception e) {
LOGGER.log(Level.ERROR, "Get nice users failed", e);
}
return ret;
}
/**
* Gets invite user count of a user specified by the given user id.
*
* @param userId the given user id
* @return invited user count
*/
public int getInvitedUserCount(final String userId) {
final Query query = new Query().setFilter(CompositeFilterOperator.and(
new PropertyFilter(Pointtransfer.TO_ID, FilterOperator.EQUAL, userId),
CompositeFilterOperator.or(
new PropertyFilter(Pointtransfer.TYPE, FilterOperator.EQUAL,
Pointtransfer.TRANSFER_TYPE_C_INVITECODE_USED),
new PropertyFilter(Pointtransfer.TYPE, FilterOperator.EQUAL,
Pointtransfer.TRANSFER_TYPE_C_INVITE_REGISTER))
));
try {
return (int) pointtransferRepository.count(query);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets invited user count failed", e);
return 0;
}
}
/**
* Gets latest logged in users by the specified time.
*
* @param time the specified start time
* @param currentPageNum the specified current page number
* @param pageSize the specified page size
* @param windowSize the specified window size
* @return for example, <pre>
* {
* "pagination": {
* "paginationPageCount": 100,
* "paginationPageNums": [1, 2, 3, 4, 5]
* },
* "users": [{
* "oId": "",
* "userName": "",
* "userEmail": "",
* "userPassword": "",
* "roleName": "",
* ....
* }, ....]
* }
* </pre>
* @throws ServiceException service exception
* @see Pagination
*/
public JSONObject getLatestLoggedInUsers(final long time, final int currentPageNum, final int pageSize,
final int windowSize) throws ServiceException {
final JSONObject ret = new JSONObject();
final Query query = new Query().addSort(Keys.OBJECT_ID, SortDirection.DESCENDING)
.setCurrentPageNum(currentPageNum).setPageSize(pageSize)
.setFilter(CompositeFilterOperator.and(
new PropertyFilter(UserExt.USER_STATUS, FilterOperator.EQUAL, UserExt.USER_STATUS_C_VALID),
new PropertyFilter(UserExt.USER_LATEST_LOGIN_TIME, FilterOperator.GREATER_THAN_OR_EQUAL, time)
));
JSONObject result = null;
try {
result = userRepository.get(query);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets latest logged in user failed", e);
throw new ServiceException(e);
}
final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);
final JSONObject pagination = new JSONObject();
ret.put(Pagination.PAGINATION, pagination);
final List<Integer> pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize);
pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
final JSONArray users = result.optJSONArray(Keys.RESULTS);
ret.put(User.USERS, users);
return ret;
}
/**
* Gets user count of the specified day.
*
* @param day the specified day
* @return user count
*/
public int getUserCntInDay(final Date day) {
final long time = day.getTime();
final long start = Times.getDayStartTime(time);
final long end = Times.getDayEndTime(time);
final Query query = new Query().setFilter(CompositeFilterOperator.and(
new PropertyFilter(Keys.OBJECT_ID, FilterOperator.GREATER_THAN_OR_EQUAL, start),
new PropertyFilter(Keys.OBJECT_ID, FilterOperator.LESS_THAN, end),
new PropertyFilter(UserExt.USER_STATUS, FilterOperator.EQUAL, UserExt.USER_STATUS_C_VALID)
));
try {
return (int) userRepository.count(query);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Count day user failed", e);
return 1;
}
}
/**
* Gets user count of the specified month.
*
* @param month the specified month
* @return user count
*/
public int getUserCntInMonth(final Date month) {
final long time = month.getTime();
final long start = Times.getMonthStartTime(time);
final long end = Times.getMonthEndTime(time);
final Query query = new Query().setFilter(CompositeFilterOperator.and(
new PropertyFilter(Keys.OBJECT_ID, FilterOperator.GREATER_THAN_OR_EQUAL, start),
new PropertyFilter(Keys.OBJECT_ID, FilterOperator.LESS_THAN, end),
new PropertyFilter(UserExt.USER_STATUS, FilterOperator.EQUAL, UserExt.USER_STATUS_C_VALID)
));
try {
return (int) userRepository.count(query);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Count month user failed", e);
return 1;
}
}
/**
* Loads all usernames from database.
*/
public void loadUserNames() {
USER_NAMES.clear();
final Query query = new Query().setPageCount(1);
query.setFilter(new PropertyFilter(User.USER_NAME, FilterOperator.NOT_EQUAL, UserExt.NULL_USER_NAME));
query.addProjection(User.USER_NAME, String.class);
query.addProjection(UserExt.USER_AVATAR_URL, String.class);
try {
final JSONObject result = userRepository.get(query); // XXX: Performance Issue
final JSONArray array = result.optJSONArray(Keys.RESULTS);
for (int i = 0; i < array.length(); i++) {
final JSONObject user = array.optJSONObject(i);
final JSONObject u = new JSONObject();
u.put(User.USER_NAME, user.optString(User.USER_NAME));
u.put(UserExt.USER_T_NAME_LOWER_CASE, user.optString(User.USER_NAME).toLowerCase());
final String avatar = avatarQueryService.getAvatarURLByUser(UserExt.USER_AVATAR_VIEW_MODE_C_STATIC,
user, "20");
u.put(UserExt.USER_AVATAR_URL, avatar);
USER_NAMES.add(u);
}
Collections.sort(USER_NAMES, (u1, u2) -> {
final String u1Name = u1.optString(UserExt.USER_T_NAME_LOWER_CASE);
final String u2Name = u2.optString(UserExt.USER_T_NAME_LOWER_CASE);
return u1Name.compareTo(u2Name);
});
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Loads usernames error", e);
}
}
/**
* Gets usernames by the specified name prefix.
*
* @param namePrefix the specified name prefix
* @return a list of usernames, for example <pre>
* [
* {
* "userName": "",
* "userAvatarURL": "",
* }, ....
* ]
* </pre>
*/
public List<JSONObject> getUserNamesByPrefix(final String namePrefix) {
final JSONObject nameToSearch = new JSONObject();
nameToSearch.put(UserExt.USER_T_NAME_LOWER_CASE, namePrefix.toLowerCase());
int index = Collections.binarySearch(USER_NAMES, nameToSearch, (u1, u2) -> {
String u1Name = u1.optString(UserExt.USER_T_NAME_LOWER_CASE);
final String inputName = u2.optString(UserExt.USER_T_NAME_LOWER_CASE);
if (u1Name.length() < inputName.length()) {
return u1Name.compareTo(inputName);
}
u1Name = u1Name.substring(0, inputName.length());
return u1Name.compareTo(inputName);
});
final List<JSONObject> ret = new ArrayList<>();
if (index < 0) {
return ret;
}
int start = index;
int end = index;
while (start > -1 && USER_NAMES.get(start).optString(UserExt.USER_T_NAME_LOWER_CASE).startsWith(namePrefix.toLowerCase())) {
start--;
}
start++;
if (start < index - 5) {
end = start + 5;
} else {
while (end < USER_NAMES.size() && end < index + 5 && USER_NAMES.get(end).optString(UserExt.USER_T_NAME_LOWER_CASE).startsWith(namePrefix.toLowerCase())) {
end++;
if (end >= start + 5) {
break;
}
}
}
return USER_NAMES.subList(start, end);
}
/**
* Gets the current user.
*
* @param request the specified request
* @return the current user, {@code null} if not found
* @throws ServiceException service exception
*/
public JSONObject getCurrentUser(final HttpServletRequest request) throws ServiceException {
final JSONObject currentUser = Sessions.currentUser(request);
if (null == currentUser) {
return null;
}
final String id = currentUser.optString(Keys.OBJECT_ID);
return getUser(id);
}
/**
* Gets the administrators.
*
* @return administrators, returns an empty list if not found or error
* @throws ServiceException service exception
*/
public List<JSONObject> getAdmins() throws ServiceException {
try {
return userRepository.getAdmins();
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets admins failed", e);
throw new ServiceException(e);
}
}
/**
* Gets the super administrator.
*
* @return super administrator
* @throws ServiceException service exception
*/
public JSONObject getSA() throws ServiceException {
return getAdmins().get(0);
}
/**
* Gets the default commenter.
*
* @return default commenter
* @throws ServiceException service exception
*/
public JSONObject getDefaultCommenter() throws ServiceException {
final JSONObject ret = getUserByName(UserExt.DEFAULT_CMTER_NAME);
ret.remove(UserExt.USER_T_POINT_HEX);
ret.remove(UserExt.USER_T_POINT_CC);
return ret;
}
/**
* Gets a user by the specified email.
*
* @param email the specified email
* @return user, returns {@code null} if not found
* @throws ServiceException service exception
*/
public JSONObject getUserByEmail(final String email) throws ServiceException {
try {
return userRepository.getByEmail(email);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets user by email[" + email + "] failed", e);
throw new ServiceException(e);
}
}
/**
* Gets user names from the specified text.
* <p>
* A user name is between @ and a punctuation, a blank or a line break (\n). For example, the specified text is
* <pre>@88250 It is a nice day. @Vanessa, we are on the way.</pre> There are two user names in the text,
* 88250 and Vanessa.
* </p>
*
* @param text the specified text
* @return user names, returns an empty set if not found
*/
public Set<String> getUserNames(final String text) {
final Set<String> ret = new HashSet<>();
int idx = text.indexOf('@');
if (-1 == idx) {
return ret;
}
String copy = text.trim();
copy = copy.replaceAll("\\n", " ");
//(?=\\pP)匹配标点符号-http://www.cnblogs.com/qixuejia/p/4211428.html
copy = copy.replaceAll("(?=\\pP)[^@]", " ");
String[] uNames = StringUtils.substringsBetween(copy, "@", " ");
String tail = StringUtils.substringAfterLast(copy, "@");
if (tail.contains(" ")) {
tail = null;
}
if (null != tail) {
if (null == uNames) {
uNames = new String[1];
uNames[0] = tail;
} else {
uNames = Arrays.copyOf(uNames, uNames.length + 1);
uNames[uNames.length - 1] = tail;
}
}
String[] uNames2 = StringUtils.substringsBetween(copy, "@", "<");
final Set<String> maybeUserNameSet;
if (null == uNames) {
uNames = uNames2;
if (null == uNames) {
return ret;
}
maybeUserNameSet = CollectionUtils.arrayToSet(uNames);
} else {
maybeUserNameSet = CollectionUtils.arrayToSet(uNames);
if (null != uNames2) {
maybeUserNameSet.addAll(CollectionUtils.arrayToSet(uNames2));
}
}
for (String maybeUserName : maybeUserNameSet) {
maybeUserName = maybeUserName.trim();
if (null != getUserByName(maybeUserName)) { // Found a user
ret.add(maybeUserName);
}
}
return ret;
}
/**
* Gets a user by the specified name.
*
* @param name the specified name
* @return user, returns {@code null} if not found
*/
public JSONObject getUserByName(final String name) {
try {
final JSONObject ret = userRepository.getByName(name);
if (null == ret) {
return null;
}
final int point = ret.optInt(UserExt.USER_POINT);
final int appRole = ret.optInt(UserExt.USER_APP_ROLE);
if (UserExt.USER_APP_ROLE_C_HACKER == appRole) {
ret.put(UserExt.USER_T_POINT_HEX, Integer.toHexString(point));
} else {
ret.put(UserExt.USER_T_POINT_CC, UserExt.toCCString(point));
}
return ret;
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets user by name[" + name + "] failed", e);
return null;
}
}
/**
* Gets users by the specified request json object.
*
* @param requestJSONObject the specified request json object, for example,
* "userNameOrEmail": "", // optional
* "paginationCurrentPageNum": 1,
* "paginationPageSize": 20,
* "paginationWindowSize": 10,
* , see {@link Pagination} for more details
* @return for example, <pre>
* {
* "pagination": {
* "paginationPageCount": 100,
* "paginationPageNums": [1, 2, 3, 4, 5]
* },
* "users": [{
* "oId": "",
* "userName": "",
* "userEmail": "",
* "userPassword": "",
* "roleName": "",
* ....
* }, ....]
* }
* </pre>
* @throws ServiceException service exception
* @see Pagination
*/
public JSONObject getUsers(final JSONObject requestJSONObject) throws ServiceException {
final JSONObject ret = new JSONObject();
final int currentPageNum = requestJSONObject.optInt(Pagination.PAGINATION_CURRENT_PAGE_NUM);
final int pageSize = requestJSONObject.optInt(Pagination.PAGINATION_PAGE_SIZE);
final int windowSize = requestJSONObject.optInt(Pagination.PAGINATION_WINDOW_SIZE);
final Query query = new Query().addSort(Keys.OBJECT_ID, SortDirection.DESCENDING).
setCurrentPageNum(currentPageNum).setPageSize(pageSize);
if (requestJSONObject.has(Common.USER_NAME_OR_EMAIL)) {
final String nameOrEmail = requestJSONObject.optString(Common.USER_NAME_OR_EMAIL);
final List<Filter> filters = new ArrayList<>();
filters.add(new PropertyFilter(User.USER_NAME, FilterOperator.EQUAL, nameOrEmail));
filters.add(new PropertyFilter(User.USER_EMAIL, FilterOperator.EQUAL, nameOrEmail));
query.setFilter(new CompositeFilter(CompositeFilterOperator.OR, filters));
}
JSONObject result = null;
try {
result = userRepository.get(query);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets users failed", e);
throw new ServiceException(e);
}
final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);
final JSONObject pagination = new JSONObject();
ret.put(Pagination.PAGINATION, pagination);
final List<Integer> pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize);
pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
final JSONArray users = result.optJSONArray(Keys.RESULTS);
ret.put(User.USERS, users);
for (int i = 0; i < users.length(); i++) {
final JSONObject user = users.optJSONObject(i);
user.put(UserExt.USER_T_CREATE_TIME, new Date(user.optLong(Keys.OBJECT_ID)));
avatarQueryService.fillUserAvatarURL(UserExt.USER_AVATAR_VIEW_MODE_C_ORIGINAL, user);
final JSONObject role = roleQueryService.getRole(user.optString(User.USER_ROLE));
user.put(Role.ROLE_NAME, role.optString(Role.ROLE_NAME));
}
return ret;
}
/**
* Gets users by the specified request json object.
*
* @param requestJSONObject the specified request json object, for example,
* "userCity": "",
* "userLatestLoginTime": long, // optional, default to 0
* "paginationCurrentPageNum": 1,
* "paginationPageSize": 20,
* "paginationWindowSize": 10,
* }, see {@link Pagination} for more details
* @return for example, <pre>
* {
* "pagination": {
* "paginationPageCount": 100,
* "paginationPageNums": [1, 2, 3, 4, 5]
* },
* "users": [{
* "oId": "",
* "userName": "",
* "userEmail": "",
* "userPassword": "",
* "roleName": "",
* ....
* }, ....]
* }
* </pre>
* @throws ServiceException service exception
* @see Pagination
*/
public JSONObject getUsersByCity(final JSONObject requestJSONObject) throws ServiceException {
final JSONObject ret = new JSONObject();
final int currentPageNum = requestJSONObject.optInt(Pagination.PAGINATION_CURRENT_PAGE_NUM);
final int pageSize = requestJSONObject.optInt(Pagination.PAGINATION_PAGE_SIZE);
final int windowSize = requestJSONObject.optInt(Pagination.PAGINATION_WINDOW_SIZE);
final String city = requestJSONObject.optString(UserExt.USER_CITY);
final long latestTime = requestJSONObject.optLong(UserExt.USER_LATEST_LOGIN_TIME);
final Query query = new Query().addSort(UserExt.USER_LATEST_LOGIN_TIME, SortDirection.DESCENDING)
.setCurrentPageNum(currentPageNum).setPageSize(pageSize)
.setFilter(CompositeFilterOperator.and(
new PropertyFilter(UserExt.USER_CITY, FilterOperator.EQUAL, city),
new PropertyFilter(UserExt.USER_STATUS, FilterOperator.EQUAL, UserExt.USER_STATUS_C_VALID),
new PropertyFilter(UserExt.USER_LATEST_LOGIN_TIME, FilterOperator.GREATER_THAN_OR_EQUAL, latestTime)
));
JSONObject result = null;
try {
result = userRepository.get(query);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets users by city error", e);
throw new ServiceException(e);
}
final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);
final JSONObject pagination = new JSONObject();
ret.put(Pagination.PAGINATION, pagination);
final List<Integer> pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize);
pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
final JSONArray users = result.optJSONArray(Keys.RESULTS);
try {
for (int i = 0; i < users.length(); i++) {
JSONObject user = users.getJSONObject(i);
users.getJSONObject(i).put(Common.IS_FOLLOWING,
followRepository.exists(requestJSONObject.optString(Keys.OBJECT_ID), user.optString(Keys.OBJECT_ID),
Follow.FOLLOWING_TYPE_C_USER));
}
} catch (final RepositoryException | JSONException e) {
LOGGER.log(Level.ERROR, "Fills following failed", e);
}
ret.put(User.USERS, users);
return ret;
}
/**
* Gets a user by the specified user id.
*
* @param userId the specified user id
* @return for example, <pre>
* {
* "oId": "",
* "userName": "",
* "userEmail": "",
* "userPassword": "",
* ....
* }
* </pre>, returns {@code null} if not found
* @throws ServiceException service exception
*/
public JSONObject getUser(final String userId) throws ServiceException {
try {
return userRepository.get(userId);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Gets a user failed", e);
throw new ServiceException(e);
}
}
/**
* Gets the URL of user logout.
*
* @param redirectURL redirect URL after logged in
* @return logout URL, returns {@code null} if the user is not logged in
*/
public String getLogoutURL(final String redirectURL) {
String to = Latkes.getServePath();
try {
to = URLEncoder.encode(to + redirectURL, "UTF-8");
} catch (final UnsupportedEncodingException e) {
LOGGER.log(Level.ERROR, "URL encode[string={0}]", redirectURL);
}
return Latkes.getContextPath() + "/logout?goto=" + to;
}
/**
* Gets the URL of user login.
*
* @param redirectURL redirect URL after logged in
* @return login URL
*/
public String getLoginURL(final String redirectURL) {
String to = Latkes.getServePath();
try {
to = URLEncoder.encode(to + redirectURL, "UTF-8");
} catch (final UnsupportedEncodingException e) {
LOGGER.log(Level.ERROR, "URL encode[string={0}]", redirectURL);
}
return Latkes.getContextPath() + "/login?goto=" + to;
}
}