Java tutorial
/* * Copyright 2006-2007 Open Source Applications Foundation * * 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 org.osaf.cosmo.mc; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.osaf.cosmo.eim.EimException; import org.osaf.cosmo.eim.EimRecordSet; import org.osaf.cosmo.eim.EimRecordSetIterator; import org.osaf.cosmo.eim.schema.EimValidationException; import org.osaf.cosmo.eim.schema.ItemTranslator; import org.osaf.cosmo.model.CalendarCollectionStamp; import org.osaf.cosmo.model.CollectionItem; import org.osaf.cosmo.model.CollectionLockedException; import org.osaf.cosmo.model.ContentItem; import org.osaf.cosmo.model.EntityFactory; import org.osaf.cosmo.model.HomeCollectionItem; import org.osaf.cosmo.model.IcalUidInUseException; import org.osaf.cosmo.model.Item; import org.osaf.cosmo.model.ItemTombstone; import org.osaf.cosmo.model.ModelValidationException; import org.osaf.cosmo.model.ModificationUid; import org.osaf.cosmo.model.NoteItem; import org.osaf.cosmo.model.NoteOccurrence; import org.osaf.cosmo.model.Ticket; import org.osaf.cosmo.model.TicketType; import org.osaf.cosmo.model.Tombstone; import org.osaf.cosmo.model.UidInUseException; import org.osaf.cosmo.model.User; import org.osaf.cosmo.security.CosmoSecurityException; import org.osaf.cosmo.security.CosmoSecurityManager; import org.osaf.cosmo.server.ServiceLocator; import org.osaf.cosmo.service.ContentService; import org.osaf.cosmo.service.UserService; import org.springframework.dao.OptimisticLockingFailureException; /** * The standard implementation for * <code>MorseCodeController</code> that uses the Cosmo service APIs. * * @see MorseCodeController */ public class StandardMorseCodeController implements MorseCodeController { private static final Log log = LogFactory.getLog(StandardMorseCodeController.class); private ContentService contentService; private UserService userService; private CosmoSecurityManager securityManager; private EntityFactory entityFactory; private static final HashSet<String> EMPTY_TICKETS = new HashSet<String>(0); /** * Returns information about every collection in the user's home * collection. * * @param username the username of the user whose collections are * to be described * @param locator the service locator used to resolve collection URLs * * @throws UnknownUserException if the user is not found * @throws MorseCodeException if an unknown error occurs */ public CollectionService discoverCollections(String username, ServiceLocator locator) { if (log.isDebugEnabled()) log.debug("discovering collections for " + username); User user = userService.getUser(username); if (user == null) throw new UnknownUserException(username); HomeCollectionItem home = contentService.getRootItem(user); return new CollectionService(home, locator, securityManager.getSecurityContext()); } /** * Causes the identified collection and all contained items to be * immediately removed from storage. * * @param uid the uid of the collection to delete * * @throws UnknownCollectionException if the specified collection * is not found * @throws NotCollectionException if the specified item is not a * collection * @throws MorseCodeException if an unknown error occurs */ public void deleteCollection(String uid) { if (log.isDebugEnabled()) log.debug("deleting collection " + uid); Item item = contentService.findItemByUid(uid); if (item == null) throw new UnknownCollectionException(uid); if (!(item instanceof CollectionItem)) throw new NotCollectionException(item); contentService.removeCollection((CollectionItem) item); } /** * Creates a collection identified by the given uid and populates * the collection with items with the provided states. If ticket * types are provided, creates one ticket of each type on the * collection. The publish is atomic; the entire publish fails if * the collection or any contained item cannot be created. * * If a parent uid is provided, the associated collection becomes * the parent of the new collection. * * @param uid the uid of the collection to publish * @param parentUid the (optional) uid of the collection to set as * the parent for the published collection * @param records the EIM record sets describing the collection * and the items with which it is initially populated * @param ticketTypes a set of ticket types to create on the * collection, one per type * @returns the initial <code>SyncToken</code> for the collection * @throws IllegalArgumentException if the authenticated principal * is not a <code>User</code> but no parent uid was specified * @throws UidInUseException if the specified uid is already in * use by any item * @throws UnknownCollectionException if the collection specified * by the given parent uid is not found * @throws NotCollectionException if the item specified * by the given parent uid is not a collection * @throws ValidationException if the recordset contains invalid * data according to the records' schemas * @throws MorseCodeException if an unknown error occurs */ public PubCollection publishCollection(String uid, String parentUid, PubRecords records, Set<TicketType> ticketTypes) { if (log.isDebugEnabled()) { if (parentUid != null) log.debug("publishing collection " + uid + " with parent " + parentUid); else log.debug("publishing collection " + uid); } CollectionItem parent = null; if (parentUid == null) { User user = securityManager.getSecurityContext().getUser(); if (user == null) throw new IllegalArgumentException( "Parent uid must be provided if authentication principal is not a user"); parent = contentService.getRootItem(user); } else { Item parentItem = contentService.findItemByUid(parentUid); if (!(parentItem instanceof CollectionItem)) throw new NotCollectionException(parentItem); parent = (CollectionItem) parentItem; } // check for existing collection try { if (contentService.findItemByUid(uid) != null) throw new CollectionExistsException(uid); } catch (CosmoSecurityException e) { // Security exception will be thrown for existing collections // that aren't owned by user throw new CollectionExistsException(uid); } CollectionItem collection = entityFactory.createCollection(); User owner = computeItemOwner(); collection.setUid(uid); collection.setOwner(owner); collection.setName(uid); if (records.getName() != null) collection.setDisplayName(records.getName()); else collection.setDisplayName(uid); if (records.getHue() != null) collection.setHue(records.getHue()); // stamp it as a calendar CalendarCollectionStamp ccs = entityFactory.createCalendarCollectionStamp(collection); collection.addStamp(ccs); Set<Item> toUpdate = recordsToItems(records.getRecordSets(), collection); for (TicketType type : ticketTypes) collection.addTicket(entityFactory.createTicket(type)); // throws UidinUseException try { collection = contentService.createCollection(parent, collection, toUpdate); } catch (IcalUidInUseException e) { throw new UidConflictException(e); } catch (ModelValidationException e) { if (e.getOffendingObject() instanceof Item) { Item item = (Item) e.getOffendingObject(); throw new ValidationException(item.getUid(), e.getMessage()); } else { throw new ValidationException(null, e.getMessage()); } } return new PubCollection(collection); } /** * Retrieves the current state of every item contained within the * identified collection. * * @param uid the uid of the collection to subscribe to * * @returns a <code>SubRecords</code> describing the current * state of the collection * @throws UnknownCollectionException if the specified collection * is not found * @throws NotCollectionException if the specified item is not a * collection * @throws MorseCodeException if an unknown error occurs */ public SubRecords subscribeToCollection(String uid) { if (log.isDebugEnabled()) log.debug("subscribing to collection " + uid); Item item = contentService.findItemByUid(uid); if (item == null) throw new UnknownCollectionException(uid); if (!(item instanceof CollectionItem)) throw new NotCollectionException(item); CollectionItem collection = (CollectionItem) item; SubRecords subRecords = new SubRecords(collection, getAllItems(collection)); // Ensure collection has not been modified since reading the data Date lastModified = collection.getModifiedDate(); collection = (CollectionItem) contentService.findItemByUid(uid); while (!collection.getModifiedDate().equals(lastModified)) { // If it has been modified, then re-read data, otherwise we // could return inconsistent data if (log.isDebugEnabled()) log.debug("collection " + uid + " modified while subscribing, retrying"); subRecords = new SubRecords(collection, getAllItems(collection)); lastModified = collection.getModifiedDate(); collection = (CollectionItem) contentService.findItemByUid(uid); } return subRecords; } /** * Retrieves the current state of each non-collection child item * from the identified collection that has changed since the time * that the given synchronization token was valid. * * @param uid the uid of the collection to subscribe to * @param token the sync token describing the last known state of * the collection * * @returns a <code>SubRecords</code> describing the current * state of the changed items * @throws UnknownCollectionException if the specified collection * is not found * @throws NotCollectionException if the specified item is not a * collection * @throws MorseCodeException if an unknown error occurs */ public SubRecords synchronizeCollection(String uid, SyncToken token) { if (log.isDebugEnabled()) log.debug("synchronizing collection " + uid + " with token " + token.serialize()); Item item = contentService.findItemByUid(uid); if (item == null) throw new UnknownCollectionException(uid); if (!(item instanceof CollectionItem)) throw new NotCollectionException(item); CollectionItem collection = (CollectionItem) item; // If collection hasn't changed, don't bother querying for children // just return empty SubRecords if (token.isValid(collection)) return new SubRecords(collection, new ArrayList<ContentItem>(0)); SubRecords subRecords = new SubRecords(collection, getModifiedItems(token, collection), getRecentTombstones(token, collection), token); // ensure collection has not been modified since reading the data Date lastModified = collection.getModifiedDate(); collection = (CollectionItem) contentService.findItemByUid(uid); while (!collection.getModifiedDate().equals(lastModified)) { // If it has been modified, then re-read data, otherwise we // could return inconsistent data if (log.isDebugEnabled()) log.debug("collection " + uid + " modified while syncing, retrying"); subRecords = new SubRecords(collection, getModifiedItems(token, collection), getRecentTombstones(token, collection), token); lastModified = collection.getModifiedDate(); collection = (CollectionItem) contentService.findItemByUid(uid); } return subRecords; } /** * Updates the items within the identified collection that * correspond to the provided <code>EimRecordSet</code>s. The * update is atomic; the entire update fails if any single item * cannot be successfully saved with its new state. * * The collection is locked at the beginning of the update. Any * other update that begins before this update has completed, and * the collection unlocked, will fail immediately with a * <code>CollectionLockedException</code>. Any subscribe or * synchronize operation that begins during this update will * return the state of the collection immediately prior to the * beginning of this update. * * @param uid the uid of the collection to subscribe to * @param token the sync token describing the last known state of * the collection * @param records the EIM records describing the collection and * the items with which it is updated * @returns a new <code>SyncToken</code> that invalidates any * previously issued * @throws UnknownCollectionException if the specified collection * is not found * @throws NotCollectionException if the specified item is not a * collection * @throws CollectionLockedException if the collection is * currently locked by another update * @throws StaleCollectionException if the collection has been * updated since the provided sync token was generated * @throws ValidationException if the recordset contains invalid * data according to the records' schemas * @throws MorseCodeException if an unknown error occurs */ public PubCollection updateCollection(String uid, SyncToken token, PubRecords records) { if (log.isDebugEnabled()) { log.debug("updating collection " + uid); } Item item = contentService.findItemByUid(uid); if (item == null) throw new UnknownCollectionException(uid); if (!(item instanceof CollectionItem)) throw new NotCollectionException(item); CollectionItem collection = (CollectionItem) item; if (!token.isValid(collection)) { if (log.isDebugEnabled()) log.debug("collection state is changed"); throw new StaleCollectionException(uid); } if (records.getName() != null) collection.setDisplayName(records.getName()); if (records.getHue() != null) collection.setHue(records.getHue()); Set<Item> toUpdate = recordsToItems(records.getRecordSets(), collection); try { // throws CollectionLockedException, and may throw // ConcurrencyFailureException collection = contentService.updateCollection(collection, toUpdate); } catch (OptimisticLockingFailureException cfe) { // This means the data has been updated since the last sync token, // so a StaleCollectionException should be thrown throw new StaleCollectionException(uid); } catch (IcalUidInUseException e) { throw new UidConflictException(e); } catch (ModelValidationException e) { if (e.getOffendingObject() instanceof Item) { Item oi = (Item) e.getOffendingObject(); throw new ValidationException(oi.getUid(), e.getMessage()); } else { throw new ValidationException(null, e.getMessage()); } } return new PubCollection(collection); } // our methods /** */ public ContentService getContentService() { return contentService; } /** */ public void setContentService(ContentService service) { contentService = service; } /** */ public UserService getUserService() { return userService; } /** */ public void setUserService(UserService service) { userService = service; } /** */ public CosmoSecurityManager getSecurityManager() { return securityManager; } /** */ public void setSecurityManager(CosmoSecurityManager securityManager) { this.securityManager = securityManager; } public EntityFactory getEntityFactory() { return entityFactory; } public void setEntityFactory(EntityFactory entityFactory) { this.entityFactory = entityFactory; } /** */ public void init() { if (contentService == null) throw new IllegalStateException("contentService is required"); if (userService == null) throw new IllegalStateException("userService is required"); if (securityManager == null) throw new IllegalStateException("securityManager is required"); if (entityFactory == null) throw new IllegalStateException("entityFactory is required"); } private User computeItemOwner() { User owner = securityManager.getSecurityContext().getUser(); if (owner != null) return owner; Ticket ticket = securityManager.getSecurityContext().getTicket(); if (ticket != null) return ticket.getOwner(); throw new MorseCodeException("authenticated principal neither user nor ticket"); } private Set<Item> recordsToItems(EimRecordSetIterator i, CollectionItem collection) { // All child item for collection (both current and new) indexed by Uid HashMap<String, Item> allChildrenByUid = new HashMap<String, Item>(); LinkedHashSet<Item> children = new LinkedHashSet<Item>(); // Index all existing children for (Item child : collection.getChildren()) allChildrenByUid.put(child.getUid(), child); try { while (i.hasNext()) { EimRecordSet recordset = i.next(); try { Item item = contentService.findItemByUid(recordset.getUuid()); if (item != null && !(item instanceof ContentItem)) throw new ValidationException(recordset.getUuid(), "Child item " + recordset.getUuid() + " is not a content item"); ContentItem child = (ContentItem) item; // Handle case where item is a NoteOccurence, in which case // a new modification NoteItem needs to be created if (child instanceof NoteOccurrence) { if (recordset.isDeleted() == false) child = createChildItem((NoteOccurrence) child, collection, recordset, allChildrenByUid); else child = null; } // Handle case where recordset is to be deleted, but the // target item doesn't exist. if (child == null && recordset.isDeleted() == true) throw new ValidationException(recordset.getUuid(), "Tried to delete child item " + recordset.getUuid() + " , but it does not exist"); // Handle case where item doesn't exist, so create a new one if (child == null) child = createChildItem(collection, recordset, allChildrenByUid); children.add(child); // apply recordset new ItemTranslator(child).applyRecords(recordset); } catch (EimValidationException e) { throw new ValidationException(recordset.getUuid(), "could not apply EIM recordset " + recordset.getUuid() + " due to invalid data", e); } } } catch (EimException e) { throw new MorseCodeException("unknown EIM translation problem", e); } return children; } // creates a new item and adds it as a child of the collection private ContentItem createChildItem(CollectionItem collection, EimRecordSet recordset, Map<String, Item> allChildrenByUid) { ContentItem child = createBaseChildItem(collection, recordset, allChildrenByUid); if (child.getUid().contains(ModificationUid.RECURRENCEID_DELIMITER)) handleModificationItem((NoteItem) child, allChildrenByUid); return child; } // creates a new item based off a NoteOccurrence private ContentItem createChildItem(NoteOccurrence occurrence, CollectionItem collection, EimRecordSet recordset, Map<String, Item> allChildrenByUid) { NoteItem child = (NoteItem) createBaseChildItem(collection, recordset, allChildrenByUid); child.setModifies(occurrence.getMasterNote()); return child; } private ContentItem createBaseChildItem(CollectionItem collection, EimRecordSet recordset, Map<String, Item> allChildrenByUid) { NoteItem child = entityFactory.createNote(); child.setUid(recordset.getUuid()); child.setIcalUid(child.getUid()); child.setOwner(collection.getOwner()); allChildrenByUid.put(child.getUid(), child); return child; } private boolean handleModificationItem(NoteItem noteMod, Map<String, Item> allChildrenByUid) { ModificationUid modUid = null; try { modUid = new ModificationUid(noteMod.getUid()); } catch (ModelValidationException e) { throw new ValidationException(noteMod.getUid(), "invalid modification uid: " + noteMod.getUid()); } String parentUid = modUid.getParentUid(); // Find parent note item by looking through collection's children. Item parentNote = allChildrenByUid.get(parentUid); if (parentNote != null && parentNote instanceof NoteItem) { noteMod.setModifies((NoteItem) parentNote); return true; } // mods should not have icaluid as its inherited from the master noteMod.setIcalUid(null); log.debug("could not find parent item for " + noteMod.getUid()); throw new ValidationException(noteMod.getUid(), "no parent found for " + noteMod.getUid()); } private List<ContentItem> getAllItems(CollectionItem collection) { ArrayList<ContentItem> itemList = new ArrayList<ContentItem>(); Set<ContentItem> allItems = contentService.loadChildren(collection, null); for (ContentItem item : allItems) { if (isShareableItem(item)) itemList.add(item); } return itemList; } private List<ContentItem> getModifiedItems(SyncToken prevToken, CollectionItem collection) { ArrayList<ContentItem> itemList = new ArrayList<ContentItem>(); Set<ContentItem> items = contentService.loadChildren(collection, new Date(prevToken.getTimestamp())); for (ContentItem item : items) { if (isShareableItem(item)) itemList.add(item); } return itemList; } private List<ItemTombstone> getRecentTombstones(SyncToken prevToken, CollectionItem collection) { ArrayList<ItemTombstone> tombstones = new ArrayList<ItemTombstone>(); if (prevToken.isValid(collection)) return tombstones; for (Tombstone tombstone : collection.getTombstones()) { if (tombstone instanceof ItemTombstone) if (prevToken.isTombstoneRecent(tombstone)) tombstones.add((ItemTombstone) tombstone); } return tombstones; } private boolean isShareableItem(Item item) { // only share NoteItems until Chandler and Cosmo UI can cope // with non-Note items return item instanceof NoteItem; } }