public static FreeBusy getWorkingHours(
      Account authAcct, boolean asAdmin, Account account, String name, long start, long end)
      throws ServiceException {
    // If free/busy viewing is blocked, so is viewing working hours.
    AccessManager accessMgr = AccessManager.getInstance();
    boolean accountAceAllowed = accessMgr.canDo(authAcct, account, User.R_viewFreeBusy, asAdmin);
    if (!accountAceAllowed) return FreeBusy.nodataFreeBusy(account.getName(), start, end);

    // Get the working hours preference and parse it.
    String workingHoursPref = account.getPrefCalendarWorkingHours();
    HoursByDay workingHoursByDay = new HoursByDay(workingHoursPref);

    // Build a recurrence rule for each day of the week and expand over the time range.
    IntervalList intervals = new IntervalList(start, end);
    ICalTimeZone tz = Util.getAccountTimeZone(account);
    TimeZoneMap tzmap = new TimeZoneMap(tz);
    StartSpec startSpec = new StartSpec(start, tz);
    for (int day = 1; day <= 7; ++day) {
      TimeRange timeRange = workingHoursByDay.getHoursForDay(day);
      if (timeRange.enabled) {
        IRecurrence rrule = getRecurrenceForDay(day, startSpec, timeRange, tz, tzmap);
        List<Instance> instances = rrule.expandInstances(0, start, end);
        for (Instance inst : instances) {
          Interval ival =
              new Interval(inst.getStart(), inst.getEnd(), IcalXmlStrMap.FBTYPE_BUSY_UNAVAILABLE);
          intervals.addInterval(ival);
        }
      }
    }
    // Invert FREE and BUSY_UNAVAILABLE in the intervals so that working hours are displayed as free
    // and non-working
    // hours are shown as out-of-office.
    for (Iterator<Interval> iter = intervals.iterator(); iter.hasNext(); ) {
      Interval interval = iter.next();
      String status = interval.getStatus();
      interval.setStatus(invertStatus(status));
    }
    return new FreeBusy(name, intervals, start, end);
  }
  private void compile() throws ServiceException {
    assert clauses != null;
    if (clauses.isEmpty()) {
      return;
    }

    // Convert list of Queries into list of QueryOperations, then optimize them.
    // this generates all of the query operations
    operation = parseTree.compile(mailbox);
    ZimbraLog.search.debug("OP=%s", operation);

    // expand the is:local and is:remote parts into in:(LIST)'s
    operation = operation.expandLocalRemotePart(mailbox);
    ZimbraLog.search.debug("AFTEREXP=%s", operation);

    // optimize the query down
    operation = operation.optimize(mailbox);
    ZimbraLog.search.debug("OPTIMIZED=%s", operation);
    if (operation == null || operation instanceof NoTermQueryOperation) {
      operation = new NoResultsQueryOperation();
      return;
    }

    // Use the OperationContext to update the set of visible referenced folders, local AND remote.
    Set<QueryTarget> targets = operation.getQueryTargets();
    assert (operation instanceof UnionQueryOperation
        || QueryTarget.getExplicitTargetCount(targets) <= 1);

    // easiest to treat the query two unions: one the LOCAL and one REMOTE parts
    UnionQueryOperation remoteOps = new UnionQueryOperation();
    UnionQueryOperation localOps = new UnionQueryOperation();

    if (operation instanceof UnionQueryOperation) {
      UnionQueryOperation union = (UnionQueryOperation) operation;
      // separate out the LOCAL vs REMOTE parts...
      for (QueryOperation op : union.operations) {
        Set<QueryTarget> set = op.getQueryTargets();

        // this assertion OK because we have already distributed multi-target query ops
        // during the optimize() step
        assert (QueryTarget.getExplicitTargetCount(set) <= 1);

        // the assertion above is critical: the code below all assumes
        // that we only have ONE target (ie we've already distributed if necessary)

        if (QueryTarget.hasExternalTarget(set)) {
          remoteOps.add(op);
        } else {
          localOps.add(op);
        }
      }
    } else {
      // single target: might be local, might be remote
      Set<QueryTarget> set = operation.getQueryTargets();
      // this assertion OK because we have already distributed multi-target query ops
      // during the optimize() step
      assert (QueryTarget.getExplicitTargetCount(set) <= 1);

      if (QueryTarget.hasExternalTarget(set)) {
        remoteOps.add(operation);
      } else {
        localOps.add(operation);
      }
    }

    // Handle the REMOTE side:
    if (!remoteOps.operations.isEmpty()) {
      // Since optimize() has already been run, we know that each of our ops
      // only has one target (or none).  Find those operations which have
      // an external target and wrap them in RemoteQueryOperations

      // iterate backwards so we can remove/add w/o screwing iteration
      for (int i = remoteOps.operations.size() - 1; i >= 0; i--) {
        QueryOperation op = remoteOps.operations.get(i);
        Set<QueryTarget> set = op.getQueryTargets();

        // this assertion OK because we have already distributed multi-target query ops
        // during the optimize() step
        assert (QueryTarget.getExplicitTargetCount(set) <= 1);

        // the assertion above is critical: the code below all assumes
        // that we only have ONE target (ie we've already distributed if necessary)

        if (QueryTarget.hasExternalTarget(set)) {
          remoteOps.operations.remove(i);
          boolean foundOne = false;
          // find a remoteOp to add this one to
          for (QueryOperation remoteOp : remoteOps.operations) {
            if (remoteOp instanceof RemoteQueryOperation) {
              if (((RemoteQueryOperation) remoteOp).tryAddOredOperation(op)) {
                foundOne = true;
                break;
              }
            }
          }
          if (!foundOne) {
            RemoteQueryOperation remoteOp = new RemoteQueryOperation();
            remoteOp.tryAddOredOperation(op);
            remoteOps.operations.add(i, remoteOp);
          }
        }
      }

      // ...we need to call setup on every RemoteQueryOperation we end up with...
      for (QueryOperation remoteOp : remoteOps.operations) {
        assert (remoteOp instanceof RemoteQueryOperation);
        try {
          ((RemoteQueryOperation) remoteOp).setup(protocol, octxt.getAuthToken(), params);
        } catch (Exception e) {
          ZimbraLog.search.info("Ignoring %s during RemoteQuery generation for %s", e, remoteOps);
        }
      }
    }

    // For the LOCAL parts of the query, do permission checks, do trash/spam exclusion
    if (!localOps.operations.isEmpty()) {
      ZimbraLog.search.debug("LOCAL_IN=%s", localOps);

      Account authAcct = octxt != null ? octxt.getAuthenticatedUser() : mailbox.getAccount();

      // Now, for all the LOCAL PARTS of the query, add the trash/spam exclusion part
      boolean includeTrash = false;
      boolean includeSpam = false;
      if (params.inDumpster()) {
        // Always include trash and spam for dumpster searches.  Excluding spam is a client side
        // choice.
        includeTrash = true;
        includeSpam = true;
        if (!mailbox.hasFullAdminAccess(
            octxt)) { // If the requester is not an admin, limit to recent date range.
          long now = octxt != null ? octxt.getTimestamp() : System.currentTimeMillis();
          long mdate = now - authAcct.getDumpsterUserVisibleAge();
          IntersectionQueryOperation and = new IntersectionQueryOperation();
          DBQueryOperation db = new DBQueryOperation();
          db.addMDateRange(mdate, false, -1L, false, true);
          and.addQueryOp((QueryOperation) localOps.clone());
          and.addQueryOp(db);
          localOps.operations.clear();
          localOps.operations.add(and);
        }
      } else {
        includeTrash = authAcct.isPrefIncludeTrashInSearch();
        ;
        includeSpam = authAcct.isPrefIncludeSpamInSearch();
      }
      if (!includeTrash || !includeSpam) {
        // First check that we aren't specifically looking for items in one of these.
        // For instance, in ZWC, if "Include Shared Items" is selected, the Trash list may currently
        // use a
        // search similar to : 'in:"trash" (inid:565 OR is:local)'
        // where 565 is the folder ID for a shared folder.  We don't want to end up doing a search
        // for items
        // that are both in "trash" and NOT in "trash"...
        for (Query q : clauses) {
          if (q.toString().equalsIgnoreCase("Q(IN:Trash)")) {
            includeTrash = true;
          }
          if (q.toString().equalsIgnoreCase("Q(IN:Junk)")) {
            includeSpam = true;
          }
        }
      }
      if (!includeTrash || !includeSpam) {
        List<QueryOperation> toAdd = new ArrayList<QueryOperation>();
        for (Iterator<QueryOperation> iter = localOps.operations.iterator(); iter.hasNext(); ) {
          QueryOperation cur = iter.next();
          if (!cur.hasSpamTrashSetting()) {
            QueryOperation newOp = cur.ensureSpamTrashSetting(mailbox, includeTrash, includeSpam);
            if (newOp != cur) {
              iter.remove();
              toAdd.add(newOp);
            }
          }
        }
        localOps.operations.addAll(toAdd);
        ZimbraLog.search.debug("LOCAL_AFTER_TRASH/SPAM/DUMPSTER=%s", localOps);
      }

      // Check to see if we need to filter out private appointment data
      boolean allowPrivateAccess = true;
      if (octxt != null) {
        allowPrivateAccess =
            AccessManager.getInstance()
                .allowPrivateAccess(
                    octxt.getAuthenticatedUser(),
                    mailbox.getAccount(),
                    octxt.isUsingAdminPrivileges());
      }

      //
      // bug 28892 - ACL.RIGHT_PRIVATE support:
      //
      // Basically, if ACL.RIGHT_PRIVATE is set somewhere, and if we're excluding private items from
      // search, then we need to run the query twice -- once over the whole mailbox with
      // private items excluded and then UNION it with a second run, this time only in the
      // RIGHT_PRIVATE enabled folders, with private items enabled.
      //
      UnionQueryOperation clonedLocal = null;
      Set<Folder> hasFolderRightPrivateSet = new HashSet<Folder>();

      // ...don't do any of this if they aren't asking for a calendar type...
      Set<MailItem.Type> types = params.getTypes();
      boolean hasCalendarType =
          types.contains(MailItem.Type.APPOINTMENT) || types.contains(MailItem.Type.TASK);
      if (hasCalendarType && !allowPrivateAccess && getTextOperationCount(localOps) > 0) {
        // the searcher is NOT allowed to see private items globally....lets check
        // to see if there are any individual folders that they DO have rights to...
        // if there are any, then we'll need to run special searches in those
        // folders
        Set<Folder> allVisibleFolders = mailbox.getVisibleFolders(octxt);
        if (allVisibleFolders == null) {
          allVisibleFolders = new HashSet<Folder>();
          allVisibleFolders.addAll(mailbox.getFolderList(octxt, SortBy.NONE));
        }
        for (Folder f : allVisibleFolders) {
          if (f.getType() == MailItem.Type.FOLDER
              && CalendarItem.allowPrivateAccess(f, authAcct, false)) {
            hasFolderRightPrivateSet.add(f);
          }
        }
        if (!hasFolderRightPrivateSet.isEmpty()) {
          clonedLocal = (UnionQueryOperation) localOps.clone();
        }
      }

      Set<Folder> visibleFolders = mailbox.getVisibleFolders(octxt);

      localOps = handleLocalPermissionChecks(localOps, visibleFolders, allowPrivateAccess);

      ZimbraLog.search.debug("LOCAL_AFTER_PERM_CHECKS=%s", localOps);

      if (!hasFolderRightPrivateSet.isEmpty()) {
        ZimbraLog.search.debug("CLONED_LOCAL_BEFORE_PERM=%s", clonedLocal);

        // now we're going to setup the clonedLocal tree
        // to run with private access ALLOWED, over the set of folders
        // that have RIGHT_PRIVATE (note that we build this list from the visible
        // folder list, so we are
        clonedLocal = handleLocalPermissionChecks(clonedLocal, hasFolderRightPrivateSet, true);

        ZimbraLog.search.debug("CLONED_LOCAL_AFTER_PERM=%s", clonedLocal);

        // clonedLocal should only have the single INTERSECT in it
        assert (clonedLocal.operations.size() == 1);

        QueryOperation optimizedClonedLocal = clonedLocal.optimize(mailbox);
        ZimbraLog.search.debug("CLONED_LOCAL_AFTER_OPTIMIZE=%s", optimizedClonedLocal);

        UnionQueryOperation withPrivateExcluded = localOps;
        localOps = new UnionQueryOperation();
        localOps.add(withPrivateExcluded);
        localOps.add(optimizedClonedLocal);

        ZimbraLog.search.debug("LOCAL_WITH_CLONED=%s", localOps);

        //
        // we should end up with:
        //
        // localOps =
        //    UNION(withPrivateExcluded,
        //          UNION(INTERSECT(clonedLocal,
        //                          UNION(hasFolderRightPrivateList)
        //                         )
        //                )
        //          )
        //
      }
    }

    UnionQueryOperation union = new UnionQueryOperation();
    union.add(localOps);
    union.add(remoteOps);
    ZimbraLog.search.debug("BEFORE_FINAL_OPT=%s", union);
    operation = union.optimize(mailbox);

    ZimbraLog.search.debug("COMPILED=%s", operation);
  }