private PersonFavourite addFavouriteDocumentOrFolder(
      String userName, Type type, NodeRef nodeRef) {
    Map<PersonFavouriteKey, PersonFavourite> personFavourites = getFavouriteNodes(userName, type);
    PersonFavourite personFavourite = getPersonFavourite(userName, type, nodeRef);
    if (personFavourite == null) {
      Date createdAt = new Date();
      final String name = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
      personFavourite = new PersonFavourite(userName, nodeRef, type, name, createdAt);
      personFavourites.put(personFavourite.getKey(), personFavourite);
      updateFavouriteNodes(userName, type, personFavourites);

      QName nodeClass = nodeService.getType(nodeRef);
      final String finalRef = nodeRef.getId();
      final QName nodeType = nodeClass;

      eventPublisher.publishEvent(
          new EventPreparator() {
            @Override
            public Event prepareEvent(String user, String networkId, String transactionId) {
              return new ActivityEvent(
                  "favorite.added",
                  transactionId,
                  networkId,
                  user,
                  finalRef,
                  null,
                  nodeType == null ? null : nodeType.toString(),
                  Client.asType(ClientType.script),
                  null,
                  name,
                  null,
                  0l,
                  null);
            }
          });

      OnAddFavouritePolicy policy = onAddFavouriteDelegate.get(nodeRef, nodeClass);
      policy.onAddFavourite(userName, nodeRef);
    }

    return personFavourite;
  }
  private boolean removeFavouriteNode(String userName, Type type, NodeRef nodeRef) {
    boolean exists = false;

    Map<PersonFavouriteKey, PersonFavourite> personFavourites = getFavouriteNodes(userName, type);

    PersonFavouriteKey personFavouriteKey = new PersonFavouriteKey(userName, null, type, nodeRef);
    PersonFavourite personFavourite = personFavourites.remove(personFavouriteKey);
    exists = personFavourite != null;
    updateFavouriteNodes(userName, type, personFavourites);

    QName nodeClass = nodeService.getType(nodeRef);
    final String finalRef = nodeRef.getId();
    final QName nodeType = nodeClass;

    eventPublisher.publishEvent(
        new EventPreparator() {
          @Override
          public Event prepareEvent(String user, String networkId, String transactionId) {
            return new ActivityEvent(
                "favorite.removed",
                transactionId,
                networkId,
                user,
                finalRef,
                null,
                nodeType == null ? null : nodeType.toString(),
                Client.asType(ClientType.script),
                null,
                null,
                null,
                0l,
                null);
          }
        });

    OnRemoveFavouritePolicy policy = onRemoveFavouriteDelegate.get(nodeRef, nodeClass);
    policy.onRemoveFavourite(userName, nodeRef);

    return exists;
  }
  /**
   * Stream content implementation
   *
   * @param req The request
   * @param res The response
   * @param reader The reader
   * @param nodeRef The content nodeRef if applicable
   * @param propertyQName The content property if applicable
   * @param attach Indicates whether the content should be streamed as an attachment or not
   * @param modified Modified date of content
   * @param eTag ETag to use
   * @param attachFileName Optional file name to use when attach is <code>true</code>
   * @throws IOException
   */
  public void streamContentImpl(
      WebScriptRequest req,
      WebScriptResponse res,
      ContentReader reader,
      final NodeRef nodeRef,
      final QName propertyQName,
      final boolean attach,
      final Date modified,
      String eTag,
      final String attachFileName,
      Map<String, Object> model)
      throws IOException {
    setAttachment(null, res, attach, attachFileName);

    // establish mimetype
    String mimetype = reader.getMimetype();
    String extensionPath = req.getExtensionPath();
    if (mimetype == null || mimetype.length() == 0) {
      mimetype = MimetypeMap.MIMETYPE_BINARY;
      int extIndex = extensionPath.lastIndexOf('.');
      if (extIndex != -1) {
        String ext = extensionPath.substring(extIndex + 1);
        mimetype = mimetypeService.getMimetype(ext);
      }
    }

    res.setHeader(HEADER_ACCEPT_RANGES, "bytes");
    try {
      boolean processedRange = false;
      String range = req.getHeader(HEADER_CONTENT_RANGE);
      final long size = reader.getSize();
      final String encoding = reader.getEncoding();

      if (attach) {
        final String finalMimetype = mimetype;

        eventPublisher.publishEvent(
            new EventPreparator() {
              @Override
              public Event prepareEvent(String user, String networkId, String transactionId) {
                String siteId = siteService.getSiteShortName(nodeRef);

                return new ContentEventImpl(
                    ContentEvent.DOWNLOAD,
                    user,
                    networkId,
                    transactionId,
                    nodeRef.getId(),
                    siteId,
                    propertyQName.toString(),
                    Client.webclient,
                    attachFileName,
                    finalMimetype,
                    size,
                    encoding);
              }
            });
      }

      if (range == null) {
        range = req.getHeader(HEADER_RANGE);
      }
      if (range != null) {
        if (logger.isDebugEnabled()) logger.debug("Found content range header: " + range);

        // ensure the range header is starts with "bytes=" and process the range(s)
        if (range.length() > 6) {
          if (range.indexOf(',') != -1 && (nodeRef == null || propertyQName == null)) {
            if (logger.isInfoEnabled()) logger.info("Multi-range only supported for nodeRefs");
          } else {
            HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(contentService);
            processedRange =
                rangeProcessor.processRange(
                    res,
                    reader,
                    range.substring(6),
                    nodeRef,
                    propertyQName,
                    mimetype,
                    req.getHeader(HEADER_USER_AGENT));
          }
        }
      }
      if (processedRange == false) {
        if (logger.isDebugEnabled()) logger.debug("Sending complete file content...");

        // set mimetype for the content and the character encoding for the stream
        res.setContentType(mimetype);
        res.setContentEncoding(encoding);

        // return the complete entity range
        res.setHeader(
            HEADER_CONTENT_RANGE,
            "bytes 0-" + Long.toString(size - 1L) + "/" + Long.toString(size));
        res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(size));

        // set caching
        setResponseCache(res, modified, eTag, model);

        // get the content and stream directly to the response output stream
        // assuming the repository is capable of streaming in chunks, this should allow large files
        // to be streamed directly to the browser response stream.
        reader.getContent(res.getOutputStream());
      }
    } catch (SocketException e1) {
      // the client cut the connection - our mission was accomplished apart from a little error
      // message
      if (logger.isInfoEnabled()) logger.info("Client aborted stream read:\n\tcontent: " + reader);
    } catch (ContentIOException e2) {
      if (logger.isInfoEnabled()) logger.info("Client aborted stream read:\n\tcontent: " + reader);
    }
  }