001: /*
002: * Copyright 2002-2005 the original author or authors.
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: */
016:
017: package info.jtrac;
018:
019: import info.jtrac.domain.AbstractItem;
020: import info.jtrac.domain.Attachment;
021: import info.jtrac.domain.Config;
022: import info.jtrac.domain.Counts;
023: import info.jtrac.domain.CountsHolder;
024: import info.jtrac.domain.Field;
025: import info.jtrac.domain.History;
026: import info.jtrac.domain.Item;
027: import info.jtrac.domain.ItemItem;
028: import info.jtrac.domain.ItemRefId;
029: import info.jtrac.domain.ItemSearch;
030: import info.jtrac.domain.Metadata;
031: import info.jtrac.domain.Space;
032: import info.jtrac.domain.SpaceSequence;
033: import info.jtrac.domain.User;
034: import info.jtrac.domain.UserSpaceRole;
035: import info.jtrac.lucene.IndexSearcher;
036: import info.jtrac.lucene.Indexer;
037: import info.jtrac.mail.MailSender;
038: import info.jtrac.util.AttachmentUtils;
039: import java.io.File;
040: import java.util.ArrayList;
041: import java.util.Collections;
042: import java.util.Date;
043: import java.util.HashMap;
044: import java.util.HashSet;
045: import java.util.LinkedHashMap;
046: import java.util.LinkedHashSet;
047:
048: import java.util.List;
049: import java.util.Locale;
050: import java.util.Map;
051: import java.util.Random;
052: import java.util.Set;
053:
054: import org.acegisecurity.providers.encoding.PasswordEncoder;
055: import org.acegisecurity.userdetails.UserDetails;
056: import org.acegisecurity.userdetails.UsernameNotFoundException;
057: import org.springframework.context.MessageSource;
058: import org.springframework.util.StringUtils;
059: import org.apache.wicket.markup.html.form.upload.FileUpload;
060: import org.slf4j.Logger;
061: import org.slf4j.LoggerFactory;
062:
063: /**
064: * Jtrac Service Layer implementation
065: * This is where all the business logic is
066: * For data persistence this delegates to JtracDao
067: */
068: public class JtracImpl implements Jtrac {
069:
070: private final Logger logger = LoggerFactory.getLogger(getClass());
071:
072: private JtracDao dao;
073: private PasswordEncoder passwordEncoder;
074: private MailSender mailSender;
075: private Indexer indexer;
076: private IndexSearcher indexSearcher;
077: private MessageSource messageSource;
078:
079: private Map<String, String> locales;
080: private String defaultLocale = "en";
081: private String releaseVersion;
082: private String releaseTimestamp;
083: private String jtracHome;
084: private int attachmentMaxSizeInMb = 5;
085: private int sessionTimeoutInMinutes = 30;
086:
087: public void setLocaleList(String[] array) {
088: locales = new LinkedHashMap<String, String>();
089: for (String localeString : array) {
090: Locale locale = StringUtils.parseLocaleString(localeString);
091: locales.put(localeString, localeString + " - "
092: + locale.getDisplayName());
093: }
094: logger.info("available locales configured " + locales);
095: }
096:
097: public void setDao(JtracDao dao) {
098: this .dao = dao;
099: }
100:
101: public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
102: this .passwordEncoder = passwordEncoder;
103: }
104:
105: public void setIndexSearcher(IndexSearcher indexSearcher) {
106: this .indexSearcher = indexSearcher;
107: }
108:
109: public void setIndexer(Indexer indexer) {
110: this .indexer = indexer;
111: }
112:
113: public void setMessageSource(MessageSource messageSource) {
114: this .messageSource = messageSource;
115: }
116:
117: public void setReleaseTimestamp(String releaseTimestamp) {
118: this .releaseTimestamp = releaseTimestamp;
119: }
120:
121: public void setReleaseVersion(String releaseVersion) {
122: this .releaseVersion = releaseVersion;
123: }
124:
125: public void setJtracHome(String jtracHome) {
126: this .jtracHome = jtracHome;
127: }
128:
129: public String getJtracHome() {
130: return jtracHome;
131: }
132:
133: public int getAttachmentMaxSizeInMb() {
134: return attachmentMaxSizeInMb;
135: }
136:
137: public int getSessionTimeoutInMinutes() {
138: return sessionTimeoutInMinutes;
139: }
140:
141: /**
142: * this has not been factored into the util package or a helper class
143: * because it depends on the PasswordEncoder configured
144: */
145: public String generatePassword() {
146: byte[] ab = new byte[1];
147: Random r = new Random();
148: r.nextBytes(ab);
149: return passwordEncoder.encodePassword(new String(ab), null)
150: .substring(24);
151: }
152:
153: /**
154: * this has not been factored into the util package or a helper class
155: * because it depends on the PasswordEncoder configured
156: */
157: public String encodeClearText(String clearText) {
158: return passwordEncoder.encodePassword(clearText, null);
159: }
160:
161: public Map<String, String> getLocales() {
162: return locales;
163: }
164:
165: public String getDefaultLocale() {
166: return defaultLocale;
167: }
168:
169: /**
170: * this is automatically called by spring init-method hook on
171: * startup, also called whenever config is edited to refresh
172: * TODO move config into a settings class to reduce service clutter
173: */
174: public void init() {
175: Map<String, String> config = loadAllConfig();
176: initMailSender(config);
177: initDefaultLocale(config.get("locale.default"));
178: initAttachmentMaxSize(config.get("attachment.maxsize"));
179: initSessionTimeout(config.get("session.timeout"));
180: }
181:
182: private void initMailSender(Map<String, String> config) {
183: this .mailSender = new MailSender(config, messageSource,
184: defaultLocale);
185: }
186:
187: private void initDefaultLocale(String localeString) {
188: if (localeString == null || !locales.containsKey(localeString)) {
189: logger.warn("invalid default locale configured = '"
190: + localeString + "', using " + this .defaultLocale);
191: } else {
192: this .defaultLocale = localeString;
193: }
194: logger.info("default locale set to '" + this .defaultLocale
195: + "'");
196: }
197:
198: private void initAttachmentMaxSize(String s) {
199: try {
200: this .attachmentMaxSizeInMb = Integer.parseInt(s);
201: } catch (Exception e) {
202: logger.warn("invalid attachment max size '" + s
203: + "', using " + attachmentMaxSizeInMb);
204: }
205: logger.info("attachment max size set to "
206: + this .attachmentMaxSizeInMb + " MB");
207: }
208:
209: private void initSessionTimeout(String s) {
210: try {
211: this .sessionTimeoutInMinutes = Integer.parseInt(s);
212: } catch (Exception e) {
213: logger.warn("invalid session timeout '" + s + "', using "
214: + this .sessionTimeoutInMinutes);
215: }
216: logger.info("session timeout set to "
217: + this .sessionTimeoutInMinutes + " minutes");
218: }
219:
220: //==========================================================================
221:
222: private Attachment getAttachment(FileUpload fileUpload) {
223: if (fileUpload == null) {
224: return null;
225: }
226: logger.debug("fileUpload not null");
227: String fileName = AttachmentUtils.cleanFileName(fileUpload
228: .getClientFileName());
229: Attachment attachment = new Attachment();
230: attachment.setFileName(fileName);
231: dao.storeAttachment(attachment);
232: attachment.setFilePrefix(attachment.getId());
233: return attachment;
234: }
235:
236: private void writeToFile(FileUpload fileUpload,
237: Attachment attachment) {
238: if (fileUpload == null) {
239: return;
240: }
241: File file = AttachmentUtils.getFile(attachment, jtracHome);
242: try {
243: fileUpload.writeTo(file);
244: } catch (Exception e) {
245: throw new RuntimeException(e);
246: }
247:
248: }
249:
250: public synchronized void storeItem(Item item, FileUpload fileUpload) {
251: History history = new History(item);
252: Attachment attachment = getAttachment(fileUpload);
253: if (attachment != null) {
254: item.add(attachment);
255: history.setAttachment(attachment);
256: }
257: Date now = new Date();
258: item.setTimeStamp(now);
259: history.setTimeStamp(now);
260: item.add(history);
261: SpaceSequence spaceSequence = dao.loadSpaceSequence(item
262: .getSpace().getSpaceSequence().getId());
263: item.setSequenceNum(spaceSequence.next());
264: // the synchronize for this storeItem method and the hibernate flush() call in the dao implementation
265: // are important to prevent duplicate sequence numbers
266: dao.storeSpaceSequence(spaceSequence);
267: // this will at the moment execute unnecessary updates (bug in Hibernate handling of "version" property)
268: // se http://opensource.atlassian.com/projects/hibernate/browse/HHH-1401
269: // TODO confirm if above does not happen anymore
270: dao.storeItem(item);
271: writeToFile(fileUpload, attachment);
272: indexer.index(item);
273: indexer.index(history);
274: if (item.isSendNotifications()) {
275: mailSender.send(item);
276: }
277: }
278:
279: public void updateItem(Item item, User user) {
280: logger.debug("update item called");
281: History history = new History(item);
282: history.setAssignedTo(null);
283: history.setStatus(null);
284: history.setLoggedBy(user);
285: history.setComment(item.getEditReason());
286: history.setTimeStamp(new Date());
287: item.add(history);
288: dao.storeItem(item); // merge edits + history
289: // TODO index?
290: if (item.isSendNotifications()) {
291: mailSender.send(item);
292: }
293: }
294:
295: public synchronized void storeHistoryForItem(long itemId,
296: History history, FileUpload fileUpload) {
297: Item item = dao.loadItem(itemId);
298: // first apply edits onto item record before we change the item status
299: // the item.getEditableFieldList routine depends on the current State of the item
300: for (Field field : item.getEditableFieldList(history
301: .getLoggedBy())) {
302: Object value = history.getValue(field.getName());
303: if (value != null) {
304: item.setValue(field.getName(), value);
305: }
306: }
307: if (history.getStatus() != null) {
308: item.setStatus(history.getStatus());
309: item.setAssignedTo(history.getAssignedTo()); // this may be null, when closing
310: }
311: item.setItemUsers(history.getItemUsers());
312: history.setTimeStamp(new Date());
313: Attachment attachment = getAttachment(fileUpload);
314: if (attachment != null) {
315: item.add(attachment);
316: history.setAttachment(attachment);
317: }
318: item.add(history);
319: dao.storeItem(item);
320: writeToFile(fileUpload, attachment);
321: indexer.index(history);
322: if (history.isSendNotifications()) {
323: mailSender.send(item);
324: }
325: }
326:
327: public Item loadItem(long id) {
328: return dao.loadItem(id);
329: }
330:
331: public Item loadItemByRefId(String refId) {
332: ItemRefId itemRefId = new ItemRefId(refId); // throws runtime exception if invalid id
333: List<Item> items = dao.findItems(itemRefId.getSequenceNum(),
334: itemRefId.getPrefixCode());
335: if (items.size() == 0) {
336: return null;
337: }
338: return items.get(0);
339: }
340:
341: public History loadHistory(long id) {
342: return dao.loadHistory(id);
343: }
344:
345: public List<Item> findItems(ItemSearch itemSearch) {
346: String searchText = itemSearch.getSearchText();
347: if (searchText != null) {
348: List<Long> hits = indexSearcher
349: .findItemIdsContainingText(searchText);
350: if (hits.size() == 0) {
351: itemSearch.setResultCount(0);
352: return Collections.<Item> emptyList();
353: }
354: itemSearch.setItemIds(hits);
355: }
356: return dao.findItems(itemSearch);
357: }
358:
359: public void removeItem(Item item) {
360: if (item.getRelatingItems() != null) {
361: for (ItemItem itemItem : item.getRelatingItems()) {
362: removeItemItem(itemItem);
363: }
364: }
365: if (item.getRelatedItems() != null) {
366: for (ItemItem itemItem : item.getRelatedItems()) {
367: removeItemItem(itemItem);
368: }
369: }
370: dao.removeItem(item);
371: }
372:
373: public void removeItemItem(ItemItem itemItem) {
374: dao.removeItemItem(itemItem);
375: }
376:
377: public int loadCountOfRecordsHavingFieldNotNull(Space space,
378: Field field) {
379: return dao.loadCountOfRecordsHavingFieldNotNull(space, field);
380: }
381:
382: public int bulkUpdateFieldToNull(Space space, Field field) {
383: return dao.bulkUpdateFieldToNull(space, field);
384: }
385:
386: public int loadCountOfRecordsHavingFieldWithValue(Space space,
387: Field field, int optionKey) {
388: return dao.loadCountOfRecordsHavingFieldWithValue(space, field,
389: optionKey);
390: }
391:
392: public int bulkUpdateFieldToNullForValue(Space space, Field field,
393: int optionKey) {
394: return dao.bulkUpdateFieldToNullForValue(space, field,
395: optionKey);
396: }
397:
398: public int loadCountOfRecordsHavingStatus(Space space, int status) {
399: return dao.loadCountOfRecordsHavingStatus(space, status);
400: }
401:
402: public int bulkUpdateStatusToOpen(Space space, int status) {
403: return dao.bulkUpdateStatusToOpen(space, status);
404: }
405:
406: public int bulkUpdateRenameSpaceRole(Space space,
407: String oldRoleKey, String newRoleKey) {
408: return dao.bulkUpdateRenameSpaceRole(space, oldRoleKey,
409: newRoleKey);
410: }
411:
412: public int bulkUpdateDeleteSpaceRole(Space space, String roleKey) {
413: return dao.bulkUpdateDeleteSpaceRole(space, roleKey);
414: }
415:
416: // ========= Acegi UserDetailsService implementation ==========
417: public UserDetails loadUserByUsername(String loginName) {
418: List<User> users = null;
419: if (loginName.indexOf("@") != -1) {
420: users = dao.findUsersByEmail(loginName);
421: } else {
422: users = dao.findUsersByLoginName(loginName);
423: }
424: if (users.size() == 0) {
425: throw new UsernameNotFoundException("User not found for '"
426: + loginName + "'");
427: }
428: logger.debug("loadUserByUserName success for '" + loginName
429: + "'");
430: User user = users.get(0);
431: // if some spaces have guest access enabled, allocate these spaces as well
432: Set<Space> userSpaces = user.getSpaces();
433: for (Space s : findSpacesWhereGuestAllowed()) {
434: if (!userSpaces.contains(s)) {
435: user.addSpaceWithRole(s, "ROLE_GUEST");
436:
437: }
438: }
439: for (UserSpaceRole usr : user.getSpaceRoles()) {
440: logger.debug("UserSpaceRole: " + usr);
441: // this is a hack, the effect of the next line would be to
442: // override hibernate lazy loading and get the space and associated metadata.
443: // since this only happens only once on authentication and simplifies a lot of
444: // code later because the security principal is "fully prepared",
445: // this is hopefully pardonable. The downside is that there may be as many extra db hits
446: // as there are spaces allocated for the user. Hibernate caching should alleviate this
447: usr.isAbleToCreateNewItem();
448: }
449: return user;
450: }
451:
452: public User loadUser(long id) {
453: return dao.loadUser(id);
454: }
455:
456: public User loadUser(String loginName) {
457: List<User> users = dao.findUsersByLoginName(loginName);
458: if (users.size() == 0) {
459: return null;
460: }
461: return users.get(0);
462: }
463:
464: public void storeUser(User user) {
465: dao.storeUser(user);
466: }
467:
468: public void storeUser(User user, String password,
469: boolean sendNotifications) {
470: if (password == null) {
471: password = generatePassword();
472: }
473: user.setPassword(encodeClearText(password));
474: storeUser(user);
475: if (sendNotifications) {
476: mailSender.sendUserPassword(user, password);
477: }
478: }
479:
480: public void removeUser(User user) {
481: dao.removeUser(user);
482: }
483:
484: public List<User> findAllUsers() {
485: return dao.findAllUsers();
486: }
487:
488: public List<User> findUsersMatching(String searchText,
489: String searchOn) {
490: return dao.findUsersMatching(searchText, searchOn);
491: }
492:
493: public List<User> findUsersForSpace(long spaceId) {
494: return dao.findUsersForSpace(spaceId);
495: }
496:
497: public List<UserSpaceRole> findUserRolesForSpace(long spaceId) {
498: return dao.findUserRolesForSpace(spaceId);
499: }
500:
501: public List<User> findUsersWithRoleForSpace(long spaceId,
502: String roleKey) {
503: return dao.findUsersWithRoleForSpace(spaceId, roleKey);
504: }
505:
506: public List<User> findUsersForUser(User user) {
507: Set<Space> spaces = user.getSpaces();
508: if (spaces.size() == 0) {
509: // this will happen when a user has no spaces allocated
510: return Collections.emptyList();
511: }
512: // must be a better way to make this unique?
513: List<User> users = dao.findUsersForSpaceSet(spaces);
514: Set<User> userSet = new LinkedHashSet<User>(users);
515: return new ArrayList<User>(userSet);
516: }
517:
518: public List<User> findUnallocatedUsersForSpace(long spaceId) {
519: List<User> users = findAllUsers();
520: Space space = loadSpace(spaceId);
521: Set<String> roleKeys = space.getMetadata().getRoles().keySet();
522: Set<UserSpaceRole> userSpaceRoles = new HashSet(
523: findUserRolesForSpace(spaceId));
524: List<User> unallocated = new ArrayList<User>();
525: // spaces have multiple roles, find users that have not been
526: // allocated all roles for the given space
527: for (User user : users) {
528: for (String roleKey : roleKeys) {
529: UserSpaceRole usr = new UserSpaceRole(user, space,
530: roleKey);
531: if (!userSpaceRoles.contains(usr)) {
532: unallocated.add(user);
533: break;
534: }
535: }
536: }
537: return unallocated;
538: }
539:
540: public int loadCountOfHistoryInvolvingUser(User user) {
541: return dao.loadCountOfHistoryInvolvingUser(user);
542: }
543:
544: //==========================================================================
545:
546: public CountsHolder loadCountsForUser(User user) {
547: return dao.loadCountsForUser(user);
548: }
549:
550: public Counts loadCountsForUserSpace(User user, Space space) {
551: return dao.loadCountsForUserSpace(user, space);
552: }
553:
554: //==========================================================================
555:
556: public void storeUserSpaceRole(User user, Space space,
557: String roleKey) {
558: user.addSpaceWithRole(space, roleKey);
559: dao.storeUser(user);
560: }
561:
562: public void removeUserSpaceRole(UserSpaceRole userSpaceRole) {
563: User user = userSpaceRole.getUser();
564: user.removeSpaceWithRole(userSpaceRole.getSpace(),
565: userSpaceRole.getRoleKey());
566: // dao.storeUser(user);
567: dao.removeUserSpaceRole(userSpaceRole);
568: }
569:
570: public UserSpaceRole loadUserSpaceRole(long id) {
571: return dao.loadUserSpaceRole(id);
572: }
573:
574: //==========================================================================
575:
576: public Space loadSpace(long id) {
577: return dao.loadSpace(id);
578: }
579:
580: public Space loadSpace(String prefixCode) {
581: List<Space> spaces = dao.findSpacesByPrefixCode(prefixCode);
582: if (spaces.size() == 0) {
583: return null;
584: }
585: return spaces.get(0);
586: }
587:
588: public void storeSpace(Space space) {
589: dao.storeSpace(space);
590: }
591:
592: public List<Space> findAllSpaces() {
593: return dao.findAllSpaces();
594: }
595:
596: public List<Space> findSpacesWhereGuestAllowed() {
597: return dao.findSpacesWhereGuestAllowed();
598: }
599:
600: public List<Space> findUnallocatedSpacesForUser(long userId) {
601: List<Space> spaces = findAllSpaces();
602: User user = loadUser(userId);
603: Set<UserSpaceRole> usrs = user.getUserSpaceRoles();
604: List<Space> unallocated = new ArrayList<Space>();
605: // spaces have multiple roles, find spaces that have roles
606: // not yet assigned to the user
607: for (Space space : spaces) {
608: for (String roleKey : space.getMetadata().getRoles()
609: .keySet()) {
610: UserSpaceRole usr = new UserSpaceRole(user, space,
611: roleKey);
612: if (!usrs.contains(usr)) {
613: unallocated.add(space);
614: break;
615: }
616: }
617: }
618: return unallocated;
619: }
620:
621: public void removeSpace(Space space) {
622: logger.info("proceeding to delete space: " + space);
623: dao.bulkUpdateDeleteSpaceRole(space, null);
624: dao.bulkUpdateDeleteItemsForSpace(space);
625: dao.removeSpace(space);
626: logger.info("successfully deleted space");
627: }
628:
629: //==========================================================================
630:
631: public void storeMetadata(Metadata metadata) {
632: dao.storeMetadata(metadata);
633: }
634:
635: public Metadata loadMetadata(long id) {
636: return dao.loadMetadata(id);
637: }
638:
639: //==========================================================================
640:
641: public Map<String, String> loadAllConfig() {
642: List<Config> list = dao.findAllConfig();
643: Map<String, String> allConfig = new HashMap<String, String>(
644: list.size());
645: for (Config c : list) {
646: allConfig.put(c.getParam(), c.getValue());
647: }
648: return allConfig;
649: }
650:
651: // TODO must be some nice generic way to do this
652: public void storeConfig(Config config) {
653: dao.storeConfig(config);
654: if (config.isMailConfig()) {
655: initMailSender(loadAllConfig());
656: } else if (config.isLocaleConfig()) {
657: initDefaultLocale(config.getValue());
658: } else if (config.isAttachmentConfig()) {
659: initAttachmentMaxSize(config.getValue());
660: } else if (config.isSessionTimeoutConfig()) {
661: initSessionTimeout(config.getValue());
662: }
663: }
664:
665: public String loadConfig(String param) {
666: Config config = dao.loadConfig(param);
667: if (config == null) {
668: return null;
669: }
670: String value = config.getValue();
671: if (value == null || value.trim().equals("")) {
672: return null;
673: }
674: return value;
675: }
676:
677: //========================================================
678:
679: public void rebuildIndexes() {
680: clearIndexes();
681: List<AbstractItem> items = dao.findAllItems();
682: for (AbstractItem item : items) {
683: indexer.index(item);
684: }
685: }
686:
687: public List<AbstractItem> findAllItems() {
688: // this returns all Item and all History records for indexing
689: return dao.findAllItems();
690: }
691:
692: public void clearIndexes() {
693: File file = new File(jtracHome + "/indexes");
694: for (File f : file.listFiles()) {
695: f.delete();
696: }
697: }
698:
699: public void index(AbstractItem item) {
700: indexer.index(item);
701: }
702:
703: public boolean validateTextSearchQuery(String text) {
704: return indexSearcher.validateQuery(text);
705: }
706:
707: //==========================================================================
708:
709: public void executeHourlyTask() {
710: logger.debug("hourly task called");
711: }
712:
713: /* configured to be called every five minutes */
714: public void executePollingTask() {
715: logger.debug("polling task called");
716: }
717:
718: //==========================================================================
719:
720: public String getReleaseVersion() {
721: return releaseVersion;
722: }
723:
724: public String getReleaseTimestamp() {
725: return releaseTimestamp;
726: }
727:
728: }
|