/* * Copyright 2002-2005 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package info.jtrac; import info.jtrac.domain.Attachment; import info.jtrac.domain.BatchInfo; import info.jtrac.domain.Config; import info.jtrac.domain.Counts; import info.jtrac.domain.CountsHolder; import info.jtrac.domain.Field; import info.jtrac.domain.History; import info.jtrac.domain.Item; import info.jtrac.domain.ItemItem; import info.jtrac.domain.ItemRefId; import info.jtrac.domain.ItemSearch; import info.jtrac.domain.ItemUser; import info.jtrac.domain.Metadata; import info.jtrac.domain.Role; import info.jtrac.domain.Space; import info.jtrac.domain.SpaceSequence; import info.jtrac.domain.State; import info.jtrac.domain.User; import info.jtrac.domain.UserSpaceRole; import info.jtrac.lucene.IndexSearcher; import info.jtrac.lucene.Indexer; import info.jtrac.mail.MailSender; import info.jtrac.util.AttachmentUtils; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.Set; import org.acegisecurity.providers.encoding.PasswordEncoder; import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.springframework.context.MessageSource; import org.springframework.util.StringUtils; import org.apache.wicket.markup.html.form.upload.FileUpload; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Jtrac Service Layer implementation * This is where all the business logic is * For data persistence this delegates to JtracDao */ public class JtracImpl implements Jtrac { private static final Logger logger = LoggerFactory.getLogger(JtracImpl.class); private JtracDao dao; private PasswordEncoder passwordEncoder; private MailSender mailSender; private Indexer indexer; private IndexSearcher indexSearcher; private MessageSource messageSource; private Map<String, String> locales; private String defaultLocale = "en"; private String releaseVersion; private String releaseTimestamp; private String jtracHome; private int attachmentMaxSizeInMb = 5; private int sessionTimeoutInMinutes = 30; public void setLocaleList(String[] array) { locales = new LinkedHashMap<String, String>(); for(String localeString : array) { Locale locale = StringUtils.parseLocaleString(localeString); locales.put(localeString, localeString + " - " + locale.getDisplayName()); } logger.info("available locales configured " + locales); } public void setDao(JtracDao dao) { this.dao = dao; } public void setPasswordEncoder(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } public void setIndexSearcher(IndexSearcher indexSearcher) { this.indexSearcher = indexSearcher; } public void setIndexer(Indexer indexer) { this.indexer = indexer; } public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } public void setReleaseTimestamp(String releaseTimestamp) { this.releaseTimestamp = releaseTimestamp; } public void setReleaseVersion(String releaseVersion) { this.releaseVersion = releaseVersion; } public void setJtracHome(String jtracHome) { this.jtracHome = jtracHome; } public String getJtracHome() { return jtracHome; } public int getAttachmentMaxSizeInMb() { return attachmentMaxSizeInMb; } public int getSessionTimeoutInMinutes() { return sessionTimeoutInMinutes; } /** * this has not been factored into the util package or a helper class * because it depends on the PasswordEncoder configured */ public String generatePassword() { byte[] ab = new byte[1]; Random r = new Random(); r.nextBytes(ab); return passwordEncoder.encodePassword(new String(ab), null).substring(24); } /** * this has not been factored into the util package or a helper class * because it depends on the PasswordEncoder configured */ public String encodeClearText(String clearText) { return passwordEncoder.encodePassword(clearText, null); } public Map<String, String> getLocales() { return locales; } public String getDefaultLocale() { return defaultLocale; } /** * this is automatically called by spring init-method hook on * startup, also called whenever config is edited to refresh * TODO move config into a settings class to reduce service clutter */ public void init() { Map<String, String> config = loadAllConfig(); initDefaultLocale(config.get("locale.default")); initMailSender(config); initAttachmentMaxSize(config.get("attachment.maxsize")); initSessionTimeout(config.get("session.timeout")); } private void initMailSender(Map<String, String> config) { this.mailSender = new MailSender(config, messageSource, defaultLocale); } private void initDefaultLocale(String localeString) { if (localeString == null || !locales.containsKey(localeString)) { logger.warn("invalid default locale configured = '" + localeString + "', using " + this.defaultLocale); } else { this.defaultLocale = localeString; } logger.info("default locale set to '" + this.defaultLocale + "'"); } private void initAttachmentMaxSize(String s) { try { this.attachmentMaxSizeInMb = Integer.parseInt(s); } catch(Exception e) { logger.warn("invalid attachment max size '" + s + "', using " + attachmentMaxSizeInMb); } logger.info("attachment max size set to " + this.attachmentMaxSizeInMb + " MB"); } private void initSessionTimeout(String s) { try { this.sessionTimeoutInMinutes = Integer.parseInt(s); } catch(Exception e) { logger.warn("invalid session timeout '" + s + "', using " + this.sessionTimeoutInMinutes); } logger.info("session timeout set to " + this.sessionTimeoutInMinutes + " minutes"); } //========================================================================== private Attachment getAttachment(FileUpload fileUpload) { if(fileUpload == null) { return null; } logger.debug("fileUpload not null"); String fileName = AttachmentUtils.cleanFileName(fileUpload.getClientFileName()); Attachment attachment = new Attachment(); attachment.setFileName(fileName); dao.storeAttachment(attachment); attachment.setFilePrefix(attachment.getId()); return attachment; } private void writeToFile(FileUpload fileUpload, Attachment attachment) { if(fileUpload == null) { return; } File file = AttachmentUtils.getFile(attachment, jtracHome); try { fileUpload.writeTo(file); } catch (Exception e) { throw new RuntimeException(e); } } public synchronized void storeItem(Item item, FileUpload fileUpload) { History history = new History(item); Attachment attachment = getAttachment(fileUpload); if(attachment != null) { item.add(attachment); history.setAttachment(attachment); } // timestamp can be set by import, then retain Date now = item.getTimeStamp(); if(now == null) { now = new Date(); } item.setTimeStamp(now); history.setTimeStamp(now); item.add(history); item.setSequenceNum(dao.loadNextSequenceNum(item.getSpace().getId())); // this will at the moment execute unnecessary updates (bug in Hibernate handling of "version" property) // se http://opensource.atlassian.com/projects/hibernate/browse/HHH-1401 // TODO confirm if above does not happen anymore dao.storeItem(item); writeToFile(fileUpload, attachment); if(indexer != null) { indexer.index(item); indexer.index(history); } if (item.isSendNotifications()) { mailSender.send(item); } } public synchronized void storeItems(List<Item> items) { for(Item item : items) { item.setSendNotifications(false); if(item.getStatus() == State.CLOSED) { // we support CLOSED items for import also but for consistency // simulate the item first created OPEN and then being CLOSED item.setStatus(State.OPEN); History history = new History(); history.setTimeStamp(item.getTimeStamp()); // need to do this as storeHistoryForItem does some role checks // and so to avoid lazy initialization exception history.setLoggedBy(loadUser(item.getLoggedBy().getId())); history.setAssignedTo(item.getAssignedTo()); history.setComment("-"); history.setStatus(State.CLOSED); history.setSendNotifications(false); storeItem(item, null); storeHistoryForItem(item.getId(), history, null); } else { storeItem(item, null); } } } public synchronized void updateItem(Item item, User user) { logger.debug("update item called"); History history = new History(item); history.setAssignedTo(null); history.setStatus(null); history.setLoggedBy(user); history.setComment(item.getEditReason()); history.setTimeStamp(new Date()); item.add(history); dao.storeItem(item); // merge edits + history // TODO index? if (item.isSendNotifications()) { mailSender.send(item); } } public synchronized void storeHistoryForItem(long itemId, History history, FileUpload fileUpload) { Item item = dao.loadItem(itemId); // first apply edits onto item record before we change the item status // the item.getEditableFieldList routine depends on the current State of the item for(Field field : item.getEditableFieldList(history.getLoggedBy())) { Object value = history.getValue(field.getName()); if (value != null) { item.setValue(field.getName(), value); } } if (history.getStatus() != null) { item.setStatus(history.getStatus()); item.setAssignedTo(history.getAssignedTo()); // this may be null, when closing } item.setItemUsers(history.getItemUsers()); // may have been set if this is an import if(history.getTimeStamp() == null) { history.setTimeStamp(new Date()); } Attachment attachment = getAttachment(fileUpload); if(attachment != null) { item.add(attachment); history.setAttachment(attachment); } item.add(history); dao.storeItem(item); writeToFile(fileUpload, attachment); if(indexer != null) { indexer.index(history); } if (history.isSendNotifications()) { mailSender.send(item); } } public Item loadItem(long id) { return dao.loadItem(id); } public Item loadItemByRefId(String refId) { ItemRefId itemRefId = new ItemRefId(refId); // throws runtime exception if invalid id List<Item> items = dao.findItems(itemRefId.getSequenceNum(), itemRefId.getPrefixCode()); if (items.size() == 0) { return null; } return items.get(0); } public History loadHistory(long id) { return dao.loadHistory(id); } public List<Item> findItems(ItemSearch itemSearch) { String searchText = itemSearch.getSearchText(); if (searchText != null) { List<Long> hits = indexSearcher.findItemIdsContainingText(searchText); if (hits.size() == 0) { itemSearch.setResultCount(0); return Collections.<Item>emptyList(); } itemSearch.setItemIds(hits); } return dao.findItems(itemSearch); } public int loadCountOfAllItems() { return dao.loadCountOfAllItems(); } public List<Item> findAllItems(int firstResult, int batchSize) { return dao.findAllItems(firstResult, batchSize); } public void removeItem(Item item) { if(item.getRelatingItems() != null) { for(ItemItem itemItem : item.getRelatingItems()) { removeItemItem(itemItem); } } if(item.getRelatedItems() != null) { for(ItemItem itemItem : item.getRelatedItems()) { removeItemItem(itemItem); } } dao.removeItem(item); } public void removeItemItem(ItemItem itemItem) { dao.removeItemItem(itemItem); } public int loadCountOfRecordsHavingFieldNotNull(Space space, Field field) { return dao.loadCountOfRecordsHavingFieldNotNull(space, field); } public int bulkUpdateFieldToNull(Space space, Field field) { return dao.bulkUpdateFieldToNull(space, field); } public int loadCountOfRecordsHavingFieldWithValue(Space space, Field field, int optionKey) { return dao.loadCountOfRecordsHavingFieldWithValue(space, field, optionKey); } public int bulkUpdateFieldToNullForValue(Space space, Field field, int optionKey) { return dao.bulkUpdateFieldToNullForValue(space, field, optionKey); } public int loadCountOfRecordsHavingStatus(Space space, int status) { return dao.loadCountOfRecordsHavingStatus(space, status); } public int bulkUpdateStatusToOpen(Space space, int status) { return dao.bulkUpdateStatusToOpen(space, status); } public int bulkUpdateRenameSpaceRole(Space space, String oldRoleKey, String newRoleKey) { return dao.bulkUpdateRenameSpaceRole(space, oldRoleKey, newRoleKey); } public int bulkUpdateDeleteSpaceRole(Space space, String roleKey) { return dao.bulkUpdateDeleteSpaceRole(space, roleKey); } // ========= Acegi UserDetailsService implementation ========== public UserDetails loadUserByUsername(String loginName) { List<User> users = null; if (loginName.indexOf("@") != -1) { users = dao.findUsersByEmail(loginName); } else { users = dao.findUsersByLoginName(loginName); } if (users.size() == 0) { throw new UsernameNotFoundException("User not found for '" + loginName + "'"); } logger.debug("loadUserByUserName success for '" + loginName + "'"); User user = users.get(0); // if some spaces have guest access enabled, allocate these spaces as well Set<Space> userSpaces = user.getSpaces(); logger.debug("user spaces: " + userSpaces); for(Space s : findSpacesWhereGuestAllowed()) { if(!userSpaces.contains(s)) { user.addSpaceWithRole(s, Role.ROLE_GUEST); } } for(UserSpaceRole usr : user.getSpaceRoles()) { logger.debug("UserSpaceRole: " + usr); // this is a hack, the effect of the next line would be to // override hibernate lazy loading and get the space and associated metadata. // since this only happens only once on authentication and simplifies a lot of // code later because the security principal is "fully prepared", // this is hopefully pardonable. The downside is that there may be as many extra db hits // as there are spaces allocated for the user. Hibernate caching should alleviate this usr.isAbleToCreateNewItem(); } return user; } public User loadUser(long id) { return dao.loadUser(id); } public User loadUser(String loginName) { List<User> users = dao.findUsersByLoginName(loginName); if (users.size() == 0) { return null; } return users.get(0); } public void storeUser(User user) { user.clearNonPersistentRoles(); dao.storeUser(user); } public void storeUser(User user, String password, boolean sendNotifications) { if (password == null) { password = generatePassword(); } user.setPassword(encodeClearText(password)); storeUser(user); if(sendNotifications) { mailSender.sendUserPassword(user, password); } } public void removeUser(User user) { for(ItemUser iu : dao.findItemUsersByUser(user)) { dao.removeItemUser(iu); } dao.removeUser(user); } public List<User> findAllUsers() { return dao.findAllUsers(); } public List<User> findUsersWhereIdIn(List<Long> ids) { return dao.findUsersWhereIdIn(ids); } public List<User> findUsersMatching(String searchText, String searchOn) { return dao.findUsersMatching(searchText, searchOn); } public List<User> findUsersForSpace(long spaceId) { return dao.findUsersForSpace(spaceId); } public List<UserSpaceRole> findUserRolesForSpace(long spaceId) { return dao.findUserRolesForSpace(spaceId); } public Map<Long, List<UserSpaceRole>> loadUserRolesMapForSpace(long spaceId) { List<UserSpaceRole> list = dao.findUserRolesForSpace(spaceId); Map<Long, List<UserSpaceRole>> map = new LinkedHashMap<Long, List<UserSpaceRole>>(); for(UserSpaceRole usr : list) { long userId = usr.getUser().getId(); List<UserSpaceRole> value = map.get(userId); if(value == null) { value = new ArrayList<UserSpaceRole>(); map.put(userId, value); } value.add(usr); } return map; } public Map<Long, List<UserSpaceRole>> loadSpaceRolesMapForUser(long userId) { List<UserSpaceRole> list = dao.findSpaceRolesForUser(userId); Map<Long, List<UserSpaceRole>> map = new LinkedHashMap<Long, List<UserSpaceRole>>(); for(UserSpaceRole usr : list) { long spaceId = usr.getSpace() == null ? 0 : usr.getSpace().getId(); List<UserSpaceRole> value = map.get(spaceId); if(value == null) { value = new ArrayList<UserSpaceRole>(); map.put(spaceId, value); } value.add(usr); } return map; } public List<User> findUsersWithRoleForSpace(long spaceId, String roleKey) { return dao.findUsersWithRoleForSpace(spaceId, roleKey); } public List<User> findUsersForUser(User user) { Set<Space> spaces = user.getSpaces(); if(spaces.size() == 0) { // this will happen when a user has no spaces allocated return Collections.emptyList(); } // must be a better way to make this unique? List<User> users = dao.findUsersForSpaceSet(spaces); Set<User> userSet = new LinkedHashSet<User>(users); return new ArrayList<User>(userSet); } public List<User> findUsersNotFullyAllocatedToSpace(long spaceId) { // trying to reduce database hits and lazy loading as far as possible List<User> notAtAllAllocated = dao.findUsersNotAllocatedToSpace(spaceId); List<UserSpaceRole> usrs = dao.findUserRolesForSpace(spaceId); List<User> notFullyAllocated = new ArrayList<User>(notAtAllAllocated); if(usrs.size() == 0) { return notFullyAllocated; } Space space = usrs.get(0).getSpace(); Set<UserSpaceRole> allocated = new HashSet(usrs); Set<String> roleKeys = new HashSet(space.getMetadata().getAllRoleKeys()); Set<User> processed = new HashSet<User>(usrs.size()); Set<User> superUsers = new HashSet(dao.findSuperUsers()); for(UserSpaceRole usr : usrs) { User user = usr.getUser(); if(processed.contains(user)) { continue; } processed.add(user); // not using the user object as it is db intensive boolean isSuperUser = superUsers.contains(user); for(String roleKey : roleKeys) { if(isSuperUser && Role.isAdmin(roleKey)) { continue; } UserSpaceRole temp = new UserSpaceRole(user, space, roleKey); if(!allocated.contains(temp)) { notFullyAllocated.add(user); break; } } } Collections.sort(notFullyAllocated); return notFullyAllocated; } public int loadCountOfHistoryInvolvingUser(User user) { return dao.loadCountOfHistoryInvolvingUser(user); } //========================================================================== public CountsHolder loadCountsForUser(User user) { return dao.loadCountsForUser(user); } public Counts loadCountsForUserSpace(User user, Space space) { return dao.loadCountsForUserSpace(user, space); } //========================================================================== public void storeUserSpaceRole(User user, Space space, String roleKey) { user.addSpaceWithRole(space, roleKey); storeUser(user); } public void removeUserSpaceRole(UserSpaceRole userSpaceRole) { User user = userSpaceRole.getUser(); user.removeSpaceWithRole(userSpaceRole.getSpace(), userSpaceRole.getRoleKey()); // dao.storeUser(user); dao.removeUserSpaceRole(userSpaceRole); } public UserSpaceRole loadUserSpaceRole(long id) { return dao.loadUserSpaceRole(id); } //========================================================================== public Space loadSpace(long id) { return dao.loadSpace(id); } public Space loadSpace(String prefixCode) { List<Space> spaces = dao.findSpacesByPrefixCode(prefixCode); if (spaces.size() == 0) { return null; } return spaces.get(0); } public void storeSpace(Space space) { boolean newSpace = space.getId() == 0; dao.storeSpace(space); if(newSpace) { SpaceSequence ss = new SpaceSequence(); ss.setNextSeqNum(1); ss.setId(space.getId()); dao.storeSpaceSequence(ss); } } public List<Space> findAllSpaces() { return dao.findAllSpaces(); } public List<Space> findSpacesWhereIdIn(List<Long> ids) { return dao.findSpacesWhereIdIn(ids); } public List<Space> findSpacesWhereGuestAllowed() { return dao.findSpacesWhereGuestAllowed(); } public List<Space> findSpacesNotFullyAllocatedToUser(long userId) { // trying to reduce database hits and lazy loading as far as possible List<Space> notAtAllAllocated = dao.findSpacesNotAllocatedToUser(userId); List<UserSpaceRole> usrs = dao.findSpaceRolesForUser(userId); List<Space> notFullyAllocated = new ArrayList(notAtAllAllocated); if(usrs.size() == 0) { return notFullyAllocated; } Set<UserSpaceRole> allocated = new HashSet(usrs); Set<Space> processed = new HashSet<Space>(usrs.size()); User user = usrs.get(0).getUser(); boolean isSuperUser = user.isSuperUser(); for(UserSpaceRole usr : usrs) { Space space = usr.getSpace(); if(space == null || processed.contains(space)) { continue; } processed.add(space); for(String roleKey : space.getMetadata().getAllRoleKeys()) { if(isSuperUser && Role.isAdmin(roleKey)) { continue; } UserSpaceRole temp = new UserSpaceRole(user, space, roleKey); if(!allocated.contains(temp)) { notFullyAllocated.add(space); break; } } } Collections.sort(notFullyAllocated); return notFullyAllocated; } public void removeSpace(Space space) { logger.info("proceeding to delete space: " + space); dao.bulkUpdateDeleteSpaceRole(space, null); dao.bulkUpdateDeleteItemsForSpace(space); dao.removeSpace(space); logger.info("successfully deleted space"); } //========================================================================== public void storeMetadata(Metadata metadata) { dao.storeMetadata(metadata); } public Metadata loadMetadata(long id) { return dao.loadMetadata(id); } //========================================================================== public Map<String, String> loadAllConfig() { List<Config> list = dao.findAllConfig(); Map<String, String> allConfig = new HashMap<String, String>(list.size()); for (Config c : list) { allConfig.put(c.getParam(), c.getValue()); } return allConfig; } // TODO must be some nice generic way to do this public void storeConfig(Config config) { dao.storeConfig(config); if(config.isMailConfig()) { initMailSender(loadAllConfig()); } else if(config.isLocaleConfig()) { initDefaultLocale(config.getValue()); } else if(config.isAttachmentConfig()) { initAttachmentMaxSize(config.getValue()); } else if(config.isSessionTimeoutConfig()) { initSessionTimeout(config.getValue()); } } public String loadConfig(String param) { Config config = dao.loadConfig(param); if (config == null) { return null; } String value = config.getValue(); if (value == null || value.trim().equals("")) { return null; } return value; } //======================================================== public void rebuildIndexes(BatchInfo batchInfo) { File file = new File(jtracHome + "/indexes"); for (File f : file.listFiles()) { logger.debug("deleting file: " + f); f.delete(); } logger.info("existing index files deleted successfully"); int totalSize = dao.loadCountOfAllItems(); batchInfo.setTotalSize(totalSize); logger.info("total items to index: " + totalSize); int firstResult = 0; long lastFetchedId = 0; while(true) { logger.info("processing batch starting from: " + firstResult + ", current: " + batchInfo.getCurrentPosition()); List<Item> items = dao.findAllItems(firstResult, batchInfo.getBatchSize()); for (Item item : items) { indexer.index(item); // currently history is indexed separately from item // not sure if this is a good thing, maybe it gives // more flexibility e.g. fine-grained search results int historyCount = 0; for(History history : item.getHistory()) { indexer.index(history); historyCount++; } if(logger.isDebugEnabled()) { logger.debug("indexed item: " + item.getId() + " : " + item.getRefId() + ", history: " + historyCount); } batchInfo.incrementPosition(); lastFetchedId = item.getId(); } if(logger.isDebugEnabled()) { logger.debug("size of current batch: " + items.size()); logger.debug("last fetched Id: " + lastFetchedId); } firstResult += batchInfo.getBatchSize(); if(logger.isDebugEnabled()) { logger.debug("setting firstResult to: " + firstResult); } if(batchInfo.isComplete()) { logger.info("batch completed at position: " + batchInfo.getCurrentPosition()); break; } } } public boolean validateTextSearchQuery(String text) { return indexSearcher.validateQuery(text); } //========================================================================== public void executeHourlyTask() { logger.debug("hourly task called"); } /* configured to be called every five minutes */ public void executePollingTask() { logger.debug("polling task called"); } //========================================================================== public String getReleaseVersion() { return releaseVersion; } public String getReleaseTimestamp() { return releaseTimestamp; } }