[CalendarServer-changes] [12211] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:23:57 PDT 2014


Revision: 12211
          http://trac.calendarserver.org//changeset/12211
Author:   cdaboo at apple.com
Date:     2014-01-02 09:19:18 -0800 (Thu, 02 Jan 2014)
Log Message:
-----------
Merge cross-pod sharing work to trunk.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/push/test/test_notifier.py
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/calendarserver/tools/dbinspect.py
    CalendarServer/trunk/calendarserver/tools/purge.py
    CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py
    CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml
    CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml
    CalendarServer/trunk/calendarserver/tools/test/test_export.py
    CalendarServer/trunk/calendarserver/tools/test/test_purge.py
    CalendarServer/trunk/conf/caldavd-test-podB.plist
    CalendarServer/trunk/support/build.sh
    CalendarServer/trunk/twisted/plugins/caldav.py
    CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/resource.py
    CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml
    CalendarServer/trunk/twistedcaldav/directory/test/augments.xml
    CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
    CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py
    CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/sharing.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
    CalendarServer/trunk/twistedcaldav/test/test_sharing.py
    CalendarServer/trunk/twistedcaldav/test/test_xml.py
    CalendarServer/trunk/txdav/base/datastore/subpostgres.py
    CalendarServer/trunk/txdav/base/datastore/util.py
    CalendarServer/trunk/txdav/caldav/datastore/index_file.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py
    CalendarServer/trunk/txdav/caldav/datastore/test/util.py
    CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py
    CalendarServer/trunk/txdav/caldav/icalendarstore.py
    CalendarServer/trunk/txdav/carddav/datastore/index_file.py
    CalendarServer/trunk/txdav/carddav/datastore/sql.py
    CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py
    CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/trunk/txdav/common/datastore/sql_tables.py
    CalendarServer/trunk/txdav/common/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/common/datastore/test/util.py
    CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py
    CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py
    CalendarServer/trunk/txdav/common/icommondatastore.py
    CalendarServer/trunk/txdav/common/idirectoryservice.py
    CalendarServer/trunk/txweb2/http.py

Added Paths:
-----------
    CalendarServer/trunk/txdav/caldav/datastore/query/
    CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py
    CalendarServer/trunk/txdav/caldav/datastore/query/builder.py
    CalendarServer/trunk/txdav/caldav/datastore/query/filter.py
    CalendarServer/trunk/txdav/caldav/datastore/query/generator.py
    CalendarServer/trunk/txdav/caldav/datastore/query/test/
    CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py
    CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py
    CalendarServer/trunk/txdav/caldav/datastore/sql_external.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py
    CalendarServer/trunk/txdav/carddav/datastore/query/
    CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py
    CalendarServer/trunk/txdav/carddav/datastore/query/builder.py
    CalendarServer/trunk/txdav/carddav/datastore/query/filter.py
    CalendarServer/trunk/txdav/carddav/datastore/query/test/
    CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py
    CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py
    CalendarServer/trunk/txdav/carddav/datastore/sql_external.py
    CalendarServer/trunk/txdav/common/datastore/podding/
    CalendarServer/trunk/txdav/common/datastore/podding/__init__.py
    CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
    CalendarServer/trunk/txdav/common/datastore/podding/request.py
    CalendarServer/trunk/txdav/common/datastore/podding/resource.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/
    CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
    CalendarServer/trunk/txdav/common/datastore/query/
    CalendarServer/trunk/txdav/common/datastore/query/__init__.py
    CalendarServer/trunk/txdav/common/datastore/query/expression.py
    CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py
    CalendarServer/trunk/txdav/common/datastore/query/generator.py
    CalendarServer/trunk/txdav/common/datastore/query/test/
    CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py
    CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py
    CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py
    CalendarServer/trunk/txdav/common/datastore/sql_external.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql

Removed Paths:
-------------
    CalendarServer/trunk/twistedcaldav/query/
    CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py
    CalendarServer/trunk/txdav/caldav/datastore/query/builder.py
    CalendarServer/trunk/txdav/caldav/datastore/query/filter.py
    CalendarServer/trunk/txdav/caldav/datastore/query/generator.py
    CalendarServer/trunk/txdav/caldav/datastore/query/test/
    CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py
    CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py
    CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py
    CalendarServer/trunk/txdav/carddav/datastore/query/builder.py
    CalendarServer/trunk/txdav/carddav/datastore/query/filter.py
    CalendarServer/trunk/txdav/carddav/datastore/query/test/
    CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py
    CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py
    CalendarServer/trunk/txdav/common/datastore/podding/__init__.py
    CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
    CalendarServer/trunk/txdav/common/datastore/podding/request.py
    CalendarServer/trunk/txdav/common/datastore/podding/resource.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/
    CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
    CalendarServer/trunk/txdav/common/datastore/query/__init__.py
    CalendarServer/trunk/txdav/common/datastore/query/expression.py
    CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py
    CalendarServer/trunk/txdav/common/datastore/query/generator.py
    CalendarServer/trunk/txdav/common/datastore/query/test/
    CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py
    CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py
    CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py
    CalendarServer/trunk/txdav/common/datastore/sql_legacy.py

Property Changed:
----------------
    CalendarServer/trunk/


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalDAVTester/trunk:11193-11198
/CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/performance-tweaks:11824-11836
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/start-service-start-loop:11060-11065
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalDAVTester/trunk:11193-11198
/CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/release/CalendarServer-5.2-dev:11972
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/cross-pod-sharing:12038-12191
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/performance-tweaks:11824-11836
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/start-service-start-loop:11060-11065
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593

Modified: CalendarServer/trunk/calendarserver/push/test/test_notifier.py
===================================================================
--- CalendarServer/trunk/calendarserver/push/test/test_notifier.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/push/test/test_notifier.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -184,10 +184,10 @@
 class NotifierFactory(StoreTestCase):
 
     requirements = {
-        "home1" : {
+        "user01" : {
             "calendar_1" : {}
         },
-        "home2" : {
+        "user02" : {
             "calendar_1" : {}
         },
     }
@@ -208,23 +208,23 @@
     @inlineCallbacks
     def test_homeNotifier(self):
 
-        home = yield self.homeUnderTest()
+        home = yield self.homeUnderTest(name="user01")
         yield home.notifyChanged(category=ChangeCategory.default)
         self.assertEquals(self.notifierFactory.history,
-            [("/CalDAV/example.com/home1/", PushPriority.high)])
+            [("/CalDAV/example.com/user01/", PushPriority.high)])
         yield self.commit()
 
 
     @inlineCallbacks
     def test_calendarNotifier(self):
 
-        calendar = yield self.calendarUnderTest()
+        calendar = yield self.calendarUnderTest(home="user01")
         yield calendar.notifyChanged(category=ChangeCategory.default)
         self.assertEquals(
             set(self.notifierFactory.history),
             set([
-                ("/CalDAV/example.com/home1/", PushPriority.high),
-                ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high)])
+                ("/CalDAV/example.com/user01/", PushPriority.high),
+                ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high)])
         )
         yield self.commit()
 
@@ -232,28 +232,28 @@
     @inlineCallbacks
     def test_shareWithNotifier(self):
 
-        calendar = yield self.calendarUnderTest()
-        yield calendar.inviteUserToShare("home2", _BIND_MODE_WRITE, "")
+        calendar = yield self.calendarUnderTest(home="user01")
+        yield calendar.inviteUserToShare("user02", _BIND_MODE_WRITE, "")
         self.assertEquals(
             set(self.notifierFactory.history),
             set([
-                ("/CalDAV/example.com/home1/", PushPriority.high),
-                ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high),
-                ("/CalDAV/example.com/home2/", PushPriority.high),
-                ("/CalDAV/example.com/home2/notification/", PushPriority.high),
+                ("/CalDAV/example.com/user01/", PushPriority.high),
+                ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high),
+                ("/CalDAV/example.com/user02/", PushPriority.high),
+                ("/CalDAV/example.com/user02/notification/", PushPriority.high),
             ])
         )
         yield self.commit()
 
-        calendar = yield self.calendarUnderTest()
-        yield calendar.uninviteUserFromShare("home2")
+        calendar = yield self.calendarUnderTest(home="user01")
+        yield calendar.uninviteUserFromShare("user02")
         self.assertEquals(
             set(self.notifierFactory.history),
             set([
-                ("/CalDAV/example.com/home1/", PushPriority.high),
-                ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high),
-                ("/CalDAV/example.com/home2/", PushPriority.high),
-                ("/CalDAV/example.com/home2/notification/", PushPriority.high),
+                ("/CalDAV/example.com/user01/", PushPriority.high),
+                ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high),
+                ("/CalDAV/example.com/user02/", PushPriority.high),
+                ("/CalDAV/example.com/user02/notification/", PushPriority.high),
             ])
         )
         yield self.commit()
@@ -262,20 +262,20 @@
     @inlineCallbacks
     def test_sharedCalendarNotifier(self):
 
-        calendar = yield self.calendarUnderTest()
-        shareeView = yield calendar.inviteUserToShare("home2", _BIND_MODE_WRITE, "")
+        calendar = yield self.calendarUnderTest(home="user01")
+        shareeView = yield calendar.inviteUserToShare("user02", _BIND_MODE_WRITE, "")
         yield shareeView.acceptShare("")
         shareName = shareeView.name()
         yield self.commit()
         self.notifierFactory.reset()
 
-        shared = yield self.calendarUnderTest(home="home2", name=shareName)
+        shared = yield self.calendarUnderTest(home="user02", name=shareName)
         yield shared.notifyChanged(category=ChangeCategory.default)
         self.assertEquals(
             set(self.notifierFactory.history),
             set([
-                ("/CalDAV/example.com/home1/", PushPriority.high),
-                ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high)])
+                ("/CalDAV/example.com/user01/", PushPriority.high),
+                ("/CalDAV/example.com/user01/calendar_1/", PushPriority.high)])
         )
         yield self.commit()
 
@@ -283,12 +283,12 @@
     @inlineCallbacks
     def test_notificationNotifier(self):
 
-        notifications = yield self.transactionUnderTest().notificationsWithUID("home1")
+        notifications = yield self.transactionUnderTest().notificationsWithUID("user01")
         yield notifications.notifyChanged(category=ChangeCategory.default)
         self.assertEquals(
             set(self.notifierFactory.history),
             set([
-                ("/CalDAV/example.com/home1/", PushPriority.high),
-                ("/CalDAV/example.com/home1/notification/", PushPriority.high)])
+                ("/CalDAV/example.com/user01/", PushPriority.high),
+                ("/CalDAV/example.com/user01/notification/", PushPriority.high)])
         )
         yield self.commit()

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1691,11 +1691,9 @@
                 raise StoreNotAvailable()
 
             from twisted.internet import reactor
-            pool = PeerConnectionPool(reactor, store.newTransaction,
-                                      7654, schema)
+            pool = PeerConnectionPool(reactor, store.newTransaction, config.WorkQueue.ampPort, schema)
             store.queuer = store.queuer.transferProposalCallbacks(pool)
-            controlSocket.addFactory(_QUEUE_ROUTE,
-                                     pool.workerListenerFactory())
+            controlSocket.addFactory(_QUEUE_ROUTE, pool.workerListenerFactory())
             # TODO: now that we have the shared control socket, we should get
             # rid of the connection dispenser and make a shared / async
             # connection pool implementation that can dispense transactions

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -92,6 +92,7 @@
 from calendarserver.webadmin.resource import WebAdminResource
 from calendarserver.webcal.resource import WebCalendarResource
 
+from txdav.common.datastore.podding.resource import ConduitResource
 from txdav.common.datastore.sql import CommonDataStore as CommonSQLDataStore
 from txdav.common.datastore.file import CommonDataStore as CommonFileDataStore
 from txdav.common.datastore.sql import current_sql_schema
@@ -407,6 +408,7 @@
     rootResourceClass = RootResource
     calendarResourceClass = DirectoryCalendarHomeProvisioningResource
     iScheduleResourceClass = IScheduleInboxResource
+    conduitResourceClass = ConduitResource
     timezoneServiceResourceClass = TimezoneServiceResource
     timezoneStdServiceResourceClass = TimezoneStdServiceResource
     webCalendarResourceClass = WebCalendarResource
@@ -636,7 +638,7 @@
             addSystemEventTrigger("after", "startup", timezoneStdService.onStartup)
 
     #
-    # iSchedule service for podding
+    # iSchedule/cross-pod service for podding
     #
     if config.Servers.Enabled:
         log.info("Setting up iSchedule podding inbox resource: {cls}", cls=iScheduleResourceClass)
@@ -648,6 +650,14 @@
         )
         root.putChild(config.Servers.InboxName, ischedule)
 
+        log.info("Setting up podding conduit resource: {cls}", cls=conduitResourceClass)
+
+        conduit = conduitResourceClass(
+            root,
+            newStore,
+        )
+        root.putChild(config.Servers.ConduitName, conduit)
+
     #
     # iSchedule service (not used for podding)
     #

Modified: CalendarServer/trunk/calendarserver/tools/dbinspect.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/dbinspect.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/dbinspect.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -37,8 +37,8 @@
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.query import calendarqueryfilter
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from txdav.caldav.datastore.query.filter import Filter
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
 from uuid import UUID
 import os
@@ -757,10 +757,10 @@
                           name="VCALENDAR",
                        )
                   )
-        filter = calendarqueryfilter.Filter(filter)
+        filter = Filter(filter)
         filter.settimezone(None)
 
-        matches = yield calendar._index.indexedSearch(filter, useruid=uid, fbtype=False)
+        matches = yield calendar.search(filter, useruid=uid, fbtype=False)
         if matches is None:
             returnValue(None)
         for name, _ignore_uid, _ignore_type in matches:

Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/purge.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -30,10 +30,9 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
 from twistedcaldav.directory.directory import DirectoryRecord
-from twistedcaldav.query import calendarqueryfilter
 
+from txdav.caldav.datastore.query.filter import Filter
 from txdav.xml import element as davxml
 
 
@@ -817,13 +816,13 @@
         query_filter = caldavxml.Filter(
               caldavxml.ComponentFilter(
                   caldavxml.ComponentFilter(
-                      TimeRange(start=whenString,),
+                      caldavxml.TimeRange(start=whenString,),
                       name=("VEVENT",),
                   ),
                   name="VCALENDAR",
                )
           )
-        query_filter = calendarqueryfilter.Filter(query_filter)
+        query_filter = Filter(query_filter)
 
         count = 0
         txn = self.store.newTransaction()
@@ -844,7 +843,7 @@
                     childNames.append(childName)
             else:
                 # events matching filter
-                for childName, _ignore_childUid, _ignore_childType in (yield calendar._index.indexedSearch(query_filter)):
+                for childName, _ignore_childUid, _ignore_childType in (yield calendar.search(query_filter)):
                     childNames.append(childName)
             yield txn.commit()
 

Modified: CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/shell/test/test_vfs.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -28,45 +28,55 @@
 
 class TestListEntry(TestCase):
     def test_toString(self):
-        self.assertEquals(ListEntry(None, File  , "thingo"           ).toString(), "thingo" )
-        self.assertEquals(ListEntry(None, File  , "thingo", Foo="foo").toString(), "thingo" )
-        self.assertEquals(ListEntry(None, Folder, "thingo"           ).toString(), "thingo/")
+        self.assertEquals(ListEntry(None, File  , "thingo").toString(), "thingo")
+        self.assertEquals(ListEntry(None, File  , "thingo", Foo="foo").toString(), "thingo")
+        self.assertEquals(ListEntry(None, Folder, "thingo").toString(), "thingo/")
         self.assertEquals(ListEntry(None, Folder, "thingo", Foo="foo").toString(), "thingo/")
 
+
     def test_fieldNamesImplicit(self):
         # This test assumes File doesn't set list.fieldNames.
         assert not hasattr(File.list, "fieldNames")
 
         self.assertEquals(set(ListEntry(File(None, ()), File, "thingo").fieldNames), set(("Name",)))
 
+
     def test_fieldNamesExplicit(self):
         def fieldNames(fileClass):
             return ListEntry(fileClass(None, ()), fileClass, "thingo", Flavor="Coconut", Style="Hard")
 
         # Full list
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile1(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ("Name", "Flavor")
-        self.assertEquals(fieldNames(MyFile).fieldNames, ("Name", "Flavor"))
+        self.assertEquals(fieldNames(MyFile1).fieldNames, ("Name", "Flavor"))
 
+
         # Full list, different order
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile2(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ("Flavor", "Name")
-        self.assertEquals(fieldNames(MyFile).fieldNames, ("Flavor", "Name"))
+        self.assertEquals(fieldNames(MyFile2).fieldNames, ("Flavor", "Name"))
 
+
         # Omits Name, which is implicitly added
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile3(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ("Flavor",)
-        self.assertEquals(fieldNames(MyFile).fieldNames, ("Name", "Flavor"))
+        self.assertEquals(fieldNames(MyFile3).fieldNames, ("Name", "Flavor"))
 
+
         # Emtpy
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile4(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ()
-        self.assertEquals(fieldNames(MyFile).fieldNames, ("Name",))
+        self.assertEquals(fieldNames(MyFile4).fieldNames, ("Name",))
 
+
     def test_toFieldsImplicit(self):
         # This test assumes File doesn't set list.fieldNames.
         assert not hasattr(File.list, "fieldNames")
@@ -77,33 +87,41 @@
             ("thingo", "Coconut", "Hard")
         )
 
+
     def test_toFieldsExplicit(self):
         def fields(fileClass):
             return tuple(ListEntry(fileClass(None, ()), fileClass, "thingo", Flavor="Coconut", Style="Hard").toFields())
 
         # Full list
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile1(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ("Name", "Flavor")
-        self.assertEquals(fields(MyFile), ("thingo", "Coconut"))
+        self.assertEquals(fields(MyFile1), ("thingo", "Coconut"))
 
+
         # Full list, different order
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile2(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ("Flavor", "Name")
-        self.assertEquals(fields(MyFile), ("Coconut", "thingo"))
+        self.assertEquals(fields(MyFile2), ("Coconut", "thingo"))
 
+
         # Omits Name, which is implicitly added
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile3(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ("Flavor",)
-        self.assertEquals(fields(MyFile), ("thingo", "Coconut"))
+        self.assertEquals(fields(MyFile3), ("thingo", "Coconut"))
 
+
         # Emtpy
-        class MyFile(File):
-            def list(self): return succeed(())
+        class MyFile4(File):
+            def list(self):
+                return succeed(())
             list.fieldNames = ()
-        self.assertEquals(fields(MyFile), ("thingo",))
+        self.assertEquals(fields(MyFile4), ("thingo",))
 
 
 
@@ -114,6 +132,7 @@
     def __init__(self, testCase):
         self.testCase = testCase
 
+
     def mktemp(self):
         return self.testCase.mktemp()
 
@@ -129,8 +148,9 @@
         """
         Create a L{UIDsFolder}.
         """
-        self.svc = ShellService(store=(yield buildStore(self, None)),
-                                directory=DirectoryStubber(self).service(),
+        directory = DirectoryStubber(self).service()
+        self.svc = ShellService(store=(yield buildStore(self, None, directoryService=directory)),
+                                directory=directory,
                                 options=None, reactor=None, config=None)
         self.folder = UIDsFolder(self.svc, ())
 
@@ -156,6 +176,3 @@
               {"Record Type": "users", "Short Name": "dreid",
               "Full Name": "David Reid", "Name": dreid}]
         )
-
-
-

Modified: CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/calverify/accounts.xml	2014-01-02 17:19:18 UTC (rev 12211)
@@ -47,4 +47,11 @@
     <name>Example User4</name>
     <email-address>example4 at example.com</email-address>
   </user>
+  <user>
+    <uid>home1</uid>
+    <guid>home1</guid>
+    <password>home1</password>
+    <name>Home 1</name>
+    <email-address>home1 at example.com</email-address>
+  </user>
 </accounts>

Modified: CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/purge/accounts.xml	2014-01-02 17:19:18 UTC (rev 12211)
@@ -26,4 +26,25 @@
     <name>Example User</name>
     <email-address>example at example.com</email-address>
   </user>
+  <user>
+    <uid>example2</uid>
+    <guid>37DB0C90-4DB1-4932-BC69-3DAB66F374F5</guid>
+    <password>example2</password>
+    <name>Example User 2</name>
+    <email-address>example2 at example.com</email-address>
+  </user>
+  <user>
+    <uid>home1</uid>
+    <guid>home1</guid>
+    <password>home1</password>
+    <name>Home 1</name>
+    <email-address>home1 at example.com</email-address>
+  </user>
+  <user>
+    <uid>home2</uid>
+    <guid>home2</guid>
+    <password>home2</password>
+    <name>Home 2</name>
+    <email-address>home2 at example.com</email-address>
+  </user>
 </accounts>

Modified: CalendarServer/trunk/calendarserver/tools/test/test_export.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_export.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/test_export.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -277,7 +277,7 @@
         """
         yield populateCalendarsFrom(
             {
-                "home1": {
+                "user01": {
                     "calendar1": {
                         "valentines-day.ics": (valentines, {})
                     }
@@ -291,7 +291,7 @@
 
         io = StringIO()
         yield exportToFile(
-            [(yield self.txn().calendarHomeWithUID("home1"))
+            [(yield self.txn().calendarHomeWithUID("user01"))
               .calendarWithName("calendar1")], io
         )
         self.assertEquals(Component.fromString(io.getvalue()),
@@ -306,7 +306,7 @@
         """
         yield populateCalendarsFrom(
             {
-                "home1": {
+                "user01": {
                     "calendar1": {
                         "valentines-day.ics": (valentines, {}),
                         "new-years-day.ics": (newYears, {})
@@ -324,7 +324,7 @@
 
         io = StringIO()
         yield exportToFile(
-            [(yield self.txn().calendarHomeWithUID("home1"))
+            [(yield self.txn().calendarHomeWithUID("user01"))
               .calendarWithName("calendar1")], io
         )
         self.assertEquals(Component.fromString(io.getvalue()),
@@ -342,7 +342,7 @@
         """
         yield populateCalendarsFrom(
             {
-                "home1": {
+                "user01": {
                     "calendar1": {
                         "1.ics": (one, {}), # EST
                         "2.ics": (another, {}), # EST
@@ -354,7 +354,7 @@
 
         io = StringIO()
         yield exportToFile(
-            [(yield self.txn().calendarHomeWithUID("home1"))
+            [(yield self.txn().calendarHomeWithUID("user01"))
               .calendarWithName("calendar1")], io
         )
         result = Component.fromString(io.getvalue())

Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_purge.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/calendarserver/tools/test/test_purge.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -806,16 +806,6 @@
 
     @inlineCallbacks
     def setUp(self):
-        self.patch(config.DirectoryService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "purge", "accounts.xml"
-            )
-        )
-        self.patch(config.ResourceService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "purge", "resources.xml"
-            )
-        )
         yield super(PurgePrincipalTests, self).setUp()
 
         txn = self._sqlCalendarStore.newTransaction()
@@ -850,6 +840,20 @@
         (yield txn.commit())
 
 
+    def configure(self):
+        super(PurgePrincipalTests, self).configure()
+        self.patch(config.DirectoryService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "purge", "accounts.xml"
+            )
+        )
+        self.patch(config.ResourceService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "purge", "resources.xml"
+            )
+        )
+
+
     @inlineCallbacks
     def populate(self):
         yield populateCalendarsFrom(self.requirements, self.storeUnderTest())

Modified: CalendarServer/trunk/conf/caldavd-test-podB.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test-podB.plist	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/conf/caldavd-test-podB.plist	2014-01-02 17:19:18 UTC (rev 12211)
@@ -43,6 +43,13 @@
     <array>
     </array>
 
+	<!--  Work Queue -->
+	<key>WorkQueue</key>
+	<dict>
+		<key>ampPort</key>
+		<integer>7655</integer>
+	</dict>
+
     <!-- Server root -->
     <key>ServerRoot</key>
     <string>./data/podB</string>
@@ -94,6 +101,19 @@
       </dict>
     </dict>
 
+    <!-- Sqlite ProxyDB Service - must use the same one as Pod A-->
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>./data/podA/Data/proxies.sqlite</string>
+      </dict>
+    </dict>
+
     <key>ProxyLoadFromFile</key>
     <string>./conf/auth/proxies-test-pod.xml</string>
 

Modified: CalendarServer/trunk/support/build.sh
===================================================================
--- CalendarServer/trunk/support/build.sh	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/support/build.sh	2014-01-02 17:19:18 UTC (rev 12211)
@@ -864,7 +864,7 @@
     "${n}" "twisted" "${p}" \
     "${pypi}/T/${n}/${p}.tar.bz2";
 
-  local v="12171";
+  local v="12210";
   local n="twext";
   local p="${n}-${v}";
   py_dependency -fe -r "${v}" \

Modified: CalendarServer/trunk/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/trunk/twisted/plugins/caldav.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twisted/plugins/caldav.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -23,7 +23,6 @@
 from twisted.internet.protocol import Factory
 Factory.noisy = False
 
-
 def serviceMakerProperty(propname):
     def getProperty(self):
         return getattr(reflect.namedClass(self.serviceMakerClass), propname)

Modified: CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -23,46 +23,46 @@
     "OpenDirectoryBackingService", "VCardRecord",
 ]
 
-import traceback
-import hashlib
+from calendarserver.platform.darwin.od import opendirectory, dsattributes, dsquery
 
-import os
-import sys
-import time
-
-from os import listdir
-from os.path import join, abspath
-from tempfile import mkstemp, gettempdir
-from random import random
-
+from pycalendar.datetime import DateTime
+from pycalendar.vcard.adr import Adr
 from pycalendar.vcard.n import N
-from pycalendar.vcard.adr import Adr
-from pycalendar.datetime import DateTime
 
-from socket import getfqdn
 
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
 from twext.python.filepath import CachingFilePath as FilePath
-from txdav.xml import element as davxml
-from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
+
 from txweb2.dav.resource import DAVPropertyMixIn
 from txweb2.dav.util import joinURL
 from txweb2.http_headers import MimeType, generateContentType, ETag
 
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
 
 from twistedcaldav import customxml, carddavxml
+from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.config import config
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.query import addressbookqueryfilter
 from twistedcaldav.vcard import Component, Property, vCardProductID
 
+from txdav.carddav.datastore.query.filter import IsNotDefined, ParameterFilter, \
+    TextMatch
+from txdav.xml import element as davxml
+from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
+
+from os import listdir
+from os.path import join, abspath
+from random import random
+from socket import getfqdn
+from tempfile import mkstemp, gettempdir
 from xmlrpclib import datetime
+import hashlib
+import os
+import sys
+import time
+import traceback
 
-from calendarserver.platform.darwin.od import opendirectory, dsattributes, dsquery
-
 class OpenDirectoryBackingService(DirectoryService):
     """
     Open Directory implementation of L{IDirectoryService}.
@@ -830,11 +830,11 @@
                 if not constant and not allAttrStrings:
                     return (False, [], [])
 
-                if propFilter.qualifier and isinstance(propFilter.qualifier, addressbookqueryfilter.IsNotDefined):
+                if propFilter.qualifier and isinstance(propFilter.qualifier, IsNotDefined):
                     return definedExpression(False, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
 
-                paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, addressbookqueryfilter.ParameterFilter)]
-                textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, addressbookqueryfilter.TextMatch)]
+                paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, ParameterFilter)]
+                textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, TextMatch)]
                 propFilterAllOf = propFilter.propfilter_test == "allof"
 
                 # handle parameter filter elements

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -922,6 +922,23 @@
 
 
     @inlineCallbacks
+    def proxyMode(self, principal):
+        """
+        Determine whether what proxy mode this principal has in relation to the one specified.
+        """
+
+        read_uids = (yield self.proxyFor(False))
+        if principal in read_uids:
+            returnValue("read")
+
+        write_uids = (yield self.proxyFor(True))
+        if principal in write_uids:
+            returnValue("write")
+
+        returnValue("none")
+
+
+    @inlineCallbacks
     def proxyFor(self, read_write, resolve_memberships=True):
 
         proxyFors = set()

Modified: CalendarServer/trunk/twistedcaldav/directory/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/resource.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/resource.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -22,6 +22,8 @@
 
 from twistedcaldav.client.reverseproxy import ReverseProxyResource
 
+from twisted.internet.defer import succeed
+
 __all__ = ["DirectoryReverseProxyResource"]
 
 class DirectoryReverseProxyResource(ReverseProxyResource):
@@ -35,3 +37,32 @@
 
     def url(self):
         return joinURL(self.parent.url(), self.record.uid)
+
+
+    def hasQuota(self, request):
+        return succeed(False)
+
+
+    def hasQuotaRoot(self, request):
+        return succeed(False)
+
+
+    def quotaRootResource(self, request):
+        """
+        Return the quota root for this resource.
+
+        @return: L{DAVResource} or C{None}
+        """
+
+        return succeed(None)
+
+
+    def checkPrivileges(
+        self, request, privileges, recurse=False,
+        principal=None, inherited_aces=None
+    ):
+        return succeed(None)
+
+
+    def hasProperty(self, property, request):
+        return succeed(False)

Modified: CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml	2014-01-02 17:19:18 UTC (rev 12211)
@@ -139,7 +139,7 @@
       <member type="users">delegateviagroup</member>
     </members>
   </group>
-  <user repeat="2">
+  <user repeat="100">
     <uid>user%02d</uid>
     <guid>user%02d</guid>
     <password>%02duser</password>

Modified: CalendarServer/trunk/twistedcaldav/directory/test/augments.xml
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/augments.xml	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/test/augments.xml	2014-01-02 17:19:18 UTC (rev 12211)
@@ -57,7 +57,7 @@
     <enable-calendar>false</enable-calendar>
     <enable-addressbook>false</enable-addressbook>
   </record>
-  <record repeat="2">
+  <record repeat="100">
     <uid>user%02d</uid>
     <enable>true</enable>
     <enable-calendar>true</enable-calendar>

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -999,8 +999,9 @@
         Exercise the default recordsMatchingTokens implementation
         """
         records = list((yield self.directoryService.recordsMatchingTokens(["Use", "01"])))
-        self.assertEquals(len(records), 1)
-        self.assertEquals(records[0].shortNames[0], "user01")
+        self.assertNotEquals(len(records), 0)
+        shorts = [record.shortNames[0] for record in records]
+        self.assertTrue("user01" in shorts)
 
         records = list((yield self.directoryService.recordsMatchingTokens(['"quotey"'],
             context=self.directoryService.searchContext_attendee)))

Modified: CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -27,7 +27,6 @@
 
 from twext.python.log import Logger
 from txweb2 import responsecode
-from txdav.xml import element as davxml
 from txweb2.dav.http import ErrorResponse, MultiStatusResponse
 from txweb2.dav.method.report import NumberOfMatchesWithinLimits
 from txweb2.dav.util import joinURL
@@ -37,9 +36,11 @@
 from twistedcaldav.config import config
 from twistedcaldav.carddavxml import carddav_namespace, NResults
 from twistedcaldav.method import report_common
-from twistedcaldav.query import addressbookqueryfilter
 
-from txdav.common.icommondatastore import ConcurrentModification
+from txdav.carddav.datastore.query.filter import Filter
+from txdav.common.icommondatastore import ConcurrentModification, \
+    IndexedSearchException
+from txdav.xml import element as davxml
 
 log = Logger()
 
@@ -62,7 +63,7 @@
     responses = []
 
     xmlfilter = addressbook_query.filter
-    filter = addressbookqueryfilter.Filter(xmlfilter)
+    filter = Filter(xmlfilter)
     query = addressbook_query.props
     limit = addressbook_query.limit
 
@@ -209,7 +210,7 @@
                                                         carddavxml.TextMatch.fromString(resource_name[:-4]),
                                                         name="UID", # attributes
                                                         ), ])
-                            vCardFilter = addressbookqueryfilter.Filter(vCardFilter)
+                            vCardFilter = Filter(vCardFilter)
 
                             directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery(vCardFilter, query, max_number_of_results[0]))
 
@@ -230,11 +231,13 @@
 
                     # Check for disabled access
                     if filteredaces is not None:
-                        # See whether the filter is valid for an index only query
-                        index_query_ok = addrresource.index().searchValid(filter)
-
-                        # Get list of children that match the search and have read access
-                        names = [name for name, ignore_uid in (yield addrresource.index().search(filter))] #@UnusedVariable
+                        index_query_ok = True
+                        try:
+                            # Get list of children that match the search and have read access
+                            names = [name for name, ignore_uid in (yield addrresource.search(filter))] #@UnusedVariable
+                        except IndexedSearchException:
+                            names = yield addrresource.listChildren()
+                            index_query_ok = False
                         if not names:
                             return
 
@@ -277,7 +280,7 @@
                                                     carddavxml.TextMatch.fromString(resource_name[:-4]),
                                                     name="UID", # attributes
                                                     ), ])
-                        vCardFilter = addressbookqueryfilter.Filter(vCardFilter)
+                        vCardFilter = Filter(vCardFilter)
 
                         yield  maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter)
                         handled = True

Modified: CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -20,8 +20,7 @@
 
 __all__ = ["report_urn_ietf_params_xml_ns_caldav_calendar_query"]
 
-from twisted.internet.defer import inlineCallbacks, returnValue, \
-    maybeDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twext.python.log import Logger
 from txweb2 import responsecode
@@ -39,8 +38,8 @@
     ConcurrentModification
 from twistedcaldav.instance import TooManyInstancesError
 from twistedcaldav.method import report_common
-from twistedcaldav.query import calendarqueryfilter
 
+from txdav.caldav.datastore.query.filter import Filter
 from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
 from txdav.xml import element as davxml
 
@@ -66,7 +65,7 @@
     responses = []
 
     xmlfilter = calendar_query.filter
-    filter = calendarqueryfilter.Filter(xmlfilter)
+    filter = Filter(xmlfilter)
     props = calendar_query.props
 
     assert props is not None
@@ -190,13 +189,11 @@
             if filteredaces is not None:
                 index_query_ok = True
                 try:
-                    # Get list of children that match the search and have read
-                    # access
-                    records = yield maybeDeferred(calresource.index().indexedSearch, filter)
+                    # Get list of children that match the search and have read access
+                    names = [name for name, ignore_uid, ignore_type in (yield calresource.search(filter))]
                 except IndexedSearchException:
-                    records = yield maybeDeferred(calresource.index().bruteForceSearch)
+                    names = yield calresource.listChildren()
                     index_query_ok = False
-                names = [name for name, ignore_uid, ignore_type in records]
 
                 if not names:
                     returnValue(True)

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -36,17 +36,16 @@
 except ImportError:
     from md5 import new as md5
 
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python.failure import Failure
+
 from txweb2 import responsecode
-
-from txdav.xml import element
 from txweb2.dav.http import statusForFailure
 from txweb2.dav.method.propfind import propertyName
 from txweb2.dav.method.report import NumberOfMatchesWithinLimits
 from txweb2.dav.method.report import max_number_of_matches
 from txweb2.dav.resource import AccessDeniedError
-from txweb2.http import HTTPError
+from txweb2.http import HTTPError, StatusResponse
 
 from twext.python.log import Logger
 
@@ -65,9 +64,9 @@
 from twistedcaldav.instance import InstanceList
 from twistedcaldav.memcacher import Memcacher
 
-from twistedcaldav.query import calendarqueryfilter
-
+from txdav.caldav.datastore.query.filter import Filter
 from txdav.common.icommondatastore import IndexedSearchException
+from txdav.xml import element
 
 from pycalendar.duration import Duration
 from pycalendar.datetime import DateTime
@@ -583,17 +582,18 @@
                           name="VCALENDAR",
                        )
                   )
-        filter = calendarqueryfilter.Filter(filter)
+        filter = Filter(filter)
         tzinfo = filter.settimezone(tz)
 
         try:
-            resources = yield maybeDeferred(calresource.index().indexedSearch,
-                filter, useruid=useruid, fbtype=True
-            )
+            resources = yield calresource.search(filter, useruid=useruid, fbtype=True)
             if caching:
                 yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources)
         except IndexedSearchException:
-            resources = yield maybeDeferred(calresource.index().bruteForceSearch)
+            raise HTTPError(StatusResponse(
+                responsecode.INTERNAL_SERVER_ERROR,
+                "Failed freebusy query"
+            ))
 
     else:
         # Log extended item

Modified: CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -23,9 +23,8 @@
 from urllib import unquote
 
 from twext.python.log import Logger
+
 from txweb2 import responsecode
-from txdav.xml import element as davxml
-from txdav.xml.base import dav_namespace
 from txweb2.dav.http import ErrorResponse, MultiStatusResponse
 from txweb2.dav.resource import AccessDeniedError
 from txweb2.http import HTTPError, StatusResponse
@@ -37,11 +36,14 @@
 from twistedcaldav.carddavxml import carddav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.method import report_common
-from txdav.common.icommondatastore import ConcurrentModification
 from twistedcaldav.method.report_common import COLLECTION_TYPE_CALENDAR, \
     COLLECTION_TYPE_ADDRESSBOOK
-from twistedcaldav.query import addressbookqueryfilter
 
+from txdav.carddav.datastore.query.filter import Filter
+from txdav.common.icommondatastore import ConcurrentModification
+from txdav.xml import element as davxml
+from txdav.xml.base import dav_namespace
+
 log = Logger()
 
 @inlineCallbacks
@@ -265,7 +267,7 @@
                     returnValue(None)
 
                 addressBookFilter = carddavxml.Filter(*vCardFilters)
-                addressBookFilter = addressbookqueryfilter.Filter(addressBookFilter)
+                addressBookFilter = Filter(addressBookFilter)
                 if self.directory.cacheQuery:
                     # add vcards to directory address book and run "normal case" below
                     limit = config.DirectoryAddressBook.MaxQueryResults
@@ -333,11 +335,11 @@
                     parent = (yield child.locateParent(request, resource_uri))
 
                     if collection_type == COLLECTION_TYPE_CALENDAR:
-                        if not parent.isCalendarCollection() or not (yield parent.index().resourceExists(name)):
+                        if not parent.isCalendarCollection() or not (yield parent.resourceExists(name)):
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                             continue
                     elif collection_type == COLLECTION_TYPE_ADDRESSBOOK:
-                        if not parent.isAddressBookCollection() or not (yield parent.index().resourceExists(name)):
+                        if not parent.isAddressBookCollection() or not (yield parent.resourceExists(name)):
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                             continue
 
@@ -367,11 +369,11 @@
                     parent = (yield self.locateParent(request, resource_uri))
 
                     if collection_type == COLLECTION_TYPE_CALENDAR:
-                        if not parent.isPseudoCalendarCollection() or not (yield parent.index().resourceExists(name)):
+                        if not parent.isPseudoCalendarCollection() or not (yield parent.resourceExists(name)):
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                             continue
                     elif collection_type == COLLECTION_TYPE_ADDRESSBOOK:
-                        if not parent.isAddressBookCollection() or not (yield parent.index().resourceExists(name)):
+                        if not parent.isAddressBookCollection() or not (yield parent.resourceExists(name)):
                             responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                             continue
                     child = self

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -2667,10 +2667,9 @@
     @inlineCallbacks
     def _indexWhatChanged(self, revision, depth):
         # The newstore implementation supports this directly
-        changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
+        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
             revision, depth
         )
-        notallowed = []
 
         # Need to insert some addition items on first sync
         if revision == 0:
@@ -2894,10 +2893,9 @@
     @inlineCallbacks
     def _indexWhatChanged(self, revision, depth):
         # The newstore implementation supports this directly
-        changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
+        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
             revision, depth
         )
-        notallowed = []
 
         # Need to insert some addition items on first sync
         if revision == 0:

Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/sharing.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -267,10 +267,10 @@
         """
         if self._newStoreObject.direct():
             owner = self.principalForUID(self._newStoreObject.ownerHome().uid())
+            sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
             if owner.record.recordType == WikiDirectoryService.recordType_wikis:
                 # Access level comes from what the wiki has granted to the
                 # sharee
-                sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
                 userID = sharee.record.guid
                 wikiID = owner.record.shortNames[0]
                 access = (yield getWikiAccess(userID, wikiID))
@@ -281,7 +281,12 @@
                 else:
                     returnValue(None)
             else:
-                returnValue("original")
+                # Check proxy access
+                proxy_mode = yield sharee.proxyMode(owner)
+                if proxy_mode == "none":
+                    returnValue("original")
+                else:
+                    returnValue("read-write" if proxy_mode == "write" else "read-only")
         else:
             # Invited shares use access mode from the invite
             # Get the access for self
@@ -318,7 +323,7 @@
         sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
         access = yield self._checkAccessControl()
 
-        if access == "original":
+        if access == "original" and not self._newStoreObject.ownerHome().external():
             original = (yield request.locateResource(self._share_url))
             result = (yield original.accessControlList(request, *args, **kwargs))
             returnValue(result)
@@ -805,6 +810,12 @@
 
         # Accept the share
         shareeView = yield self._newStoreHome.acceptShare(inviteUID, summary)
+        if shareeView is None:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "invalid-share"),
+                "Invite UID not valid",
+            ))
 
         # Return the URL of the shared collection
         sharedAsURL = joinURL(self.url(), shareeView.shareName())
@@ -820,7 +831,13 @@
     def declineShare(self, request, inviteUID):
 
         # Remove it if it is in the DB
-        yield self._newStoreHome.declineShare(inviteUID)
+        result = yield self._newStoreHome.declineShare(inviteUID)
+        if not result:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "invalid-share"),
+                "Invite UID not valid",
+            ))
         returnValue(Response(code=responsecode.NO_CONTENT))
 
 

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -318,6 +318,13 @@
                                     # upgrade.
 
     #
+    # Work queue configuration information
+    #
+    "WorkQueue" : {
+        "ampPort": 7654,            # Port used for hosts in a cluster to take to each other
+    },
+
+    #
     # Types of service provided
     #
     "EnableCalDAV"  : True, # Enable CalDAV service
@@ -361,7 +368,7 @@
     #
     # Directory service
     #
-    #    A directory service provides information about principals (eg.
+    #    A directory service provides information about principals (e.g.
     #    users, groups, locations and resources) to the server.
     #
     "DirectoryService": {
@@ -825,8 +832,9 @@
     "Servers" : {
         "Enabled": False,                   # Multiple servers enabled or not
         "ConfigFile": "localservers.xml",   # File path for server information
-        "MaxClients": 5,                    # Pool size for connections to between servers
+        "MaxClients": 5,                    # Pool size for connections between servers
         "InboxName": "podding",             # Name for top-level inbox resource
+        "ConduitName": "conduit",           # Name for top-level cross-pod resource
     },
 
     #
@@ -1067,8 +1075,11 @@
         def _loadImport(childDict):
             # Look for an import and read that one as the main config and merge the current one into that
             if "ImportConfig" in childDict and childDict.ImportConfig:
-                configRoot = os.path.join(childDict.ServerRoot, childDict.ConfigRoot)
-                path = _expandPath(fullServerPath(configRoot, childDict.ImportConfig))
+                if childDict.ImportConfig[0] != ".":
+                    configRoot = os.path.join(childDict.ServerRoot, childDict.ConfigRoot)
+                    path = _expandPath(fullServerPath(configRoot, childDict.ImportConfig))
+                else:
+                    path = childDict.ImportConfig
                 if os.path.exists(path):
                     importDict = ConfigDict(self._parseConfigFromFile(path))
                     if importDict:
@@ -1564,6 +1575,7 @@
                         (direction,))
 
 
+
 def _updateSharing(configDict, reloading=False):
     #
     # Sharing
@@ -1574,6 +1586,7 @@
         PerUserDataFilter.IGNORE_X_PROPERTIES.append(propertyName)
 
 
+
 def _updateServers(configDict, reloading=False):
     from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
     if configDict.Servers.Enabled:

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -286,13 +286,6 @@
         return self._parentResource
 
 
-    def index(self):
-        """
-        Retrieve the new-style index wrapper.
-        """
-        return self._newStoreObject.retrieveOldIndex()
-
-
     def exists(self):
         # FIXME: tests
         return self._newStoreObject is not None
@@ -303,7 +296,6 @@
         # The newstore implementation supports this directly
         returnValue(
             (yield self._newStoreObject.resourceNamesSinceToken(revision))
-            + ([],)
         )
 
 
@@ -347,6 +339,18 @@
         return self._newStoreObject.countObjectResources()
 
 
+    @inlineCallbacks
+    def resourceExists(self, name):
+        """
+        Indicate whether a resource with the specified name exists.
+
+        @return: C{True} if it exists
+        @rtype: C{bool}
+        """
+        allNames = yield self._newStoreObject.listObjectResources()
+        returnValue(name in allNames)
+
+
     def name(self):
         return self._name
 
@@ -462,6 +466,8 @@
         if self.isShareeResource():
             log.debug("Removing shared collection %s" % (self,))
             yield self.removeShareeResource(request)
+            # Re-initialize to get stuff setup again now we have no object
+            self._initializeWithHomeChild(None, self._parentResource)
             returnValue(NO_CONTENT)
 
         log.debug("Deleting collection %s" % (self,))
@@ -488,11 +494,6 @@
 
         # Now do normal delete
 
-        # Handle sharing
-        wasShared = self.isShared()
-        if wasShared:
-            yield self.downgradeFromShare(request)
-
         # Actually delete it.
         yield self._newStoreObject.remove()
 
@@ -968,6 +969,10 @@
                     )
 
 
+    def search(self, filter, **kwargs):
+        return self._newStoreObject.search(filter, **kwargs)
+
+
     def notifierID(self):
         return "%s/%s" % self._newStoreObject.notifierID()
 
@@ -1121,7 +1126,7 @@
         isowner = (yield self.isOwner(request))
         accessPrincipal = (yield self.resourceOwnerPrincipal(request))
 
-        for name, _ignore_uid, _ignore_type in (yield maybeDeferred(self.index().bruteForceSearch)):
+        for name in (yield self._newStoreObject.listObjectResources()):
             try:
                 child = yield request.locateChildResource(self, name)
             except TypeError:
@@ -2421,6 +2426,7 @@
 
         try:
             response = (yield self.storeMove(request, destinationparent, destination.name()))
+            self._newStoreObject = None
             returnValue(response)
 
         # Handle the various store errors
@@ -3371,6 +3377,8 @@
         if self.isShareeResource():
             log.debug("Removing shared resource %s" % (self,))
             yield self.removeShareeResource(request)
+            # Re-initialize to get stuff setup again now we have no object
+            self._initializeWithObject(None, self._newStoreParent)
             returnValue(NO_CONTENT)
         elif self._newStoreObject.isGroupForSharedAddressBook():
             abCollectionResource = (yield request.locateResource(parentForURL(request.uri)))
@@ -3673,7 +3681,6 @@
         # The newstore implementation supports this directly
         returnValue(
             (yield self._newStoreNotifications.resourceNamesSinceToken(revision))
-            + ([],)
         )
 
 

Modified: CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -27,7 +27,6 @@
 from twistedcaldav import caldavxml
 from twistedcaldav import ical
 
-from twistedcaldav.query import calendarqueryfilter
 from twistedcaldav.config import config
 from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
 from twisted.internet.defer import inlineCallbacks, returnValue
@@ -36,6 +35,7 @@
 from twistedcaldav.ical import Component
 from txdav.caldav.icalendarstore import ComponentUpdateState
 from twistedcaldav.directory.directory import DirectoryService
+from txdav.caldav.datastore.query.filter import TimeRange
 
 
 @inlineCallbacks
@@ -167,7 +167,7 @@
                             cal = property.calendar()
                             instances = cal.expandTimeRanges(query_timerange.end)
                             vevents = [x for x in cal.subcomponents() if x.name() == "VEVENT"]
-                            if not calendarqueryfilter.TimeRange(query_timerange).matchinstance(vevents[0], instances):
+                            if not TimeRange(query_timerange).matchinstance(vevents[0], instances):
                                 self.fail("REPORT property %r returned calendar %s outside of request time range %r"
                                           % (property, property.calendar, query_timerange))
 

Modified: CalendarServer/trunk/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_sharing.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/test/test_sharing.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -30,6 +30,7 @@
 from twistedcaldav.test.test_cache import StubResponseCacheResource
 from twistedcaldav.test.util import norequest, StoreTestCase, SimpleStoreRequest
 
+from txdav.caldav.datastore.test.util import buildDirectory
 from txdav.common.datastore.sql_tables import _BIND_MODE_DIRECT
 from txdav.xml import element as davxml
 from txdav.xml.parser import WebDAVDocument
@@ -738,6 +739,8 @@
         home is at /.  Return the name of the newly shared calendar in the
         sharee's home.
         """
+
+        self._sqlCalendarStore._directoryService = buildDirectory(homes=("wiki-testing",))
         wcreate = self._sqlCalendarStore.newTransaction("create wiki")
         yield wcreate.calendarHomeWithUID("wiki-testing", create=True)
         yield wcreate.commit()

Modified: CalendarServer/trunk/twistedcaldav/test/test_xml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_xml.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/twistedcaldav/test/test_xml.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -17,12 +17,15 @@
 import os
 
 from twisted.trial.unittest import SkipTest
+
 from twistedcaldav.ical import Component
-from twistedcaldav.query import calendarqueryfilter
 import twistedcaldav.test.util
 from twistedcaldav.caldavxml import ComponentFilter, PropertyFilter, TextMatch, \
     Filter, TimeRange
 
+from txdav.caldav.datastore.query.filter import Filter as storeFilter
+from txdav.caldav.datastore.query.filter import ComponentFilter as storeComponentFilter
+
 class XML (twistedcaldav.test.util.TestCase):
     """
     XML tests
@@ -46,7 +49,7 @@
             else:
                 no = ""
 
-            if has != calendarqueryfilter.ComponentFilter(
+            if has != storeComponentFilter(
                 ComponentFilter(
                     ComponentFilter(
                         name=component_name
@@ -70,7 +73,7 @@
             else:
                 no = ""
 
-            if has != calendarqueryfilter.ComponentFilter(
+            if has != storeComponentFilter(
                 ComponentFilter(
                     ComponentFilter(
                         PropertyFilter(
@@ -106,7 +109,7 @@
             else:
                 no = ""
 
-            if has != calendarqueryfilter.ComponentFilter(
+            if has != storeComponentFilter(
                 ComponentFilter(
                     ComponentFilter(
                         PropertyFilter(
@@ -148,7 +151,7 @@
             else:
                 no = ""
 
-            if has != calendarqueryfilter.Filter(
+            if has != storeFilter(
                 Filter(
                     ComponentFilter(
                         ComponentFilter(

Modified: CalendarServer/trunk/txdav/base/datastore/subpostgres.py
===================================================================
--- CalendarServer/trunk/txdav/base/datastore/subpostgres.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/base/datastore/subpostgres.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -228,7 +228,7 @@
                 # in /tmp and based on a hash of the data store directory
                 digest = md5(dataStoreDirectory.path).hexdigest()
                 socketDir = "/tmp/ccs_postgres_" + digest
-                
+
             self.socketDir = CachingFilePath(socketDir)
             self.host = self.socketDir.path
             self.port = None

Modified: CalendarServer/trunk/txdav/base/datastore/util.py
===================================================================
--- CalendarServer/trunk/txdav/base/datastore/util.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/base/datastore/util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -98,6 +98,12 @@
         return "objectWithResourceID:%s:%s" % (homeResourceID, resourceID)
 
 
+    # Home child objects by external id
+
+    def keyForObjectWithExternalID(self, homeResourceID, externalID):
+        return "objectWithExternalID:%s:%s" % (homeResourceID, externalID)
+
+
     # Home metadata (Created/Modified)
 
     def keyForHomeMetaData(self, homeResourceID):

Modified: CalendarServer/trunk/txdav/caldav/datastore/index_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/index_file.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/index_file.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -15,6 +15,7 @@
 # limitations under the License.
 ##
 
+
 """
 CalDAV Index.
 
@@ -43,12 +44,14 @@
 
 from twext.python.log import Logger
 
+from txdav.caldav.datastore.query.builder import buildExpression
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.common.datastore.query.filegenerator import sqllitegenerator
 from txdav.common.icommondatastore import SyncTokenValidException, \
     ReservationError, IndexedSearchException
 
 from twistedcaldav.dateops import pyCalendarTodatetime
 from twistedcaldav.ical import Component
-from twistedcaldav.query import calendarquery, calendarqueryfilter
 from twistedcaldav.sql import AbstractSQLDatabase
 from twistedcaldav.sql import db_prefix
 from twistedcaldav.instance import InvalidOverriddenInstanceError
@@ -275,6 +278,7 @@
 
         changed = []
         deleted = []
+        invalid = []
         for name, wasdeleted in results:
             if name:
                 if wasdeleted == 'Y':
@@ -285,7 +289,7 @@
             else:
                 raise SyncTokenValidException
 
-        return changed, deleted,
+        return (changed, deleted, invalid)
 
 
     def lastRevision(self):
@@ -320,7 +324,7 @@
 
         # Make sure we have a proper Filter element and get the partial SQL
         # statement to use.
-        if isinstance(filter, calendarqueryfilter.Filter):
+        if isinstance(filter, Filter):
             if fbtype:
                 # Lookup the useruid - try the empty (default) one if needed
                 dbuseruid = self._db_value_for_sql(
@@ -330,7 +334,7 @@
             else:
                 dbuseruid = ""
 
-            qualifiers = calendarquery.sqlcalendarquery(filter, None, dbuseruid, fbtype)
+            qualifiers = sqlcalendarquery(filter, None, dbuseruid, fbtype)
             if qualifiers is not None:
                 # Determine how far we need to extend the current expansion of
                 # events. If we have an open-ended time-range we will expand one
@@ -437,6 +441,24 @@
 
 
 
+def sqlcalendarquery(filter, calendarid=None, userid=None, freebusy=False):
+    """
+    Convert the supplied calendar-query into a partial SQL statement.
+
+    @param filter: the L{Filter} for the calendar-query to convert.
+    @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+            Or return C{None} if it is not possible to create an SQL query to fully match the calendar-query.
+    """
+    try:
+        expression = buildExpression(filter, sqllitegenerator.FIELDS)
+        sql = sqllitegenerator(expression, calendarid, userid, freebusy)
+        return sql.generate()
+    except ValueError:
+        return None
+
+
+
 class CalendarIndex (AbstractCalendarIndex):
     """
     Calendar index - abstract class for indexer that indexes calendar objects in a collection.

Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/builder.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/builder.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/builder.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,227 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twistedcaldav.dateops import floatoffset, pyCalendarTodatetime
-
-from txdav.caldav.datastore.query.filter import ComponentFilter, PropertyFilter, TextMatch, TimeRange
-from txdav.common.datastore.query import expression
-
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
-    "buildExpression",
-]
-
-
-
-# SQL Index column (field) names
-
-def buildExpression(filter, fields):
-    """
-    Convert the supplied calendar-query into an expression tree.
-
-    @param filter: the L{Filter} for the calendar-query to convert.
-    @return: a L{baseExpression} for the expression tree.
-    """
-
-    # Lets assume we have a valid filter from the outset.
-
-    # Top-level filter contains exactly one comp-filter element
-    assert filter.child is not None
-    vcalfilter = filter.child
-    assert isinstance(vcalfilter, ComponentFilter)
-    assert vcalfilter.filter_name == "VCALENDAR"
-
-    if len(vcalfilter.filters) > 0:
-        # Determine logical expression grouping
-        logical = expression.andExpression if vcalfilter.filter_test == "allof" else expression.orExpression
-
-        # Only comp-filters are handled
-        for _ignore in [x for x in vcalfilter.filters if not isinstance(x, ComponentFilter)]:
-            raise ValueError
-
-        return compfilterListExpression(vcalfilter.filters, fields, logical)
-    else:
-        return expression.allExpression()
-
-
-
-def compfilterListExpression(compfilters, fields, logical):
-    """
-    Create an expression for a list of comp-filter elements.
-
-    @param compfilters: the C{list} of L{ComponentFilter} elements.
-    @return: a L{baseExpression} for the expression tree.
-    """
-
-    if len(compfilters) == 1:
-        return compfilterExpression(compfilters[0], fields)
-    else:
-        return logical([compfilterExpression(c, fields) for c in compfilters])
-
-
-
-def compfilterExpression(compfilter, fields):
-    """
-    Create an expression for a single comp-filter element.
-
-    @param compfilter: the L{ComponentFilter} element.
-    @return: a L{baseExpression} for the expression tree.
-    """
-
-    # Handle is-not-defined case
-    if not compfilter.defined:
-        # Test for TYPE != <<component-type name>>
-        return expression.isnotExpression(fields["TYPE"], compfilter.filter_name, True)
-
-    # Determine logical expression grouping
-    logical = expression.andExpression if compfilter.filter_test == "allof" else expression.orExpression
-
-    expressions = []
-    if isinstance(compfilter.filter_name, str) or isinstance(compfilter.filter_name, unicode):
-        expressions.append(expression.isExpression(fields["TYPE"], compfilter.filter_name, True))
-    else:
-        expressions.append(expression.inExpression(fields["TYPE"], compfilter.filter_name, True))
-
-    # Handle time-range
-    if compfilter.qualifier and isinstance(compfilter.qualifier, TimeRange):
-        start, end, startfloat, endfloat = getTimerangeArguments(compfilter.qualifier)
-        expressions.append(expression.timerangeExpression(start, end, startfloat, endfloat))
-
-    # Handle properties - we can only do UID right now
-    props = []
-    for p in [x for x in compfilter.filters if isinstance(x, PropertyFilter)]:
-        props.append(propfilterExpression(p, fields))
-    if len(props) > 1:
-        propsExpression = logical(props)
-    elif len(props) == 1:
-        propsExpression = props[0]
-    else:
-        propsExpression = None
-
-    # Handle embedded components - we do not right now as our Index does not handle them
-    comps = []
-    for _ignore in [x for x in compfilter.filters if isinstance(x, ComponentFilter)]:
-        raise ValueError
-    if len(comps) > 1:
-        compsExpression = logical(comps)
-    elif len(comps) == 1:
-        compsExpression = comps[0]
-    else:
-        compsExpression = None
-
-    # Now build compound expression
-    if ((propsExpression is not None) and (compsExpression is not None)):
-        expressions.append(logical([propsExpression, compsExpression]))
-    elif propsExpression is not None:
-        expressions.append(propsExpression)
-    elif compsExpression is not None:
-        expressions.append(compsExpression)
-
-    # Now build return expression
-    return expression.andExpression(expressions)
-
-
-
-def propfilterExpression(propfilter, fields):
-    """
-    Create an expression for a single prop-filter element.
-
-    @param propfilter: the L{PropertyFilter} element.
-    @return: a L{baseExpression} for the expression tree.
-    """
-
-    # Only handle UID right now
-    if propfilter.filter_name != "UID":
-        raise ValueError
-
-    # Handle is-not-defined case
-    if not propfilter.defined:
-        # Test for <<field>> != "*"
-        return expression.isExpression(fields["UID"], "", True)
-
-    # Determine logical expression grouping
-    logical = expression.andExpression if propfilter.filter_test == "allof" else expression.orExpression
-
-    # Handle time-range - we cannot do this with our Index right now
-    if propfilter.qualifier and isinstance(propfilter.qualifier, TimeRange):
-        raise ValueError
-
-    # Handle text-match
-    tm = None
-    if propfilter.qualifier and isinstance(propfilter.qualifier, TextMatch):
-        if propfilter.qualifier.match_type == "equals":
-            tm = expression.isnotExpression if propfilter.qualifier.negate else expression.isExpression
-        elif propfilter.qualifier.match_type == "contains":
-            tm = expression.notcontainsExpression if propfilter.qualifier.negate else expression.containsExpression
-        elif propfilter.qualifier.match_type == "starts-with":
-            tm = expression.notstartswithExpression if propfilter.qualifier.negate else expression.startswithExpression
-        elif propfilter.qualifier.match_type == "ends-with":
-            tm = expression.notendswithExpression if propfilter.qualifier.negate else expression.endswithExpression
-        tm = tm(fields[propfilter.filter_name], propfilter.qualifier.text, propfilter.qualifier.caseless)
-
-    # Handle embedded parameters - we do not right now as our Index does not handle them
-    params = []
-    for _ignore in propfilter.filters:
-        raise ValueError
-    if len(params) > 1:
-        paramsExpression = logical(params)
-    elif len(params) == 1:
-        paramsExpression = params[0]
-    else:
-        paramsExpression = None
-
-    # Now build return expression
-    if (tm is not None) and (paramsExpression is not None):
-        return logical([tm, paramsExpression])
-    elif tm is not None:
-        return tm
-    elif paramsExpression is not None:
-        return paramsExpression
-    else:
-        return None
-
-
-
-def getTimerangeArguments(timerange):
-    """
-    Get start/end and floating start/end (adjusted for timezone offset) values from the
-    supplied time-range test.
-
-    @param timerange: the L{TimeRange} used in the query.
-    @return: C{tuple} of C{str} for start, end, startfloat, endfloat
-    """
-
-    # Start/end in UTC
-    start = timerange.start
-    end = timerange.end
-
-    # Get timezone
-    tzinfo = timerange.tzinfo
-
-    # Now force to floating UTC
-    startfloat = floatoffset(start, tzinfo) if start else None
-    endfloat = floatoffset(end, tzinfo) if end else None
-
-    return (
-        pyCalendarTodatetime(start) if start else None,
-        pyCalendarTodatetime(end) if end else None,
-        pyCalendarTodatetime(startfloat) if startfloat else None,
-        pyCalendarTodatetime(endfloat) if endfloat else None,
-    )

Copied: CalendarServer/trunk/txdav/caldav/datastore/query/builder.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/builder.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/builder.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/builder.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,227 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twistedcaldav.dateops import floatoffset, pyCalendarTodatetime
+
+from txdav.caldav.datastore.query.filter import ComponentFilter, PropertyFilter, TextMatch, TimeRange
+from txdav.common.datastore.query import expression
+
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+    "buildExpression",
+]
+
+
+
+# SQL Index column (field) names
+
+def buildExpression(filter, fields):
+    """
+    Convert the supplied calendar-query into an expression tree.
+
+    @param filter: the L{Filter} for the calendar-query to convert.
+    @return: a L{baseExpression} for the expression tree.
+    """
+
+    # Lets assume we have a valid filter from the outset.
+
+    # Top-level filter contains exactly one comp-filter element
+    assert filter.child is not None
+    vcalfilter = filter.child
+    assert isinstance(vcalfilter, ComponentFilter)
+    assert vcalfilter.filter_name == "VCALENDAR"
+
+    if len(vcalfilter.filters) > 0:
+        # Determine logical expression grouping
+        logical = expression.andExpression if vcalfilter.filter_test == "allof" else expression.orExpression
+
+        # Only comp-filters are handled
+        for _ignore in [x for x in vcalfilter.filters if not isinstance(x, ComponentFilter)]:
+            raise ValueError
+
+        return compfilterListExpression(vcalfilter.filters, fields, logical)
+    else:
+        return expression.allExpression()
+
+
+
+def compfilterListExpression(compfilters, fields, logical):
+    """
+    Create an expression for a list of comp-filter elements.
+
+    @param compfilters: the C{list} of L{ComponentFilter} elements.
+    @return: a L{baseExpression} for the expression tree.
+    """
+
+    if len(compfilters) == 1:
+        return compfilterExpression(compfilters[0], fields)
+    else:
+        return logical([compfilterExpression(c, fields) for c in compfilters])
+
+
+
+def compfilterExpression(compfilter, fields):
+    """
+    Create an expression for a single comp-filter element.
+
+    @param compfilter: the L{ComponentFilter} element.
+    @return: a L{baseExpression} for the expression tree.
+    """
+
+    # Handle is-not-defined case
+    if not compfilter.defined:
+        # Test for TYPE != <<component-type name>>
+        return expression.isnotExpression(fields["TYPE"], compfilter.filter_name, True)
+
+    # Determine logical expression grouping
+    logical = expression.andExpression if compfilter.filter_test == "allof" else expression.orExpression
+
+    expressions = []
+    if isinstance(compfilter.filter_name, str) or isinstance(compfilter.filter_name, unicode):
+        expressions.append(expression.isExpression(fields["TYPE"], compfilter.filter_name, True))
+    else:
+        expressions.append(expression.inExpression(fields["TYPE"], compfilter.filter_name, True))
+
+    # Handle time-range
+    if compfilter.qualifier and isinstance(compfilter.qualifier, TimeRange):
+        start, end, startfloat, endfloat = getTimerangeArguments(compfilter.qualifier)
+        expressions.append(expression.timerangeExpression(start, end, startfloat, endfloat))
+
+    # Handle properties - we can only do UID right now
+    props = []
+    for p in [x for x in compfilter.filters if isinstance(x, PropertyFilter)]:
+        props.append(propfilterExpression(p, fields))
+    if len(props) > 1:
+        propsExpression = logical(props)
+    elif len(props) == 1:
+        propsExpression = props[0]
+    else:
+        propsExpression = None
+
+    # Handle embedded components - we do not right now as our Index does not handle them
+    comps = []
+    for _ignore in [x for x in compfilter.filters if isinstance(x, ComponentFilter)]:
+        raise ValueError
+    if len(comps) > 1:
+        compsExpression = logical(comps)
+    elif len(comps) == 1:
+        compsExpression = comps[0]
+    else:
+        compsExpression = None
+
+    # Now build compound expression
+    if ((propsExpression is not None) and (compsExpression is not None)):
+        expressions.append(logical([propsExpression, compsExpression]))
+    elif propsExpression is not None:
+        expressions.append(propsExpression)
+    elif compsExpression is not None:
+        expressions.append(compsExpression)
+
+    # Now build return expression
+    return expression.andExpression(expressions)
+
+
+
+def propfilterExpression(propfilter, fields):
+    """
+    Create an expression for a single prop-filter element.
+
+    @param propfilter: the L{PropertyFilter} element.
+    @return: a L{baseExpression} for the expression tree.
+    """
+
+    # Only handle UID right now
+    if propfilter.filter_name != "UID":
+        raise ValueError
+
+    # Handle is-not-defined case
+    if not propfilter.defined:
+        # Test for <<field>> != "*"
+        return expression.isExpression(fields["UID"], "", True)
+
+    # Determine logical expression grouping
+    logical = expression.andExpression if propfilter.filter_test == "allof" else expression.orExpression
+
+    # Handle time-range - we cannot do this with our Index right now
+    if propfilter.qualifier and isinstance(propfilter.qualifier, TimeRange):
+        raise ValueError
+
+    # Handle text-match
+    tm = None
+    if propfilter.qualifier and isinstance(propfilter.qualifier, TextMatch):
+        if propfilter.qualifier.match_type == "equals":
+            tm = expression.isnotExpression if propfilter.qualifier.negate else expression.isExpression
+        elif propfilter.qualifier.match_type == "contains":
+            tm = expression.notcontainsExpression if propfilter.qualifier.negate else expression.containsExpression
+        elif propfilter.qualifier.match_type == "starts-with":
+            tm = expression.notstartswithExpression if propfilter.qualifier.negate else expression.startswithExpression
+        elif propfilter.qualifier.match_type == "ends-with":
+            tm = expression.notendswithExpression if propfilter.qualifier.negate else expression.endswithExpression
+        tm = tm(fields[propfilter.filter_name], propfilter.qualifier.text, propfilter.qualifier.caseless)
+
+    # Handle embedded parameters - we do not right now as our Index does not handle them
+    params = []
+    for _ignore in propfilter.filters:
+        raise ValueError
+    if len(params) > 1:
+        paramsExpression = logical(params)
+    elif len(params) == 1:
+        paramsExpression = params[0]
+    else:
+        paramsExpression = None
+
+    # Now build return expression
+    if (tm is not None) and (paramsExpression is not None):
+        return logical([tm, paramsExpression])
+    elif tm is not None:
+        return tm
+    elif paramsExpression is not None:
+        return paramsExpression
+    else:
+        return None
+
+
+
+def getTimerangeArguments(timerange):
+    """
+    Get start/end and floating start/end (adjusted for timezone offset) values from the
+    supplied time-range test.
+
+    @param timerange: the L{TimeRange} used in the query.
+    @return: C{tuple} of C{str} for start, end, startfloat, endfloat
+    """
+
+    # Start/end in UTC
+    start = timerange.start
+    end = timerange.end
+
+    # Get timezone
+    tzinfo = timerange.tzinfo
+
+    # Now force to floating UTC
+    startfloat = floatoffset(start, tzinfo) if start else None
+    endfloat = floatoffset(end, tzinfo) if end else None
+
+    return (
+        pyCalendarTodatetime(start) if start else None,
+        pyCalendarTodatetime(end) if end else None,
+        pyCalendarTodatetime(startfloat) if startfloat else None,
+        pyCalendarTodatetime(endfloat) if endfloat else None,
+    )

Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/filter.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,913 +0,0 @@
-##
-# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-"""
-Object model of CALDAV:filter element used in an addressbook-query.
-"""
-
-__all__ = [
-    "Filter",
-]
-
-from twext.python.log import Logger
-
-from twistedcaldav.caldavxml import caldav_namespace, CalDAVTimeZoneElement
-from twistedcaldav.dateops import timeRangesOverlap
-from twistedcaldav.ical import Component, Property
-
-from pycalendar.datetime import DateTime
-from pycalendar.timezone import Timezone
-
-log = Logger()
-
-
-class FilterBase(object):
-    """
-    Determines which matching components are returned.
-    """
-
-    serialized_name = None
-    deserialize_names = {}
-
-    @classmethod
-    def serialize_register(cls, register):
-        cls.deserialize_names[register.serialized_name] = register
-
-
-    def __init__(self, xml_element):
-        pass
-
-
-    @classmethod
-    def deserialize(cls, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        obj = cls.deserialize_names[data["type"]](None)
-        obj._deserialize(data)
-        return obj
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        pass
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        return {
-            "type": self.serialized_name,
-        }
-
-
-    def match(self, item, access=None):
-        raise NotImplementedError
-
-
-    def valid(self, level=0):
-        raise NotImplementedError
-
-
-
-class Filter(FilterBase):
-    """
-    Determines which matching components are returned.
-    """
-
-    serialized_name = "Filter"
-
-    def __init__(self, xml_element):
-
-        super(Filter, self).__init__(xml_element)
-        if xml_element is None:
-            return
-
-        # One comp-filter element must be present
-        if len(xml_element.children) != 1 or xml_element.children[0].qname() != (caldav_namespace, "comp-filter"):
-            raise ValueError("Invalid CALDAV:filter element: %s" % (xml_element,))
-
-        self.child = ComponentFilter(xml_element.children[0])
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        self.child = FilterBase.deserialize(data["child"])
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        result = super(Filter, self).serialize()
-        result.update({
-            "child": self.child.serialize(),
-        })
-        return result
-
-
-    def match(self, component, access=None):
-        """
-        Returns True if the given calendar component matches this filter, False
-        otherwise.
-        """
-
-        # We only care about certain access restrictions.
-        if access not in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
-            access = None
-
-        # We need to prepare ourselves for a time-range query by pre-calculating
-        # the set of instances up to the latest time-range limit. That way we can
-        # avoid having to do some form of recurrence expansion for each query sub-part.
-        maxend, isStartTime = self.getmaxtimerange()
-        if maxend:
-            if isStartTime:
-                if component.isRecurringUnbounded():
-                    # Unbounded recurrence is always within a start-only time-range
-                    instances = None
-                else:
-                    # Expand the instances up to infinity
-                    instances = component.expandTimeRanges(DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), ignoreInvalidInstances=True)
-            else:
-                instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
-        else:
-            instances = None
-        self.child.setInstances(instances)
-
-        # <filter> contains exactly one <comp-filter>
-        return self.child.match(component, access)
-
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt iCalendar
-        data object model.
-
-        @return: True if valid, False otherwise
-        """
-
-        # Must have one child element for VCALENDAR
-        return self.child.valid(0)
-
-
-    def settimezone(self, tzelement):
-        """
-        Set the default timezone to use with this query.
-        @param calendar: a L{Component} for the VCALENDAR containing the one
-            VTIMEZONE that we want
-        @return: the L{Timezone} derived from the VTIMEZONE or utc.
-        """
-
-        if tzelement is None:
-            tz = None
-        elif isinstance(tzelement, CalDAVTimeZoneElement):
-            tz = tzelement.gettimezone()
-        elif isinstance(tzelement, Component):
-            tz = tzelement.gettimezone()
-        if tz is None:
-            tz = Timezone(utc=True)
-        self.child.settzinfo(tz)
-        return tz
-
-
-    def getmaxtimerange(self):
-        """
-        Get the date farthest into the future in any time-range elements
-        """
-
-        return self.child.getmaxtimerange(None, False)
-
-
-    def getmintimerange(self):
-        """
-        Get the date farthest into the past in any time-range elements. That is either
-        the start date, or if start is not present, the end date.
-        """
-
-        return self.child.getmintimerange(None, False)
-
-FilterBase.serialize_register(Filter)
-
-
-
-class FilterChildBase(FilterBase):
-    """
-    CalDAV filter element.
-    """
-
-    def __init__(self, xml_element):
-
-        super(FilterChildBase, self).__init__(xml_element)
-        if xml_element is None:
-            return
-
-        qualifier = None
-        filters = []
-
-        for child in xml_element.children:
-            qname = child.qname()
-
-            if qname in (
-                (caldav_namespace, "is-not-defined"),
-                (caldav_namespace, "time-range"),
-                (caldav_namespace, "text-match"),
-            ):
-                if qualifier is not None:
-                    raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
-
-                if qname == (caldav_namespace, "is-not-defined"):
-                    qualifier = IsNotDefined(child)
-                elif qname == (caldav_namespace, "time-range"):
-                    qualifier = TimeRange(child)
-                elif qname == (caldav_namespace, "text-match"):
-                    qualifier = TextMatch(child)
-
-            elif qname == (caldav_namespace, "comp-filter"):
-                filters.append(ComponentFilter(child))
-            elif qname == (caldav_namespace, "prop-filter"):
-                filters.append(PropertyFilter(child))
-            elif qname == (caldav_namespace, "param-filter"):
-                filters.append(ParameterFilter(child))
-            else:
-                raise ValueError("Unknown child element: %s" % (qname,))
-
-        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
-            raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
-
-        self.qualifier = qualifier
-        self.filters = filters
-        self.filter_name = xml_element.attributes["name"]
-        if isinstance(self.filter_name, unicode):
-            self.filter_name = self.filter_name.encode("utf-8")
-        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
-
-        filter_test = xml_element.attributes.get("test", "allof")
-        if filter_test not in ("anyof", "allof"):
-            raise ValueError("Test must be only one of anyof, allof")
-        self.filter_test = filter_test
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
-        self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
-        self.filter_name = data["filter_name"]
-        self.defined = data["defined"]
-        self.filter_test = data["filter_test"]
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        result = super(FilterChildBase, self).serialize()
-        result.update({
-            "qualifier": self.qualifier.serialize() if self.qualifier else None,
-            "filters": [filter.serialize() for filter in self.filters],
-            "filter_name": self.filter_name,
-            "defined": self.defined,
-            "filter_test": self.filter_test,
-        })
-        return result
-
-
-    def match(self, item, access=None):
-        """
-        Returns True if the given calendar item (either a component, property or parameter value)
-        matches this filter, False otherwise.
-        """
-
-        # Always return True for the is-not-defined case as the result of this will
-        # be negated by the caller
-        if not self.defined:
-            return True
-
-        if self.qualifier and not self.qualifier.match(item, access):
-            return False
-
-        if len(self.filters) > 0:
-            allof = self.filter_test == "allof"
-            for filter in self.filters:
-                if allof != filter._match(item, access):
-                    return not allof
-            return allof
-        else:
-            return True
-
-
-
-class ComponentFilter (FilterChildBase):
-    """
-    Limits a search to only the chosen component types.
-    """
-
-    serialized_name = "ComponentFilter"
-
-    def match(self, item, access):
-        """
-        Returns True if the given calendar item (which is a component)
-        matches this filter, False otherwise.
-        This specialization uses the instance matching option of the time-range filter
-        to minimize instance expansion.
-        """
-
-        # Always return True for the is-not-defined case as the result of this will
-        # be negated by the caller
-        if not self.defined:
-            return True
-
-        if self.qualifier and not self.qualifier.matchinstance(item, self.instances):
-            return False
-
-        if len(self.filters) > 0:
-            allof = self.filter_test == "allof"
-            for filter in self.filters:
-                if allof != filter._match(item, access):
-                    return not allof
-            return allof
-        else:
-            return True
-
-
-    def _match(self, component, access):
-        # At least one subcomponent must match (or is-not-defined is set)
-        for subcomponent in component.subcomponents():
-            # If access restrictions are in force, restrict matching to specific components only.
-            # In particular do not match VALARM.
-            if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
-                continue
-
-            # Try to match the component name
-            if isinstance(self.filter_name, str):
-                if subcomponent.name() != self.filter_name:
-                    continue
-            else:
-                if subcomponent.name() not in self.filter_name:
-                    continue
-            if self.match(subcomponent, access):
-                break
-        else:
-            return not self.defined
-        return self.defined
-
-
-    def setInstances(self, instances):
-        """
-        Give the list of instances to each comp-filter element.
-        @param instances: the list of instances.
-        """
-        self.instances = instances
-        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
-            compfilter.setInstances(instances)
-
-
-    def valid(self, level):
-        """
-        Indicate whether this filter element's structure is valid wrt iCalendar
-        data object model.
-
-        @param level: the nesting level of this filter element, 0 being the top comp-filter.
-        @return:      True if valid, False otherwise
-        """
-
-        # Check for time-range
-        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-
-        if level == 0:
-            # Must have VCALENDAR at the top
-            if (self.filter_name != "VCALENDAR") or timerange:
-                log.info("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
-                return False
-        elif level == 1:
-            # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
-            if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
-                log.info("comp-filter wrong component type: %s" % (self.filter_name,))
-                return False
-
-            # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
-            if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
-                log.info("time-range cannot be used with component %s" % (self.filter_name,))
-                return False
-        elif level == 2:
-            # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
-            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
-                log.info("comp-filter wrong sub-component type: %s" % (self.filter_name,))
-                return False
-
-            # time-range only on VALARM, AVAILABLE
-            if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
-                log.info("time-range cannot be used with sub-component %s" % (self.filter_name,))
-                return False
-        else:
-            # Disallow all standard iCal components anywhere else
-            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
-                log.info("comp-filter wrong standard component type: %s" % (self.filter_name,))
-                return False
-
-        # Test each property
-        for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
-            if not propfilter.valid():
-                return False
-
-        # Test each component
-        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
-            if not compfilter.valid(level + 1):
-                return False
-
-        # Test the time-range
-        if timerange:
-            if not self.qualifier.valid():
-                return False
-
-        return True
-
-
-    def settzinfo(self, tzinfo):
-        """
-        Set the default timezone to use with this query.
-        @param tzinfo: a L{Timezone} to use.
-        """
-
-        # Give tzinfo to any TimeRange we have
-        if isinstance(self.qualifier, TimeRange):
-            self.qualifier.settzinfo(tzinfo)
-
-        # Pass down to sub components/properties
-        for x in self.filters:
-            x.settzinfo(tzinfo)
-
-
-    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
-        """
-        Get the date farthest into the future in any time-range elements
-
-        @param currentMaximum: current future value to compare with
-        @type currentMaximum: L{DateTime}
-        """
-
-        # Give tzinfo to any TimeRange we have
-        isStartTime = False
-        if isinstance(self.qualifier, TimeRange):
-            isStartTime = self.qualifier.end is None
-            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
-            if currentMaximum is None or currentMaximum < compareWith:
-                currentMaximum = compareWith
-                currentIsStartTime = isStartTime
-
-        # Pass down to sub components/properties
-        for x in self.filters:
-            currentMaximum, currentIsStartTime = x.getmaxtimerange(currentMaximum, currentIsStartTime)
-
-        return currentMaximum, currentIsStartTime
-
-
-    def getmintimerange(self, currentMinimum, currentIsEndTime):
-        """
-        Get the date farthest into the past in any time-range elements. That is either
-        the start date, or if start is not present, the end date.
-        """
-
-        # Give tzinfo to any TimeRange we have
-        isEndTime = False
-        if isinstance(self.qualifier, TimeRange):
-            isEndTime = self.qualifier.start is None
-            compareWith = self.qualifier.end if isEndTime else self.qualifier.start
-            if currentMinimum is None or currentMinimum > compareWith:
-                currentMinimum = compareWith
-                currentIsEndTime = isEndTime
-
-        # Pass down to sub components/properties
-        for x in self.filters:
-            currentMinimum, currentIsEndTime = x.getmintimerange(currentMinimum, currentIsEndTime)
-
-        return currentMinimum, currentIsEndTime
-
-FilterBase.serialize_register(ComponentFilter)
-
-
-
-class PropertyFilter (FilterChildBase):
-    """
-    Limits a search to specific properties.
-    """
-
-    serialized_name = "PropertyFilter"
-
-    def _match(self, component, access):
-        # When access restriction is in force, we need to only allow matches against the properties
-        # allowed by the access restriction level.
-        if access:
-            allowedProperties = Component.confidentialPropertiesMap.get(component.name(), None)
-            if allowedProperties and access == Component.ACCESS_RESTRICTED:
-                allowedProperties += Component.extraRestrictedProperties
-        else:
-            allowedProperties = None
-
-        # At least one property must match (or is-not-defined is set)
-        for property in component.properties():
-            # Apply access restrictions, if any.
-            if allowedProperties is not None and property.name().upper() not in allowedProperties:
-                continue
-            if property.name().upper() == self.filter_name.upper() and self.match(property, access):
-                break
-        else:
-            return not self.defined
-        return self.defined
-
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt iCalendar
-        data object model.
-
-        @return:      True if valid, False otherwise
-        """
-
-        # Check for time-range
-        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-
-        # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
-        if timerange and self.filter_name.upper() not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
-            log.info("time-range cannot be used with property %s" % (self.filter_name,))
-            return False
-
-        # Test the time-range
-        if timerange:
-            if not self.qualifier.valid():
-                return False
-
-        # No other tests
-        return True
-
-
-    def settzinfo(self, tzinfo):
-        """
-        Set the default timezone to use with this query.
-        @param tzinfo: a L{Timezone} to use.
-        """
-
-        # Give tzinfo to any TimeRange we have
-        if isinstance(self.qualifier, TimeRange):
-            self.qualifier.settzinfo(tzinfo)
-
-
-    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
-        """
-        Get the date farthest into the future in any time-range elements
-
-        @param currentMaximum: current future value to compare with
-        @type currentMaximum: L{DateTime}
-        """
-
-        # Give tzinfo to any TimeRange we have
-        isStartTime = False
-        if isinstance(self.qualifier, TimeRange):
-            isStartTime = self.qualifier.end is None
-            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
-            if currentMaximum is None or currentMaximum < compareWith:
-                currentMaximum = compareWith
-                currentIsStartTime = isStartTime
-
-        return currentMaximum, currentIsStartTime
-
-
-    def getmintimerange(self, currentMinimum, currentIsEndTime):
-        """
-        Get the date farthest into the past in any time-range elements. That is either
-        the start date, or if start is not present, the end date.
-        """
-
-        # Give tzinfo to any TimeRange we have
-        isEndTime = False
-        if isinstance(self.qualifier, TimeRange):
-            isEndTime = self.qualifier.start is None
-            compareWith = self.qualifier.end if isEndTime else self.qualifier.start
-            if currentMinimum is None or currentMinimum > compareWith:
-                currentMinimum = compareWith
-                currentIsEndTime = isEndTime
-
-        return currentMinimum, currentIsEndTime
-
-FilterBase.serialize_register(PropertyFilter)
-
-
-
-class ParameterFilter (FilterChildBase):
-    """
-    Limits a search to specific parameters.
-    """
-
-    serialized_name = "ParameterFilter"
-
-    def _match(self, property, access):
-
-        # At least one parameter must match (or is-not-defined is set)
-        result = not self.defined
-        for parameterName in property.parameterNames():
-            if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValue(parameterName)], access):
-                result = self.defined
-                break
-
-        return result
-
-FilterBase.serialize_register(ParameterFilter)
-
-
-
-class IsNotDefined (FilterBase):
-    """
-    Specifies that the named iCalendar item does not exist.
-    """
-
-    serialized_name = "IsNotDefined"
-
-    def match(self, component, access=None):
-        # Oddly, this needs always to return True so that it appears there is
-        # a match - but we then "negate" the result if is-not-defined is set.
-        # Actually this method should never be called as we special case the
-        # is-not-defined option.
-        return True
-
-FilterBase.serialize_register(IsNotDefined)
-
-
-class TextMatch (FilterBase):
-    """
-    Specifies a substring match on a property or parameter value.
-    (CalDAV-access-09, section 9.6.4)
-    """
-    serialized_name = "TextMatch"
-
-    def __init__(self, xml_element):
-
-        super(TextMatch, self).__init__(xml_element)
-        if xml_element is None:
-            return
-
-        self.text = str(xml_element)
-        if "caseless" in xml_element.attributes:
-            caseless = xml_element.attributes["caseless"]
-            if caseless == "yes":
-                self.caseless = True
-            elif caseless == "no":
-                self.caseless = False
-        else:
-            self.caseless = True
-
-        if "negate-condition" in xml_element.attributes:
-            negate = xml_element.attributes["negate-condition"]
-            if negate == "yes":
-                self.negate = True
-            elif negate == "no":
-                self.negate = False
-        else:
-            self.negate = False
-
-        if "match-type" in xml_element.attributes:
-            self.match_type = xml_element.attributes["match-type"]
-            if self.match_type not in (
-                "equals",
-                "contains",
-                "starts-with",
-                "ends-with",
-            ):
-                self.match_type = "contains"
-        else:
-            self.match_type = "contains"
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        self.text = data["text"]
-        self.caseless = data["caseless"]
-        self.negate = data["negate"]
-        self.match_type = data["match_type"]
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        result = super(TextMatch, self).serialize()
-        result.update({
-            "text": self.text,
-            "caseless": self.caseless,
-            "negate": self.negate,
-            "match_type": self.match_type,
-        })
-        return result
-
-
-    def match(self, item, access):
-        """
-        Match the text for the item.
-        If the item is a property, then match the property value,
-        otherwise it may be a list of parameter values - try to match anyone of those
-        """
-        if item is None:
-            return False
-
-        if isinstance(item, Property):
-            values = [item.strvalue()]
-        else:
-            values = item
-
-        test = unicode(self.text, "utf-8")
-        if self.caseless:
-            test = test.lower()
-
-        def _textCompare(s):
-            if self.caseless:
-                s = s.lower()
-
-            if self.match_type == "equals":
-                return s == test
-            elif self.match_type == "contains":
-                return s.find(test) != -1
-            elif self.match_type == "starts-with":
-                return s.startswith(test)
-            elif self.match_type == "ends-with":
-                return s.endswith(test)
-            else:
-                return False
-
-        for value in values:
-            # NB Its possible that we have a text list value which appears as a Python list,
-            # so we need to check for that and iterate over the list.
-            if isinstance(value, list):
-                for subvalue in value:
-                    if _textCompare(unicode(subvalue, "utf-8")):
-                        return not self.negate
-            else:
-                if _textCompare(unicode(value, "utf-8")):
-                    return not self.negate
-
-        return self.negate
-
-FilterBase.serialize_register(TextMatch)
-
-
-
-class TimeRange (FilterBase):
-    """
-    Specifies a time for testing components against.
-    """
-
-    serialized_name = "TimeRange"
-
-    def __init__(self, xml_element):
-
-        super(TimeRange, self).__init__(xml_element)
-        if xml_element is None:
-            return
-
-        # One of start or end must be present
-        if "start" not in xml_element.attributes and "end" not in xml_element.attributes:
-            raise ValueError("One of 'start' or 'end' must be present in CALDAV:time-range")
-
-        self.start = DateTime.parseText(xml_element.attributes["start"]) if "start" in xml_element.attributes else None
-        self.end = DateTime.parseText(xml_element.attributes["end"]) if "end" in xml_element.attributes else None
-        self.tzinfo = None
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        self.start = DateTime.parseText(data["start"]) if data["start"] else None
-        self.end = DateTime.parseText(data["end"]) if data["end"] else None
-        self.tzinfo = Timezone(tzid=data["tzinfo"]) if data["tzinfo"] else None
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        result = super(TimeRange, self).serialize()
-        result.update({
-            "start": self.start.getText() if self.start else None,
-            "end": self.end.getText() if self.end else None,
-            "tzinfo": self.tzinfo.getTimezoneID() if self.tzinfo else None,
-        })
-        return result
-
-
-    def settzinfo(self, tzinfo):
-        """
-        Set the default timezone to use with this query.
-        @param tzinfo: a L{Timezone} to use.
-        """
-
-        # Give tzinfo to any TimeRange we have
-        self.tzinfo = tzinfo
-
-
-    def valid(self, level=0):
-        """
-        Indicate whether the time-range is valid (must be date-time in UTC).
-
-        @return:      True if valid, False otherwise
-        """
-
-        if self.start is not None and self.start.isDateOnly():
-            log.info("start attribute in <time-range> is not a date-time: %s" % (self.start,))
-            return False
-        if self.end is not None and self.end.isDateOnly():
-            log.info("end attribute in <time-range> is not a date-time: %s" % (self.end,))
-            return False
-        if self.start is not None and not self.start.utc():
-            log.info("start attribute in <time-range> is not UTC: %s" % (self.start,))
-            return False
-        if self.end is not None and not self.end.utc():
-            log.info("end attribute in <time-range> is not UTC: %s" % (self.end,))
-            return False
-
-        # No other tests
-        return True
-
-
-    def match(self, property, access=None):
-        """
-        NB This is only called when doing a time-range match on a property.
-        """
-        if property is None:
-            return False
-        else:
-            return property.containsTimeRange(self.start, self.end, self.tzinfo)
-
-
-    def matchinstance(self, component, instances):
-        """
-        Test whether this time-range element causes a match to the specified component
-        using the specified set of instances to determine the expanded time ranges.
-        @param component: the L{Component} to test.
-        @param instances: the list of expanded instances.
-        @return: True if the time-range query matches, False otherwise.
-        """
-        if component is None:
-            return False
-
-        assert instances is not None or self.end is None, "Failure to expand instance for time-range filter: %r" % (self,)
-
-        # Special case open-ended unbounded
-        if instances is None:
-            if component.getRecurrenceIDUTC() is None:
-                return True
-            else:
-                # See if the overridden component's start is past the start
-                start, _ignore_end = component.getEffectiveStartEnd()
-                if start is None:
-                    return True
-                else:
-                    return start >= self.start
-
-        # Handle alarms as a special case
-        alarms = (component.name() == "VALARM")
-        if alarms:
-            testcomponent = component._parent
-        else:
-            testcomponent = component
-
-        for key in instances:
-            instance = instances[key]
-
-            # First make sure components match
-            if not testcomponent.same(instance.component):
-                continue
-
-            if alarms:
-                # Get all the alarm triggers for this instance and test each one
-                triggers = instance.getAlarmTriggers()
-                for trigger in triggers:
-                    if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
-                        return True
-            else:
-                # Regular instance overlap test
-                if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
-                    return True
-
-        return False
-
-FilterBase.serialize_register(TimeRange)

Copied: CalendarServer/trunk/txdav/caldav/datastore/query/filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/filter.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/filter.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,913 @@
+##
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+"""
+Object model of CALDAV:filter element used in an addressbook-query.
+"""
+
+__all__ = [
+    "Filter",
+]
+
+from twext.python.log import Logger
+
+from twistedcaldav.caldavxml import caldav_namespace, CalDAVTimeZoneElement
+from twistedcaldav.dateops import timeRangesOverlap
+from twistedcaldav.ical import Component, Property
+
+from pycalendar.datetime import DateTime
+from pycalendar.timezone import Timezone
+
+log = Logger()
+
+
+class FilterBase(object):
+    """
+    Determines which matching components are returned.
+    """
+
+    serialized_name = None
+    deserialize_names = {}
+
+    @classmethod
+    def serialize_register(cls, register):
+        cls.deserialize_names[register.serialized_name] = register
+
+
+    def __init__(self, xml_element):
+        pass
+
+
+    @classmethod
+    def deserialize(cls, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        obj = cls.deserialize_names[data["type"]](None)
+        obj._deserialize(data)
+        return obj
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        pass
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        return {
+            "type": self.serialized_name,
+        }
+
+
+    def match(self, item, access=None):
+        raise NotImplementedError
+
+
+    def valid(self, level=0):
+        raise NotImplementedError
+
+
+
+class Filter(FilterBase):
+    """
+    Determines which matching components are returned.
+    """
+
+    serialized_name = "Filter"
+
+    def __init__(self, xml_element):
+
+        super(Filter, self).__init__(xml_element)
+        if xml_element is None:
+            return
+
+        # One comp-filter element must be present
+        if len(xml_element.children) != 1 or xml_element.children[0].qname() != (caldav_namespace, "comp-filter"):
+            raise ValueError("Invalid CALDAV:filter element: %s" % (xml_element,))
+
+        self.child = ComponentFilter(xml_element.children[0])
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        self.child = FilterBase.deserialize(data["child"])
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        result = super(Filter, self).serialize()
+        result.update({
+            "child": self.child.serialize(),
+        })
+        return result
+
+
+    def match(self, component, access=None):
+        """
+        Returns True if the given calendar component matches this filter, False
+        otherwise.
+        """
+
+        # We only care about certain access restrictions.
+        if access not in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
+            access = None
+
+        # We need to prepare ourselves for a time-range query by pre-calculating
+        # the set of instances up to the latest time-range limit. That way we can
+        # avoid having to do some form of recurrence expansion for each query sub-part.
+        maxend, isStartTime = self.getmaxtimerange()
+        if maxend:
+            if isStartTime:
+                if component.isRecurringUnbounded():
+                    # Unbounded recurrence is always within a start-only time-range
+                    instances = None
+                else:
+                    # Expand the instances up to infinity
+                    instances = component.expandTimeRanges(DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), ignoreInvalidInstances=True)
+            else:
+                instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
+        else:
+            instances = None
+        self.child.setInstances(instances)
+
+        # <filter> contains exactly one <comp-filter>
+        return self.child.match(component, access)
+
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt iCalendar
+        data object model.
+
+        @return: True if valid, False otherwise
+        """
+
+        # Must have one child element for VCALENDAR
+        return self.child.valid(0)
+
+
+    def settimezone(self, tzelement):
+        """
+        Set the default timezone to use with this query.
+        @param calendar: a L{Component} for the VCALENDAR containing the one
+            VTIMEZONE that we want
+        @return: the L{Timezone} derived from the VTIMEZONE or utc.
+        """
+
+        if tzelement is None:
+            tz = None
+        elif isinstance(tzelement, CalDAVTimeZoneElement):
+            tz = tzelement.gettimezone()
+        elif isinstance(tzelement, Component):
+            tz = tzelement.gettimezone()
+        if tz is None:
+            tz = Timezone(utc=True)
+        self.child.settzinfo(tz)
+        return tz
+
+
+    def getmaxtimerange(self):
+        """
+        Get the date farthest into the future in any time-range elements
+        """
+
+        return self.child.getmaxtimerange(None, False)
+
+
+    def getmintimerange(self):
+        """
+        Get the date farthest into the past in any time-range elements. That is either
+        the start date, or if start is not present, the end date.
+        """
+
+        return self.child.getmintimerange(None, False)
+
+FilterBase.serialize_register(Filter)
+
+
+
+class FilterChildBase(FilterBase):
+    """
+    CalDAV filter element.
+    """
+
+    def __init__(self, xml_element):
+
+        super(FilterChildBase, self).__init__(xml_element)
+        if xml_element is None:
+            return
+
+        qualifier = None
+        filters = []
+
+        for child in xml_element.children:
+            qname = child.qname()
+
+            if qname in (
+                (caldav_namespace, "is-not-defined"),
+                (caldav_namespace, "time-range"),
+                (caldav_namespace, "text-match"),
+            ):
+                if qualifier is not None:
+                    raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
+
+                if qname == (caldav_namespace, "is-not-defined"):
+                    qualifier = IsNotDefined(child)
+                elif qname == (caldav_namespace, "time-range"):
+                    qualifier = TimeRange(child)
+                elif qname == (caldav_namespace, "text-match"):
+                    qualifier = TextMatch(child)
+
+            elif qname == (caldav_namespace, "comp-filter"):
+                filters.append(ComponentFilter(child))
+            elif qname == (caldav_namespace, "prop-filter"):
+                filters.append(PropertyFilter(child))
+            elif qname == (caldav_namespace, "param-filter"):
+                filters.append(ParameterFilter(child))
+            else:
+                raise ValueError("Unknown child element: %s" % (qname,))
+
+        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
+            raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
+
+        self.qualifier = qualifier
+        self.filters = filters
+        self.filter_name = xml_element.attributes["name"]
+        if isinstance(self.filter_name, unicode):
+            self.filter_name = self.filter_name.encode("utf-8")
+        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
+
+        filter_test = xml_element.attributes.get("test", "allof")
+        if filter_test not in ("anyof", "allof"):
+            raise ValueError("Test must be only one of anyof, allof")
+        self.filter_test = filter_test
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
+        self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
+        self.filter_name = data["filter_name"]
+        self.defined = data["defined"]
+        self.filter_test = data["filter_test"]
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        result = super(FilterChildBase, self).serialize()
+        result.update({
+            "qualifier": self.qualifier.serialize() if self.qualifier else None,
+            "filters": [filter.serialize() for filter in self.filters],
+            "filter_name": self.filter_name,
+            "defined": self.defined,
+            "filter_test": self.filter_test,
+        })
+        return result
+
+
+    def match(self, item, access=None):
+        """
+        Returns True if the given calendar item (either a component, property or parameter value)
+        matches this filter, False otherwise.
+        """
+
+        # Always return True for the is-not-defined case as the result of this will
+        # be negated by the caller
+        if not self.defined:
+            return True
+
+        if self.qualifier and not self.qualifier.match(item, access):
+            return False
+
+        if len(self.filters) > 0:
+            allof = self.filter_test == "allof"
+            for filter in self.filters:
+                if allof != filter._match(item, access):
+                    return not allof
+            return allof
+        else:
+            return True
+
+
+
+class ComponentFilter (FilterChildBase):
+    """
+    Limits a search to only the chosen component types.
+    """
+
+    serialized_name = "ComponentFilter"
+
+    def match(self, item, access):
+        """
+        Returns True if the given calendar item (which is a component)
+        matches this filter, False otherwise.
+        This specialization uses the instance matching option of the time-range filter
+        to minimize instance expansion.
+        """
+
+        # Always return True for the is-not-defined case as the result of this will
+        # be negated by the caller
+        if not self.defined:
+            return True
+
+        if self.qualifier and not self.qualifier.matchinstance(item, self.instances):
+            return False
+
+        if len(self.filters) > 0:
+            allof = self.filter_test == "allof"
+            for filter in self.filters:
+                if allof != filter._match(item, access):
+                    return not allof
+            return allof
+        else:
+            return True
+
+
+    def _match(self, component, access):
+        # At least one subcomponent must match (or is-not-defined is set)
+        for subcomponent in component.subcomponents():
+            # If access restrictions are in force, restrict matching to specific components only.
+            # In particular do not match VALARM.
+            if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
+                continue
+
+            # Try to match the component name
+            if isinstance(self.filter_name, str):
+                if subcomponent.name() != self.filter_name:
+                    continue
+            else:
+                if subcomponent.name() not in self.filter_name:
+                    continue
+            if self.match(subcomponent, access):
+                break
+        else:
+            return not self.defined
+        return self.defined
+
+
+    def setInstances(self, instances):
+        """
+        Give the list of instances to each comp-filter element.
+        @param instances: the list of instances.
+        """
+        self.instances = instances
+        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+            compfilter.setInstances(instances)
+
+
+    def valid(self, level):
+        """
+        Indicate whether this filter element's structure is valid wrt iCalendar
+        data object model.
+
+        @param level: the nesting level of this filter element, 0 being the top comp-filter.
+        @return:      True if valid, False otherwise
+        """
+
+        # Check for time-range
+        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+
+        if level == 0:
+            # Must have VCALENDAR at the top
+            if (self.filter_name != "VCALENDAR") or timerange:
+                log.info("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
+                return False
+        elif level == 1:
+            # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
+            if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
+                log.info("comp-filter wrong component type: %s" % (self.filter_name,))
+                return False
+
+            # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
+            if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
+                log.info("time-range cannot be used with component %s" % (self.filter_name,))
+                return False
+        elif level == 2:
+            # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
+            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
+                log.info("comp-filter wrong sub-component type: %s" % (self.filter_name,))
+                return False
+
+            # time-range only on VALARM, AVAILABLE
+            if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
+                log.info("time-range cannot be used with sub-component %s" % (self.filter_name,))
+                return False
+        else:
+            # Disallow all standard iCal components anywhere else
+            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
+                log.info("comp-filter wrong standard component type: %s" % (self.filter_name,))
+                return False
+
+        # Test each property
+        for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
+            if not propfilter.valid():
+                return False
+
+        # Test each component
+        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+            if not compfilter.valid(level + 1):
+                return False
+
+        # Test the time-range
+        if timerange:
+            if not self.qualifier.valid():
+                return False
+
+        return True
+
+
+    def settzinfo(self, tzinfo):
+        """
+        Set the default timezone to use with this query.
+        @param tzinfo: a L{Timezone} to use.
+        """
+
+        # Give tzinfo to any TimeRange we have
+        if isinstance(self.qualifier, TimeRange):
+            self.qualifier.settzinfo(tzinfo)
+
+        # Pass down to sub components/properties
+        for x in self.filters:
+            x.settzinfo(tzinfo)
+
+
+    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+        """
+        Get the date farthest into the future in any time-range elements
+
+        @param currentMaximum: current future value to compare with
+        @type currentMaximum: L{DateTime}
+        """
+
+        # Give tzinfo to any TimeRange we have
+        isStartTime = False
+        if isinstance(self.qualifier, TimeRange):
+            isStartTime = self.qualifier.end is None
+            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+            if currentMaximum is None or currentMaximum < compareWith:
+                currentMaximum = compareWith
+                currentIsStartTime = isStartTime
+
+        # Pass down to sub components/properties
+        for x in self.filters:
+            currentMaximum, currentIsStartTime = x.getmaxtimerange(currentMaximum, currentIsStartTime)
+
+        return currentMaximum, currentIsStartTime
+
+
+    def getmintimerange(self, currentMinimum, currentIsEndTime):
+        """
+        Get the date farthest into the past in any time-range elements. That is either
+        the start date, or if start is not present, the end date.
+        """
+
+        # Give tzinfo to any TimeRange we have
+        isEndTime = False
+        if isinstance(self.qualifier, TimeRange):
+            isEndTime = self.qualifier.start is None
+            compareWith = self.qualifier.end if isEndTime else self.qualifier.start
+            if currentMinimum is None or currentMinimum > compareWith:
+                currentMinimum = compareWith
+                currentIsEndTime = isEndTime
+
+        # Pass down to sub components/properties
+        for x in self.filters:
+            currentMinimum, currentIsEndTime = x.getmintimerange(currentMinimum, currentIsEndTime)
+
+        return currentMinimum, currentIsEndTime
+
+FilterBase.serialize_register(ComponentFilter)
+
+
+
+class PropertyFilter (FilterChildBase):
+    """
+    Limits a search to specific properties.
+    """
+
+    serialized_name = "PropertyFilter"
+
+    def _match(self, component, access):
+        # When access restriction is in force, we need to only allow matches against the properties
+        # allowed by the access restriction level.
+        if access:
+            allowedProperties = Component.confidentialPropertiesMap.get(component.name(), None)
+            if allowedProperties and access == Component.ACCESS_RESTRICTED:
+                allowedProperties += Component.extraRestrictedProperties
+        else:
+            allowedProperties = None
+
+        # At least one property must match (or is-not-defined is set)
+        for property in component.properties():
+            # Apply access restrictions, if any.
+            if allowedProperties is not None and property.name().upper() not in allowedProperties:
+                continue
+            if property.name().upper() == self.filter_name.upper() and self.match(property, access):
+                break
+        else:
+            return not self.defined
+        return self.defined
+
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt iCalendar
+        data object model.
+
+        @return:      True if valid, False otherwise
+        """
+
+        # Check for time-range
+        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+
+        # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
+        if timerange and self.filter_name.upper() not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
+            log.info("time-range cannot be used with property %s" % (self.filter_name,))
+            return False
+
+        # Test the time-range
+        if timerange:
+            if not self.qualifier.valid():
+                return False
+
+        # No other tests
+        return True
+
+
+    def settzinfo(self, tzinfo):
+        """
+        Set the default timezone to use with this query.
+        @param tzinfo: a L{Timezone} to use.
+        """
+
+        # Give tzinfo to any TimeRange we have
+        if isinstance(self.qualifier, TimeRange):
+            self.qualifier.settzinfo(tzinfo)
+
+
+    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+        """
+        Get the date farthest into the future in any time-range elements
+
+        @param currentMaximum: current future value to compare with
+        @type currentMaximum: L{DateTime}
+        """
+
+        # Give tzinfo to any TimeRange we have
+        isStartTime = False
+        if isinstance(self.qualifier, TimeRange):
+            isStartTime = self.qualifier.end is None
+            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+            if currentMaximum is None or currentMaximum < compareWith:
+                currentMaximum = compareWith
+                currentIsStartTime = isStartTime
+
+        return currentMaximum, currentIsStartTime
+
+
+    def getmintimerange(self, currentMinimum, currentIsEndTime):
+        """
+        Get the date farthest into the past in any time-range elements. That is either
+        the start date, or if start is not present, the end date.
+        """
+
+        # Give tzinfo to any TimeRange we have
+        isEndTime = False
+        if isinstance(self.qualifier, TimeRange):
+            isEndTime = self.qualifier.start is None
+            compareWith = self.qualifier.end if isEndTime else self.qualifier.start
+            if currentMinimum is None or currentMinimum > compareWith:
+                currentMinimum = compareWith
+                currentIsEndTime = isEndTime
+
+        return currentMinimum, currentIsEndTime
+
+FilterBase.serialize_register(PropertyFilter)
+
+
+
+class ParameterFilter (FilterChildBase):
+    """
+    Limits a search to specific parameters.
+    """
+
+    serialized_name = "ParameterFilter"
+
+    def _match(self, property, access):
+
+        # At least one parameter must match (or is-not-defined is set)
+        result = not self.defined
+        for parameterName in property.parameterNames():
+            if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValue(parameterName)], access):
+                result = self.defined
+                break
+
+        return result
+
+FilterBase.serialize_register(ParameterFilter)
+
+
+
+class IsNotDefined (FilterBase):
+    """
+    Specifies that the named iCalendar item does not exist.
+    """
+
+    serialized_name = "IsNotDefined"
+
+    def match(self, component, access=None):
+        # Oddly, this needs always to return True so that it appears there is
+        # a match - but we then "negate" the result if is-not-defined is set.
+        # Actually this method should never be called as we special case the
+        # is-not-defined option.
+        return True
+
+FilterBase.serialize_register(IsNotDefined)
+
+
+class TextMatch (FilterBase):
+    """
+    Specifies a substring match on a property or parameter value.
+    (CalDAV-access-09, section 9.6.4)
+    """
+    serialized_name = "TextMatch"
+
+    def __init__(self, xml_element):
+
+        super(TextMatch, self).__init__(xml_element)
+        if xml_element is None:
+            return
+
+        self.text = str(xml_element)
+        if "caseless" in xml_element.attributes:
+            caseless = xml_element.attributes["caseless"]
+            if caseless == "yes":
+                self.caseless = True
+            elif caseless == "no":
+                self.caseless = False
+        else:
+            self.caseless = True
+
+        if "negate-condition" in xml_element.attributes:
+            negate = xml_element.attributes["negate-condition"]
+            if negate == "yes":
+                self.negate = True
+            elif negate == "no":
+                self.negate = False
+        else:
+            self.negate = False
+
+        if "match-type" in xml_element.attributes:
+            self.match_type = xml_element.attributes["match-type"]
+            if self.match_type not in (
+                "equals",
+                "contains",
+                "starts-with",
+                "ends-with",
+            ):
+                self.match_type = "contains"
+        else:
+            self.match_type = "contains"
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        self.text = data["text"]
+        self.caseless = data["caseless"]
+        self.negate = data["negate"]
+        self.match_type = data["match_type"]
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        result = super(TextMatch, self).serialize()
+        result.update({
+            "text": self.text,
+            "caseless": self.caseless,
+            "negate": self.negate,
+            "match_type": self.match_type,
+        })
+        return result
+
+
+    def match(self, item, access):
+        """
+        Match the text for the item.
+        If the item is a property, then match the property value,
+        otherwise it may be a list of parameter values - try to match anyone of those
+        """
+        if item is None:
+            return False
+
+        if isinstance(item, Property):
+            values = [item.strvalue()]
+        else:
+            values = item
+
+        test = unicode(self.text, "utf-8")
+        if self.caseless:
+            test = test.lower()
+
+        def _textCompare(s):
+            if self.caseless:
+                s = s.lower()
+
+            if self.match_type == "equals":
+                return s == test
+            elif self.match_type == "contains":
+                return s.find(test) != -1
+            elif self.match_type == "starts-with":
+                return s.startswith(test)
+            elif self.match_type == "ends-with":
+                return s.endswith(test)
+            else:
+                return False
+
+        for value in values:
+            # NB Its possible that we have a text list value which appears as a Python list,
+            # so we need to check for that and iterate over the list.
+            if isinstance(value, list):
+                for subvalue in value:
+                    if _textCompare(unicode(subvalue, "utf-8")):
+                        return not self.negate
+            else:
+                if _textCompare(unicode(value, "utf-8")):
+                    return not self.negate
+
+        return self.negate
+
+FilterBase.serialize_register(TextMatch)
+
+
+
+class TimeRange (FilterBase):
+    """
+    Specifies a time for testing components against.
+    """
+
+    serialized_name = "TimeRange"
+
+    def __init__(self, xml_element):
+
+        super(TimeRange, self).__init__(xml_element)
+        if xml_element is None:
+            return
+
+        # One of start or end must be present
+        if "start" not in xml_element.attributes and "end" not in xml_element.attributes:
+            raise ValueError("One of 'start' or 'end' must be present in CALDAV:time-range")
+
+        self.start = DateTime.parseText(xml_element.attributes["start"]) if "start" in xml_element.attributes else None
+        self.end = DateTime.parseText(xml_element.attributes["end"]) if "end" in xml_element.attributes else None
+        self.tzinfo = None
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        self.start = DateTime.parseText(data["start"]) if data["start"] else None
+        self.end = DateTime.parseText(data["end"]) if data["end"] else None
+        self.tzinfo = Timezone(tzid=data["tzinfo"]) if data["tzinfo"] else None
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        result = super(TimeRange, self).serialize()
+        result.update({
+            "start": self.start.getText() if self.start else None,
+            "end": self.end.getText() if self.end else None,
+            "tzinfo": self.tzinfo.getTimezoneID() if self.tzinfo else None,
+        })
+        return result
+
+
+    def settzinfo(self, tzinfo):
+        """
+        Set the default timezone to use with this query.
+        @param tzinfo: a L{Timezone} to use.
+        """
+
+        # Give tzinfo to any TimeRange we have
+        self.tzinfo = tzinfo
+
+
+    def valid(self, level=0):
+        """
+        Indicate whether the time-range is valid (must be date-time in UTC).
+
+        @return:      True if valid, False otherwise
+        """
+
+        if self.start is not None and self.start.isDateOnly():
+            log.info("start attribute in <time-range> is not a date-time: %s" % (self.start,))
+            return False
+        if self.end is not None and self.end.isDateOnly():
+            log.info("end attribute in <time-range> is not a date-time: %s" % (self.end,))
+            return False
+        if self.start is not None and not self.start.utc():
+            log.info("start attribute in <time-range> is not UTC: %s" % (self.start,))
+            return False
+        if self.end is not None and not self.end.utc():
+            log.info("end attribute in <time-range> is not UTC: %s" % (self.end,))
+            return False
+
+        # No other tests
+        return True
+
+
+    def match(self, property, access=None):
+        """
+        NB This is only called when doing a time-range match on a property.
+        """
+        if property is None:
+            return False
+        else:
+            return property.containsTimeRange(self.start, self.end, self.tzinfo)
+
+
+    def matchinstance(self, component, instances):
+        """
+        Test whether this time-range element causes a match to the specified component
+        using the specified set of instances to determine the expanded time ranges.
+        @param component: the L{Component} to test.
+        @param instances: the list of expanded instances.
+        @return: True if the time-range query matches, False otherwise.
+        """
+        if component is None:
+            return False
+
+        assert instances is not None or self.end is None, "Failure to expand instance for time-range filter: %r" % (self,)
+
+        # Special case open-ended unbounded
+        if instances is None:
+            if component.getRecurrenceIDUTC() is None:
+                return True
+            else:
+                # See if the overridden component's start is past the start
+                start, _ignore_end = component.getEffectiveStartEnd()
+                if start is None:
+                    return True
+                else:
+                    return start >= self.start
+
+        # Handle alarms as a special case
+        alarms = (component.name() == "VALARM")
+        if alarms:
+            testcomponent = component._parent
+        else:
+            testcomponent = component
+
+        for key in instances:
+            instance = instances[key]
+
+            # First make sure components match
+            if not testcomponent.same(instance.component):
+                continue
+
+            if alarms:
+                # Get all the alarm triggers for this instance and test each one
+                triggers = instance.getAlarmTriggers()
+                for trigger in triggers:
+                    if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
+                        return True
+            else:
+                # Regular instance overlap test
+                if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
+                    return True
+
+        return False
+
+FilterBase.serialize_register(TimeRange)

Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/generator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/generator.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/generator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,207 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twext.enterprise.dal.syntax import Select
-
-from txdav.common.datastore.query import expression
-from txdav.common.datastore.query.generator import SQLQueryGenerator
-from txdav.common.datastore.sql_tables import schema
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
-    "CalDAVSQLQueryGenerator",
-]
-
-class CalDAVSQLQueryGenerator(SQLQueryGenerator):
-
-    _timerange = schema.TIME_RANGE
-    _transparency = schema.TRANSPARENCY
-
-    def __init__(self, expr, collection, whereid, userid=None, freebusy=False):
-        """
-
-        @param expr: the query expression object model
-        @type expr: L{expression}
-        @param collection: the resource targeted by the query
-        @type collection: L{CommonHomeChild}
-        @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
-        @type userid: C{str}
-        @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
-        @type freebusy: C{bool}
-        """
-        super(CalDAVSQLQueryGenerator, self).__init__(expr, collection, whereid)
-        self.userid = userid if userid else ""
-        self.freebusy = freebusy
-        self.usedtimerange = False
-
-
-    def generate(self):
-        """
-        Generate the actual SQL statement from the passed in expression tree.
-
-        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
-            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
-        """
-
-        # Init state
-        self.arguments = {}
-        self.argcount = 0
-        obj = self.collection._objectSchema
-
-        columns = [obj.RESOURCE_NAME, obj.ICALENDAR_UID, obj.ICALENDAR_TYPE]
-        if self.freebusy:
-            columns.extend([
-                obj.ORGANIZER,
-                self._timerange.FLOATING,
-                self._timerange.START_DATE,
-                self._timerange.END_DATE,
-                self._timerange.FBTYPE,
-                self._timerange.TRANSPARENT,
-                self._transparency.TRANSPARENT,
-            ])
-
-        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
-        if self.whereid:
-
-            test = expression.isExpression(obj.CALENDAR_RESOURCE_ID, self.whereid, True)
-
-            # Since timerange expression already have the calendar resource-id test in them, do not
-            # add the additional term to those. When the additional term is added, add it as the first
-            # component in the AND expression to hopefully get the DB to use its index first
-
-            # Top-level timerange expression already has calendar resource-id restriction in it
-            if isinstance(self.expression, expression.timerangeExpression):
-                pass
-
-            # Top-level OR - check each component
-            elif isinstance(self.expression, expression.orExpression):
-
-                def _hasTopLevelTimerange(testexpr):
-                    if isinstance(testexpr, expression.timerangeExpression):
-                        return True
-                    elif isinstance(testexpr, expression.andExpression):
-                        return any([isinstance(expr, expression.timerangeExpression) for expr in testexpr.expressions])
-                    else:
-                        return False
-
-                hasTimerange = any([_hasTopLevelTimerange(expr) for expr in self.expression.expressions])
-
-                if hasTimerange:
-                    # timerange expression forces a join on calendarid
-                    pass
-                else:
-                    # AND the whole thing with calendarid
-                    self.expression = test.andWith(self.expression)
-
-            # Top-level AND - only add additional expression if timerange not present
-            elif isinstance(self.expression, expression.andExpression):
-                hasTimerange = any([isinstance(expr, expression.timerangeExpression) for expr in self.expression.expressions])
-                if not hasTimerange:
-                    # AND the whole thing
-                    self.expression = test.andWith(self.expression)
-
-            # Just use the id test
-            elif isinstance(self.expression, expression.allExpression):
-                self.expression = test
-
-            # Just AND the entire thing
-            else:
-                self.expression = test.andWith(self.expression)
-
-        # Generate ' where ...' partial statement
-        where = self.generateExpression(self.expression)
-
-        if self.usedtimerange:
-            where = where.And(self._timerange.CALENDAR_OBJECT_RESOURCE_ID == obj.RESOURCE_ID).And(self._timerange.CALENDAR_RESOURCE_ID == self.whereid)
-
-        # Set of tables depends on use of timespan and fb use
-        if self.usedtimerange:
-            if self.freebusy:
-                tables = obj.join(
-                    self._timerange.join(
-                        self._transparency,
-                        on=(self._timerange.INSTANCE_ID == self._transparency.TIME_RANGE_INSTANCE_ID).And(self._transparency.USER_ID == self.userid),
-                        type="left outer"
-                    ),
-                    type=","
-                )
-            else:
-                tables = obj.join(self._timerange, type=",")
-        else:
-            tables = obj
-
-        select = Select(
-            columns,
-            From=tables,
-            Where=where,
-            Distinct=True,
-        )
-
-        return select, self.arguments, self.usedtimerange
-
-
-    def generateExpression(self, expr):
-        """
-        Generate an expression and all it's subexpressions.
-
-        @param expr: the L{baseExpression} derived class to write out.
-        """
-
-        # Generate based on each type of expression we might encounter
-        partial = None
-
-        # time-range
-        if isinstance(expr, expression.timerangeExpression):
-            if expr.start and expr.end:
-                partial = (
-                    (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end).And(self._timerange.END_DATE > expr.start)
-                ).Or(
-                    (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat).And(self._timerange.END_DATE > expr.startfloat)
-                )
-            elif expr.start and expr.end is None:
-                partial = (
-                    (self._timerange.FLOATING == False).And(self._timerange.END_DATE > expr.start)
-                ).Or(
-                    (self._timerange.FLOATING == True).And(self._timerange.END_DATE > expr.startfloat)
-                )
-            elif not expr.start and expr.end:
-                partial = (
-                    (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end)
-                ).Or(
-                    (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat)
-                )
-            self.usedtimerange = True
-
-        else:
-            partial = super(CalDAVSQLQueryGenerator, self).generateExpression(expr)
-
-        return partial
-
-
-    def addArgument(self, arg):
-        """
-
-        @param arg: the C{str} of the argument to add
-        """
-
-        # Append argument to the list and add the appropriate substitution string to the output stream.
-        self.argcount += 1
-        argname = "arg{}".format(self.argcount)
-        self.arguments[argname] = arg
-        return argname

Copied: CalendarServer/trunk/txdav/caldav/datastore/query/generator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/generator.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/generator.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/generator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,207 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twext.enterprise.dal.syntax import Select
+
+from txdav.common.datastore.query import expression
+from txdav.common.datastore.query.generator import SQLQueryGenerator
+from txdav.common.datastore.sql_tables import schema
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+    "CalDAVSQLQueryGenerator",
+]
+
+class CalDAVSQLQueryGenerator(SQLQueryGenerator):
+
+    _timerange = schema.TIME_RANGE
+    _transparency = schema.TRANSPARENCY
+
+    def __init__(self, expr, collection, whereid, userid=None, freebusy=False):
+        """
+
+        @param expr: the query expression object model
+        @type expr: L{expression}
+        @param collection: the resource targeted by the query
+        @type collection: L{CommonHomeChild}
+        @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
+        @type userid: C{str}
+        @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
+        @type freebusy: C{bool}
+        """
+        super(CalDAVSQLQueryGenerator, self).__init__(expr, collection, whereid)
+        self.userid = userid if userid else ""
+        self.freebusy = freebusy
+        self.usedtimerange = False
+
+
+    def generate(self):
+        """
+        Generate the actual SQL statement from the passed in expression tree.
+
+        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+        """
+
+        # Init state
+        self.arguments = {}
+        self.argcount = 0
+        obj = self.collection._objectSchema
+
+        columns = [obj.RESOURCE_NAME, obj.ICALENDAR_UID, obj.ICALENDAR_TYPE]
+        if self.freebusy:
+            columns.extend([
+                obj.ORGANIZER,
+                self._timerange.FLOATING,
+                self._timerange.START_DATE,
+                self._timerange.END_DATE,
+                self._timerange.FBTYPE,
+                self._timerange.TRANSPARENT,
+                self._transparency.TRANSPARENT,
+            ])
+
+        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
+        if self.whereid:
+
+            test = expression.isExpression(obj.CALENDAR_RESOURCE_ID, self.whereid, True)
+
+            # Since timerange expression already have the calendar resource-id test in them, do not
+            # add the additional term to those. When the additional term is added, add it as the first
+            # component in the AND expression to hopefully get the DB to use its index first
+
+            # Top-level timerange expression already has calendar resource-id restriction in it
+            if isinstance(self.expression, expression.timerangeExpression):
+                pass
+
+            # Top-level OR - check each component
+            elif isinstance(self.expression, expression.orExpression):
+
+                def _hasTopLevelTimerange(testexpr):
+                    if isinstance(testexpr, expression.timerangeExpression):
+                        return True
+                    elif isinstance(testexpr, expression.andExpression):
+                        return any([isinstance(expr, expression.timerangeExpression) for expr in testexpr.expressions])
+                    else:
+                        return False
+
+                hasTimerange = any([_hasTopLevelTimerange(expr) for expr in self.expression.expressions])
+
+                if hasTimerange:
+                    # timerange expression forces a join on calendarid
+                    pass
+                else:
+                    # AND the whole thing with calendarid
+                    self.expression = test.andWith(self.expression)
+
+            # Top-level AND - only add additional expression if timerange not present
+            elif isinstance(self.expression, expression.andExpression):
+                hasTimerange = any([isinstance(expr, expression.timerangeExpression) for expr in self.expression.expressions])
+                if not hasTimerange:
+                    # AND the whole thing
+                    self.expression = test.andWith(self.expression)
+
+            # Just use the id test
+            elif isinstance(self.expression, expression.allExpression):
+                self.expression = test
+
+            # Just AND the entire thing
+            else:
+                self.expression = test.andWith(self.expression)
+
+        # Generate ' where ...' partial statement
+        where = self.generateExpression(self.expression)
+
+        if self.usedtimerange:
+            where = where.And(self._timerange.CALENDAR_OBJECT_RESOURCE_ID == obj.RESOURCE_ID).And(self._timerange.CALENDAR_RESOURCE_ID == self.whereid)
+
+        # Set of tables depends on use of timespan and fb use
+        if self.usedtimerange:
+            if self.freebusy:
+                tables = obj.join(
+                    self._timerange.join(
+                        self._transparency,
+                        on=(self._timerange.INSTANCE_ID == self._transparency.TIME_RANGE_INSTANCE_ID).And(self._transparency.USER_ID == self.userid),
+                        type="left outer"
+                    ),
+                    type=","
+                )
+            else:
+                tables = obj.join(self._timerange, type=",")
+        else:
+            tables = obj
+
+        select = Select(
+            columns,
+            From=tables,
+            Where=where,
+            Distinct=True,
+        )
+
+        return select, self.arguments, self.usedtimerange
+
+
+    def generateExpression(self, expr):
+        """
+        Generate an expression and all it's subexpressions.
+
+        @param expr: the L{baseExpression} derived class to write out.
+        """
+
+        # Generate based on each type of expression we might encounter
+        partial = None
+
+        # time-range
+        if isinstance(expr, expression.timerangeExpression):
+            if expr.start and expr.end:
+                partial = (
+                    (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end).And(self._timerange.END_DATE > expr.start)
+                ).Or(
+                    (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat).And(self._timerange.END_DATE > expr.startfloat)
+                )
+            elif expr.start and expr.end is None:
+                partial = (
+                    (self._timerange.FLOATING == False).And(self._timerange.END_DATE > expr.start)
+                ).Or(
+                    (self._timerange.FLOATING == True).And(self._timerange.END_DATE > expr.startfloat)
+                )
+            elif not expr.start and expr.end:
+                partial = (
+                    (self._timerange.FLOATING == False).And(self._timerange.START_DATE < expr.end)
+                ).Or(
+                    (self._timerange.FLOATING == True).And(self._timerange.START_DATE < expr.endfloat)
+                )
+            self.usedtimerange = True
+
+        else:
+            partial = super(CalDAVSQLQueryGenerator, self).generateExpression(expr)
+
+        return partial
+
+
+    def addArgument(self, arg):
+        """
+
+        @param arg: the C{str} of the argument to add
+        """
+
+        # Append argument to the list and add the appropriate substitution string to the output stream.
+        self.argcount += 1
+        argname = "arg{}".format(self.argcount)
+        self.arguments[argname] = arg
+        return argname

Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/test_filter.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,435 +0,0 @@
-##
-# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from pycalendar.timezone import Timezone
-
-from twext.enterprise.dal.syntax import SQLFragment, Parameter
-
-from twistedcaldav.test.util import TestCase
-from twistedcaldav import caldavxml
-from twistedcaldav.timezones import TimezoneCache
-
-from txdav.caldav.datastore.index_file import sqlcalendarquery
-from txdav.caldav.datastore.query.builder import buildExpression
-from txdav.caldav.datastore.query.filter import Filter, FilterBase, TimeRange, \
-    PropertyFilter, TextMatch
-from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
-from txdav.common.datastore.sql_tables import schema
-
-from dateutil.tz import tzutc
-import datetime
-from twistedcaldav.ical import Component
-
-class TestQueryFilter(TestCase):
-
-    _objectSchema = schema.CALENDAR_OBJECT
-    _queryFields = {
-        "UID": _objectSchema.UID,
-        "TYPE": _objectSchema.ICALENDAR_TYPE,
-    }
-
-    def setUp(self):
-        super(TestQueryFilter, self).setUp()
-        TimezoneCache.create()
-
-
-    def test_query(self):
-        """
-        Basic query test - no time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
-        expression = buildExpression(filter, self._queryFields)
-        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
-        select, args, usedtimerange = sql.generate()
-
-        self.assertEqual(select.toSQL(), SQLFragment(
-            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE in (?, ?, ?)",
-            [1234, Parameter('arg1', 3)]
-        ))
-        self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
-        self.assertEqual(usedtimerange, False)
-
-
-    def test_query_timerange(self):
-        """
-        Basic query test - with time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
-        expression = buildExpression(filter, self._queryFields)
-        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
-        select, args, usedtimerange = sql.generate()
-
-        self.assertEqual(select.toSQL(), SQLFragment(
-            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
-            [Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
-        ))
-        self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
-        self.assertEqual(usedtimerange, True)
-
-
-    def test_query_freebusy(self):
-        """
-        Basic query test - with time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
-        expression = buildExpression(filter, self._queryFields)
-        sql = CalDAVSQLQueryGenerator(expression, self, 1234, "user01", True)
-        select, args, usedtimerange = sql.generate()
-
-        self.assertEqual(select.toSQL(), SQLFragment(
-            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE, ORGANIZER, FLOATING, START_DATE, END_DATE, FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT from CALENDAR_OBJECT, TIME_RANGE left outer join TRANSPARENCY on INSTANCE_ID = TIME_RANGE_INSTANCE_ID and USER_ID = ? where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
-            ['user01', Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
-        ))
-        self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
-        self.assertEqual(usedtimerange, True)
-
-
-    def test_query_not_extended(self):
-        """
-        Query test - two terms not anyof
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[
-                    caldavxml.ComponentFilter(
-                        **{"name":("VEVENT")}
-                    ),
-                    caldavxml.ComponentFilter(
-                        **{"name":("VTODO")}
-                    ),
-                ],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
-        expression = buildExpression(filter, self._queryFields)
-        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
-        select, args, usedtimerange = sql.generate()
-
-        self.assertEqual(select.toSQL(), SQLFragment(
-            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE = ? and ICALENDAR_TYPE = ?",
-            [1234, "VEVENT", "VTODO"]
-        ))
-        self.assertEqual(args, {})
-        self.assertEqual(usedtimerange, False)
-
-
-    def test_query_extended(self):
-        """
-        Extended query test - two terms with anyof
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[
-                    caldavxml.ComponentFilter(
-                        *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
-                        **{"name":("VEVENT")}
-                    ),
-                    caldavxml.ComponentFilter(
-                        **{"name":("VTODO")}
-                    ),
-                ],
-                **{"name": "VCALENDAR", "test": "anyof"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
-        expression = buildExpression(filter, self._queryFields)
-        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
-        select, args, usedtimerange = sql.generate()
-
-        self.assertEqual(select.toSQL(), SQLFragment(
-            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where (ICALENDAR_TYPE = ? and (FLOATING = ? and END_DATE > ? or FLOATING = ? and END_DATE > ?) or ICALENDAR_TYPE = ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
-            ['VEVENT', False, datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 'VTODO', 1234]
-        ))
-        self.assertEqual(args, {})
-        self.assertEqual(usedtimerange, True)
-
-
-    def test_sqllite_query(self):
-        """
-        Basic query test - single term.
-        Only UID can be queried via sql.
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        sql, args = sqlcalendarquery(filter, 1234)
-
-        self.assertTrue(sql.find("RESOURCE") != -1)
-        self.assertTrue(sql.find("TIMESPAN") == -1)
-        self.assertTrue(sql.find("TRANSPARENCY") == -1)
-        self.assertTrue("VEVENT" in args)
-
-
-
-class TestQueryFilterSerialize(TestCase):
-
-    def setUp(self):
-        super(TestQueryFilterSerialize, self).setUp()
-        TimezoneCache.create()
-
-
-    def test_query(self):
-        """
-        Basic query test - no time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-        j = filter.serialize()
-        self.assertEqual(j["type"], "Filter")
-
-        f = FilterBase.deserialize(j)
-        self.assertTrue(isinstance(f, Filter))
-
-
-    def test_timerange_query(self):
-        """
-        Basic query test with time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-        j = filter.serialize()
-        self.assertEqual(j["type"], "Filter")
-
-        f = FilterBase.deserialize(j)
-        self.assertTrue(isinstance(f, Filter))
-        self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
-        self.assertTrue(isinstance(f.child.filters[0].qualifier.tzinfo, Timezone))
-        self.assertEqual(f.child.filters[0].qualifier.tzinfo.getTimezoneID(), "America/New_York")
-
-
-    def test_query_not_extended(self):
-        """
-        Basic query test with time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[
-                    caldavxml.ComponentFilter(
-                        **{"name":("VEVENT")}
-                    ),
-                    caldavxml.ComponentFilter(
-                        **{"name":("VTODO")}
-                    ),
-                ],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-        j = filter.serialize()
-        self.assertEqual(j["type"], "Filter")
-
-        f = FilterBase.deserialize(j)
-        self.assertTrue(isinstance(f, Filter))
-        self.assertEqual(len(f.child.filters), 2)
-
-
-    def test_query_extended(self):
-        """
-        Basic query test with time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[
-                    caldavxml.ComponentFilter(
-                        *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
-                        **{"name":("VEVENT")}
-                    ),
-                    caldavxml.ComponentFilter(
-                        **{"name":("VTODO")}
-                    ),
-                ],
-                **{"name": "VCALENDAR", "test": "anyof"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-        j = filter.serialize()
-        self.assertEqual(j["type"], "Filter")
-
-        f = FilterBase.deserialize(j)
-        self.assertTrue(isinstance(f, Filter))
-        self.assertEqual(len(f.child.filters), 2)
-        self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
-
-
-    def test_query_text(self):
-        """
-        Basic query test with time range
-        """
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[
-                    caldavxml.ComponentFilter(
-                        caldavxml.PropertyFilter(
-                            caldavxml.TextMatch.fromString("1234", False),
-                            name="UID",
-                        ),
-                        **{"name":("VEVENT")}
-                    ),
-                ],
-                **{"name": "VCALENDAR", "test": "anyof"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-        j = filter.serialize()
-        self.assertEqual(j["type"], "Filter")
-
-        f = FilterBase.deserialize(j)
-        self.assertTrue(isinstance(f, Filter))
-        self.assertTrue(isinstance(f.child.filters[0].filters[0], PropertyFilter))
-        self.assertTrue(isinstance(f.child.filters[0].filters[0].qualifier, TextMatch))
-        self.assertEqual(f.child.filters[0].filters[0].qualifier.text, "1234")
-
-
-
-class TestQueryFilterMatch(TestCase):
-
-    def setUp(self):
-        super(TestQueryFilterMatch, self).setUp()
-        TimezoneCache.create()
-
-
-    def test_vlarm_undefined(self):
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    *[caldavxml.ComponentFilter(
-                        caldavxml.IsNotDefined(),
-                        **{"name":"VALARM"}
-                    )],
-                    **{"name":"VEVENT"}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-        filter.child.settzinfo(Timezone(tzid="America/New_York"))
-
-        self.assertFalse(filter.match(
-            Component.fromString("""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//Example Inc.//Example Calendar//EN
-VERSION:2.0
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US/Eastern
-BEGIN:DAYLIGHT
-DTSTART:20000404T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20001026T020000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-DTSTAMP:20051222T210412Z
-CREATED:20060102T150000Z
-DTSTART;TZID=US/Eastern:20130102T100000
-DURATION:PT1H
-RRULE:FREQ=DAILY;COUNT=5
-SUMMARY:event 5
-UID:945113826375CBB89184DC36 at ninevah.local
-CATEGORIES:cool,hot
-CATEGORIES:warm
-BEGIN:VALARM
-ACTION:AUDIO
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-"""
-        )))

Copied: CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/query/test/test_filter.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/query/test/test_filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,435 @@
+##
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from pycalendar.timezone import Timezone
+
+from twext.enterprise.dal.syntax import SQLFragment, Parameter
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav import caldavxml
+from twistedcaldav.timezones import TimezoneCache
+
+from txdav.caldav.datastore.index_file import sqlcalendarquery
+from txdav.caldav.datastore.query.builder import buildExpression
+from txdav.caldav.datastore.query.filter import Filter, FilterBase, TimeRange, \
+    PropertyFilter, TextMatch
+from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
+from txdav.common.datastore.sql_tables import schema
+
+from dateutil.tz import tzutc
+import datetime
+from twistedcaldav.ical import Component
+
+class TestQueryFilter(TestCase):
+
+    _objectSchema = schema.CALENDAR_OBJECT
+    _queryFields = {
+        "UID": _objectSchema.UID,
+        "TYPE": _objectSchema.ICALENDAR_TYPE,
+    }
+
+    def setUp(self):
+        super(TestQueryFilter, self).setUp()
+        TimezoneCache.create()
+
+
+    def test_query(self):
+        """
+        Basic query test - no time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+        expression = buildExpression(filter, self._queryFields)
+        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+        select, args, usedtimerange = sql.generate()
+
+        self.assertEqual(select.toSQL(), SQLFragment(
+            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE in (?, ?, ?)",
+            [1234, Parameter('arg1', 3)]
+        ))
+        self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
+        self.assertEqual(usedtimerange, False)
+
+
+    def test_query_timerange(self):
+        """
+        Basic query test - with time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+        expression = buildExpression(filter, self._queryFields)
+        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+        select, args, usedtimerange = sql.generate()
+
+        self.assertEqual(select.toSQL(), SQLFragment(
+            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
+            [Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
+        ))
+        self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
+        self.assertEqual(usedtimerange, True)
+
+
+    def test_query_freebusy(self):
+        """
+        Basic query test - with time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+        expression = buildExpression(filter, self._queryFields)
+        sql = CalDAVSQLQueryGenerator(expression, self, 1234, "user01", True)
+        select, args, usedtimerange = sql.generate()
+
+        self.assertEqual(select.toSQL(), SQLFragment(
+            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE, ORGANIZER, FLOATING, START_DATE, END_DATE, FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT from CALENDAR_OBJECT, TIME_RANGE left outer join TRANSPARENCY on INSTANCE_ID = TIME_RANGE_INSTANCE_ID and USER_ID = ? where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
+            ['user01', Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 13, 0, tzinfo=tzutc()), datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 1234]
+        ))
+        self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")})
+        self.assertEqual(usedtimerange, True)
+
+
+    def test_query_not_extended(self):
+        """
+        Query test - two terms not anyof
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[
+                    caldavxml.ComponentFilter(
+                        **{"name":("VEVENT")}
+                    ),
+                    caldavxml.ComponentFilter(
+                        **{"name":("VTODO")}
+                    ),
+                ],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+        expression = buildExpression(filter, self._queryFields)
+        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+        select, args, usedtimerange = sql.generate()
+
+        self.assertEqual(select.toSQL(), SQLFragment(
+            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_TYPE = ? and ICALENDAR_TYPE = ?",
+            [1234, "VEVENT", "VTODO"]
+        ))
+        self.assertEqual(args, {})
+        self.assertEqual(usedtimerange, False)
+
+
+    def test_query_extended(self):
+        """
+        Extended query test - two terms with anyof
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[
+                    caldavxml.ComponentFilter(
+                        *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
+                        **{"name":("VEVENT")}
+                    ),
+                    caldavxml.ComponentFilter(
+                        **{"name":("VTODO")}
+                    ),
+                ],
+                **{"name": "VCALENDAR", "test": "anyof"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+        expression = buildExpression(filter, self._queryFields)
+        sql = CalDAVSQLQueryGenerator(expression, self, 1234)
+        select, args, usedtimerange = sql.generate()
+
+        self.assertEqual(select.toSQL(), SQLFragment(
+            "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where (ICALENDAR_TYPE = ? and (FLOATING = ? and END_DATE > ? or FLOATING = ? and END_DATE > ?) or ICALENDAR_TYPE = ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?",
+            ['VEVENT', False, datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 'VTODO', 1234]
+        ))
+        self.assertEqual(args, {})
+        self.assertEqual(usedtimerange, True)
+
+
+    def test_sqllite_query(self):
+        """
+        Basic query test - single term.
+        Only UID can be queried via sql.
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        sql, args = sqlcalendarquery(filter, 1234)
+
+        self.assertTrue(sql.find("RESOURCE") != -1)
+        self.assertTrue(sql.find("TIMESPAN") == -1)
+        self.assertTrue(sql.find("TRANSPARENCY") == -1)
+        self.assertTrue("VEVENT" in args)
+
+
+
+class TestQueryFilterSerialize(TestCase):
+
+    def setUp(self):
+        super(TestQueryFilterSerialize, self).setUp()
+        TimezoneCache.create()
+
+
+    def test_query(self):
+        """
+        Basic query test - no time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+        j = filter.serialize()
+        self.assertEqual(j["type"], "Filter")
+
+        f = FilterBase.deserialize(j)
+        self.assertTrue(isinstance(f, Filter))
+
+
+    def test_timerange_query(self):
+        """
+        Basic query test with time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+        j = filter.serialize()
+        self.assertEqual(j["type"], "Filter")
+
+        f = FilterBase.deserialize(j)
+        self.assertTrue(isinstance(f, Filter))
+        self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
+        self.assertTrue(isinstance(f.child.filters[0].qualifier.tzinfo, Timezone))
+        self.assertEqual(f.child.filters[0].qualifier.tzinfo.getTimezoneID(), "America/New_York")
+
+
+    def test_query_not_extended(self):
+        """
+        Basic query test with time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[
+                    caldavxml.ComponentFilter(
+                        **{"name":("VEVENT")}
+                    ),
+                    caldavxml.ComponentFilter(
+                        **{"name":("VTODO")}
+                    ),
+                ],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+        j = filter.serialize()
+        self.assertEqual(j["type"], "Filter")
+
+        f = FilterBase.deserialize(j)
+        self.assertTrue(isinstance(f, Filter))
+        self.assertEqual(len(f.child.filters), 2)
+
+
+    def test_query_extended(self):
+        """
+        Basic query test with time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[
+                    caldavxml.ComponentFilter(
+                        *[caldavxml.TimeRange(**{"start":"20060605T160000Z", })],
+                        **{"name":("VEVENT")}
+                    ),
+                    caldavxml.ComponentFilter(
+                        **{"name":("VTODO")}
+                    ),
+                ],
+                **{"name": "VCALENDAR", "test": "anyof"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+        j = filter.serialize()
+        self.assertEqual(j["type"], "Filter")
+
+        f = FilterBase.deserialize(j)
+        self.assertTrue(isinstance(f, Filter))
+        self.assertEqual(len(f.child.filters), 2)
+        self.assertTrue(isinstance(f.child.filters[0].qualifier, TimeRange))
+
+
+    def test_query_text(self):
+        """
+        Basic query test with time range
+        """
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[
+                    caldavxml.ComponentFilter(
+                        caldavxml.PropertyFilter(
+                            caldavxml.TextMatch.fromString("1234", False),
+                            name="UID",
+                        ),
+                        **{"name":("VEVENT")}
+                    ),
+                ],
+                **{"name": "VCALENDAR", "test": "anyof"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+        j = filter.serialize()
+        self.assertEqual(j["type"], "Filter")
+
+        f = FilterBase.deserialize(j)
+        self.assertTrue(isinstance(f, Filter))
+        self.assertTrue(isinstance(f.child.filters[0].filters[0], PropertyFilter))
+        self.assertTrue(isinstance(f.child.filters[0].filters[0].qualifier, TextMatch))
+        self.assertEqual(f.child.filters[0].filters[0].qualifier.text, "1234")
+
+
+
+class TestQueryFilterMatch(TestCase):
+
+    def setUp(self):
+        super(TestQueryFilterMatch, self).setUp()
+        TimezoneCache.create()
+
+
+    def test_vlarm_undefined(self):
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    *[caldavxml.ComponentFilter(
+                        caldavxml.IsNotDefined(),
+                        **{"name":"VALARM"}
+                    )],
+                    **{"name":"VEVENT"}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+        filter.child.settzinfo(Timezone(tzid="America/New_York"))
+
+        self.assertFalse(filter.match(
+            Component.fromString("""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20051222T210412Z
+CREATED:20060102T150000Z
+DTSTART;TZID=US/Eastern:20130102T100000
+DURATION:PT1H
+RRULE:FREQ=DAILY;COUNT=5
+SUMMARY:event 5
+UID:945113826375CBB89184DC36 at ninevah.local
+CATEGORIES:cool,hot
+CATEGORIES:warm
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+        )))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -32,10 +32,11 @@
 from twistedcaldav.ical import Component, Property, iCalendarProductID
 from twistedcaldav.instance import InstanceList
 from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.query import calendarqueryfilter
 
+from txdav.caldav.datastore.query.filter import Filter
 from txdav.caldav.icalendarstore import QueryMaxResources
-from txdav.common.icommondatastore import IndexedSearchException
+from txdav.common.icommondatastore import IndexedSearchException, \
+    InternalDataStoreError
 
 import uuid
 
@@ -92,7 +93,6 @@
 
 
 
- at inlineCallbacks
 def generateFreeBusyInfo(
     calresource,
     fbinfo,
@@ -107,6 +107,86 @@
     logItems=None,
 ):
     """
+    Get freebusy information for a calendar. Different behavior for internal vs external calendars.
+
+    See L{_internalGenerateFreeBusyInfo} for argument description.
+    """
+
+    # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
+    # can be split across L{CalendarObject} and L{CalendarObjectExternal}
+    if calresource.external():
+        return _externalGenerateFreeBusyInfo(
+            calresource,
+            fbinfo,
+            timerange,
+            matchtotal,
+            excludeuid,
+            organizer,
+            organizerPrincipal,
+            same_calendar_user,
+            servertoserver,
+            event_details,
+            logItems
+        )
+    else:
+        return _internalGenerateFreeBusyInfo(
+            calresource,
+            fbinfo,
+            timerange,
+            matchtotal,
+            excludeuid,
+            organizer,
+            organizerPrincipal,
+            same_calendar_user,
+            servertoserver,
+            event_details,
+            logItems
+        )
+
+
+
+ at inlineCallbacks
+def _externalGenerateFreeBusyInfo(
+    calresource,
+    fbinfo,
+    timerange,
+    matchtotal,
+    excludeuid=None,
+    organizer=None,
+    organizerPrincipal=None,
+    same_calendar_user=False,
+    servertoserver=False,
+    event_details=None,
+    logItems=None,
+):
+    """
+    Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
+    any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
+
+    See L{_internalGenerateFreeBusyInfo} for argument description.
+    """
+    fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid, organizer, organizerPrincipal, same_calendar_user, servertoserver, event_details)
+    for i in range(3):
+        fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
+    returnValue(matchtotal)
+
+
+
+ at inlineCallbacks
+def _internalGenerateFreeBusyInfo(
+    calresource,
+    fbinfo,
+    timerange,
+    matchtotal,
+    excludeuid=None,
+    organizer=None,
+    organizerPrincipal=None,
+    same_calendar_user=False,
+    servertoserver=False,
+    event_details=None,
+    logItems=None,
+):
+    """
     Run a free busy report on the specified calendar collection
     accumulating the free busy info for later processing.
     @param calresource: the L{Calendar} for a calendar collection.
@@ -203,23 +283,23 @@
 
         # Create fake filter element to match time-range
         filter = caldavxml.Filter(
-                      caldavxml.ComponentFilter(
-                          caldavxml.ComponentFilter(
-                              cache_timerange if caching else timerange,
-                              name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
-                          ),
-                          name="VCALENDAR",
-                       )
-                  )
-        filter = calendarqueryfilter.Filter(filter)
+            caldavxml.ComponentFilter(
+                caldavxml.ComponentFilter(
+                    cache_timerange if caching else timerange,
+                    name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+                ),
+                name="VCALENDAR",
+            )
+        )
+        filter = Filter(filter)
         tzinfo = filter.settimezone(tz)
 
         try:
-            resources = yield calresource._index.indexedSearch(filter, useruid=attendee_uid, fbtype=True)
+            resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True)
             if caching:
                 yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
         except IndexedSearchException:
-            resources = yield calresource._index.bruteForceSearch()
+            raise InternalDataStoreError("Invalid indexedSearch query")
 
     else:
         # Log extended item

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -84,6 +84,12 @@
         self._thisServer = None
 
 
+    def addServer(self, server):
+        self._servers[server.id] = server
+        if server.thisServer:
+            self._thisServer = server
+
+
     def getServerById(self, id):
         return self._servers.get(id)
 
@@ -125,16 +131,22 @@
     Represents a server.
     """
 
-    def __init__(self):
-        self.id = None
-        self.uri = None
-        self.thisServer = False
+    def __init__(self, id=None, uri=None, sharedSecret=None, thisServer=False):
+        self.id = id
+        self.uri = uri
+        self.thisServer = thisServer
         self.ips = set()
         self.allowed_from_ips = set()
-        self.shared_secret = None
+        self.shared_secret = sharedSecret
         self.isImplicit = True
 
 
+    def details(self):
+        if not hasattr(self, "ssl"):
+            self._parseDetails()
+        return (self.ssl, self.host, self.port, self.path,)
+
+
     def check(self, ignoreIPLookupFailures=False):
         # Check whether this matches the current server
         parsed_uri = urlparse.urlparse(self.uri)
@@ -215,7 +227,28 @@
         return (SERVER_SECRET_HEADER, self.shared_secret,)
 
 
+    def _parseDetails(self):
+        # Extract scheme, host, port and path
+        if self.uri.startswith("http://"):
+            self.ssl = False
+            rest = self.uri[7:]
+        elif self.uri.startswith("https://"):
+            self.ssl = True
+            rest = self.uri[8:]
 
+        splits = rest.split("/", 1)
+        hostport = splits[0].split(":")
+        self.host = hostport[0]
+        if len(hostport) > 1:
+            self.port = int(hostport[1])
+        else:
+            self.port = {False: 80, True: 443}[self.ssl]
+        self.path = "/"
+        if len(splits) > 1:
+            self.path += splits[1]
+
+
+
 ELEMENT_SERVERS = "servers"
 ELEMENT_SERVER = "server"
 ELEMENT_ID = "id"

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/xml.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -259,14 +259,18 @@
 
 
     @classmethod
-    def fromCalendar(clazz, calendar):
+    def fromCalendar(clazz, calendar, format=None):
+        attrs = {}
+        if format is not None and format != "text/calendar":
+            attrs["content-type"] = format
+
         if isinstance(calendar, str):
             if not calendar:
                 raise ValueError("Missing calendar data")
             return clazz(PCDATAElement(calendar))
         elif isinstance(calendar, iComponent):
             assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
-            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
+            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=format)))
         else:
             raise ValueError("Not a calendar: %s" % (calendar,))
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -57,6 +57,9 @@
 from twistedcaldav.memcacher import Memcacher
 
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.query.builder import buildExpression
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
 from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
 from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
 from txdav.caldav.datastore.util import AttachmentRetrievalTransport, \
@@ -72,12 +75,11 @@
     AttendeeAllowedError, InvalidPerUserDataMerge, ComponentUpdateState, \
     ValidOrganizerError, ShareeAllowedError, ComponentRemoveState, \
     InvalidDefaultCalendar, \
-    InvalidAttachmentOperation, DuplicatePrivateCommentsError
+    InvalidAttachmentOperation, DuplicatePrivateCommentsError, \
+    TimeRangeUpperLimit, TimeRangeLowerLimit
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
-from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator, \
-    PostgresLegacyInboxIndexEmulator
 from txdav.common.datastore.sql_tables import _ATTACHMENTS_MODE_NONE, \
     _ATTACHMENTS_MODE_WRITE, schema, _BIND_MODE_OWN, \
     _ATTACHMENTS_MODE_READ, _TRANSP_OPAQUE, _TRANSP_TRANSPARENT
@@ -87,7 +89,8 @@
     InvalidObjectResourceError, ObjectResourceNameAlreadyExistsError, \
     ObjectResourceNameNotAllowedError, TooManyObjectResourcesError, \
     InvalidUIDError, UIDExistsError, UIDExistsElsewhereError, \
-    InvalidResourceMove, InvalidComponentForStoreError
+    InvalidResourceMove, InvalidComponentForStoreError, \
+    NoSuchObjectResourceError
 from txdav.xml import element
 
 from txdav.idav import ChangeCategory
@@ -433,12 +436,6 @@
         "VPOLL": "_default_polls",
     }
 
-    def __init__(self, transaction, ownerUID):
-
-        self._childClass = Calendar
-        super(CalendarHome, self).__init__(transaction, ownerUID)
-
-
     @classmethod
     def metadataColumns(cls):
         """
@@ -957,6 +954,12 @@
     _objectSchema = schema.CALENDAR_OBJECT
     _timeRangeSchema = schema.TIME_RANGE
 
+    # Mapping of iCalendar property name to DB column name
+    _queryFields = {
+        "UID": _objectSchema.UID,
+        "TYPE": _objectSchema.ICALENDAR_TYPE,
+    }
+
     _supportedComponents = None
 
     def __init__(self, *args, **kw):
@@ -964,10 +967,6 @@
         Initialize a calendar pointing at a record in a database.
         """
         super(Calendar, self).__init__(*args, **kw)
-        if self.isInbox():
-            self._index = PostgresLegacyInboxIndexEmulator(self)
-        else:
-            self._index = PostgresLegacyIndexEmulator(self)
         self._transp = _TRANSP_OPAQUE
 
 
@@ -1085,6 +1084,45 @@
         self.viewerHome().removedCalendarResource(child.uid())
 
 
+    @inlineCallbacks
+    def moveObjectResourceHere(self, name, component):
+        """
+        Create a new child in this collection as part of a move operation. This needs to be split out because
+        behavior differs for sub-classes and cross-pod operations.
+
+        @param name: new name to use in new parent
+        @type name: C{str} or C{None} for existing name
+        @param component: data for new resource
+        @type component: L{Component}
+        """
+
+        # Cross-pod calls come in with component as str or unicode
+        if isinstance(component, str) or isinstance(component, unicode):
+            try:
+                component = self._objectResourceClass._componentClass.fromString(component)
+            except InvalidICalendarDataError as e:
+                raise InvalidComponentForStoreError(str(e))
+
+        yield self._createCalendarObjectWithNameInternal(name, component, internal_state=ComponentUpdateState.RAW)
+
+
+    @inlineCallbacks
+    def moveObjectResourceAway(self, rid, child=None):
+        """
+        Remove the child as the result of a move operation. This needs to be split out because
+        behavior differs for sub-classes and cross-pod operations.
+
+        @param rid: the child resource-id to move
+        @type rid: C{int}
+        @param child: the child resource to move - might be C{None} for cross-pod
+        @type child: L{CommonObjectResource}
+        """
+
+        if child is None:
+            child = yield self.objectResourceWithID(rid)
+        yield child._removeInternal(internal_state=ComponentRemoveState.INTERNAL)
+
+
     def calendarObjectsInTimeRange(self, start, end, timeZone):
         raise NotImplementedError()
 
@@ -1302,6 +1340,37 @@
         yield self.setDefaultAlarm("empty", False, False)
 
 
+    def getInviteCopyProperties(self):
+        """
+        Get a dictionary of property name/values (as strings) for properties that are shadowable and
+        need to be copied to a sharee's collection when an external (cross-pod) share is created.
+        Sub-classes should override to expose the properties they care about.
+        """
+        props = {}
+        for elem in (element.DisplayName, caldavxml.CalendarDescription, caldavxml.CalendarTimeZone, customxml.CalendarColor,):
+            if PropertyName.fromElement(elem) in self.properties():
+                props[elem.sname()] = str(self.properties()[PropertyName.fromElement(elem)])
+        return props
+
+
+    def setInviteCopyProperties(self, props):
+        """
+        Copy a set of shadowable properties (as name/value strings) onto this shared resource when
+        a cross-pod invite is processed. Sub-classes should override to expose the properties they
+        care about.
+        """
+        # Initialize these for all shares
+        for elem in (caldavxml.CalendarDescription, caldavxml.CalendarTimeZone,):
+            if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+                self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+        # Only initialize these for direct shares
+        if self.direct():
+            for elem in (element.DisplayName, customxml.CalendarColor,):
+                if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+                    self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+
     # FIXME: this is DAV-ish.  Data store calendar objects don't have
     # mime types.  -wsv
     def contentType(self):
@@ -1312,6 +1381,196 @@
 
 
     @inlineCallbacks
+    def search(self, filter, useruid=None, fbtype=False):
+        """
+        Finds resources matching the given qualifiers.
+        @param filter: the L{Filter} for the calendar-query to execute.
+        @return: an iterable of tuples for each resource matching the
+            given C{qualifiers}. The tuples are C{(name, uid)}, where
+            C{name} is the resource name, C{uid} is the resource UID.
+        """
+
+        # We might be passed an L{Filter} or a serialization of one
+        if isinstance(filter, dict):
+            try:
+                filter = Filter.deserialize(filter)
+            except Exception:
+                file = None
+
+        # Make sure we have a proper Filter element and get the partial SQL statement to use.
+        sql_stmt = self._sqlquery(filter, useruid, fbtype)
+
+        # No result means it is too complex for us
+        if sql_stmt is None:
+            raise IndexedSearchException()
+        sql_stmt, args, usedtimerange = sql_stmt
+
+        # Check for time-range re-expand
+        if usedtimerange is not None:
+
+            today = DateTime.getToday()
+
+            # Determine how far we need to extend the current expansion of
+            # events. If we have an open-ended time-range we will expand
+            # one year past the start. That should catch bounded
+            # recurrences - unbounded will have been indexed with an
+            # "infinite" value always included.
+            maxDate, isStartDate = filter.getmaxtimerange()
+            if maxDate:
+                maxDate = maxDate.duplicate()
+                maxDate.offsetDay(1)
+                maxDate.setDateOnly(True)
+                upperLimit = today + Duration(days=config.FreeBusyIndexExpandMaxDays)
+                if maxDate > upperLimit:
+                    raise TimeRangeUpperLimit(upperLimit)
+                if isStartDate:
+                    maxDate += Duration(days=365)
+
+            # Determine if the start date is too early for the restricted range we
+            # are applying. If it is today or later we don't need to worry about truncation
+            # in the past.
+            minDate, _ignore_isEndDate = filter.getmintimerange()
+            if minDate >= today:
+                minDate = None
+            if minDate is not None and config.FreeBusyIndexLowerLimitDays:
+                truncateLowerLimit = today - Duration(days=config.FreeBusyIndexLowerLimitDays)
+                if minDate < truncateLowerLimit:
+                    raise TimeRangeLowerLimit(truncateLowerLimit)
+
+            if maxDate is not None or minDate is not None:
+                yield self.testAndUpdateIndex(minDate, maxDate)
+
+        rowiter = yield sql_stmt.on(self._txn, **args)
+
+        # Check result for missing resources
+        results = []
+        for row in rowiter:
+            if fbtype:
+                row = list(row)
+                row[4] = 'Y' if row[4] else 'N'
+                row[7] = indexfbtype_to_icalfbtype[row[7]]
+                if row[9] is not None:
+                    row[8] = row[9]
+                row[8] = 'T' if row[8] else 'F'
+                del row[9]
+            results.append(row)
+
+        returnValue(results)
+
+
+    def _sqlquery(self, filter, useruid, fbtype):
+        """
+        Convert the supplied addressbook-query into a partial SQL statement.
+
+        @param filter: the L{Filter} for the addressbook-query to convert.
+        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+                and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+                Or return C{None} if it is not possible to create an SQL query to fully match the addressbook-query.
+        """
+
+        if not isinstance(filter, Filter):
+            return None
+
+        try:
+            expression = buildExpression(filter, self._queryFields)
+            sql = CalDAVSQLQueryGenerator(expression, self, self.id(), useruid, fbtype)
+            return sql.generate()
+        except ValueError:
+            return None
+
+
+    @classproperty
+    def _notExpandedWithinQuery(cls): #@NoSelf
+        """
+        Query to find resources that need to be re-expanded
+        """
+        co = schema.CALENDAR_OBJECT
+        return Select(
+            [co.RESOURCE_NAME],
+            From=co,
+            Where=((co.RECURRANCE_MIN > Parameter("minDate"))
+                .Or(co.RECURRANCE_MAX < Parameter("maxDate")))
+                .And(co.CALENDAR_RESOURCE_ID == Parameter("resourceID"))
+        )
+
+
+    @inlineCallbacks
+    def notExpandedWithin(self, minDate, maxDate):
+        """
+        Gives all resources which have not been expanded beyond a given date
+        in the database.  (Unused; see above L{postgresqlgenerator}.
+        """
+        returnValue([row[0] for row in (
+            yield self._notExpandedWithinQuery.on(
+                self._txn,
+                minDate=pyCalendarTodatetime(normalizeForIndex(minDate)) if minDate is not None else None,
+                maxDate=pyCalendarTodatetime(normalizeForIndex(maxDate)),
+                resourceID=self._resourceID))]
+        )
+
+
+    @inlineCallbacks
+    def reExpandResource(self, name, expand_start, expand_end):
+        """
+        Given a resource name, remove it from the database and re-add it
+        with a longer expansion.
+        """
+        obj = yield self.calendarObjectWithName(name)
+
+        # Use a new transaction to do this update quickly without locking the row for too long. However, the original
+        # transaction may have the row locked, so use wait=False and if that fails, fall back to using the original txn.
+
+        newTxn = obj.transaction().store().newTransaction()
+        try:
+            yield obj.lock(wait=False, txn=newTxn)
+        except NoSuchObjectResourceError:
+            yield newTxn.commit()
+            returnValue(None)
+        except:
+            yield newTxn.abort()
+            newTxn = None
+
+        # Now do the re-expand using the appropriate transaction
+        try:
+            doExpand = False
+            if newTxn is None:
+                doExpand = True
+            else:
+                # We repeat this check because the resource may have been re-expanded by someone else
+                rmin, rmax = (yield obj.recurrenceMinMax(txn=newTxn))
+
+                # If the resource is not fully expanded, see if within the required range or not.
+                # Note that expand_start could be None if no lower limit is applied, but expand_end will
+                # never be None
+                if rmax is not None and rmax < expand_end:
+                    doExpand = True
+                if rmin is not None and expand_start is not None and rmin > expand_start:
+                    doExpand = True
+
+            if doExpand:
+                yield obj.updateDatabase(
+                    (yield obj.component()),
+                    expand_until=expand_end,
+                    reCreate=True,
+                    txn=newTxn,
+                )
+        finally:
+            if newTxn is not None:
+                yield newTxn.commit()
+
+
+    @inlineCallbacks
+    def testAndUpdateIndex(self, minDate, maxDate):
+        # Find out if the index is expanded far enough
+        names = yield self.notExpandedWithin(minDate, maxDate)
+
+        # Actually expand recurrence max
+        for name in names:
+            self.log.info("Search falls outside range of index for %s %s to %s" % (name, minDate, maxDate))
+            yield self.reExpandResource(name, minDate, maxDate)
+
+
+    @inlineCallbacks
     def splitCollectionByComponentTypes(self):
         """
         If the calendar contains iCalendar data with different component types, then split it into separate collections
@@ -1524,6 +1783,7 @@
     implements(ICalendarObject)
 
     _objectSchema = schema.CALENDAR_OBJECT
+    _componentClass = VComponent
 
     def __init__(self, calendar, name, uid, resourceID=None, options=None):
 
@@ -1542,36 +1802,20 @@
         self._cachedComponent = None
         self._cachedCommponentPerUser = {}
 
-    _allColumns = [
-        _objectSchema.RESOURCE_ID,
-        _objectSchema.RESOURCE_NAME,
-        _objectSchema.UID,
-        _objectSchema.MD5,
-        Len(_objectSchema.TEXT),
-        _objectSchema.ATTACHMENTS_MODE,
-        _objectSchema.DROPBOX_ID,
-        _objectSchema.ACCESS,
-        _objectSchema.SCHEDULE_OBJECT,
-        _objectSchema.SCHEDULE_TAG,
-        _objectSchema.SCHEDULE_ETAGS,
-        _objectSchema.PRIVATE_COMMENTS,
-        _objectSchema.CREATED,
-        _objectSchema.MODIFIED
-    ]
 
-
     @classmethod
     @inlineCallbacks
     def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
 
-        child = (yield cls.objectWithName(parent, name, None))
+        child = (yield cls.objectWithName(parent, name))
         if child:
             raise ObjectResourceNameAlreadyExistsError(name)
 
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
 
-        objectResource = cls(parent, name, None, None, options=options)
+        c = cls._externalClass if parent.external() else cls
+        objectResource = c(parent, name, None, None, options=options)
         yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state, split_details=split_details)
         yield objectResource._loadPropertyStore(created=True)
 
@@ -1581,27 +1825,51 @@
         returnValue(objectResource)
 
 
-    def _initFromRow(self, row):
+    @classmethod
+    def _allColumns(cls): #@NoSelf
         """
-        Given a select result using the columns from L{_allColumns}, initialize
-        the calendar object resource state.
+        Full set of columns in the object table that need to be loaded to
+        initialize the object resource state.
         """
-        (self._resourceID,
-         self._name,
-         self._uid,
-         self._md5,
-         self._size,
-         self._attachment,
-         self._dropboxID,
-         self._access,
-         self._schedule_object,
-         self._schedule_tag,
-         self._schedule_etags,
-         self._private_comments,
-         self._created,
-         self._modified,) = tuple(row)
+        obj = cls._objectSchema
+        return [
+            obj.RESOURCE_ID,
+            obj.RESOURCE_NAME,
+            obj.UID,
+            obj.MD5,
+            Len(obj.TEXT),
+            obj.ATTACHMENTS_MODE,
+            obj.DROPBOX_ID,
+            obj.ACCESS,
+            obj.SCHEDULE_OBJECT,
+            obj.SCHEDULE_TAG,
+            obj.SCHEDULE_ETAGS,
+            obj.PRIVATE_COMMENTS,
+            obj.CREATED,
+            obj.MODIFIED
+        ]
 
 
+    @classmethod
+    def _rowAttributes(cls): #@NoSelf
+        return (
+            "_resourceID",
+            "_name",
+            "_uid",
+            "_md5",
+            "_size",
+            "_attachment",
+            "_dropboxID",
+            "_access",
+            "_schedule_object",
+            "_schedule_tag",
+            "_schedule_etags",
+            "_private_comments",
+            "_created",
+            "_modified",
+         )
+
+
     @property
     def _calendar(self):
         return self._parentCollection
@@ -2183,6 +2451,12 @@
         Scheduling will be done automatically.
         """
 
+        # Cross-pod calls come in with component as str or unicode
+        if isinstance(component, str) or isinstance(component, unicode):
+            try:
+                component = self._componentClass.fromString(component)
+            except InvalidICalendarDataError as e:
+                raise InvalidComponentForStoreError(str(e))
         return self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL, smart_merge)
 
 
@@ -2734,9 +3008,10 @@
                 yield NamedLock.acquire(self._txn, "ImplicitUIDLock:%s" % (hashlib.md5(calendar.resourceUID()).hexdigest(),))
 
         # Need to also remove attachments
-        if self._dropboxID:
-            yield DropBoxAttachment.resourceRemoved(self._txn, self._resourceID, self._dropboxID)
-        yield ManagedAttachment.resourceRemoved(self._txn, self._resourceID)
+        if internal_state != ComponentRemoveState.INTERNAL:
+            if self._dropboxID:
+                yield DropBoxAttachment.resourceRemoved(self._txn, self._resourceID, self._dropboxID)
+            yield ManagedAttachment.resourceRemoved(self._txn, self._resourceID)
         yield super(CalendarObject, self).remove()
 
         # Do scheduling
@@ -4419,4 +4694,10 @@
 
         returnValue(location)
 
+# Hook-up class relationships at the end after they have all been defined
+from txdav.caldav.datastore.sql_external import CalendarHomeExternal, CalendarExternal, CalendarObjectExternal
+CalendarHome._externalClass = CalendarHomeExternal
+CalendarHome._childClass = Calendar
+Calendar._externalClass = CalendarExternal
 Calendar._objectResourceClass = CalendarObject
+CalendarObject._externalClass = CalendarObjectExternal

Copied: CalendarServer/trunk/txdav/caldav/datastore/sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql_external.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql_external.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,217 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+"""
+SQL backend for CalDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+
+from twext.python.log import Logger
+
+from txdav.caldav.datastore.sql import CalendarHome, Calendar, CalendarObject
+from txdav.caldav.icalendarstore import ComponentUpdateState, ComponentRemoveState
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+    CommonObjectResourceExternal
+
+log = Logger()
+
+class CalendarHomeExternal(CommonHomeExternal, CalendarHome):
+    """
+    Wrapper for a CalendarHome that is external and only supports a limited set of operations.
+    """
+
+    def __init__(self, transaction, ownerUID, resourceID):
+
+        CalendarHome.__init__(self, transaction, ownerUID)
+        CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+    def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getCalendarResourcesForUID(self, uid):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def calendarObjectWithDropboxID(self, dropboxID):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAllDropboxIDs(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAllAttachmentNames(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAllManagedIDs(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def createdHome(self):
+        """
+        No children - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def splitCalendars(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def ensureDefaultCalendarsExist(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def setDefaultCalendar(self, calendar, componentType):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def defaultCalendar(self, componentType, create=True):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def isDefaultCalendar(self, calendar):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getDefaultAlarm(self, vevent, timed):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def setDefaultAlarm(self, alarm, vevent, timed):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAvailability(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def setAvailability(self, availability):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CalendarExternal(CommonHomeChildExternal, Calendar):
+    """
+    SQL-based implementation of L{ICalendar}.
+    """
+    pass
+
+
+
+class CalendarObjectExternal(CommonObjectResourceExternal, CalendarObject):
+    """
+    SQL-based implementation of L{ICalendarObject}.
+    """
+
+    @classmethod
+    def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
+        raise AssertionError("CalendarObjectExternal: not supported")
+
+
+    def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, smart_merge=False, split_details=None):
+        raise AssertionError("CalendarObjectExternal: not supported")
+
+
+    def _removeInternal(self, internal_state=ComponentRemoveState.NORMAL):
+        raise AssertionError("CalendarObjectExternal: not supported")
+
+
+    @inlineCallbacks
+    def addAttachment(self, rids, content_type, filename, stream):
+        result = yield self._txn.store().conduit.send_add_attachment(self, rids, content_type, filename, stream)
+        managedID, location = result
+        returnValue((ManagedAttachmentExternal(str(managedID)), str(location),))
+
+
+    @inlineCallbacks
+    def updateAttachment(self, managed_id, content_type, filename, stream):
+        result = yield self._txn.store().conduit.send_update_attachment(self, managed_id, content_type, filename, stream)
+        managedID, location = result
+        returnValue((ManagedAttachmentExternal(str(managedID)), str(location),))
+
+
+    @inlineCallbacks
+    def removeAttachment(self, rids, managed_id):
+        yield self._txn.store().conduit.send_remove_attachment(self, rids, managed_id)
+        returnValue(None)
+
+
+
+class ManagedAttachmentExternal(object):
+    """
+    Fake managed attachment object returned from L{CalendarObjectExternal.addAttachment} and
+    L{CalendarObjectExternal.updateAttachment}.
+    """
+
+    def __init__(self, managedID):
+        self._managedID = managedID
+
+
+    def managedID(self):
+        return self._managedID
+
+
+CalendarExternal._objectResourceClass = CalendarObjectExternal

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -416,12 +416,14 @@
         yield coll.removeNotificationObjectWithUID("1")
         st2 = yield coll.syncToken()
         rev2 = self.token2revision(st2)
-        changed, deleted = yield coll.resourceNamesSinceToken(rev)
+        changed, deleted, invalid = yield coll.resourceNamesSinceToken(rev)
         self.assertEquals(set(changed), set(["2.xml"]))
         self.assertEquals(set(deleted), set(["1.xml"]))
-        changed, deleted = yield coll.resourceNamesSinceToken(rev2)
+        self.assertEquals(len(invalid), 0)
+        changed, deleted, invalid = yield coll.resourceNamesSinceToken(rev2)
         self.assertEquals(set(changed), set([]))
         self.assertEquals(set(deleted), set([]))
+        self.assertEquals(len(invalid), 0)
 
 
     @inlineCallbacks
@@ -1602,7 +1604,7 @@
 
         home = yield self.homeUnderTest()
 
-        changed, deleted = yield home.resourceNamesSinceToken(
+        changed, deleted, invalid = yield home.resourceNamesSinceToken(
             self.token2revision(st), "infinity")
 
         self.assertEquals(set(changed), set(["calendar_1/",
@@ -1610,11 +1612,13 @@
                                              "calendar_1/2.ics",
                                              "other-calendar/"]))
         self.assertEquals(set(deleted), set(["calendar_1/2.ics"]))
+        self.assertEquals(invalid, [])
 
-        changed, deleted = yield home.resourceNamesSinceToken(
+        changed, deleted, invalid = yield home.resourceNamesSinceToken(
             self.token2revision(st2), "infinity")
         self.assertEquals(changed, [])
         self.assertEquals(deleted, [])
+        self.assertEquals(invalid, [])
 
 
     @inlineCallbacks
@@ -1634,12 +1638,14 @@
         yield obj1.remove()
         st2 = yield cal.syncToken()
         rev2 = self.token2revision(st2)
-        changed, deleted = yield cal.resourceNamesSinceToken(rev)
+        changed, deleted, invalid = yield cal.resourceNamesSinceToken(rev)
         self.assertEquals(set(changed), set(["new.ics"]))
         self.assertEquals(set(deleted), set(["2.ics"]))
-        changed, deleted = yield cal.resourceNamesSinceToken(rev2)
+        self.assertEquals(len(invalid), 0)
+        changed, deleted, invalid = yield cal.resourceNamesSinceToken(rev2)
         self.assertEquals(set(changed), set([]))
         self.assertEquals(set(deleted), set([]))
+        self.assertEquals(len(invalid), 0)
 
 
     @inlineCallbacks
@@ -1703,7 +1709,7 @@
         L{ICalendarStore.withEachCalendarHomeDo} executes its C{action}
         argument repeatedly with all homes that have been created.
         """
-        additionalUIDs = set('alpha-uid home2 home3 beta-uid'.split())
+        additionalUIDs = set('user01 home2 home3 uid1'.split())
         txn = self.transactionUnderTest()
         for name in additionalUIDs:
             yield txn.calendarHomeWithUID(name, create=True)

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -19,6 +19,7 @@
 from twisted.internet.task import deferLater
 
 from txdav.caldav.datastore.index_file import Index, MemcachedUIDReserver
+from txdav.caldav.datastore.query.filter import Filter
 from txdav.common.icommondatastore import ReservationError, \
     InternalDataStoreError
 
@@ -26,7 +27,6 @@
 from twistedcaldav.caldavxml import TimeRange
 from twistedcaldav.ical import Component, InvalidICalendarDataError
 from twistedcaldav.instance import InvalidOverriddenInstanceError
-from twistedcaldav.query import calendarqueryfilter
 from twistedcaldav.test.util import InMemoryMemcacheProtocol
 import twistedcaldav.test.util
 
@@ -480,7 +480,7 @@
                       name="VCALENDAR",
                    )
               )
-            filter = calendarqueryfilter.Filter(filter)
+            filter = Filter(filter)
 
             resources = yield self.db.indexedSearch(filter)
             index_results = set()
@@ -666,7 +666,7 @@
                       name="VCALENDAR",
                    )
               )
-            filter = calendarqueryfilter.Filter(filter)
+            filter = Filter(filter)
 
             resources = yield self.db.indexedSearch(filter, fbtype=True)
             index_results = set()
@@ -1073,7 +1073,7 @@
                       name="VCALENDAR",
                    )
               )
-            filter = calendarqueryfilter.Filter(filter)
+            filter = Filter(filter)
 
             for useruid, instances in peruserinstances:
                 resources = yield self.db.indexedSearch(filter, useruid=useruid, fbtype=True)
@@ -1142,12 +1142,12 @@
         self.db.deleteResource("data3.ics")
 
         tests = (
-            (0, (["data1.ics", "data2.ics", ], [],)),
-            (1, (["data2.ics", ], ["data3.ics", ],)),
-            (2, ([], ["data3.ics", ],)),
-            (3, ([], ["data3.ics", ],)),
-            (4, ([], [],)),
-            (5, ([], [],)),
+            (0, (["data1.ics", "data2.ics", ], [], [],)),
+            (1, (["data2.ics", ], ["data3.ics", ], [],)),
+            (2, ([], ["data3.ics", ], [],)),
+            (3, ([], ["data3.ics", ], [],)),
+            (4, ([], [], [],)),
+            (5, ([], [], [],)),
         )
 
         for revision, results in tests:

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -13,14 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
-from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser, \
-    LocalCalendarUser
-from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
-from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
-from txweb2 import responsecode
-from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.instance import InvalidOverriddenInstanceError
 
 """
 Tests for txdav.caldav.datastore.postgres, mostly based on
@@ -32,7 +24,8 @@
 
 from twext.enterprise.dal.syntax import Select, Parameter, Insert, Delete, \
     Update
-from twistedcaldav.ical import Component as VComponent
+
+from txweb2 import responsecode
 from txweb2.http_headers import MimeType
 from txweb2.stream import MemoryStream
 
@@ -47,9 +40,16 @@
 from twistedcaldav.config import config
 from twistedcaldav.dateops import datetimeMktime
 from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs
-from twistedcaldav.query import calendarqueryfilter
+from twistedcaldav.instance import InvalidOverriddenInstanceError
 
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
+from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser, \
+    LocalCalendarUser
+from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
+from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
+from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
 from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
     test_event_text
 from txdav.caldav.datastore.test.test_file import setUpCalendarStore
@@ -57,14 +57,13 @@
 from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
 from txdav.caldav.icalendarstore import ComponentUpdateState, InvalidDefaultCalendar
 from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource
-from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
     _BIND_STATUS_ACCEPTED
 from txdav.common.datastore.test.util import populateCalendarsFrom, \
     CommonCommonTests
 from txdav.common.icommondatastore import NoSuchObjectResourceError
-from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
 from txdav.idav import ChangeCategory
+from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
 
 import datetime
 
@@ -407,10 +406,10 @@
                           name="VCALENDAR",
                        )
                   )
-        filter = calendarqueryfilter.Filter(filter)
+        filter = Filter(filter)
         filter.settimezone(None)
 
-        results = yield toCalendar._index.indexedSearch(filter, 'user01', True)
+        results = yield toCalendar.search(filter, 'user01', True)
         self.assertEquals(len(results), 1)
         _ignore_name, uid, _ignore_type, _ignore_organizer, _ignore_float, _ignore_start, _ignore_end, _ignore_fbtype, transp = results[0]
         self.assertEquals(uid, "uid4")
@@ -618,7 +617,7 @@
 
         @inlineCallbacks
         def _defer1():
-            yield cal1.createObjectResourceWithName("1.ics", VComponent.fromString(
+            yield cal1.createObjectResourceWithName("1.ics", Component.fromString(
 """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -664,7 +663,7 @@
 
         @inlineCallbacks
         def _defer2():
-            yield cal2.createObjectResourceWithName("2.ics", VComponent.fromString(
+            yield cal2.createObjectResourceWithName("2.ics", Component.fromString(
 """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -825,7 +824,7 @@
         # Create calendar object
         calendar1 = yield self.calendarUnderTest()
         name = "test.ics"
-        component = VComponent.fromString(test_event_text)
+        component = Component.fromString(test_event_text)
         metadata = {
             "accessMode": "PUBLIC",
             "isScheduleObject": True,
@@ -873,7 +872,7 @@
         inbox = yield home.createCalendarWithName("inbox")
 
         name = "test.ics"
-        component = VComponent.fromString(test_event_text)
+        component = Component.fromString(test_event_text)
         metadata = {
             "accessMode": "PUBLIC",
             "isScheduleObject": True,
@@ -922,7 +921,7 @@
         """
         home = yield self.homeUnderTest()
         inbox = yield home.createCalendarWithName("inbox")
-        component = VComponent.fromString(test_event_text)
+        component = Component.fromString(test_event_text)
         inboxItem = yield inbox.createCalendarObjectWithName("inbox.ics", component)
         self.assertEquals(ChangeCategory.inbox, inboxItem.removeNotifyCategory())
         yield self.commit()
@@ -935,7 +934,7 @@
         """
         home = yield self.homeUnderTest()
         nonInbox = yield home.createCalendarWithName("noninbox")
-        component = VComponent.fromString(test_event_text)
+        component = Component.fromString(test_event_text)
         nonInboxItem = yield nonInbox.createCalendarObjectWithName("inbox.ics", component)
         self.assertEquals(ChangeCategory.default, nonInboxItem.removeNotifyCategory())
         yield self.commit()
@@ -1105,9 +1104,10 @@
         self.assertTrue(calendar2_vtodo is not None)
         children = yield calendar2_vtodo.listCalendarObjects()
         self.assertEqual(len(children), 2)
-        changed, deleted = yield calendar2_vtodo.resourceNamesSinceToken(None)
+        changed, deleted, invalid = yield calendar2_vtodo.resourceNamesSinceToken(None)
         self.assertEqual(sorted(changed), ["3.ics", "5.ics"])
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
         result = yield calendar2_vtodo.getSupportedComponents()
         self.assertEquals(result, "VTODO")
         self.assertTrue(pkey in calendar2_vtodo.properties())
@@ -1118,9 +1118,10 @@
         self.assertEqual(len(children), 3)
         new_sync_token2 = yield calendar2.syncToken()
         self.assertNotEqual(new_sync_token2, original_sync_token2)
-        changed, deleted = yield calendar2.resourceNamesSinceToken(original_sync_token2)
+        changed, deleted, invalid = yield calendar2.resourceNamesSinceToken(original_sync_token2)
         self.assertEqual(len(changed), 0)
         self.assertEqual(sorted(deleted), ["3.ics", "5.ics"])
+        self.assertEqual(len(invalid), 0)
         result = yield calendar2.getSupportedComponents()
         self.assertEquals(result, "VEVENT")
         self.assertTrue(pkey in calendar2.properties())
@@ -1369,7 +1370,7 @@
     @inlineCallbacks
     def test_notExpandedWithin(self):
         """
-        Test PostgresLegacyIndexEmulator.notExpandedWithin to make sure it returns the correct
+        Test Calendar.notExpandedWithin to make sure it returns the correct
         result based on the ranges passed in.
         """
 
@@ -1378,7 +1379,6 @@
         # Create the index on a new calendar
         home = yield self.homeUnderTest()
         newcalendar = yield home.createCalendarWithName("index_testing")
-        index = PostgresLegacyIndexEmulator(newcalendar)
 
         # Create the calendar object to use for testing
         nowYear = self.nowYear["now"]
@@ -1406,37 +1406,37 @@
         # Fully within range
         testMin = DateTime(nowYear, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
         testMax = DateTime(nowYear + 1, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        result = yield index.notExpandedWithin(testMin, testMax)
+        result = yield newcalendar.notExpandedWithin(testMin, testMax)
         self.assertEqual(result, [])
 
         # Upper bound exceeded
         testMin = DateTime(nowYear, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
         testMax = DateTime(nowYear + 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        result = yield index.notExpandedWithin(testMin, testMax)
+        result = yield newcalendar.notExpandedWithin(testMin, testMax)
         self.assertEqual(result, ["indexing.ics"])
 
         # Lower bound exceeded
         testMin = DateTime(nowYear - 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
         testMax = DateTime(nowYear + 1, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        result = yield index.notExpandedWithin(testMin, testMax)
+        result = yield newcalendar.notExpandedWithin(testMin, testMax)
         self.assertEqual(result, ["indexing.ics"])
 
         # Lower and upper bounds exceeded
         testMin = DateTime(nowYear - 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
         testMax = DateTime(nowYear + 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        result = yield index.notExpandedWithin(testMin, testMax)
+        result = yield newcalendar.notExpandedWithin(testMin, testMax)
         self.assertEqual(result, ["indexing.ics"])
 
         # Lower none within range
         testMin = None
         testMax = DateTime(nowYear + 1, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        result = yield index.notExpandedWithin(testMin, testMax)
+        result = yield newcalendar.notExpandedWithin(testMin, testMax)
         self.assertEqual(result, [])
 
         # Lower none and upper bounds exceeded
         testMin = None
         testMax = DateTime(nowYear + 5, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        result = yield index.notExpandedWithin(testMin, testMax)
+        result = yield newcalendar.notExpandedWithin(testMin, testMax)
         self.assertEqual(result, ["indexing.ics"])
 
 
@@ -1542,8 +1542,8 @@
 
         # Tests on inbox - resources with properties
         txn = self.transactionUnderTest()
-        yield txn.homeWithUID(ECALENDARTYPE, "byNameTest", create=True)
-        inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="byNameTest")
+        yield txn.homeWithUID(ECALENDARTYPE, "user01", create=True)
+        inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="user01")
         caldata = """BEGIN:VCALENDAR
 VERSION:2.0
 CALSCALE:GREGORIAN
@@ -1574,7 +1574,7 @@
         yield _createInboxItem("4.ics", "p4")
         yield self.commit()
 
-        inbox = yield self.calendarUnderTest(name="inbox", home="byNameTest")
+        inbox = yield self.calendarUnderTest(name="inbox", home="user01")
         yield _tests(inbox)
 
         resources = yield inbox.objectResourcesWithNames(("1.ics",))
@@ -2152,7 +2152,6 @@
 
 
 
-
 class CalendarObjectSplitting(CommonCommonTests, unittest.TestCase):
     """
     CalendarObject splitting tests

Copied: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,682 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+
+from twisted.internet.defer import inlineCallbacks
+
+from twext.python.clsprop import classproperty
+from txdav.common.datastore.test.util import populateCalendarsFrom
+from txdav.common.datastore.sql_tables import _BIND_MODE_READ, \
+    _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
+
+
+class BaseSharingTests(MultiStoreConduitTest):
+
+    """
+    Test store-based calendar sharing.
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(BaseSharingTests, self).setUp()
+        yield self.populate()
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+    cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:20131122T140000
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+"""
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        return {
+        "user01": {
+            "calendar": {
+                "cal1.ics": (cls.cal1, None,),
+            },
+            "inbox": {
+            },
+        },
+        "user02": {
+            "calendar": {
+            },
+            "inbox": {
+            },
+        },
+        "user03": {
+            "calendar": {
+            },
+            "inbox": {
+            },
+        },
+    }
+
+
+
+class CalendarSharing(BaseSharingTests):
+
+    @inlineCallbacks
+    def test_no_shares(self):
+        """
+        Test that initially there are no shares.
+        """
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_invite_sharee(self):
+        """
+        Test invite/uninvite creates/removes shares and notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertEqual(invites[0].uid, shareeView.shareUID())
+        self.assertEqual(invites[0].ownerUID, "user01")
+        self.assertEqual(invites[0].shareeUID, "puser02")
+        self.assertEqual(invites[0].mode, _BIND_MODE_READ)
+        self.assertEqual(invites[0].status, _BIND_STATUS_INVITED)
+        self.assertEqual(invites[0].summary, "summary")
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID, ])
+        yield self.otherCommit()
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        yield calendar.uninviteUserFromShare("puser02")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [])
+        yield self.otherCommit()
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+        yield calendar.setShared(False)
+        self.assertFalse(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_share(self):
+        """
+        Test that invite+accept creates shares and notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Re-accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_decline_share(self):
+        """
+        Test that invite+decline does not create shares but does create notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Decline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Redecline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_decline_share(self):
+        """
+        Test that invite+accept/decline creates/removes shares and notifications.
+        Decline via the home.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        inviteUID = shareeView.shareUID()
+
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Decline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_remove_share(self):
+        """
+        Test that invite+accept/decline creates/removes shares and notifications.
+        Decline via the shared collection (removal).
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        yield self.commit()
+
+        # Delete
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        yield shared.deleteShare()
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+
+    @inlineCallbacks
+    def test_accept_remove_accept(self):
+        yield self.createShare()
+        yield self.removeShare()
+        shared_name = yield self.createShare()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_accept_remove_accept_newcalendar(self):
+        """
+        Test that deleting and re-creating a share with the same sharer name works.
+        """
+
+        home = yield self.homeUnderTest(name="user01", create=True)
+        yield home.createCalendarWithName("shared")
+        yield self.commit()
+
+        shared_name = yield self.createShare(name="shared")
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+        yield self.removeShare(name="shared")
+        home = yield self.homeUnderTest(name="user01", create=True)
+        yield home.removeCalendarWithName("shared")
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is None)
+        yield self.otherCommit()
+
+        home = yield self.homeUnderTest(name="user01", create=True)
+        yield home.createCalendarWithName("shared")
+        yield self.commit()
+
+        shared_name = yield self.createShare(name="shared")
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_inviteProperties(self):
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield calendar.setUsedForFreeBusy(True)
+        yield self.commit()
+
+        shared_name = yield self.createShare()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertFalse(shared.isUsedForFreeBusy())
+
+
+    @inlineCallbacks
+    def test_direct_sharee(self):
+        """
+        Test invite/uninvite creates/removes shares and notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.directShareWithUser("puser02")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertEqual(invites[0].uid, shareeView.shareUID())
+        self.assertEqual(invites[0].ownerUID, "user01")
+        self.assertEqual(invites[0].shareeUID, "puser02")
+        self.assertEqual(invites[0].mode, _BIND_MODE_DIRECT)
+        self.assertEqual(invites[0].status, _BIND_STATUS_ACCEPTED)
+
+        sharedName = shareeView.name()
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="user02", name=sharedName)
+        self.assertTrue(shared is not None)
+
+        notifyHome = yield txn2.notificationsWithUID("user02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 0)
+        yield self.otherCommit()
+
+        # Remove
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        yield shared.deleteShare()
+        yield self.otherCommit()
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 0)
+
+    test_direct_sharee.skip = True
+
+    @inlineCallbacks
+    def test_sharedNotifierID(self):
+        shared_name = yield self.createShare()
+
+        home = yield self.homeUnderTest(name="user01")
+        self.assertEquals(home.notifierID(), ("CalDAV", "user01",))
+        calendar = yield home.calendarWithName("calendar")
+        self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        home = yield self.homeUnderTest(txn=txn2, name="puser02")
+        self.assertEquals(home.notifierID(), ("CalDAV", "puser02",))
+        calendar = yield home.calendarWithName(shared_name)
+        self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+
+
+    @inlineCallbacks
+    def test_sharedWithTwo(self):
+        shared_name1 = yield self.createShare(shareeGUID="puser02")
+        shared_name2 = yield self.createShare(shareeGUID="puser03")
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name1)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser03", name=shared_name2)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+
+class SharingRevisions(BaseSharingTests):
+    """
+    Test store-based sharing and interaction with revision table.
+    """
+
+    @inlineCallbacks
+    def test_shareWithRevision(self):
+        """
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        """
+        sharedName = yield self.createShare()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_updateShareRevision(self):
+        """
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        """
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        newCalName = shareeView.shareUID()
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        otherCal = yield otherHome.anyObjectWithShareUID(newCalName)
+        self.assertEqual(otherCal._bindRevision, 0)
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        shareeView = yield shareeHome.acceptShare(newCalName)
+        sharedName = shareeView.name()
+        yield self.otherCommit()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_sharedRevisions(self):
+        """
+        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+        """
+        sharedName = yield self.createShare()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+        sync_token = yield otherCal.syncToken()
+        revision = otherCal.revisionFromToken(sync_token)
+
+        changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(0)
+        self.assertNotEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
+
+        changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(revision)
+        self.assertEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
+
+        sync_token = yield otherHome.syncToken()
+        revision = otherHome.revisionFromToken(sync_token)
+
+        for depth in ("1", "infinity",):
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision - 1, depth)
+            self.assertEqual(len(changed), 0 if depth == "infinity" else 1)
+            self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+        yield self.otherCommit()
+
+        yield self.removeShare()
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+
+        for depth in ("1", "infinity",):
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 1)
+            self.assertEqual(len(invalid), 0)

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_sharing.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -554,19 +554,23 @@
         otherCal = yield self.calendarUnderTest(home="user02", name=sharedName)
         self.assertNotEqual(otherCal._bindRevision, 0)
 
-        changed, deleted = yield otherCal.resourceNamesSinceRevision(0)
+        changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(0)
         self.assertNotEqual(len(changed), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
 
-        changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision)
+        changed, deleted, invalid = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision)
         self.assertEqual(len(changed), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
 
         for depth in ("1", "infinity",):
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(0, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(0, depth)
             self.assertNotEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
             self.assertEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -13,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from txdav.caldav.datastore.test.util import buildCalendarStore
 
 """
 Tests for txdav.caldav.datastore.util.
@@ -30,6 +29,7 @@
 from twistedcaldav.test.util import TestCase
 
 from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+from txdav.caldav.datastore.test.util import buildCalendarStore
 
 from txdav.caldav.datastore.util import dropboxIDFromCalendarObject, \
     StorageTransportBase, migrateHome
@@ -323,7 +323,12 @@
     @inlineCallbacks
     def setUp(self):
         yield super(HomeMigrationTests, self).setUp()
-        self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=("conflict1", "conflict2",))
+        self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=(
+            "conflict1",
+            "conflict2",
+            "empty_home",
+            "non_empty_home",
+        ))
 
 
     def storeUnderTest(self):

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/util.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -62,18 +62,13 @@
         calendarUserAddresses,
         cutype="INDIVIDUAL",
         thisServer=True,
+        server=None,
         extras={},
     ):
 
-        super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames,
-            fullName, extras=extras)
-        self.uid = uid
-        self.shortNames = shortNames
-        self.fullName = fullName
-        self.displayName = self.fullName if self.fullName else self.shortNames[0]
+        super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName, thisServer, server, extras=extras)
         self.calendarUserAddresses = calendarUserAddresses
         self.cutype = cutype
-        self._thisServer = thisServer
 
 
     def canonicalCalendarUserAddress(self):
@@ -92,10 +87,6 @@
         return cua
 
 
-    def thisServer(self):
-        return self._thisServer
-
-
     def calendarsEnabled(self):
         return True
 
@@ -150,12 +141,19 @@
     homes.update((
         "home1",
         "home2",
-        "Home_attachments",
+        "home3",
+        "home_attachments",
         "home_bad",
         "home_defaults",
         "home_no_splits",
+        "home_provision1",
+        "home_provision2",
         "home_splits",
         "home_splits_shared",
+        "uid1",
+        "uid2",
+        "new-home",
+        "xyzzy",
     ))
     for uid in homes:
         directory.addRecord(buildDirectoryRecord(uid))

Modified: CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/icalendardirectoryservice.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -60,14 +60,6 @@
         @rtype: C{str}
         """
 
-    def thisServer(): #@NoSelf
-        """
-        Indicates whether the record is hosted on this server "pod".
-
-        @return: C{True} if hosted by this service.
-        @rtype: C{bool}
-        """
-
     def calendarsEnabled(): #@NoSelf
         """
         Indicates whether the record enabled for using the calendar service.

Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -907,7 +907,7 @@
     NORMAL_NO_IMPLICIT -    this is an application layer (user) generated remove that deliberately turns
                             off implicit scheduling operations.
 
-    INTERNAL -              remove the resource without implicit scheduling.
+    INTERNAL -              remove the resource without implicit scheduling or attachment processing.
     """
 
     NORMAL = NamedConstant()

Modified: CalendarServer/trunk/txdav/carddav/datastore/index_file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/index_file.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/index_file.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -37,10 +37,6 @@
 
 from twisted.internet.defer import maybeDeferred
 
-from twistedcaldav import carddavxml
-from txdav.common.icommondatastore import SyncTokenValidException, \
-    ReservationError
-from twistedcaldav.query import addressbookquery
 from twistedcaldav.sql import AbstractSQLDatabase
 from twistedcaldav.sql import db_prefix
 from twistedcaldav.vcard import Component
@@ -49,6 +45,12 @@
 from twistedcaldav.config import config
 from twistedcaldav.memcachepool import CachePoolUserMixIn
 
+from txdav.carddav.datastore.query.builder import buildExpression
+from txdav.carddav.datastore.query.filter import Filter
+from txdav.common.datastore.query.filegenerator import sqllitegenerator
+from txdav.common.icommondatastore import SyncTokenValidException, \
+    ReservationError
+
 log = Logger()
 
 db_basename = db_prefix + "sqlite"
@@ -218,6 +220,24 @@
 
 
 
+def sqladdressbookquery(filter, addressbookid=None):
+    """
+    Convert the supplied addressbook-query into a partial SQL statement.
+
+    @param filter: the L{Filter} for the addressbook-query to convert.
+    @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+            Or return C{None} if it is not possible to create an SQL query to fully match the addressbook-query.
+    """
+    try:
+        expression = buildExpression(filter, sqllitegenerator.FIELDS)
+        sql = sqllitegenerator(expression, addressbookid, None)
+        return sql.generate()
+    except ValueError:
+        return None
+
+
+
 class AddressBookIndex(AbstractSQLDatabase):
     """
     AddressBook collection index abstract base class that defines the apis for the index.
@@ -410,6 +430,7 @@
 
         changed = []
         deleted = []
+        invalid = []
         for name, wasdeleted in results:
             if name:
                 if wasdeleted == 'Y':
@@ -420,7 +441,7 @@
             else:
                 raise SyncTokenValidException
 
-        return changed, deleted,
+        return (changed, deleted, invalid)
 
 
     def lastRevision(self):
@@ -445,8 +466,8 @@
 
 
     def searchValid(self, filter):
-        if isinstance(filter, carddavxml.Filter):
-            qualifiers = addressbookquery.sqladdressbookquery(filter)
+        if isinstance(filter, Filter):
+            qualifiers = sqladdressbookquery(filter)
         else:
             qualifiers = None
 
@@ -466,8 +487,8 @@
         # start caching...
 
         # Make sure we have a proper Filter element and get the partial SQL statement to use.
-        if isinstance(filter, carddavxml.Filter):
-            qualifiers = addressbookquery.sqladdressbookquery(filter)
+        if isinstance(filter, Filter):
+            qualifiers = sqladdressbookquery(filter)
         else:
             qualifiers = None
         if qualifiers is not None:

Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/builder.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/builder.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/builder.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,107 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from txdav.common.datastore.query import expression
-from txdav.carddav.datastore.query.filter import TextMatch
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
-    "buildExpression",
-]
-
-
-
-# SQL Index column (field) names
-
-def buildExpression(filter, fields):
-    """
-    Convert the supplied addressbook-query into an expression tree.
-
-    @param filter: the L{Filter} for the addressbook-query to convert.
-    @return: a L{baseExpression} for the expression tree.
-    """
-    # Lets assume we have a valid filter from the outset.
-
-    # Top-level filter contains zero or more prop-filter element
-    if len(filter.children) > 0:
-        return propfilterListExpression(filter.children, fields)
-    else:
-        return expression.allExpression()
-
-
-
-def propfilterListExpression(propfilters, fields):
-    """
-    Create an expression for a list of prop-filter elements.
-
-    @param propfilters: the C{list} of L{ComponentFilter} elements.
-    @return: a L{baseExpression} for the expression tree.
-    """
-
-    if len(propfilters) == 1:
-        return propfilterExpression(propfilters[0], fields)
-    else:
-        return expression.orExpression([propfilterExpression(c, fields) for c in propfilters])
-
-
-
-def propfilterExpression(propfilter, fields):
-    """
-    Create an expression for a single prop-filter element.
-
-    @param propfilter: the L{PropertyFilter} element.
-    @return: a L{baseExpression} for the expression tree.
-    """
-
-    # Only handle UID right now
-    if propfilter.filter_name != "UID":
-        raise ValueError
-
-    # Handle is-not-defined case
-    if not propfilter.defined:
-        # Test for <<field>> != "*"
-        return expression.isExpression(fields["UID"], "", True)
-
-    # Handle embedded parameters/text-match
-    params = []
-    for filter in propfilter.filters:
-        if isinstance(filter, TextMatch):
-            if filter.match_type == "equals":
-                tm = expression.isnotExpression if filter.negate else expression.isExpression
-            elif filter.match_type == "contains":
-                tm = expression.notcontainsExpression if filter.negate else expression.containsExpression
-            elif filter.match_type == "starts-with":
-                tm = expression.notstartswithExpression if filter.negate else expression.startswithExpression
-            elif filter.match_type == "ends-with":
-                tm = expression.notendswithExpression if filter.negate else expression.endswithExpression
-            params.append(tm(fields[propfilter.filter_name], str(filter.text), True))
-        else:
-            # No embedded parameters - not right now as our Index does not handle them
-            raise ValueError
-
-    # Now build return expression
-    if len(params) > 1:
-        if propfilter.propfilter_test == "anyof":
-            return expression.orExpression(params)
-        else:
-            return expression.andExpression(params)
-    elif len(params) == 1:
-        return params[0]
-    else:
-        return None

Copied: CalendarServer/trunk/txdav/carddav/datastore/query/builder.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/builder.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/builder.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/builder.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,107 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from txdav.common.datastore.query import expression
+from txdav.carddav.datastore.query.filter import TextMatch
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+    "buildExpression",
+]
+
+
+
+# SQL Index column (field) names
+
+def buildExpression(filter, fields):
+    """
+    Convert the supplied addressbook-query into an expression tree.
+
+    @param filter: the L{Filter} for the addressbook-query to convert.
+    @return: a L{baseExpression} for the expression tree.
+    """
+    # Lets assume we have a valid filter from the outset.
+
+    # Top-level filter contains zero or more prop-filter element
+    if len(filter.children) > 0:
+        return propfilterListExpression(filter.children, fields)
+    else:
+        return expression.allExpression()
+
+
+
+def propfilterListExpression(propfilters, fields):
+    """
+    Create an expression for a list of prop-filter elements.
+
+    @param propfilters: the C{list} of L{ComponentFilter} elements.
+    @return: a L{baseExpression} for the expression tree.
+    """
+
+    if len(propfilters) == 1:
+        return propfilterExpression(propfilters[0], fields)
+    else:
+        return expression.orExpression([propfilterExpression(c, fields) for c in propfilters])
+
+
+
+def propfilterExpression(propfilter, fields):
+    """
+    Create an expression for a single prop-filter element.
+
+    @param propfilter: the L{PropertyFilter} element.
+    @return: a L{baseExpression} for the expression tree.
+    """
+
+    # Only handle UID right now
+    if propfilter.filter_name != "UID":
+        raise ValueError
+
+    # Handle is-not-defined case
+    if not propfilter.defined:
+        # Test for <<field>> != "*"
+        return expression.isExpression(fields["UID"], "", True)
+
+    # Handle embedded parameters/text-match
+    params = []
+    for filter in propfilter.filters:
+        if isinstance(filter, TextMatch):
+            if filter.match_type == "equals":
+                tm = expression.isnotExpression if filter.negate else expression.isExpression
+            elif filter.match_type == "contains":
+                tm = expression.notcontainsExpression if filter.negate else expression.containsExpression
+            elif filter.match_type == "starts-with":
+                tm = expression.notstartswithExpression if filter.negate else expression.startswithExpression
+            elif filter.match_type == "ends-with":
+                tm = expression.notendswithExpression if filter.negate else expression.endswithExpression
+            params.append(tm(fields[propfilter.filter_name], str(filter.text), True))
+        else:
+            # No embedded parameters - not right now as our Index does not handle them
+            raise ValueError
+
+    # Now build return expression
+    if len(params) > 1:
+        if propfilter.propfilter_test == "anyof":
+            return expression.orExpression(params)
+        else:
+            return expression.andExpression(params)
+    elif len(params) == 1:
+        return params[0]
+    else:
+        return None

Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/filter.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,444 +0,0 @@
-##
-# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-"""
-Object model of CARDDAV:filter element used in an addressbook-query.
-"""
-
-__all__ = [
-    "Filter",
-]
-
-from twext.python.log import Logger
-
-from twistedcaldav.carddavxml import carddav_namespace
-from twistedcaldav.vcard import Property
-
-log = Logger()
-
-class FilterBase(object):
-    """
-    Determines which matching components are returned.
-    """
-
-    serialized_name = None
-    deserialize_names = {}
-
-    @classmethod
-    def serialize_register(cls, register):
-        cls.deserialize_names[register.serialized_name] = register
-
-
-    def __init__(self, xml_element):
-        pass
-
-
-    @classmethod
-    def deserialize(cls, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        obj = cls.deserialize_names[data["type"]](None)
-        obj._deserialize(data)
-        return obj
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        pass
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        return {
-            "type": self.serialized_name,
-        }
-
-
-    def match(self, item, access=None):
-        raise NotImplementedError
-
-
-    def valid(self, level=0):
-        raise NotImplementedError
-
-
-
-class Filter(FilterBase):
-    """
-    Determines which matching components are returned.
-    """
-
-    serialized_name = "Filter"
-
-    def __init__(self, xml_element):
-
-        super(Filter, self).__init__(xml_element)
-        if xml_element is None:
-            return
-
-        filter_test = xml_element.attributes.get("test", "anyof")
-        if filter_test not in ("anyof", "allof"):
-            raise ValueError("Test must be only one of anyof, allof")
-
-        self.filter_test = filter_test
-
-        self.children = [PropertyFilter(child) for child in xml_element.children]
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        self.filter_test = data["filter_test"]
-        self.child = [FilterBase.deserialize(child) for child in data["children"]]
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        result = super(Filter, self).serialize()
-        result.update({
-            "filter_test": self.filter_test,
-            "children": [child.serialize() for child in self.children],
-        })
-        return result
-
-
-    def match(self, vcard):
-        """
-        Returns True if the given address property matches this filter, False
-        otherwise. Empty element means always match.
-        """
-
-        if len(self.children) > 0:
-            allof = self.filter_test == "allof"
-            for propfilter in self.children:
-                if allof != propfilter._match(vcard):
-                    return not allof
-            return allof
-        else:
-            return True
-
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt vCard
-        data object model.
-
-        @return: True if valid, False otherwise
-        """
-
-        # Test each property
-        for propfilter in self.children:
-            if not propfilter.valid():
-                return False
-        else:
-            return True
-
-FilterBase.serialize_register(Filter)
-
-
-
-class FilterChildBase(FilterBase):
-    """
-    CardDAV filter element.
-    """
-
-    def __init__(self, xml_element):
-
-        super(FilterChildBase, self).__init__(xml_element)
-        if xml_element is None:
-            return
-
-        qualifier = None
-        filters = []
-
-        for child in xml_element.children:
-            qname = child.qname()
-
-            if qname in (
-                (carddav_namespace, "is-not-defined"),
-            ):
-                if qualifier is not None:
-                    raise ValueError("Only one of CardDAV:is-not-defined allowed")
-                qualifier = IsNotDefined(child)
-
-            elif qname == (carddav_namespace, "text-match"):
-                filters.append(TextMatch(child))
-
-            elif qname == (carddav_namespace, "param-filter"):
-                filters.append(ParameterFilter(child))
-            else:
-                raise ValueError("Unknown child element: %s" % (qname,))
-
-        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
-            raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
-
-        if xml_element.qname() == (carddav_namespace, "prop-filter"):
-            propfilter_test = xml_element.attributes.get("test", "anyof")
-            if propfilter_test not in ("anyof", "allof"):
-                raise ValueError("Test must be only one of anyof, allof")
-        else:
-            propfilter_test = "anyof"
-
-        self.propfilter_test = propfilter_test
-        self.qualifier = qualifier
-        self.filters = filters
-        self.filter_name = xml_element.attributes["name"]
-        if isinstance(self.filter_name, unicode):
-            self.filter_name = self.filter_name.encode("utf-8")
-        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        self.propfilter_test = data["propfilter_test"]
-        self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
-        self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
-        self.filter_name = data["filter_name"]
-        self.defined = data["defined"]
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        result = super(FilterChildBase, self).serialize()
-        result.update({
-            "propfilter_test": self.propfilter_test,
-            "qualifier": self.qualifier.serialize() if self.qualifier else None,
-            "filters": [filter.serialize() for filter in self.filters],
-            "filter_name": self.filter_name,
-            "defined": self.defined,
-        })
-        return result
-
-
-    def match(self, item):
-        """
-        Returns True if the given address book item (either a property or parameter value)
-        matches this filter, False otherwise.
-        """
-
-        # Always return True for the is-not-defined case as the result of this will
-        # be negated by the caller
-        if not self.defined:
-            return True
-
-        if self.qualifier and not self.qualifier.match(item):
-            return False
-
-        if len(self.filters) > 0:
-            allof = self.propfilter_test == "allof"
-            for filter in self.filters:
-                if allof != filter._match(item):
-                    return not allof
-            return allof
-        else:
-            return True
-
-
-
-class PropertyFilter (FilterChildBase):
-    """
-    Limits a search to specific properties.
-    """
-
-    serialized_name = "PropertyFilter"
-
-    def _match(self, vcard):
-        # At least one property must match (or is-not-defined is set)
-        for property in vcard.properties():
-            if property.name().upper() == self.filter_name.upper() and self.match(property):
-                break
-        else:
-            return not self.defined
-        return self.defined
-
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt vCard
-        data object model.
-
-        @return:      True if valid, False otherwise
-        """
-
-        # No tests
-        return True
-
-FilterBase.serialize_register(PropertyFilter)
-
-
-
-class ParameterFilter (FilterChildBase):
-    """
-    Limits a search to specific parameters.
-    """
-
-    serialized_name = "ParameterFilter"
-
-    def _match(self, property):
-
-        # At least one parameter must match (or is-not-defined is set)
-        result = not self.defined
-        for parameterName in property.parameterNames():
-            if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValues(parameterName)]):
-                result = self.defined
-                break
-
-        return result
-
-FilterBase.serialize_register(ParameterFilter)
-
-
-
-class IsNotDefined (FilterBase):
-    """
-    Specifies that the named iCalendar item does not exist.
-    """
-
-    serialized_name = "IsNotDefined"
-
-    def match(self, component, access=None):
-        # Oddly, this needs always to return True so that it appears there is
-        # a match - but we then "negate" the result if is-not-defined is set.
-        # Actually this method should never be called as we special case the
-        # is-not-defined option.
-        return True
-
-FilterBase.serialize_register(IsNotDefined)
-
-
-
-class TextMatch (FilterBase):
-    """
-    Specifies a substring match on a property or parameter value.
-    """
-    serialized_name = "TextMatch"
-
-    def __init__(self, xml_element):
-
-        super(TextMatch, self).__init__(xml_element)
-        if xml_element is None:
-            return
-
-        self.text = str(xml_element)
-
-        if "collation" in xml_element.attributes:
-            self.collation = xml_element.attributes["collation"]
-        else:
-            self.collation = "i;unicode-casemap"
-
-        if "negate-condition" in xml_element.attributes:
-            self.negate = xml_element.attributes["negate-condition"]
-            if self.negate not in ("yes", "no"):
-                self.negate = "no"
-            self.negate = {"yes": True, "no": False}[self.negate]
-        else:
-            self.negate = False
-
-        if "match-type" in xml_element.attributes:
-            self.match_type = xml_element.attributes["match-type"]
-            if self.match_type not in (
-                "equals",
-                "contains",
-                "starts-with",
-                "ends-with",
-            ):
-                self.match_type = "contains"
-        else:
-            self.match_type = "contains"
-
-
-    def _deserialize(self, data):
-        """
-        Convert a JSON compatible serialization of this object into the actual object.
-        """
-        self.text = data["text"]
-        self.collation = data["collation"]
-        self.negate = data["negate"]
-        self.match_type = data["match_type"]
-
-
-    def serialize(self):
-        """
-        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
-        """
-        result = super(TextMatch, self).serialize()
-        result.update({
-            "text": self.text,
-            "collation": self.collation,
-            "negate": self.negate,
-            "match_type": self.match_type,
-        })
-        return result
-
-
-    def _match(self, item):
-        """
-        Match the text for the item.
-        If the item is a property, then match the property value,
-        otherwise it may be a list of parameter values - try to match anyone of those
-        """
-        if item is None:
-            return False
-
-        if isinstance(item, Property):
-            values = [item.strvalue()]
-        else:
-            values = item
-
-        test = unicode(self.text, "utf-8").lower()
-
-
-        def _textCompare(s):
-            # Currently ignores the collation and does caseless matching
-            s = s.lower()
-
-            if self.match_type == "equals":
-                return s == test
-            elif self.match_type == "contains":
-                return s.find(test) != -1
-            elif self.match_type == "starts-with":
-                return s.startswith(test)
-            elif self.match_type == "ends-with":
-                return s.endswith(test)
-            else:
-                return False
-
-        for value in values:
-            # NB Its possible that we have a text list value which appears as a Python list,
-            # so we need to check for that and iterate over the list.
-            if isinstance(value, list):
-                for subvalue in value:
-                    if _textCompare(unicode(subvalue, "utf-8")):
-                        return not self.negate
-            else:
-                if _textCompare(unicode(value, "utf-8")):
-                    return not self.negate
-
-        return self.negate
-
-FilterBase.serialize_register(TextMatch)

Copied: CalendarServer/trunk/txdav/carddav/datastore/query/filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/filter.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/filter.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,444 @@
+##
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+"""
+Object model of CARDDAV:filter element used in an addressbook-query.
+"""
+
+__all__ = [
+    "Filter",
+]
+
+from twext.python.log import Logger
+
+from twistedcaldav.carddavxml import carddav_namespace
+from twistedcaldav.vcard import Property
+
+log = Logger()
+
+class FilterBase(object):
+    """
+    Determines which matching components are returned.
+    """
+
+    serialized_name = None
+    deserialize_names = {}
+
+    @classmethod
+    def serialize_register(cls, register):
+        cls.deserialize_names[register.serialized_name] = register
+
+
+    def __init__(self, xml_element):
+        pass
+
+
+    @classmethod
+    def deserialize(cls, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        obj = cls.deserialize_names[data["type"]](None)
+        obj._deserialize(data)
+        return obj
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        pass
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        return {
+            "type": self.serialized_name,
+        }
+
+
+    def match(self, item, access=None):
+        raise NotImplementedError
+
+
+    def valid(self, level=0):
+        raise NotImplementedError
+
+
+
+class Filter(FilterBase):
+    """
+    Determines which matching components are returned.
+    """
+
+    serialized_name = "Filter"
+
+    def __init__(self, xml_element):
+
+        super(Filter, self).__init__(xml_element)
+        if xml_element is None:
+            return
+
+        filter_test = xml_element.attributes.get("test", "anyof")
+        if filter_test not in ("anyof", "allof"):
+            raise ValueError("Test must be only one of anyof, allof")
+
+        self.filter_test = filter_test
+
+        self.children = [PropertyFilter(child) for child in xml_element.children]
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        self.filter_test = data["filter_test"]
+        self.child = [FilterBase.deserialize(child) for child in data["children"]]
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        result = super(Filter, self).serialize()
+        result.update({
+            "filter_test": self.filter_test,
+            "children": [child.serialize() for child in self.children],
+        })
+        return result
+
+
+    def match(self, vcard):
+        """
+        Returns True if the given address property matches this filter, False
+        otherwise. Empty element means always match.
+        """
+
+        if len(self.children) > 0:
+            allof = self.filter_test == "allof"
+            for propfilter in self.children:
+                if allof != propfilter._match(vcard):
+                    return not allof
+            return allof
+        else:
+            return True
+
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt vCard
+        data object model.
+
+        @return: True if valid, False otherwise
+        """
+
+        # Test each property
+        for propfilter in self.children:
+            if not propfilter.valid():
+                return False
+        else:
+            return True
+
+FilterBase.serialize_register(Filter)
+
+
+
+class FilterChildBase(FilterBase):
+    """
+    CardDAV filter element.
+    """
+
+    def __init__(self, xml_element):
+
+        super(FilterChildBase, self).__init__(xml_element)
+        if xml_element is None:
+            return
+
+        qualifier = None
+        filters = []
+
+        for child in xml_element.children:
+            qname = child.qname()
+
+            if qname in (
+                (carddav_namespace, "is-not-defined"),
+            ):
+                if qualifier is not None:
+                    raise ValueError("Only one of CardDAV:is-not-defined allowed")
+                qualifier = IsNotDefined(child)
+
+            elif qname == (carddav_namespace, "text-match"):
+                filters.append(TextMatch(child))
+
+            elif qname == (carddav_namespace, "param-filter"):
+                filters.append(ParameterFilter(child))
+            else:
+                raise ValueError("Unknown child element: %s" % (qname,))
+
+        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
+            raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
+
+        if xml_element.qname() == (carddav_namespace, "prop-filter"):
+            propfilter_test = xml_element.attributes.get("test", "anyof")
+            if propfilter_test not in ("anyof", "allof"):
+                raise ValueError("Test must be only one of anyof, allof")
+        else:
+            propfilter_test = "anyof"
+
+        self.propfilter_test = propfilter_test
+        self.qualifier = qualifier
+        self.filters = filters
+        self.filter_name = xml_element.attributes["name"]
+        if isinstance(self.filter_name, unicode):
+            self.filter_name = self.filter_name.encode("utf-8")
+        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        self.propfilter_test = data["propfilter_test"]
+        self.qualifier = FilterBase.deserialize(data["qualifier"]) if data["qualifier"] else None
+        self.filters = [FilterBase.deserialize(filter) for filter in data["filters"]]
+        self.filter_name = data["filter_name"]
+        self.defined = data["defined"]
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        result = super(FilterChildBase, self).serialize()
+        result.update({
+            "propfilter_test": self.propfilter_test,
+            "qualifier": self.qualifier.serialize() if self.qualifier else None,
+            "filters": [filter.serialize() for filter in self.filters],
+            "filter_name": self.filter_name,
+            "defined": self.defined,
+        })
+        return result
+
+
+    def match(self, item):
+        """
+        Returns True if the given address book item (either a property or parameter value)
+        matches this filter, False otherwise.
+        """
+
+        # Always return True for the is-not-defined case as the result of this will
+        # be negated by the caller
+        if not self.defined:
+            return True
+
+        if self.qualifier and not self.qualifier.match(item):
+            return False
+
+        if len(self.filters) > 0:
+            allof = self.propfilter_test == "allof"
+            for filter in self.filters:
+                if allof != filter._match(item):
+                    return not allof
+            return allof
+        else:
+            return True
+
+
+
+class PropertyFilter (FilterChildBase):
+    """
+    Limits a search to specific properties.
+    """
+
+    serialized_name = "PropertyFilter"
+
+    def _match(self, vcard):
+        # At least one property must match (or is-not-defined is set)
+        for property in vcard.properties():
+            if property.name().upper() == self.filter_name.upper() and self.match(property):
+                break
+        else:
+            return not self.defined
+        return self.defined
+
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt vCard
+        data object model.
+
+        @return:      True if valid, False otherwise
+        """
+
+        # No tests
+        return True
+
+FilterBase.serialize_register(PropertyFilter)
+
+
+
+class ParameterFilter (FilterChildBase):
+    """
+    Limits a search to specific parameters.
+    """
+
+    serialized_name = "ParameterFilter"
+
+    def _match(self, property):
+
+        # At least one parameter must match (or is-not-defined is set)
+        result = not self.defined
+        for parameterName in property.parameterNames():
+            if parameterName.upper() == self.filter_name.upper() and self.match([property.parameterValues(parameterName)]):
+                result = self.defined
+                break
+
+        return result
+
+FilterBase.serialize_register(ParameterFilter)
+
+
+
+class IsNotDefined (FilterBase):
+    """
+    Specifies that the named iCalendar item does not exist.
+    """
+
+    serialized_name = "IsNotDefined"
+
+    def match(self, component, access=None):
+        # Oddly, this needs always to return True so that it appears there is
+        # a match - but we then "negate" the result if is-not-defined is set.
+        # Actually this method should never be called as we special case the
+        # is-not-defined option.
+        return True
+
+FilterBase.serialize_register(IsNotDefined)
+
+
+
+class TextMatch (FilterBase):
+    """
+    Specifies a substring match on a property or parameter value.
+    """
+    serialized_name = "TextMatch"
+
+    def __init__(self, xml_element):
+
+        super(TextMatch, self).__init__(xml_element)
+        if xml_element is None:
+            return
+
+        self.text = str(xml_element)
+
+        if "collation" in xml_element.attributes:
+            self.collation = xml_element.attributes["collation"]
+        else:
+            self.collation = "i;unicode-casemap"
+
+        if "negate-condition" in xml_element.attributes:
+            self.negate = xml_element.attributes["negate-condition"]
+            if self.negate not in ("yes", "no"):
+                self.negate = "no"
+            self.negate = {"yes": True, "no": False}[self.negate]
+        else:
+            self.negate = False
+
+        if "match-type" in xml_element.attributes:
+            self.match_type = xml_element.attributes["match-type"]
+            if self.match_type not in (
+                "equals",
+                "contains",
+                "starts-with",
+                "ends-with",
+            ):
+                self.match_type = "contains"
+        else:
+            self.match_type = "contains"
+
+
+    def _deserialize(self, data):
+        """
+        Convert a JSON compatible serialization of this object into the actual object.
+        """
+        self.text = data["text"]
+        self.collation = data["collation"]
+        self.negate = data["negate"]
+        self.match_type = data["match_type"]
+
+
+    def serialize(self):
+        """
+        Create a JSON compatible serialization of this object - will be used in a cross-pod request.
+        """
+        result = super(TextMatch, self).serialize()
+        result.update({
+            "text": self.text,
+            "collation": self.collation,
+            "negate": self.negate,
+            "match_type": self.match_type,
+        })
+        return result
+
+
+    def _match(self, item):
+        """
+        Match the text for the item.
+        If the item is a property, then match the property value,
+        otherwise it may be a list of parameter values - try to match anyone of those
+        """
+        if item is None:
+            return False
+
+        if isinstance(item, Property):
+            values = [item.strvalue()]
+        else:
+            values = item
+
+        test = unicode(self.text, "utf-8").lower()
+
+
+        def _textCompare(s):
+            # Currently ignores the collation and does caseless matching
+            s = s.lower()
+
+            if self.match_type == "equals":
+                return s == test
+            elif self.match_type == "contains":
+                return s.find(test) != -1
+            elif self.match_type == "starts-with":
+                return s.startswith(test)
+            elif self.match_type == "ends-with":
+                return s.endswith(test)
+            else:
+                return False
+
+        for value in values:
+            # NB Its possible that we have a text list value which appears as a Python list,
+            # so we need to check for that and iterate over the list.
+            if isinstance(value, list):
+                for subvalue in value:
+                    if _textCompare(unicode(subvalue, "utf-8")):
+                        return not self.negate
+            else:
+                if _textCompare(unicode(value, "utf-8")):
+                    return not self.negate
+
+        return self.negate
+
+FilterBase.serialize_register(TextMatch)

Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/test_filter.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,96 +0,0 @@
-##
-# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twext.enterprise.dal.syntax import SQLFragment
-
-from twisted.trial.unittest import TestCase
-
-from twistedcaldav import carddavxml
-
-from txdav.carddav.datastore.query.filter import Filter, FilterBase
-from txdav.common.datastore.sql_tables import schema
-from txdav.carddav.datastore.query.builder import buildExpression
-from txdav.common.datastore.query.generator import SQLQueryGenerator
-from txdav.carddav.datastore.index_file import sqladdressbookquery
-
-class TestQueryFilter(TestCase):
-
-    _objectSchema = schema.ADDRESSBOOK_OBJECT
-    _queryFields = {
-        "UID": _objectSchema.UID
-    }
-
-    def test_query(self):
-        """
-        Basic query test - single term.
-        Only UID can be queried via sql.
-        """
-
-        filter = carddavxml.Filter(
-            *[carddavxml.PropertyFilter(
-                carddavxml.TextMatch.fromString("Example"),
-                **{"name":"UID"}
-            )]
-        )
-        filter = Filter(filter)
-
-        expression = buildExpression(filter, self._queryFields)
-        sql = SQLQueryGenerator(expression, self, 1234)
-        select, args = sql.generate()
-
-        self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_HOME_RESOURCE_ID = ? and VCARD_UID like (? || (? || ?))", [1234, "%", "Example", "%"]))
-        self.assertEqual(args, {})
-
-
-    def test_sqllite_query(self):
-        """
-        Basic query test - single term.
-        Only UID can be queried via sql.
-        """
-
-        filter = carddavxml.Filter(
-            *[carddavxml.PropertyFilter(
-                carddavxml.TextMatch.fromString("Example"),
-                **{"name":"UID"}
-            )]
-        )
-        filter = Filter(filter)
-        sql, args = sqladdressbookquery(filter, 1234)
-
-        self.assertEqual(sql, " from RESOURCE where RESOURCE.UID GLOB :1")
-        self.assertEqual(args, ["*Example*"])
-
-
-
-class TestQueryFilterSerialize(TestCase):
-
-    def test_query(self):
-        """
-        Basic query test - no time range
-        """
-
-        filter = carddavxml.Filter(
-            *[carddavxml.PropertyFilter(
-                carddavxml.TextMatch.fromString("Example"),
-                **{"name":"UID"}
-            )]
-        )
-        filter = Filter(filter)
-        j = filter.serialize()
-        self.assertEqual(j["type"], "Filter")
-
-        f = FilterBase.deserialize(j)
-        self.assertTrue(isinstance(f, Filter))

Copied: CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/query/test/test_filter.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/query/test/test_filter.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,96 @@
+##
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twext.enterprise.dal.syntax import SQLFragment
+
+from twisted.trial.unittest import TestCase
+
+from twistedcaldav import carddavxml
+
+from txdav.carddav.datastore.query.filter import Filter, FilterBase
+from txdav.common.datastore.sql_tables import schema
+from txdav.carddav.datastore.query.builder import buildExpression
+from txdav.common.datastore.query.generator import SQLQueryGenerator
+from txdav.carddav.datastore.index_file import sqladdressbookquery
+
+class TestQueryFilter(TestCase):
+
+    _objectSchema = schema.ADDRESSBOOK_OBJECT
+    _queryFields = {
+        "UID": _objectSchema.UID
+    }
+
+    def test_query(self):
+        """
+        Basic query test - single term.
+        Only UID can be queried via sql.
+        """
+
+        filter = carddavxml.Filter(
+            *[carddavxml.PropertyFilter(
+                carddavxml.TextMatch.fromString("Example"),
+                **{"name":"UID"}
+            )]
+        )
+        filter = Filter(filter)
+
+        expression = buildExpression(filter, self._queryFields)
+        sql = SQLQueryGenerator(expression, self, 1234)
+        select, args = sql.generate()
+
+        self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_HOME_RESOURCE_ID = ? and VCARD_UID like (? || (? || ?))", [1234, "%", "Example", "%"]))
+        self.assertEqual(args, {})
+
+
+    def test_sqllite_query(self):
+        """
+        Basic query test - single term.
+        Only UID can be queried via sql.
+        """
+
+        filter = carddavxml.Filter(
+            *[carddavxml.PropertyFilter(
+                carddavxml.TextMatch.fromString("Example"),
+                **{"name":"UID"}
+            )]
+        )
+        filter = Filter(filter)
+        sql, args = sqladdressbookquery(filter, 1234)
+
+        self.assertEqual(sql, " from RESOURCE where RESOURCE.UID GLOB :1")
+        self.assertEqual(args, ["*Example*"])
+
+
+
+class TestQueryFilterSerialize(TestCase):
+
+    def test_query(self):
+        """
+        Basic query test - no time range
+        """
+
+        filter = carddavxml.Filter(
+            *[carddavxml.PropertyFilter(
+                carddavxml.TextMatch.fromString("Example"),
+                **{"name":"UID"}
+            )]
+        )
+        filter = Filter(filter)
+        j = filter.serialize()
+        self.assertEqual(j["type"], "Filter")
+
+        f = FilterBase.deserialize(j)
+        self.assertTrue(isinstance(f, Filter))

Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -14,8 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # #
-from txdav.xml import element
 
+
 """
 SQL backend for CardDAV storage.
 """
@@ -47,12 +47,14 @@
 
 from txdav.base.propertystore.base import PropertyName
 from txdav.base.propertystore.sql import PropertyStore
+from txdav.carddav.datastore.query.builder import buildExpression
+from txdav.carddav.datastore.query.filter import Filter
 from txdav.carddav.iaddressbookstore import IAddressBookHome, IAddressBook, \
     IAddressBookObject, GroupWithUnsharedAddressNotAllowedError, \
     KindChangeNotAllowedError
+from txdav.common.datastore.query.generator import SQLQueryGenerator
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, EADDRESSBOOKTYPE, SharingMixIn, SharingInvitation
-from txdav.common.datastore.sql_legacy import PostgresLegacyABIndexEmulator
 from txdav.common.datastore.sql_tables import _ABO_KIND_PERSON, \
     _ABO_KIND_GROUP, _ABO_KIND_RESOURCE, _ABO_KIND_LOCATION, schema, \
     _BIND_MODE_OWN, _BIND_MODE_WRITE, _BIND_STATUS_ACCEPTED, \
@@ -61,7 +63,8 @@
     InvalidUIDError, UIDExistsError, ObjectResourceTooBigError, \
     InvalidObjectResourceError, InvalidComponentForStoreError, \
     AllRetriesFailed, ObjectResourceNameAlreadyExistsError, \
-    SyncTokenValidException
+    SyncTokenValidException, IndexedSearchException
+from txdav.xml import element
 
 from zope.interface.declarations import implements
 
@@ -86,7 +89,6 @@
 
     def __init__(self, transaction, ownerUID):
 
-        self._childClass = AddressBook
         super(AddressBookHome, self).__init__(transaction, ownerUID)
         self._addressbookPropertyStoreID = None
         self._addressbook = None
@@ -450,6 +452,8 @@
     """
     implements(IAddressBook)
 
+    _homeType = EADDRESSBOOKTYPE
+
     # structured tables.  (new, preferred)
     _homeSchema = schema.ADDRESSBOOK_HOME
     _bindSchema = schema.SHARED_ADDRESSBOOK_BIND
@@ -458,14 +462,87 @@
     _revisionsSchema = schema.ADDRESSBOOK_OBJECT_REVISIONS
     _objectSchema = schema.ADDRESSBOOK_OBJECT
 
+    # Mapping of vCard property name to DB column name
+    _queryFields = {
+        "UID": _objectSchema.UID,
+    }
 
-    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None):
+
+    @classmethod
+    @inlineCallbacks
+    def _getDBDataIndirect(cls, home, name, resourceID, externalID):
+
+        # Get the bind row data
+        row = None
+
+        # TODO: query cacher
+
+        rows = None
+        ownerHome = None
+
+        # TODO: add queryCacher support
+
+        if rows is None:
+            # No cached copy
+            if name:
+                ownerHome = yield home._txn.addressbookHomeWithUID(name)
+                if ownerHome is None:
+                    returnValue(None)
+                resourceID = ownerHome.addressbook()._resourceID
+            rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
+                home._txn, homeID=home._resourceID, addressbookID=resourceID
+            )
+
+        if not rows:
+            returnValue(None)
+
+        groupID = None
+        overallBindStatus = _BIND_STATUS_INVITED
+        minBindRevision = None
+        for row in rows:
+            bindMode, homeID, resourceGroupID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
+            if groupID is None:
+                groupID = resourceGroupID
+            minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
+            if bindStatus == _BIND_STATUS_ACCEPTED:
+                overallBindStatus = _BIND_STATUS_ACCEPTED
+
+        if ownerHome is None:
+            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
+            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
+
+        bindData = row[:cls.bindColumnCount]
+        additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+
+        # Adjust for aggregate values
+        bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = resourceID
+        bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
+        bindData[cls.bindColumns().index(cls._bindSchema.BIND_MODE)] = _BIND_MODE_INDIRECT
+        bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)] = overallBindStatus
+        bindData[cls.bindColumns().index(cls._bindSchema.BIND_REVISION)] = minBindRevision
+        bindData[cls.bindColumns().index(cls._bindSchema.MESSAGE)] = ""
+
+        # Get the matching metadata data
+        metadataData = None
+        queryCacher = home._txn._queryCacher
+        if queryCacher:
+            # Retrieve from cache
+            cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+            metadataData = yield queryCacher.get(cacheKey)
+
+        if metadataData is None:
+            # No cached copy
+            metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+            if queryCacher:
+                # Cache the results
+                yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+        returnValue((bindData, additionalBindData, metadataData, ownerHome,))
+
+
+    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
         ownerName = ownerHome.addressbook().name() if ownerHome else None
-        super(AddressBook, self).__init__(
-            home, name, resourceID, mode, status, revision=revision,
-            message=message, ownerHome=ownerHome, ownerName=ownerName
-        )
-        self._index = PostgresLegacyABIndexEmulator(self)
+        super(AddressBook, self).__init__(home, name, resourceID, mode, status, revision=revision, message=message, ownerHome=ownerHome, ownerName=ownerName, externalID=externalID)
 
 
     def __repr__(self):
@@ -568,13 +645,14 @@
             returnValue((yield super(AddressBook, self).resourceNamesSinceRevision(revision)))
 
         # call sharedChildResourceNamesSinceRevision() and filter results
-        sharedChildChanged, sharedChildDeleted = yield self.sharedChildResourceNamesSinceRevision(revision, "infinity")
+        sharedChildChanged, sharedChildDeleted, sharedChildInvalid = yield self.sharedChildResourceNamesSinceRevision(revision, "infinity")
 
         selfPath = self.name() + '/'
         lenpath = len(selfPath)
         changed = [item[lenpath:] for item in sharedChildChanged if item.startswith(selfPath) and item != selfPath]
         deleted = [item[lenpath:] for item in sharedChildDeleted if item.startswith(selfPath) and item != selfPath]
-        returnValue((changed, deleted,))
+        invalid = [item[lenpath:] for item in sharedChildInvalid if item.startswith(selfPath) and item != selfPath]
+        returnValue((changed, deleted, invalid))
 
 
     @inlineCallbacks
@@ -611,7 +689,7 @@
                 self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
         )
         if groupBindRows:
-            bindRevisions += [groupBindRow[5] for groupBindRow in groupBindRows]
+            bindRevisions += [groupBindRow[self.bindColumns().index(self._bindSchema.BIND_REVISION)] for groupBindRow in groupBindRows]
 
         if revision != 0 and revision < max(bindRevisions):
             if depth != '1':
@@ -623,16 +701,17 @@
 
         if self.fullyShared():
             # add change for addressbook group
-            changed, deleted = yield super(AddressBook, self).sharedChildResourceNamesSinceRevision(revision, depth)
+            changed, deleted, invalid = yield super(AddressBook, self).sharedChildResourceNamesSinceRevision(revision, depth)
 
             if revision == 0 and depth != "1":
                 changed.add("%s/%s" % (path, self._groupForSharedAddressBookName(),))
 
-            returnValue((changed, deleted))
+            returnValue((changed, deleted, invalid))
 
         changed = set()
         deleted = set()
-        acceptedGroupIDs = set([groupBindRow[2] for groupBindRow in groupBindRows])
+        invalid = set()
+        acceptedGroupIDs = set([groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)] for groupBindRow in groupBindRows])
 
         allowedObjectIDs = set((yield self.expandGroupIDs(self._txn, acceptedGroupIDs)))
         oldAllowedObjectIDs = set((yield self.expandGroupIDs(self._txn, acceptedGroupIDs, revision)))
@@ -700,7 +779,7 @@
                 for addedObjectID in allowedObjectIDs:
                     changed.add("%s/%s" % (path, idToNameMap[addedObjectID],))
 
-        returnValue((changed, deleted))
+        returnValue((changed, deleted, invalid,))
 
 
     @inlineCallbacks
@@ -728,6 +807,37 @@
         )
 
 
+    def getInviteCopyProperties(self):
+        """
+        Get a dictionary of property name/values (as strings) for properties that are shadowable and
+        need to be copied to a sharee's collection when an external (cross-pod) share is created.
+        Sub-classes should override to expose the properties they care about.
+        """
+        props = {}
+        for elem in (element.DisplayName, carddavxml.AddressBookDescription,):
+            if PropertyName.fromElement(elem) in self.properties():
+                props[elem.sname()] = str(self.properties()[PropertyName.fromElement(elem)])
+        return props
+
+
+    def setInviteCopyProperties(self, props):
+        """
+        Copy a set of shadowable properties (as name/value strings) onto this shared resource when
+        a cross-pod invite is processed. Sub-classes should override to expose the properties they
+        care about.
+        """
+        # Initialize these for all shares
+        for elem in (carddavxml.AddressBookDescription,):
+            if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+                self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+        # Only initialize these for direct shares
+        if self.direct():
+            for elem in (element.DisplayName,):
+                if PropertyName.fromElement(elem) not in self.properties() and elem.sname() in props:
+                    self.properties()[PropertyName.fromElement(elem)] = elem.fromString(props[elem.sname()])
+
+
     def contentType(self):
         """
         The content type of addressbook objects is text/vcard.
@@ -736,7 +846,7 @@
 
 
     @classmethod
-    def create(cls, home, name):
+    def create(cls, home, name, externalID=None):
         if name == home.addressbook().name():
             # raise HomeChildNameAlreadyExistsError
             pass
@@ -762,13 +872,15 @@
     def remove(self):
 
         if self._resourceID == self._home._resourceID:
+
+            # Stop sharing first
+            yield self.ownerDeleteShare()
+
             # Allow remove, as a way to reset the address book to an empty state
             for abo in (yield self.objectResources()):
                 yield abo.remove()
                 yield self.removedObjectResource(abo)
 
-            yield self.unshare()  # storebridge should already have done this
-
             yield self.properties()._removeResource()
             yield self._loadPropertyStore()
 
@@ -925,6 +1037,57 @@
             returnValue((yield super(AddressBook, self).bumpModified()))
 
 
+    @inlineCallbacks
+    def search(self, filter):
+        """
+        Finds resources matching the given qualifiers.
+        @param filter: the L{Filter} for the addressbook-query to execute.
+        @return: an iterable of tuples for each resource matching the
+            given C{qualifiers}. The tuples are C{(name, uid)}, where
+            C{name} is the resource name, C{uid} is the resource UID.
+        """
+
+        # We might be passed an L{Filter} or a serialization of one
+        if isinstance(filter, dict):
+            try:
+                filter = Filter.deserialize(filter)
+            except Exception:
+                filter = None
+
+        # Make sure we have a proper Filter element and get the partial SQL statement to use.
+        sql_stmt = self._sqlquery(filter)
+
+        # No result means it is too complex for us
+        if sql_stmt is None:
+            raise IndexedSearchException()
+
+        sql_stmt, args = sql_stmt
+        rowiter = yield sql_stmt.on(self._txn, **args)
+
+        returnValue(list(rowiter))
+
+
+    def _sqlquery(self, filter):
+        """
+        Convert the supplied addressbook-query into a partial SQL statement.
+
+        @param filter: the L{Filter} for the addressbook-query to convert.
+        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+                and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+                Or return C{None} if it is not possible to create an SQL query to fully match the addressbook-query.
+        """
+
+        if not isinstance(filter, Filter):
+            return None
+
+        try:
+            expression = buildExpression(filter, self._queryFields)
+            sql = SQLQueryGenerator(expression, self, self.id())
+            return sql.generate()
+        except ValueError:
+            return None
+
+
     @classmethod
     @inlineCallbacks
     def listObjects(cls, home):
@@ -946,7 +1109,7 @@
             home._txn, homeID=home._resourceID
         )
         for groupRow in groupRows:
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
             ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
             ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID, create=True)
             names |= set([ownerHome.uid()])
@@ -974,7 +1137,7 @@
         )
         # get ownerHomeIDs
         for dataRow in dataRows:
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
+            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
             ownerHome = yield home.ownerHomeWithChildID(resourceID)
             ownerHomeToDataRowMap[ownerHome] = dataRow
 
@@ -983,7 +1146,7 @@
             home._txn, homeID=home._resourceID
         )
         for groupBindRow in groupBindRows:
-            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
             ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
             ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
             if ownerHome not in ownerHomeToDataRowMap:
@@ -1004,32 +1167,17 @@
 
             # Create the actual objects merging in properties
             for ownerHome, dataRow in ownerHomeToDataRowMap.iteritems():
-                bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
-                additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
-                metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+                bindData = dataRow[:cls.bindColumnCount]
+                additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+                metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+                propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
 
-                child = cls(
-                    home=home,
-                    name=ownerHome.uid(),
-                    resourceID=ownerHome._resourceID,
-                    mode=bindMode,
-                    status=bindStatus,
-                    revision=bindRevision,
-                    message=bindMessage,
-                    ownerHome=ownerHome,
-                )
+                # Some adjustments for addressbook share model
+                bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = ownerHome._resourceID
+                bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
 
-                for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
-                    setattr(child, attr, value)
-                for attr, value in zip(cls.metadataAttributes(), metadata):
-                    setattr(child, attr, value)
+                child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore, ownerHome)
                 child._syncTokenRevision = revisions[child._resourceID]
-                propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
-                # We have to re-adjust the property store object to account for possible shared
-                # collections as previously we loaded them all as if they were owned
-                if propstore:
-                    propstore._setDefaultUserUID(ownerHome.uid())
-                yield child._loadPropertyStore(propstore)
                 results.append(child)
 
         returnValue(results)
@@ -1092,7 +1240,7 @@
 
     @classmethod
     @inlineCallbacks
-    def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
+    def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
         # replaces objectWithName()
         """
         Synthesize and indirect child for matching name or id based on whether shared groups exist.
@@ -1104,56 +1252,17 @@
         @return: an L{CommonHomeChild} or C{None} if no such child
             exists.
         """
-        rows = None
-        ownerHome = None
 
-        # TODO: add queryCacher support
-
-        if rows is None:
-            # No cached copy
-            if name:
-                ownerHome = yield home._txn.addressbookHomeWithUID(name)
-                if ownerHome is None:
-                    returnValue(None)
-                resourceID = ownerHome.addressbook()._resourceID
-            rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
-                home._txn, homeID=home._resourceID, addressbookID=resourceID
-            )
-
-        if not rows:
+        dbData = yield cls._getDBDataIndirect(home, name, resourceID, externalID)
+        if dbData is None:
             returnValue(None)
+        bindData, additionalBindData, metadataData, ownerHome = dbData
 
-        groupID = None
-        overallBindStatus = _BIND_STATUS_INVITED
-        minBindRevision = None
-        for row in rows:
-            bindMode, homeID, resourceGroupID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
-            if groupID is None:
-                groupID = resourceGroupID
-            minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
-            if bindStatus == _BIND_STATUS_ACCEPTED:
-                overallBindStatus = _BIND_STATUS_ACCEPTED
-
-        if accepted is not None and (overallBindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
+        bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
+        if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
             returnValue(None)
-        additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
 
-        if ownerHome is None:
-            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
-            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
-
-        child = cls(
-            home=home,
-            name=ownerHome.uid(),
-            resourceID=resourceID,
-            mode=_BIND_MODE_INDIRECT,
-            status=overallBindStatus,
-            revision=minBindRevision,
-            message="",
-            ownerHome=ownerHome,
-            ownerName=ownerHome.uid()
-        )
-        yield child.initFromStore(additionalBind)
+        child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, None, ownerHome)
         returnValue(child)
 
 
@@ -1247,7 +1356,7 @@
             groupBindRows = yield AddressBookObject._unacceptedBindForHomeIDAndAddressBookID.on(
                 self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
             )
-            returnValue([groupBindRow[2] for groupBindRow in groupBindRows])
+            returnValue([groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)] for groupBindRow in groupBindRows])
 
 
     @inlineCallbacks
@@ -1261,7 +1370,7 @@
             groupBindRows = yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
                 self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
             )
-            returnValue([groupBindRow[2] for groupBindRow in groupBindRows])
+            returnValue([groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)] for groupBindRow in groupBindRows])
 
 
     @inlineCallbacks
@@ -1280,7 +1389,7 @@
             readWriteGroupIDs = set()
             readOnlyGroupIDs = set()
             for groupBindRow in groupBindRows:
-                bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+                bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
                 if bindMode == _BIND_MODE_WRITE:
                     readWriteGroupIDs.add(resourceID)
                 else:
@@ -1341,7 +1450,7 @@
         readWriteGroupIDs = []
         readOnlyGroupIDs = []
         for groupBindRow in groupBindRows:
-            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
             if bindMode == _BIND_MODE_WRITE:
                 readWriteGroupIDs.append(resourceID)
             else:
@@ -1426,7 +1535,7 @@
 
 
     @inlineCallbacks
-    def shareWith(self, shareeHome, mode, status=None, summary=None):
+    def shareWith(self, shareeHome, mode, status=None, summary=None, shareName=None):
         """
         Share this (owned) L{AddressBookObject} with another home.
 
@@ -1454,11 +1563,12 @@
 
         @inlineCallbacks
         def doInsert(subt):
-            newName = self.newShareName()
+            newName = shareName if shareName is not None else self.newShareName()
             yield self._bindInsertQuery.on(
                 subt,
                 homeID=shareeHome._resourceID,
                 resourceID=self._resourceID,
+                externalID=None,
                 name=newName,
                 mode=mode,
                 bindStatus=status,
@@ -1495,14 +1605,14 @@
 
 
     @inlineCallbacks
-    def createShare(self, shareeUID, mode, summary=None):
+    def createShare(self, shareeUID, mode, summary=None, shareName=None):
         """
         Create a new shared resource. If the mode is direct, the share is created in accepted state,
         otherwise the share is created in invited state.
         """
 
         if self._kind == _ABO_KIND_GROUP:
-            shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary)
+            shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary, shareName)
             returnValue(shareeView)
         else:
             returnValue(None)
@@ -1598,8 +1708,9 @@
                         # update revision in all remaining bind table rows for this address book
                         yield shareeView.addressbook().notifyPropertyChanged()
                         for groupBindRow in groupBindRows:
-                            if groupBindRow[2] != shareeView._resourceID:
-                                groupObject = yield shareeView.addressbook().objectResourceWithID(groupBindRow[2])
+                            resid = groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)]
+                            if resid != shareeView._resourceID:
+                                groupObject = yield shareeView.addressbook().objectResourceWithID(resid)
                                 yield groupObject._initBindRevision()
                         if shareeView.addressbook().fullyShared():
                             yield shareeView.addressbook()._initBindRevision()
@@ -1651,7 +1762,9 @@
                 yield addressbookAsShared.notifyPropertyChanged()
                 #update revision in all remaining bind table rows for this address book
                 for groupBindRow in groupBindRows:
-                    groupObject = yield addressbookAsShared.objectResourceWithID(groupBindRow[2])
+                    groupObject = yield addressbookAsShared.objectResourceWithID(
+                        groupBindRow[self.bindColumns().index(self._bindSchema.RESOURCE_ID)]
+                    )
                     yield groupObject._initBindRevision()
                 addressbookAsShared._objects = {}
                 addressbookAsShared._objectNames = None
@@ -1731,25 +1844,147 @@
     _objectSchema = schema.ADDRESSBOOK_OBJECT
     _bindSchema = schema.SHARED_GROUP_BIND
 
+    _componentClass = VCard
+
     # used by CommonHomeChild._childrenAndMetadataForHomeID() only
     # _homeChildSchema = schema.ADDRESSBOOK_OBJECT
     # _homeChildMetaDataSchema = schema.ADDRESSBOOK_OBJECT
 
 
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, parent, objectData, groupBindData=None, propstore=None):
+        """
+        Given the various database rows, build the actual class.
+
+        @param parent: the parent collection object
+        @type parent: L{AddressBook}
+        @param objectData: the standard set of object columns
+        @type objectData: C{list}
+        @param groupBindData: additional group bind data
+        @type groupBindData: C{list}
+        @param propstore: a property store to use, or C{None} to load it automatically
+        @type propstore: L{PropertyStore}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        """
+
+        c = cls._externalClass if parent.external() else cls
+        child = c(
+            parent,
+            objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+            objectData[cls._allColumns().index(cls._objectSchema.UID)],
+        )
+
+        for attr, value in zip(child._rowAttributes(), objectData):
+            setattr(child, attr, value)
+
+        yield child._loadPropertyStore(propstore)
+
+        if groupBindData:
+            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindData[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            child._bindMode = bindMode
+            child._bindStatus = bindStatus
+            child._bindMessage = bindMessage
+            child._bindName = bindName
+            child._bindRevision = bindRevision
+        else:
+            invites = yield child.sharingInvites()
+            if len(invites):
+                child._bindMessage = "shared"
+
+        returnValue(child)
+
+
+    @classmethod
+    @inlineCallbacks
+    def _getDBData(cls, parent, name, uid, resourceID):
+        """
+        Given a set of identifying information, load the data rows for the object. Only one of
+        L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+        @param parent: the parent collection object
+        @type parent: L{AddressBook}
+        @param name: the resource name
+        @type name: C{str}
+        @param uid: the UID of the data
+        @type uid: C{str}
+        @param resourceID: the resource ID
+        @type resourceID: C{int}
+        """
+
+        row = None
+        groupBindRow = None
+
+        if parent.owned() or parent.fullyShared():  # owned or fully shared
+            row = yield super(AddressBookObject, cls)._getDBData(parent, name, uid, resourceID)
+
+            # Might be special group
+            if row is None and parent.fullyShared():
+                if name:
+                    if name == parent._groupForSharedAddressBookName():
+                        row = parent._groupForSharedAddressBookRow()
+                elif uid:
+                    if uid == (yield parent._groupForSharedAddressBookUID()):
+                        row = parent._groupForSharedAddressBookRow()
+                elif resourceID:
+                    if resourceID == parent.id():
+                        rows = parent._groupForSharedAddressBookRow()
+
+        else:
+            acceptedGroupIDs = yield parent.acceptedGroupIDs()
+            allowedObjectIDs = yield parent.expandGroupIDs(parent._txn, acceptedGroupIDs)
+            rows = None
+            if name:
+                if allowedObjectIDs:
+                    rows = (yield cls._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
+                        parent._txn,
+                        name=name,
+                        resourceIDs=allowedObjectIDs,
+                    ))
+            elif uid:
+                if allowedObjectIDs:
+                    rows = (yield cls._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
+                        parent._txn,
+                        uid=uid,
+                        resourceIDs=allowedObjectIDs,
+                    ))
+            elif resourceID:
+                # Also allow invited groups
+                if resourceID in allowedObjectIDs or resourceID in (yield parent.unacceptedGroupIDs()):
+                    rows = (yield cls._allColumnsWithResourceID.on(
+                        parent._txn,
+                        resourceID=resourceID,
+                    ))
+            if rows:
+                row = rows[0]
+
+        if row is not None:
+            if row[cls._allColumns().index(cls._objectSchema.KIND)] == _ABO_KIND_GROUP:
+
+                resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+                groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
+                    parent._txn, resourceID=resourceID, homeID=parent._home._resourceID
+                )
+
+                if groupBindRows:
+                    groupBindRow = groupBindRows[0]
+
+        returnValue((row, groupBindRow,))
+
+
     def __init__(self, addressbook, name, uid, resourceID=None, options=None):
 
         self._kind = None
         self._ownerAddressBookResourceID = None
-        # _self._component is the cached, current component
-        # super._objectText now contains the text as read of the database only,
-        #     not including group member text
-        self._component = None
         self._bindMode = None
         self._bindStatus = None
         self._bindMessage = None
         self._bindName = None
         self._bindRevision = None
         super(AddressBookObject, self).__init__(addressbook, name, uid, resourceID, options)
+        self._externalID = None
         self._options = {} if options is None else options
 
 
@@ -1774,6 +2009,15 @@
         return self._resourceID == self.addressbook()._resourceID
 
 
+    def external(self):
+        """
+        Is this an external object.
+
+        @return: a string.
+        """
+        return self.addressbook().external()
+
+
     @inlineCallbacks
     def remove(self):
 
@@ -1872,7 +2116,8 @@
         yield super(AddressBookObject, self).remove()
         self._kind = None
         self._ownerAddressBookResourceID = None
-        self._component = None
+        self._objectText = None
+        self._cachedComponent = None
 
 
     @inlineCallbacks
@@ -1901,7 +2146,7 @@
         """
         obj = cls._objectSchema
         return Select(
-            cls._allColumns, From=obj,
+            cls._allColumns(), From=obj,
             Where=(column == Parameter(paramName)).And(
                 obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs)))),
         )
@@ -1921,7 +2166,7 @@
     def _allColumnsWithResourceID(cls): #@NoSelf
         obj = cls._objectSchema
         return Select(
-            cls._allColumns, From=obj,
+            cls._allColumns(), From=obj,
             Where=obj.RESOURCE_ID == Parameter("resourceID"),)
 
 
@@ -1944,7 +2189,7 @@
         )
         if groupBindRows:
             groupBindRow = groupBindRows[0]
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
 
             if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
                 returnValue(None)
@@ -1957,93 +2202,20 @@
         returnValue(None)
 
 
+    @classmethod
     @inlineCallbacks
-    def initFromStore(self):
-        """
-        Initialise this object from the store. We read in and cache all the
-        extra metadata from the DB to avoid having to do DB queries for those
-        individually later. Either the name or uid is present, so we have to
-        tweak the query accordingly.
+    def objectWith(cls, parent, name=None, uid=None, resourceID=None):
 
-        @return: L{self} if object exists in the DB, else C{None}
-        """
-        abo = None
-        if self.owned() or self.addressbook().fullyShared():  # owned or fully shared
-            abo = yield super(AddressBookObject, self).initFromStore()
+        row, groupBindRow = yield cls._getDBData(parent, name, uid, resourceID)
 
-            # Might be special group
-            if abo is None and self.addressbook().fullyShared():
-                rows = None
-                if self._name:
-                    if self._name == self.addressbook()._groupForSharedAddressBookName():
-                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
-                elif self._uid:
-                    if self._uid == (yield self.addressbook()._groupForSharedAddressBookUID()):
-                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
-                elif self._resourceID:
-                    if self.isGroupForSharedAddressBook():
-                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
-
-                if rows:
-                    self._initFromRow(tuple(rows[0]))
-                    yield self._loadPropertyStore()
-                    abo = self
-
+        if row:
+            child = yield cls.makeClass(parent, row, groupBindRow)
+            returnValue(child)
         else:
-            acceptedGroupIDs = yield self.addressbook().acceptedGroupIDs()
-            allowedObjectIDs = yield self.addressbook().expandGroupIDs(self._txn, acceptedGroupIDs)
-            rows = None
-            if self._name:
-                if allowedObjectIDs:
-                    rows = (yield self._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
-                        self._txn, name=self._name,
-                        resourceIDs=allowedObjectIDs,
-                    ))
-            elif self._uid:
-                if allowedObjectIDs:
-                    rows = (yield self._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
-                        self._txn, uid=self._uid,
-                        resourceIDs=allowedObjectIDs,
-                    ))
-            elif self._resourceID:
-                if (self._resourceID in allowedObjectIDs or
-                        self._resourceID in (yield self.addressbook().unacceptedGroupIDs())): # allow invited groups
-                    rows = (yield self._allColumnsWithResourceID.on(
-                        self._txn, resourceID=self._resourceID,
-                    ))
-            if rows:
-                self._initFromRow(tuple(rows[0]))
-                yield self._loadPropertyStore()
-                abo = self
-
-        if abo is not None:
-            if self._kind == _ABO_KIND_GROUP:
-
-                groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
-                    self._txn, resourceID=self._resourceID, homeID=self._home._resourceID
-                )
-
-                if groupBindRows:
-                    groupBindRow = groupBindRows[0]
-                    bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
-                    self._bindMode = bindMode
-                    self._bindStatus = bindStatus
-                    self._bindMessage = bindMessage
-                    self._bindName = bindName
-                    self._bindRevision = bindRevision
-                else:
-                    invites = yield self.sharingInvites()
-                    if len(invites):
-                        self._bindMessage = "shared"
-
-            yield self._loadPropertyStore()
-
-            returnValue(self)
-        else:
             returnValue(None)
 
 
-    @classproperty
+    @classmethod
     def _allColumns(cls): #@NoSelf
         """
         Full set of columns in the object table that need to be loaded to
@@ -2059,24 +2231,23 @@
             obj.MD5,
             Len(obj.TEXT),
             obj.CREATED,
-            obj.MODIFIED,
+            obj.MODIFIED
         ]
 
 
-    def _initFromRow(self, row):
-        """
-        Given a select result using the columns from L{_allColumns}, initialize
-        the object resource state.
-        """
-        (self._ownerAddressBookResourceID,
-         self._resourceID,
-         self._name,
-         self._uid,
-         self._kind,
-         self._md5,
-         self._size,
-         self._created,
-         self._modified,) = tuple(row)
+    @classmethod
+    def _rowAttributes(cls): #@NoSelf
+        return (
+            "_ownerAddressBookResourceID",
+            "_resourceID",
+            "_name",
+            "_uid",
+            "_kind",
+            "_md5",
+            "_size",
+            "_created",
+            "_modified",
+         )
 
 
     @classmethod
@@ -2099,7 +2270,7 @@
         else:
             acceptedGroupIDs = yield addressbook.acceptedGroupIDs()
             allowedObjectIDs = yield addressbook.expandGroupIDs(addressbook._txn, acceptedGroupIDs)
-            rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns, allowedObjectIDs).on(
+            rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns(), allowedObjectIDs).on(
                 addressbook._txn, resourceIDs=allowedObjectIDs
             )
         returnValue(rows)
@@ -2108,7 +2279,7 @@
     @classmethod
     def _allColumnsWithResourceIDsAndNamesQuery(cls, resourceIDs, names):
         obj = cls._objectSchema
-        return Select(cls._allColumns, From=obj,
+        return Select(cls._allColumns(), From=obj,
                       Where=(obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs))).And(
                           obj.RESOURCE_NAME.In(Parameter("names", len(names))))),)
 
@@ -2224,6 +2395,13 @@
     @inlineCallbacks
     def setComponent(self, component, inserting=False):
 
+        if isinstance(component, str) or isinstance(component, unicode):
+            component = self._componentClass.fromString(component)
+            try:
+                component = self._componentClass.fromString(component)
+            except InvalidVCardDataError as e:
+                raise InvalidComponentForStoreError(str(e))
+
         self._componentChanged = False
 
         if "coaddedUIDs" not in self._options:
@@ -2407,7 +2585,7 @@
             self._objectText = componentText
 
         self._size = len(self._objectText)
-        self._component = component
+        self._cachedComponent = component
         self._md5 = hashlib.md5(componentText).hexdigest()
         self._componentChanged = originalComponentText != componentText
 
@@ -2548,7 +2726,7 @@
         only allowed in good data.
         """
 
-        if self._component is None:
+        if self._cachedComponent is None:
 
             if self.isGroupForSharedAddressBook():
                 component = yield self.addressbook()._groupForSharedAddressBookComponent()
@@ -2608,9 +2786,9 @@
                     component.addProperty(Property("X-ADDRESSBOOKSERVER-KIND", "group"))
                     component.addProperty(Property("UID", self._uid))
 
-            self._component = component
+            self._cachedComponent = component
 
-        returnValue(self._component)
+        returnValue(self._cachedComponent)
 
 
     def moveValidation(self, destination, name):
@@ -2725,4 +2903,10 @@
         )
 
 
+# Hook-up class relationships at the end after they have all been defined
+from txdav.carddav.datastore.sql_external import AddressBookHomeExternal, AddressBookExternal, AddressBookObjectExternal
+AddressBookHome._externalClass = AddressBookHomeExternal
+AddressBookHome._childClass = AddressBook
+AddressBook._externalClass = AddressBookExternal
 AddressBook._objectResourceClass = AddressBookObject
+AddressBookObject._externalClass = AddressBookObjectExternal

Copied: CalendarServer/trunk/txdav/carddav/datastore/sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql_external.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql_external.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,83 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+"""
+SQL backend for CardDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger
+
+from txdav.carddav.datastore.sql import AddressBookHome, AddressBook, \
+    AddressBookObject
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+    CommonObjectResourceExternal
+
+log = Logger()
+
+class AddressBookHomeExternal(CommonHomeExternal, AddressBookHome):
+
+    def __init__(self, transaction, ownerUID, resourceID):
+
+        AddressBookHome.__init__(self, transaction, ownerUID)
+        CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+    def hasAddressBookResourceUIDSomewhereElse(self, uid, ok_object, mode):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAddressBookResourcesForUID(self, uid):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def createdHome(self):
+        """
+        No children - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def addressbook(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class AddressBookExternal(CommonHomeChildExternal, AddressBook):
+    """
+    SQL-based implementation of L{IAddressBook}.
+    """
+    pass
+
+
+
+class AddressBookObjectExternal(CommonObjectResourceExternal, AddressBookObject):
+    """
+    SQL-based implementation of L{IAddressBookObject}.
+    """
+    pass
+
+AddressBookExternal._objectResourceClass = AddressBookObjectExternal

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -189,12 +189,12 @@
         self.db.deleteResource("data3.vcf")
 
         tests = (
-            (0, (["data1.vcf", "data2.vcf", ], [],)),
-            (1, (["data2.vcf", ], ["data3.vcf", ],)),
-            (2, ([], ["data3.vcf", ],)),
-            (3, ([], ["data3.vcf", ],)),
-            (4, ([], [],)),
-            (5, ([], [],)),
+            (0, (["data1.vcf", "data2.vcf", ], [], [],)),
+            (1, (["data2.vcf", ], ["data3.vcf", ], [],)),
+            (2, ([], ["data3.vcf", ], [],)),
+            (3, ([], ["data3.vcf", ], [],)),
+            (4, ([], [], [],)),
+            (5, ([], [], [],)),
         )
 
         for revision, results in tests:

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -41,7 +41,7 @@
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from txdav.common.datastore.sql import EADDRESSBOOKTYPE, CommonObjectResource
 from txdav.common.datastore.sql_tables import  _ABO_KIND_PERSON, _ABO_KIND_GROUP, schema
-from txdav.common.datastore.test.util import buildStore
+from txdav.common.datastore.test.util import buildStore, cleanStore
 from txdav.carddav.datastore.sql import AddressBook
 
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
@@ -56,7 +56,22 @@
     @inlineCallbacks
     def setUp(self):
         yield super(AddressBookSQLStorageTests, self).setUp()
-        self._sqlStore = yield buildStore(self, self.notifierFactory)
+        self._sqlStore = yield buildStore(
+            self,
+            self.notifierFactory,
+            homes=(
+                "home1",
+                "home2",
+                "home3",
+                "home_bad",
+                "home_empty",
+                "homeNew",
+                "new-home",
+                "uid1",
+                "uid2",
+                "xyzzy",
+            )
+        )
         yield self.populate()
 
 
@@ -289,7 +304,7 @@
         Test that two concurrent attempts to PUT different address book object resources to the
         same address book home does not cause a deadlock.
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
 
         # Provision the home and addressbook now
         txn = addressbookStore.newTransaction()
@@ -395,7 +410,7 @@
         """
         Test that kind property UID is stored correctly in database
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
 
         # Provision the home and addressbook, one user and one group
         txn = addressbookStore.newTransaction()
@@ -441,7 +456,7 @@
         """
         Test that kind property vCard is stored correctly in database
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
 
         # Provision the home and addressbook, one user and one group
         txn = addressbookStore.newTransaction()
@@ -532,7 +547,8 @@
         """
         Test that kind property vCard is stored correctly in database
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
+        cleanStore(self, addressbookStore)
 
         # Provision the home and addressbook, one user and one group
         txn = addressbookStore.newTransaction()

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_sql_sharing.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1134,23 +1134,27 @@
         otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
         self.assertNotEqual(otherAB._bindRevision, 0)
 
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(0)
+        changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(0)
         self.assertNotEqual(len(changed), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
 
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
+        changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
         self.assertEqual(len(changed), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
 
         otherHome = yield self.addressbookHomeUnderTest(name="user02")
         for depth in ("1", "infinity",):
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(0, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(0, depth)
             self.assertNotEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
             self.assertEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
 
     @inlineCallbacks
@@ -1166,13 +1170,15 @@
         otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
         self.assertNotEqual(otherAB._bindRevision, 0)
 
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(0)
+        changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(0)
         self.assertEqual(set(changed), set(['card1.vcf', 'card2.vcf', 'group1.vcf']))
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
 
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
+        changed, deleted, invalid = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
         self.assertEqual(len(changed), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(invalid), 0)
 
         for depth, result in (
             ("1", ['addressbook/',
@@ -1184,13 +1190,15 @@
                              'user01/card2.vcf',
                              'user01/group1.vcf']
              )):
-            changed, deleted = yield otherAB.viewerHome().resourceNamesSinceRevision(0, depth)
+            changed, deleted, invalid = yield otherAB.viewerHome().resourceNamesSinceRevision(0, depth)
             self.assertEqual(set(changed), set(result))
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
-            changed, deleted = yield otherAB.viewerHome().resourceNamesSinceRevision(otherAB._bindRevision, depth)
+            changed, deleted, invalid = yield otherAB.viewerHome().resourceNamesSinceRevision(otherAB._bindRevision, depth)
             self.assertEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
 
     @inlineCallbacks

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -852,7 +852,8 @@
     def resourceNamesSinceToken(self, token, depth):
         deleted = []
         changed = []
-        return succeed((changed, deleted))
+        invalid = []
+        return succeed((changed, deleted, invalid))
 
 
     # @cached

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/common/datastore/podding/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/conduit.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,845 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twext.python.log import Logger
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from txdav.common.datastore.podding.request import ConduitRequest
-from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-from txdav.common.icommondatastore import ExternalShareFailed
-from twisted.python.reflect import namedClass
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
-from twistedcaldav.caldavxml import TimeRange
-
-
-__all__ = [
-    "PoddingConduitResource",
-]
-
-log = Logger()
-
-
-class FailedCrossPodRequestError(RuntimeError):
-    """
-    Request returned an error.
-    """
-    pass
-
-
-
-class PoddingConduit(object):
-    """
-    This class is the API/RPC bridge between cross-pod requests and the store.
-
-    Each cross-pod request/response is described by a Python C{dict} that is serialized
-    to JSON for the HTTP request/response.
-
-    Each request C{dict} has an "action" key that indicates what call is being made, and
-    the other keys are arguments to that call.
-
-    Each response C{dict} has a "result" key that indicates the call result, and other
-    optional keys for any values returned by the call.
-
-    The conduit provides two methods for each action: one for the sending side and one for
-    the receiving side, called "send_{action}" and "recv_{action}", respectively, where
-    {action} is the action value.
-
-    The "send_{action}" calls each have a set of arguments specific to the call itself. The
-    code takes care of packing that into a C{dict} and sending to the appropriate pod.
-
-    The "recv_{action}" calls take a single C{dict} argument that is the deserialized JSON
-    data from the incoming request. The return value is a C{dict} with the result.
-
-    Some simple forms of send_/recv_ methods can be auto-generated to simplify coding.
-
-    Right now this conduit is used for cross-pod sharing operations. In the future we will
-    likely use it for cross-pod migration.
-    """
-
-    conduitRequestClass = ConduitRequest
-
-    def __init__(self, store):
-        """
-        @param store: the L{CommonDataStore} in use.
-        """
-        self.store = store
-
-
-    def validRequst(self, source_guid, destination_guid):
-        """
-        Verify that the specified GUIDs are valid for the request and return the
-        matching directory records.
-
-        @param source_guid: GUID for the user on whose behalf the request is being made
-        @type source_guid: C{str}
-        @param destination_guid: GUID for the user to whom the request is being sent
-        @type destination_guid: C{str}
-
-        @return: C{tuple} of L{IStoreDirectoryRecord}
-        """
-
-        source = self.store.directoryService().recordWithUID(source_guid)
-        if source is None:
-            raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_guid))
-        if not source.thisServer():
-            raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
-
-        destination = self.store.directoryService().recordWithUID(destination_guid)
-        if destination is None:
-            raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_guid))
-        if destination.thisServer():
-            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
-
-        return (source, destination,)
-
-
-    @inlineCallbacks
-    def sendRequest(self, txn, recipient, data, stream=None, streamType=None):
-
-        request = self.conduitRequestClass(recipient.server(), data, stream, streamType)
-        try:
-            response = (yield request.doRequest(txn))
-        except Exception as e:
-            raise FailedCrossPodRequestError("Failed cross-pod request: {}".format(e))
-        returnValue(response)
-
-
-    @inlineCallbacks
-    def processRequest(self, data):
-        """
-        Process the request.
-
-        @param data: the JSON data to process
-        @type data: C{dict}
-        """
-        # Must have a dict with an "action" key
-        try:
-            action = data["action"]
-        except (KeyError, TypeError) as e:
-            log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=data)
-            raise FailedCrossPodRequestError("JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, data,))
-
-        if action == "ping":
-            result = {"result": "ok"}
-            returnValue(result)
-
-        method = "recv_{}".format(action.replace("-", "_"))
-        if not hasattr(self, method):
-            log.error("Unsupported action: {action}", action=action)
-            raise FailedCrossPodRequestError("Unsupported action: {}".format(action))
-
-        # Need a transaction to work with
-        txn = self.store.newTransaction(repr("Conduit request"))
-
-        # Do the actual request processing
-        try:
-            result = (yield getattr(self, method)(txn, data))
-        except Exception as e:
-            yield txn.abort()
-            log.error("Failed action: {action}, {ex}", action=action, ex=e)
-            raise FailedCrossPodRequestError("Failed action: {}, {}".format(action, e))
-
-        yield txn.commit()
-
-        returnValue(result)
-
-
-    #
-    # Invite related apis
-    #
-
-    @inlineCallbacks
-    def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, copy_properties, supported_components):
-        """
-        Send a sharing invite cross-pod message.
-
-        @param homeType: Type of home being shared.
-        @type homeType: C{int}
-        @param ownerUID: GUID of the sharer.
-        @type ownerUID: C{str}
-        @param ownerID: resource ID of the sharer calendar
-        @type ownerID: C{int}
-        @param ownerName: owner's name of the sharer calendar
-        @type ownerName: C{str}
-        @param shareeUID: GUID of the sharee
-        @type shareeUID: C{str}
-        @param shareUID: Resource/invite ID for sharee
-        @type shareUID: C{str}
-        @param bindMode: bind mode for the share
-        @type bindMode: C{str}
-        @param summary: sharing message
-        @type summary: C{str}
-        @param copy_properties: C{str} name/value for properties to be copied
-        @type copy_properties: C{dict}
-        @param supported_components: supproted components, may be C{None}
-        @type supported_components: C{str}
-        """
-
-        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
-
-        action = {
-            "action": "shareinvite",
-            "type": homeType,
-            "owner": ownerUID,
-            "owner_id": ownerID,
-            "owner_name": ownerName,
-            "sharee": shareeUID,
-            "share_id": shareUID,
-            "mode": bindMode,
-            "summary": summary,
-            "properties": copy_properties,
-        }
-        if supported_components is not None:
-            action["supported-components"] = supported_components
-
-        result = yield self.sendRequest(txn, recipient, action)
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def recv_shareinvite(self, txn, message):
-        """
-        Process a sharing invite cross-pod message. Message arguments as per L{send_shareinvite}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != "shareinvite":
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
-
-        # Create a share
-        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
-        if shareeHome is None or shareeHome.external():
-            raise FailedCrossPodRequestError("Invalid sharee UID specified")
-
-        try:
-            yield shareeHome.processExternalInvite(
-                message["owner"],
-                message["owner_id"],
-                message["owner_name"],
-                message["share_id"],
-                message["mode"],
-                message["summary"],
-                message["properties"],
-                supported_components=message.get("supported-components")
-            )
-        except ExternalShareFailed as e:
-            raise FailedCrossPodRequestError(str(e))
-
-        returnValue({
-            "result": "ok",
-        })
-
-
-    @inlineCallbacks
-    def send_shareuninvite(self, txn, homeType, ownerUID, ownerID, shareeUID, shareUID):
-        """
-        Send a sharing uninvite cross-pod message.
-
-        @param homeType: Type of home being shared.
-        @type homeType: C{int}
-        @param ownerUID: GUID of the sharer.
-        @type ownerUID: C{str}
-        @param ownerID: resource ID of the sharer calendar
-        @type ownerID: C{int}
-        @param shareeUID: GUID of the sharee
-        @type shareeUID: C{str}
-        @param shareUID: Resource/invite ID for sharee
-        @type shareUID: C{str}
-        """
-
-        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
-
-        action = {
-            "action": "shareuninvite",
-            "type": homeType,
-            "owner": ownerUID,
-            "owner_id": ownerID,
-            "sharee": shareeUID,
-            "share_id": shareUID,
-        }
-
-        result = yield self.sendRequest(txn, recipient, action)
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def recv_shareuninvite(self, txn, message):
-        """
-        Process a sharing uninvite cross-pod message. Message arguments as per L{send_shareuninvite}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != "shareuninvite":
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
-
-        # Create a share
-        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
-        if shareeHome is None or shareeHome.external():
-            FailedCrossPodRequestError("Invalid sharee UID specified")
-
-        try:
-            yield shareeHome.processExternalUninvite(
-                message["owner"],
-                message["owner_id"],
-                message["share_id"],
-            )
-        except ExternalShareFailed as e:
-            FailedCrossPodRequestError(str(e))
-
-        returnValue({
-            "result": "ok",
-        })
-
-
-    @inlineCallbacks
-    def send_sharereply(self, txn, homeType, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
-        """
-        Send a sharing reply cross-pod message.
-
-        @param homeType: Type of home being shared.
-        @type homeType: C{int}
-        @param ownerUID: GUID of the sharer.
-        @type ownerUID: C{str}
-        @param shareeUID: GUID of the recipient
-        @type shareeUID: C{str}
-        @param shareUID: Resource/invite ID for recipient
-        @type shareUID: C{str}
-        @param bindStatus: bind mode for the share
-        @type bindStatus: C{str}
-        @param summary: sharing message
-        @type summary: C{str}
-        """
-
-        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
-
-        action = {
-            "action": "sharereply",
-            "type": homeType,
-            "owner": ownerUID,
-            "sharee": shareeUID,
-            "share_id": shareUID,
-            "status": bindStatus,
-        }
-        if summary is not None:
-            action["summary"] = summary
-
-        result = yield self.sendRequest(txn, recipient, action)
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def recv_sharereply(self, txn, message):
-        """
-        Process a sharing reply cross-pod message. Message arguments as per L{send_sharereply}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != "sharereply":
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(message["action"]))
-
-        # Create a share
-        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
-        if ownerHome is None or ownerHome.external():
-            FailedCrossPodRequestError("Invalid owner UID specified")
-
-        try:
-            yield ownerHome.processExternalReply(
-                message["owner"],
-                message["sharee"],
-                message["share_id"],
-                message["status"],
-                summary=message.get("summary")
-            )
-        except ExternalShareFailed as e:
-            FailedCrossPodRequestError(str(e))
-
-        returnValue({
-            "result": "ok",
-        })
-
-
-    #
-    # Managed attachment related apis
-    #
-
-    @inlineCallbacks
-    def send_add_attachment(self, objectResource, rids, content_type, filename, stream):
-        """
-        Managed attachment addAttachment call.
-
-        @param objectResource: child resource having an attachment added
-        @type objectResource: L{CalendarObject}
-        @param rids: list of recurrence ids
-        @type rids: C{list}
-        @param content_type: content type of attachment data
-        @type content_type: L{MimeType}
-        @param filename: name of attachment
-        @type filename: C{str}
-        @param stream: attachment data stream
-        @type stream: L{IStream}
-        """
-
-        actionName = "add-attachment"
-        shareeView = objectResource._parentCollection
-        action, recipient = self._send(actionName, shareeView, objectResource)
-        action["rids"] = rids
-        action["filename"] = filename
-        result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
-        if result["result"] == "ok":
-            returnValue(result["value"])
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_add_attachment(self, txn, message):
-        """
-        Process an addAttachment cross-pod message. Message arguments as per L{send_add_attachment}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        actionName = "add-attachment"
-        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            attachment, location = yield objectResource.addAttachment(
-                message["rids"],
-                message["streamType"],
-                message["filename"],
-                message["stream"],
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": (attachment.managedID(), location,),
-        })
-
-
-    @inlineCallbacks
-    def send_update_attachment(self, objectResource, managed_id, content_type, filename, stream):
-        """
-        Managed attachment updateAttachment call.
-
-        @param objectResource: child resource having an attachment added
-        @type objectResource: L{CalendarObject}
-        @param managed_id: managed-id to update
-        @type managed_id: C{str}
-        @param content_type: content type of attachment data
-        @type content_type: L{MimeType}
-        @param filename: name of attachment
-        @type filename: C{str}
-        @param stream: attachment data stream
-        @type stream: L{IStream}
-        """
-
-        actionName = "update-attachment"
-        shareeView = objectResource._parentCollection
-        action, recipient = self._send(actionName, shareeView, objectResource)
-        action["managedID"] = managed_id
-        action["filename"] = filename
-        result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
-        if result["result"] == "ok":
-            returnValue(result["value"])
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_update_attachment(self, txn, message):
-        """
-        Process an updateAttachment cross-pod message. Message arguments as per L{send_update_attachment}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        actionName = "update-attachment"
-        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            attachment, location = yield objectResource.updateAttachment(
-                message["managedID"],
-                message["streamType"],
-                message["filename"],
-                message["stream"],
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": (attachment.managedID(), location,),
-        })
-
-
-    @inlineCallbacks
-    def send_remove_attachment(self, objectResource, rids, managed_id):
-        """
-        Managed attachment removeAttachment call.
-
-        @param objectResource: child resource having an attachment added
-        @type objectResource: L{CalendarObject}
-        @param rids: list of recurrence ids
-        @type rids: C{list}
-        @param managed_id: managed-id to update
-        @type managed_id: C{str}
-        """
-
-        actionName = "remove-attachment"
-        shareeView = objectResource._parentCollection
-        action, recipient = self._send(actionName, shareeView, objectResource)
-        action["rids"] = rids
-        action["managedID"] = managed_id
-        result = yield self.sendRequest(shareeView._txn, recipient, action)
-        if result["result"] == "ok":
-            returnValue(result["value"])
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_remove_attachment(self, txn, message):
-        """
-        Process an removeAttachment cross-pod message. Message arguments as per L{send_remove_attachment}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        actionName = "remove-attachment"
-        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            yield objectResource.removeAttachment(
-                message["rids"],
-                message["managedID"],
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": None,
-        })
-
-
-    #
-    # Sharer data access related apis
-    #
-
-    def _send(self, action, parent, child=None):
-        """
-        Base behavior for an operation on a L{CommonHomeChild}.
-
-        @param shareeView: sharee resource being operated on.
-        @type shareeView: L{CommonHomeChildExternal}
-        """
-
-        homeType = parent.ownerHome()._homeType
-        ownerUID = parent.ownerHome().uid()
-        ownerID = parent.external_id()
-        shareeUID = parent.viewerHome().uid()
-
-        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
-
-        result = {
-            "action": action,
-            "type": homeType,
-            "owner": ownerUID,
-            "owner_id": ownerID,
-            "sharee": shareeUID,
-        }
-        if child is not None:
-            result["resource_id"] = child.id()
-        return result, recipient
-
-
-    @inlineCallbacks
-    def _recv(self, txn, message, expected_action):
-        """
-        Base behavior for sharer data access.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != expected_action:
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
-
-        # Get a share
-        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
-        if ownerHome is None or ownerHome.external():
-            FailedCrossPodRequestError("Invalid owner UID specified")
-
-        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"])
-        if shareeHome is None or not shareeHome.external():
-            FailedCrossPodRequestError("Invalid sharee UID specified")
-
-        shareeView = yield shareeHome.childWithID(message["owner_id"])
-        if shareeView is None:
-            FailedCrossPodRequestError("Invalid shared resource specified")
-
-        resourceID = message.get("resource_id", None)
-        if resourceID is not None:
-            objectResource = yield shareeView.objectResourceWithID(resourceID)
-            if objectResource is None:
-                FailedCrossPodRequestError("Invalid owner shared object resource specified")
-        else:
-            objectResource = None
-
-        returnValue((shareeView, objectResource,))
-
-
-    #
-    # Simple calls are ones where there is no argument and a single return value. We can simplify
-    # code generation for these by dynamically generating the appropriate class methods.
-    #
-
-    @inlineCallbacks
-    def _simple_send(self, actionName, shareeView, objectResource=None, transform=None, args=None, kwargs=None):
-        """
-        A simple send operation that returns a value.
-
-        @param actionName: name of the action.
-        @type actionName: C{str}
-        @param shareeView: sharee resource being operated on.
-        @type shareeView: L{CommonHomeChildExternal}
-        @param objectResource: the resource being operated on, or C{None} for classmethod.
-        @type objectResource: L{CommonObjectResourceExternal}
-        @param transform: a function used to convert the JSON result into return values.
-        @type transform: C{callable}
-        @param args: list of optional arguments.
-        @type args: C{list}
-        @param kwargs: optional keyword arguments.
-        @type kwargs: C{dict}
-        """
-
-        action, recipient = self._send(actionName, shareeView, objectResource)
-        if args is not None:
-            action["arguments"] = args
-        if kwargs is not None:
-            action["keywords"] = kwargs
-        result = yield self.sendRequest(shareeView._txn, recipient, action)
-        if result["result"] == "ok":
-            returnValue(result["value"] if transform is None else transform(result["value"], shareeView, objectResource))
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def _simple_recv(self, txn, actionName, message, method, onHomeChild=True, transform=None):
-        """
-        A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
-        and include those only if present.
-
-        @param actionName: name of the action.
-        @type actionName: C{str}
-        @param message: message arguments
-        @type message: C{dict}
-        @param method: name of the method to execute on the shared resource to get the result.
-        @type method: C{str}
-        @param transform: method to call on returned JSON value to convert it to something useful.
-        @type transform: C{callable}
-        """
-
-        shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            if onHomeChild:
-                # Operate on the L{CommonHomeChild}
-                value = yield getattr(shareeView, method)(*message.get("arguments", ()), **message.get("keywords", {}))
-            else:
-                # Operate on the L{CommonObjectResource}
-                if objectResource is not None:
-                    value = yield getattr(objectResource, method)(*message.get("arguments", ()), **message.get("keywords", {}))
-                else:
-                    # classmethod call
-                    value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *message.get("arguments", ()), **message.get("keywords", {}))
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": transform(value, shareeView, objectResource) if transform is not None else value,
-        })
-
-
-    @inlineCallbacks
-    def send_freebusy(
-        self,
-        calresource,
-        timerange,
-        matchtotal,
-        excludeuid,
-        organizer,
-        organizerPrincipal,
-        same_calendar_user,
-        servertoserver,
-        event_details,
-    ):
-        action, recipient = self._send("freebusy", calresource)
-        action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
-        action["matchtotal"] = matchtotal
-        action["excludeuid"] = excludeuid
-        action["organizer"] = organizer
-        action["organizerPrincipal"] = organizerPrincipal
-        action["same_calendar_user"] = same_calendar_user
-        action["servertoserver"] = servertoserver
-        action["event_details"] = event_details
-        result = yield self.sendRequest(calresource._txn, recipient, action)
-        if result["result"] == "ok":
-            returnValue((result["fbresults"], result["matchtotal"],))
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_freebusy(self, txn, message):
-        """
-        Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
-        try:
-            # Operate on the L{CommonHomeChild}
-            fbinfo = [[], [], []]
-            matchtotal = yield generateFreeBusyInfo(
-                shareeView,
-                fbinfo,
-                TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
-                message["matchtotal"],
-                message["excludeuid"],
-                message["organizer"],
-                message["organizerPrincipal"],
-                message["same_calendar_user"],
-                message["servertoserver"],
-                message["event_details"],
-                logItems=None
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        for i in range(3):
-            for j in range(len(fbinfo[i])):
-                fbinfo[i][j] = fbinfo[i][j].getText()
-
-        returnValue({
-            "result": "ok",
-            "fbresults": fbinfo,
-            "matchtotal": matchtotal,
-        })
-
-
-    @staticmethod
-    def _to_tuple(value, shareeView, objectResource):
-        return tuple(value)
-
-
-    @staticmethod
-    def _to_string(value, shareeView, objectResource):
-        return str(value)
-
-
-    @staticmethod
-    def _to_externalize(value, shareeView, objectResource):
-        if isinstance(value, shareeView._objectResourceClass):
-            value = value.externalize()
-        elif value is not None:
-            value = [v.externalize() for v in value]
-        return value
-
-
-    @classmethod
-    def _make_simple_homechild_action(cls, action, method, transform_recv=None, transform_send=None):
-        setattr(
-            cls,
-            "send_{}".format(action),
-            lambda self, shareeView, *args, **kwargs:
-                self._simple_send(action, shareeView, transform=transform_send, args=args, kwargs=kwargs)
-        )
-        setattr(
-            cls,
-            "recv_{}".format(action),
-            lambda self, txn, message:
-                self._simple_recv(txn, action, message, method, transform=transform_recv)
-        )
-
-
-    @classmethod
-    def _make_simple_object_action(cls, action, method, transform_recv=None, transform_send=None):
-        setattr(
-            cls,
-            "send_{}".format(action),
-            lambda self, shareeView, objectResource, *args, **kwargs:
-                self._simple_send(action, shareeView, objectResource, transform=transform_send, args=args, kwargs=kwargs)
-        )
-        setattr(
-            cls,
-            "recv_{}".format(action),
-            lambda self, txn, message:
-                self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_recv)
-        )
-
-
-# Calls on L{CommonHomeChild} objects
-PoddingConduit._make_simple_homechild_action("countobjects", "countObjectResources")
-PoddingConduit._make_simple_homechild_action("listobjects", "listObjectResources")
-PoddingConduit._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
-PoddingConduit._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
-PoddingConduit._make_simple_homechild_action("movehere", "moveObjectResourceHere")
-PoddingConduit._make_simple_homechild_action("moveaway", "moveObjectResourceAway")
-PoddingConduit._make_simple_homechild_action("synctoken", "syncToken")
-PoddingConduit._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision", transform_send=PoddingConduit._to_tuple)
-PoddingConduit._make_simple_homechild_action("search", "search")
-
-# Calls on L{CommonObjectResource} objects
-PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("create", "create", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("setcomponent", "setComponent")
-PoddingConduit._make_simple_object_action("component", "component", transform_recv=PoddingConduit._to_string)
-PoddingConduit._make_simple_object_action("remove", "remove")

Copied: CalendarServer/trunk/txdav/common/datastore/podding/conduit.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/conduit.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/conduit.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,846 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twext.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.reflect import namedClass
+
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from txdav.common.datastore.podding.request import ConduitRequest
+from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+from txdav.common.icommondatastore import ExternalShareFailed
+
+from twistedcaldav.caldavxml import TimeRange
+
+
+__all__ = [
+    "PoddingConduitResource",
+]
+
+log = Logger()
+
+
+class FailedCrossPodRequestError(RuntimeError):
+    """
+    Request returned an error.
+    """
+    pass
+
+
+
+class PoddingConduit(object):
+    """
+    This class is the API/RPC bridge between cross-pod requests and the store.
+
+    Each cross-pod request/response is described by a Python C{dict} that is serialized
+    to JSON for the HTTP request/response.
+
+    Each request C{dict} has an "action" key that indicates what call is being made, and
+    the other keys are arguments to that call.
+
+    Each response C{dict} has a "result" key that indicates the call result, and other
+    optional keys for any values returned by the call.
+
+    The conduit provides two methods for each action: one for the sending side and one for
+    the receiving side, called "send_{action}" and "recv_{action}", respectively, where
+    {action} is the action value.
+
+    The "send_{action}" calls each have a set of arguments specific to the call itself. The
+    code takes care of packing that into a C{dict} and sending to the appropriate pod.
+
+    The "recv_{action}" calls take a single C{dict} argument that is the deserialized JSON
+    data from the incoming request. The return value is a C{dict} with the result.
+
+    Some simple forms of send_/recv_ methods can be auto-generated to simplify coding.
+
+    Right now this conduit is used for cross-pod sharing operations. In the future we will
+    likely use it for cross-pod migration.
+    """
+
+    conduitRequestClass = ConduitRequest
+
+    def __init__(self, store):
+        """
+        @param store: the L{CommonDataStore} in use.
+        """
+        self.store = store
+
+
+    def validRequst(self, source_guid, destination_guid):
+        """
+        Verify that the specified GUIDs are valid for the request and return the
+        matching directory records.
+
+        @param source_guid: GUID for the user on whose behalf the request is being made
+        @type source_guid: C{str}
+        @param destination_guid: GUID for the user to whom the request is being sent
+        @type destination_guid: C{str}
+
+        @return: C{tuple} of L{IStoreDirectoryRecord}
+        """
+
+        source = self.store.directoryService().recordWithUID(source_guid)
+        if source is None:
+            raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_guid))
+        if not source.thisServer():
+            raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
+
+        destination = self.store.directoryService().recordWithUID(destination_guid)
+        if destination is None:
+            raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_guid))
+        if destination.thisServer():
+            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
+
+        return (source, destination,)
+
+
+    @inlineCallbacks
+    def sendRequest(self, txn, recipient, data, stream=None, streamType=None):
+
+        request = self.conduitRequestClass(recipient.server(), data, stream, streamType)
+        try:
+            response = (yield request.doRequest(txn))
+        except Exception as e:
+            raise FailedCrossPodRequestError("Failed cross-pod request: {}".format(e))
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def processRequest(self, data):
+        """
+        Process the request.
+
+        @param data: the JSON data to process
+        @type data: C{dict}
+        """
+        # Must have a dict with an "action" key
+        try:
+            action = data["action"]
+        except (KeyError, TypeError) as e:
+            log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=data)
+            raise FailedCrossPodRequestError("JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, data,))
+
+        if action == "ping":
+            result = {"result": "ok"}
+            returnValue(result)
+
+        method = "recv_{}".format(action.replace("-", "_"))
+        if not hasattr(self, method):
+            log.error("Unsupported action: {action}", action=action)
+            raise FailedCrossPodRequestError("Unsupported action: {}".format(action))
+
+        # Need a transaction to work with
+        txn = self.store.newTransaction(repr("Conduit request"))
+
+        # Do the actual request processing
+        try:
+            result = (yield getattr(self, method)(txn, data))
+        except Exception as e:
+            yield txn.abort()
+            log.error("Failed action: {action}, {ex}", action=action, ex=e)
+            raise FailedCrossPodRequestError("Failed action: {}, {}".format(action, e))
+
+        yield txn.commit()
+
+        returnValue(result)
+
+
+    #
+    # Invite related apis
+    #
+
+    @inlineCallbacks
+    def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, copy_properties, supported_components):
+        """
+        Send a sharing invite cross-pod message.
+
+        @param homeType: Type of home being shared.
+        @type homeType: C{int}
+        @param ownerUID: GUID of the sharer.
+        @type ownerUID: C{str}
+        @param ownerID: resource ID of the sharer calendar
+        @type ownerID: C{int}
+        @param ownerName: owner's name of the sharer calendar
+        @type ownerName: C{str}
+        @param shareeUID: GUID of the sharee
+        @type shareeUID: C{str}
+        @param shareUID: Resource/invite ID for sharee
+        @type shareUID: C{str}
+        @param bindMode: bind mode for the share
+        @type bindMode: C{str}
+        @param summary: sharing message
+        @type summary: C{str}
+        @param copy_properties: C{str} name/value for properties to be copied
+        @type copy_properties: C{dict}
+        @param supported_components: supproted components, may be C{None}
+        @type supported_components: C{str}
+        """
+
+        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
+
+        action = {
+            "action": "shareinvite",
+            "type": homeType,
+            "owner": ownerUID,
+            "owner_id": ownerID,
+            "owner_name": ownerName,
+            "sharee": shareeUID,
+            "share_id": shareUID,
+            "mode": bindMode,
+            "summary": summary,
+            "properties": copy_properties,
+        }
+        if supported_components is not None:
+            action["supported-components"] = supported_components
+
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_shareinvite(self, txn, message):
+        """
+        Process a sharing invite cross-pod message. Message arguments as per L{send_shareinvite}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        if message["action"] != "shareinvite":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
+
+        # Create a share
+        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
+        if shareeHome is None or shareeHome.external():
+            raise FailedCrossPodRequestError("Invalid sharee UID specified")
+
+        try:
+            yield shareeHome.processExternalInvite(
+                message["owner"],
+                message["owner_id"],
+                message["owner_name"],
+                message["share_id"],
+                message["mode"],
+                message["summary"],
+                message["properties"],
+                supported_components=message.get("supported-components")
+            )
+        except ExternalShareFailed as e:
+            raise FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            "result": "ok",
+        })
+
+
+    @inlineCallbacks
+    def send_shareuninvite(self, txn, homeType, ownerUID, ownerID, shareeUID, shareUID):
+        """
+        Send a sharing uninvite cross-pod message.
+
+        @param homeType: Type of home being shared.
+        @type homeType: C{int}
+        @param ownerUID: GUID of the sharer.
+        @type ownerUID: C{str}
+        @param ownerID: resource ID of the sharer calendar
+        @type ownerID: C{int}
+        @param shareeUID: GUID of the sharee
+        @type shareeUID: C{str}
+        @param shareUID: Resource/invite ID for sharee
+        @type shareUID: C{str}
+        """
+
+        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
+
+        action = {
+            "action": "shareuninvite",
+            "type": homeType,
+            "owner": ownerUID,
+            "owner_id": ownerID,
+            "sharee": shareeUID,
+            "share_id": shareUID,
+        }
+
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_shareuninvite(self, txn, message):
+        """
+        Process a sharing uninvite cross-pod message. Message arguments as per L{send_shareuninvite}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        if message["action"] != "shareuninvite":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
+
+        # Create a share
+        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
+        if shareeHome is None or shareeHome.external():
+            FailedCrossPodRequestError("Invalid sharee UID specified")
+
+        try:
+            yield shareeHome.processExternalUninvite(
+                message["owner"],
+                message["owner_id"],
+                message["share_id"],
+            )
+        except ExternalShareFailed as e:
+            FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            "result": "ok",
+        })
+
+
+    @inlineCallbacks
+    def send_sharereply(self, txn, homeType, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
+        """
+        Send a sharing reply cross-pod message.
+
+        @param homeType: Type of home being shared.
+        @type homeType: C{int}
+        @param ownerUID: GUID of the sharer.
+        @type ownerUID: C{str}
+        @param shareeUID: GUID of the recipient
+        @type shareeUID: C{str}
+        @param shareUID: Resource/invite ID for recipient
+        @type shareUID: C{str}
+        @param bindStatus: bind mode for the share
+        @type bindStatus: C{str}
+        @param summary: sharing message
+        @type summary: C{str}
+        """
+
+        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+        action = {
+            "action": "sharereply",
+            "type": homeType,
+            "owner": ownerUID,
+            "sharee": shareeUID,
+            "share_id": shareUID,
+            "status": bindStatus,
+        }
+        if summary is not None:
+            action["summary"] = summary
+
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_sharereply(self, txn, message):
+        """
+        Process a sharing reply cross-pod message. Message arguments as per L{send_sharereply}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        if message["action"] != "sharereply":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(message["action"]))
+
+        # Create a share
+        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
+        if ownerHome is None or ownerHome.external():
+            FailedCrossPodRequestError("Invalid owner UID specified")
+
+        try:
+            yield ownerHome.processExternalReply(
+                message["owner"],
+                message["sharee"],
+                message["share_id"],
+                message["status"],
+                summary=message.get("summary")
+            )
+        except ExternalShareFailed as e:
+            FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            "result": "ok",
+        })
+
+
+    #
+    # Managed attachment related apis
+    #
+
+    @inlineCallbacks
+    def send_add_attachment(self, objectResource, rids, content_type, filename, stream):
+        """
+        Managed attachment addAttachment call.
+
+        @param objectResource: child resource having an attachment added
+        @type objectResource: L{CalendarObject}
+        @param rids: list of recurrence ids
+        @type rids: C{list}
+        @param content_type: content type of attachment data
+        @type content_type: L{MimeType}
+        @param filename: name of attachment
+        @type filename: C{str}
+        @param stream: attachment data stream
+        @type stream: L{IStream}
+        """
+
+        actionName = "add-attachment"
+        shareeView = objectResource._parentCollection
+        action, recipient = self._send(actionName, shareeView, objectResource)
+        action["rids"] = rids
+        action["filename"] = filename
+        result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
+        if result["result"] == "ok":
+            returnValue(result["value"])
+        elif result["result"] == "exception":
+            raise namedClass(result["class"])(result["message"])
+
+
+    @inlineCallbacks
+    def recv_add_attachment(self, txn, message):
+        """
+        Process an addAttachment cross-pod message. Message arguments as per L{send_add_attachment}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        actionName = "add-attachment"
+        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
+        try:
+            attachment, location = yield objectResource.addAttachment(
+                message["rids"],
+                message["streamType"],
+                message["filename"],
+                message["stream"],
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "message": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": (attachment.managedID(), location,),
+        })
+
+
+    @inlineCallbacks
+    def send_update_attachment(self, objectResource, managed_id, content_type, filename, stream):
+        """
+        Managed attachment updateAttachment call.
+
+        @param objectResource: child resource having an attachment added
+        @type objectResource: L{CalendarObject}
+        @param managed_id: managed-id to update
+        @type managed_id: C{str}
+        @param content_type: content type of attachment data
+        @type content_type: L{MimeType}
+        @param filename: name of attachment
+        @type filename: C{str}
+        @param stream: attachment data stream
+        @type stream: L{IStream}
+        """
+
+        actionName = "update-attachment"
+        shareeView = objectResource._parentCollection
+        action, recipient = self._send(actionName, shareeView, objectResource)
+        action["managedID"] = managed_id
+        action["filename"] = filename
+        result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
+        if result["result"] == "ok":
+            returnValue(result["value"])
+        elif result["result"] == "exception":
+            raise namedClass(result["class"])(result["message"])
+
+
+    @inlineCallbacks
+    def recv_update_attachment(self, txn, message):
+        """
+        Process an updateAttachment cross-pod message. Message arguments as per L{send_update_attachment}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        actionName = "update-attachment"
+        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
+        try:
+            attachment, location = yield objectResource.updateAttachment(
+                message["managedID"],
+                message["streamType"],
+                message["filename"],
+                message["stream"],
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "message": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": (attachment.managedID(), location,),
+        })
+
+
+    @inlineCallbacks
+    def send_remove_attachment(self, objectResource, rids, managed_id):
+        """
+        Managed attachment removeAttachment call.
+
+        @param objectResource: child resource having an attachment added
+        @type objectResource: L{CalendarObject}
+        @param rids: list of recurrence ids
+        @type rids: C{list}
+        @param managed_id: managed-id to update
+        @type managed_id: C{str}
+        """
+
+        actionName = "remove-attachment"
+        shareeView = objectResource._parentCollection
+        action, recipient = self._send(actionName, shareeView, objectResource)
+        action["rids"] = rids
+        action["managedID"] = managed_id
+        result = yield self.sendRequest(shareeView._txn, recipient, action)
+        if result["result"] == "ok":
+            returnValue(result["value"])
+        elif result["result"] == "exception":
+            raise namedClass(result["class"])(result["message"])
+
+
+    @inlineCallbacks
+    def recv_remove_attachment(self, txn, message):
+        """
+        Process an removeAttachment cross-pod message. Message arguments as per L{send_remove_attachment}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        actionName = "remove-attachment"
+        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
+        try:
+            yield objectResource.removeAttachment(
+                message["rids"],
+                message["managedID"],
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "message": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": None,
+        })
+
+
+    #
+    # Sharer data access related apis
+    #
+
+    def _send(self, action, parent, child=None):
+        """
+        Base behavior for an operation on a L{CommonHomeChild}.
+
+        @param shareeView: sharee resource being operated on.
+        @type shareeView: L{CommonHomeChildExternal}
+        """
+
+        homeType = parent.ownerHome()._homeType
+        ownerUID = parent.ownerHome().uid()
+        ownerID = parent.external_id()
+        shareeUID = parent.viewerHome().uid()
+
+        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+        result = {
+            "action": action,
+            "type": homeType,
+            "owner": ownerUID,
+            "owner_id": ownerID,
+            "sharee": shareeUID,
+        }
+        if child is not None:
+            result["resource_id"] = child.id()
+        return result, recipient
+
+
+    @inlineCallbacks
+    def _recv(self, txn, message, expected_action):
+        """
+        Base behavior for sharer data access.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        if message["action"] != expected_action:
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
+
+        # Get a share
+        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
+        if ownerHome is None or ownerHome.external():
+            FailedCrossPodRequestError("Invalid owner UID specified")
+
+        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"])
+        if shareeHome is None or not shareeHome.external():
+            FailedCrossPodRequestError("Invalid sharee UID specified")
+
+        shareeView = yield shareeHome.childWithID(message["owner_id"])
+        if shareeView is None:
+            FailedCrossPodRequestError("Invalid shared resource specified")
+
+        resourceID = message.get("resource_id", None)
+        if resourceID is not None:
+            objectResource = yield shareeView.objectResourceWithID(resourceID)
+            if objectResource is None:
+                FailedCrossPodRequestError("Invalid owner shared object resource specified")
+        else:
+            objectResource = None
+
+        returnValue((shareeView, objectResource,))
+
+
+    #
+    # Simple calls are ones where there is no argument and a single return value. We can simplify
+    # code generation for these by dynamically generating the appropriate class methods.
+    #
+
+    @inlineCallbacks
+    def _simple_send(self, actionName, shareeView, objectResource=None, transform=None, args=None, kwargs=None):
+        """
+        A simple send operation that returns a value.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param shareeView: sharee resource being operated on.
+        @type shareeView: L{CommonHomeChildExternal}
+        @param objectResource: the resource being operated on, or C{None} for classmethod.
+        @type objectResource: L{CommonObjectResourceExternal}
+        @param transform: a function used to convert the JSON result into return values.
+        @type transform: C{callable}
+        @param args: list of optional arguments.
+        @type args: C{list}
+        @param kwargs: optional keyword arguments.
+        @type kwargs: C{dict}
+        """
+
+        action, recipient = self._send(actionName, shareeView, objectResource)
+        if args is not None:
+            action["arguments"] = args
+        if kwargs is not None:
+            action["keywords"] = kwargs
+        result = yield self.sendRequest(shareeView._txn, recipient, action)
+        if result["result"] == "ok":
+            returnValue(result["value"] if transform is None else transform(result["value"], shareeView, objectResource))
+        elif result["result"] == "exception":
+            raise namedClass(result["class"])(result["message"])
+
+
+    @inlineCallbacks
+    def _simple_recv(self, txn, actionName, message, method, onHomeChild=True, transform=None):
+        """
+        A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
+        and include those only if present.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param message: message arguments
+        @type message: C{dict}
+        @param method: name of the method to execute on the shared resource to get the result.
+        @type method: C{str}
+        @param transform: method to call on returned JSON value to convert it to something useful.
+        @type transform: C{callable}
+        """
+
+        shareeView, objectResource = yield self._recv(txn, message, actionName)
+        try:
+            if onHomeChild:
+                # Operate on the L{CommonHomeChild}
+                value = yield getattr(shareeView, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+            else:
+                # Operate on the L{CommonObjectResource}
+                if objectResource is not None:
+                    value = yield getattr(objectResource, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+                else:
+                    # classmethod call
+                    value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *message.get("arguments", ()), **message.get("keywords", {}))
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "message": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": transform(value, shareeView, objectResource) if transform is not None else value,
+        })
+
+
+    @inlineCallbacks
+    def send_freebusy(
+        self,
+        calresource,
+        timerange,
+        matchtotal,
+        excludeuid,
+        organizer,
+        organizerPrincipal,
+        same_calendar_user,
+        servertoserver,
+        event_details,
+    ):
+        action, recipient = self._send("freebusy", calresource)
+        action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
+        action["matchtotal"] = matchtotal
+        action["excludeuid"] = excludeuid
+        action["organizer"] = organizer
+        action["organizerPrincipal"] = organizerPrincipal
+        action["same_calendar_user"] = same_calendar_user
+        action["servertoserver"] = servertoserver
+        action["event_details"] = event_details
+        result = yield self.sendRequest(calresource._txn, recipient, action)
+        if result["result"] == "ok":
+            returnValue((result["fbresults"], result["matchtotal"],))
+        elif result["result"] == "exception":
+            raise namedClass(result["class"])(result["message"])
+
+
+    @inlineCallbacks
+    def recv_freebusy(self, txn, message):
+        """
+        Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
+        try:
+            # Operate on the L{CommonHomeChild}
+            fbinfo = [[], [], []]
+            matchtotal = yield generateFreeBusyInfo(
+                shareeView,
+                fbinfo,
+                TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
+                message["matchtotal"],
+                message["excludeuid"],
+                message["organizer"],
+                message["organizerPrincipal"],
+                message["same_calendar_user"],
+                message["servertoserver"],
+                message["event_details"],
+                logItems=None
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "message": str(e),
+            })
+
+        for i in range(3):
+            for j in range(len(fbinfo[i])):
+                fbinfo[i][j] = fbinfo[i][j].getText()
+
+        returnValue({
+            "result": "ok",
+            "fbresults": fbinfo,
+            "matchtotal": matchtotal,
+        })
+
+
+    @staticmethod
+    def _to_tuple(value, shareeView, objectResource):
+        return tuple(value)
+
+
+    @staticmethod
+    def _to_string(value, shareeView, objectResource):
+        return str(value)
+
+
+    @staticmethod
+    def _to_externalize(value, shareeView, objectResource):
+        if isinstance(value, shareeView._objectResourceClass):
+            value = value.externalize()
+        elif value is not None:
+            value = [v.externalize() for v in value]
+        return value
+
+
+    @classmethod
+    def _make_simple_homechild_action(cls, action, method, transform_recv=None, transform_send=None):
+        setattr(
+            cls,
+            "send_{}".format(action),
+            lambda self, shareeView, *args, **kwargs:
+                self._simple_send(action, shareeView, transform=transform_send, args=args, kwargs=kwargs)
+        )
+        setattr(
+            cls,
+            "recv_{}".format(action),
+            lambda self, txn, message:
+                self._simple_recv(txn, action, message, method, transform=transform_recv)
+        )
+
+
+    @classmethod
+    def _make_simple_object_action(cls, action, method, transform_recv=None, transform_send=None):
+        setattr(
+            cls,
+            "send_{}".format(action),
+            lambda self, shareeView, objectResource, *args, **kwargs:
+                self._simple_send(action, shareeView, objectResource, transform=transform_send, args=args, kwargs=kwargs)
+        )
+        setattr(
+            cls,
+            "recv_{}".format(action),
+            lambda self, txn, message:
+                self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_recv)
+        )
+
+
+# Calls on L{CommonHomeChild} objects
+PoddingConduit._make_simple_homechild_action("countobjects", "countObjectResources")
+PoddingConduit._make_simple_homechild_action("listobjects", "listObjectResources")
+PoddingConduit._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
+PoddingConduit._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
+PoddingConduit._make_simple_homechild_action("movehere", "moveObjectResourceHere")
+PoddingConduit._make_simple_homechild_action("moveaway", "moveObjectResourceAway")
+PoddingConduit._make_simple_homechild_action("synctoken", "syncToken")
+PoddingConduit._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision", transform_send=PoddingConduit._to_tuple)
+PoddingConduit._make_simple_homechild_action("search", "search")
+
+# Calls on L{CommonObjectResource} objects
+PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("create", "create", transform_recv=PoddingConduit._to_externalize)
+PoddingConduit._make_simple_object_action("setcomponent", "setComponent")
+PoddingConduit._make_simple_object_action("component", "component", transform_recv=PoddingConduit._to_string)
+PoddingConduit._make_simple_object_action("remove", "remove")

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/request.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/request.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,191 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from calendarserver.version import version
-
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.python.log import Logger
-from twext.web2 import responsecode
-from twext.web2.client.http import HTTPClientProtocol, ClientRequest
-from twext.web2.dav.util import allDataFromStream
-from twext.web2.http_headers import Headers, MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.protocol import Factory
-
-from twistedcaldav.accounting import accountingEnabledForCategory, \
-    emitAccounting
-from twistedcaldav.client.pool import _configuredClientContextFactory
-from twistedcaldav.config import config
-from twistedcaldav.util import utf8String
-
-from cStringIO import StringIO
-import base64
-import json
-
-
-log = Logger()
-
-
-
-class ConduitRequest(object):
-    """
-    An HTTP request between pods. This is typically used to send and receive JSON data. However,
-    for attachments, we need to send the actual attachment data as the request body, so in that
-    case the JSON data is sent in an HTTP header.
-    """
-
-    def __init__(self, server, data, stream=None, stream_type=None):
-        self.server = server
-        self.data = json.dumps(data)
-        self.stream = stream
-        self.streamType = stream_type
-
-
-    @inlineCallbacks
-    def doRequest(self, txn):
-
-        # Generate an HTTP client request
-        try:
-            if "xpod" not in txn.logItems:
-                txn.logItems["xpod"] = 0
-            txn.logItems["xpod"] += 1
-
-            response = (yield self._processRequest())
-
-            if accountingEnabledForCategory("xPod"):
-                self.loggedResponse = yield self.logResponse(response)
-                emitAccounting("xPod", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
-
-            if response.code in (responsecode.OK,):
-                data = (yield allDataFromStream(response.stream))
-                data = json.loads(data)
-            else:
-                raise ValueError("Incorrect cross-pod response status code: {}".format(response.code))
-
-        except Exception as e:
-            # Request failed
-            log.error("Could not do cross-pod request : {request} {ex}", request=self, ex=e)
-            raise ValueError("Failed cross-pod request: {}".format(e))
-
-        returnValue(data)
-
-
-    @inlineCallbacks
-    def logRequest(self, request):
-        """
-        Log an HTTP request.
-        """
-
-        iostr = StringIO()
-        iostr.write(">>>> Request start\n\n")
-        if hasattr(request, "clientproto"):
-            protocol = "HTTP/{:d}.{:d}".format(request.clientproto[0], request.clientproto[1])
-        else:
-            protocol = "HTTP/1.1"
-        iostr.write("{} {} {}\n".format(request.method, request.uri, protocol))
-        for name, valuelist in request.headers.getAllRawHeaders():
-            for value in valuelist:
-                # Do not log authorization details
-                if name not in ("Authorization",):
-                    iostr.write("{}: {}\n".format(name, value))
-                else:
-                    iostr.write("{}: xxxxxxxxx\n".format(name))
-        iostr.write("\n")
-
-        # We need to play a trick with the request stream as we can only read it once. So we
-        # read it, store the value in a MemoryStream, and replace the request's stream with that,
-        # so the data can be read again. Note if we are sending an attachment, we won't log
-        # the attachment data as we do not want to read it all into memory.
-        if self.stream is None:
-            data = (yield allDataFromStream(request.stream))
-            iostr.write(data)
-            request.stream = MemoryStream(data if data is not None else "")
-            request.stream.doStartReading = None
-        else:
-            iostr.write("<<Stream Type: {}>>\n".format(self.streamType))
-
-        iostr.write("\n\n>>>> Request end\n")
-        returnValue(iostr.getvalue())
-
-
-    @inlineCallbacks
-    def logResponse(self, response):
-        """
-        Log an HTTP request.
-        """
-        iostr = StringIO()
-        iostr.write(">>>> Response start\n\n")
-        code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
-        iostr.write("HTTP/1.1 {:d} {}\n".format(response.code, code_message))
-        for name, valuelist in response.headers.getAllRawHeaders():
-            for value in valuelist:
-                # Do not log authorization details
-                if name not in ("WWW-Authenticate",):
-                    iostr.write("{}: {}\n".format(name, value))
-                else:
-                    iostr.write("{}: xxxxxxxxx\n".format(name))
-        iostr.write("\n")
-
-        # We need to play a trick with the response stream to ensure we don't mess it up. So we
-        # read it, store the value in a MemoryStream, and replace the response's stream with that,
-        # so the data can be read again.
-        data = (yield allDataFromStream(response.stream))
-        iostr.write(data)
-        response.stream = MemoryStream(data if data is not None else "")
-        response.stream.doStartReading = None
-
-        iostr.write("\n\n>>>> Response end\n")
-        returnValue(iostr.getvalue())
-
-
-    @inlineCallbacks
-    def _processRequest(self):
-        """
-        Process the request by sending it to the relevant server.
-
-        @return: the HTTP response.
-        @rtype: L{Response}
-        """
-        ssl, host, port, _ignore_path = self.server.details()
-        path = "/" + config.Servers.ConduitName
-
-        headers = Headers()
-        headers.setHeader("Host", utf8String(host + ":{}".format(port)))
-        if self.streamType:
-            # For attachments we put the base64-encoded JSON data into a header
-            headers.setHeader("Content-Type", self.streamType)
-            headers.addRawHeader("XPOD", base64.b64encode(self.data))
-        else:
-            headers.setHeader("Content-Type", MimeType("application", "json", params={"charset": "utf-8", }))
-        headers.setHeader("User-Agent", "CalendarServer/{}".format(version))
-        headers.addRawHeader(*self.server.secretHeader())
-
-        from twisted.internet import reactor
-        f = Factory()
-        f.protocol = HTTPClientProtocol
-        ep = GAIEndpoint(reactor, host, port, _configuredClientContextFactory() if ssl else None)
-        proto = (yield ep.connect(f))
-
-        request = ClientRequest("POST", path, headers, self.stream if self.stream is not None else self.data)
-
-        if accountingEnabledForCategory("xPod"):
-            self.loggedRequest = yield self.logRequest(request)
-
-        response = (yield proto.submitRequest(request))
-
-        returnValue(response)

Copied: CalendarServer/trunk/txdav/common/datastore/podding/request.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/request.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/request.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,192 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from calendarserver.version import version
+
+from twext.internet.gaiendpoint import GAIEndpoint
+from twext.python.log import Logger
+
+from txweb2 import responsecode
+from txweb2.client.http import HTTPClientProtocol, ClientRequest
+from txweb2.dav.util import allDataFromStream
+from txweb2.http_headers import Headers, MimeType
+from txweb2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.protocol import Factory
+
+from twistedcaldav.accounting import accountingEnabledForCategory, \
+    emitAccounting
+from twistedcaldav.client.pool import _configuredClientContextFactory
+from twistedcaldav.config import config
+from twistedcaldav.util import utf8String
+
+from cStringIO import StringIO
+import base64
+import json
+
+
+log = Logger()
+
+
+
+class ConduitRequest(object):
+    """
+    An HTTP request between pods. This is typically used to send and receive JSON data. However,
+    for attachments, we need to send the actual attachment data as the request body, so in that
+    case the JSON data is sent in an HTTP header.
+    """
+
+    def __init__(self, server, data, stream=None, stream_type=None):
+        self.server = server
+        self.data = json.dumps(data)
+        self.stream = stream
+        self.streamType = stream_type
+
+
+    @inlineCallbacks
+    def doRequest(self, txn):
+
+        # Generate an HTTP client request
+        try:
+            if "xpod" not in txn.logItems:
+                txn.logItems["xpod"] = 0
+            txn.logItems["xpod"] += 1
+
+            response = (yield self._processRequest())
+
+            if accountingEnabledForCategory("xPod"):
+                self.loggedResponse = yield self.logResponse(response)
+                emitAccounting("xPod", "", self.loggedRequest + "\n" + self.loggedResponse, "POST")
+
+            if response.code in (responsecode.OK,):
+                data = (yield allDataFromStream(response.stream))
+                data = json.loads(data)
+            else:
+                raise ValueError("Incorrect cross-pod response status code: {}".format(response.code))
+
+        except Exception as e:
+            # Request failed
+            log.error("Could not do cross-pod request : {request} {ex}", request=self, ex=e)
+            raise ValueError("Failed cross-pod request: {}".format(e))
+
+        returnValue(data)
+
+
+    @inlineCallbacks
+    def logRequest(self, request):
+        """
+        Log an HTTP request.
+        """
+
+        iostr = StringIO()
+        iostr.write(">>>> Request start\n\n")
+        if hasattr(request, "clientproto"):
+            protocol = "HTTP/{:d}.{:d}".format(request.clientproto[0], request.clientproto[1])
+        else:
+            protocol = "HTTP/1.1"
+        iostr.write("{} {} {}\n".format(request.method, request.uri, protocol))
+        for name, valuelist in request.headers.getAllRawHeaders():
+            for value in valuelist:
+                # Do not log authorization details
+                if name not in ("Authorization",):
+                    iostr.write("{}: {}\n".format(name, value))
+                else:
+                    iostr.write("{}: xxxxxxxxx\n".format(name))
+        iostr.write("\n")
+
+        # We need to play a trick with the request stream as we can only read it once. So we
+        # read it, store the value in a MemoryStream, and replace the request's stream with that,
+        # so the data can be read again. Note if we are sending an attachment, we won't log
+        # the attachment data as we do not want to read it all into memory.
+        if self.stream is None:
+            data = (yield allDataFromStream(request.stream))
+            iostr.write(data)
+            request.stream = MemoryStream(data if data is not None else "")
+            request.stream.doStartReading = None
+        else:
+            iostr.write("<<Stream Type: {}>>\n".format(self.streamType))
+
+        iostr.write("\n\n>>>> Request end\n")
+        returnValue(iostr.getvalue())
+
+
+    @inlineCallbacks
+    def logResponse(self, response):
+        """
+        Log an HTTP request.
+        """
+        iostr = StringIO()
+        iostr.write(">>>> Response start\n\n")
+        code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+        iostr.write("HTTP/1.1 {:d} {}\n".format(response.code, code_message))
+        for name, valuelist in response.headers.getAllRawHeaders():
+            for value in valuelist:
+                # Do not log authorization details
+                if name not in ("WWW-Authenticate",):
+                    iostr.write("{}: {}\n".format(name, value))
+                else:
+                    iostr.write("{}: xxxxxxxxx\n".format(name))
+        iostr.write("\n")
+
+        # We need to play a trick with the response stream to ensure we don't mess it up. So we
+        # read it, store the value in a MemoryStream, and replace the response's stream with that,
+        # so the data can be read again.
+        data = (yield allDataFromStream(response.stream))
+        iostr.write(data)
+        response.stream = MemoryStream(data if data is not None else "")
+        response.stream.doStartReading = None
+
+        iostr.write("\n\n>>>> Response end\n")
+        returnValue(iostr.getvalue())
+
+
+    @inlineCallbacks
+    def _processRequest(self):
+        """
+        Process the request by sending it to the relevant server.
+
+        @return: the HTTP response.
+        @rtype: L{Response}
+        """
+        ssl, host, port, _ignore_path = self.server.details()
+        path = "/" + config.Servers.ConduitName
+
+        headers = Headers()
+        headers.setHeader("Host", utf8String(host + ":{}".format(port)))
+        if self.streamType:
+            # For attachments we put the base64-encoded JSON data into a header
+            headers.setHeader("Content-Type", self.streamType)
+            headers.addRawHeader("XPOD", base64.b64encode(self.data))
+        else:
+            headers.setHeader("Content-Type", MimeType("application", "json", params={"charset": "utf-8", }))
+        headers.setHeader("User-Agent", "CalendarServer/{}".format(version))
+        headers.addRawHeader(*self.server.secretHeader())
+
+        from twisted.internet import reactor
+        f = Factory()
+        f.protocol = HTTPClientProtocol
+        ep = GAIEndpoint(reactor, host, port, _configuredClientContextFactory() if ssl else None)
+        proto = (yield ep.connect(f))
+
+        request = ClientRequest("POST", path, headers, self.stream if self.stream is not None else self.data)
+
+        if accountingEnabledForCategory("xPod"):
+            self.loggedRequest = yield self.logRequest(request)
+
+        response = (yield proto.submitRequest(request))
+
+        returnValue(response)

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/resource.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,193 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twext.web2 import responsecode
-from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.dav.util import allDataFromStream
-from twext.web2.http import Response, HTTPError, StatusResponse, JSONResponse
-from twext.web2.http_headers import MimeType
-
-from twisted.internet.defer import succeed, returnValue, inlineCallbacks
-
-from twistedcaldav.extensions import DAVResource, \
-    DAVResourceWithoutChildrenMixin
-from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.scheduling_store.caldav.resource import \
-    deliverSchedulePrivilegeSet
-
-from txdav.xml import element as davxml
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
-from txdav.common.datastore.podding.conduit import FailedCrossPodRequestError
-
-import base64
-import json
-
-__all__ = [
-    "ConduitResource",
-]
-
-class ConduitResource(ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
-    """
-    Podding cross-pod RPC conduit resource.
-
-    Extends L{DAVResource} to provide cross-pod RPC functionality.
-    """
-
-    def __init__(self, parent, store):
-        """
-        @param parent: the parent resource of this one.
-        """
-        assert parent is not None
-
-        DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
-        self.parent = parent
-        self.store = store
-
-
-    def deadProperties(self):
-        if not hasattr(self, "_dead_properties"):
-            self._dead_properties = NonePropertyStore(self)
-        return self._dead_properties
-
-
-    def etag(self):
-        return succeed(None)
-
-
-    def checkPreconditions(self, request):
-        return None
-
-
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
-
-
-    def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8")
-
-
-    def isCollection(self):
-        return False
-
-
-    def isCalendarCollection(self):
-        return False
-
-
-    def isPseudoCalendarCollection(self):
-        return False
-
-
-    def principalForCalendarUserAddress(self, address):
-        for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
-            if principal is not None:
-                return principal
-        return None
-
-
-    def render(self, request):
-        output = """<html>
-<head>
-<title>Podding Conduit Resource</title>
-</head>
-<body>
-<h1>Podding Conduit Resource.</h1>
-</body
-</html>"""
-
-        response = Response(200, {}, output)
-        response.headers.setHeader("content-type", MimeType("text", "html"))
-        return response
-
-
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The server-to-server POST method.
-        """
-
-        # Check shared secret
-        if not Servers.getThisServer().checkSharedSecret(request.headers):
-            self.log.error("Invalid shared secret header in cross-pod request")
-            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not authorized to make this request"))
-
-        # Look for XPOD header
-        xpod = request.headers.getRawHeaders("XPOD")
-        contentType = request.headers.getHeader("content-type")
-        if xpod is not None:
-            # Attachments are sent in the request body with the JSON data in a header. We
-            # decode the header and add the request.stream as an attribute of the JSON object.
-            xpod = xpod[0]
-            try:
-                j = json.loads(base64.b64decode(xpod))
-            except (TypeError, ValueError) as e:
-                self.log.error("Invalid JSON header in request: {ex}\n{xpod}", ex=e, xpod=xpod)
-                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON header in request: {}\n{}".format(e, xpod)))
-            j["stream"] = request.stream
-            j["streamType"] = contentType
-        else:
-            # Check content first
-            if "{}/{}".format(contentType.mediaType, contentType.mediaSubtype) != "application/json":
-                self.log.error("MIME type {mime} not allowed in request", mime=contentType)
-                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "MIME type {} not allowed in request".format(contentType)))
-
-            body = (yield allDataFromStream(request.stream))
-            try:
-                j = json.loads(body)
-            except ValueError as e:
-                self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
-                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
-
-        # Log extended item
-        if not hasattr(request, "extendedLogItems"):
-            request.extendedLogItems = {}
-        request.extendedLogItems["xpod"] = j["action"] if "action" in j else "unknown"
-
-        # Get the conduit to process the data
-        try:
-            result = yield self.store.conduit.processRequest(j)
-        except FailedCrossPodRequestError as e:
-            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
-        except Exception as e:
-            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, str(e)))
-
-        response = JSONResponse(responsecode.OK, result)
-        returnValue(response)
-
-
-    ##
-    # ACL
-    ##
-
-    def supportedPrivileges(self, request):
-        return succeed(deliverSchedulePrivilegeSet)
-
-
-    def defaultAccessControlList(self):
-        privs = (
-            davxml.Privilege(davxml.Read()),
-        )
-
-        return davxml.ACL(
-            # DAV:Read for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
-        )

Copied: CalendarServer/trunk/txdav/common/datastore/podding/resource.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/resource.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/resource.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,193 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from txweb2 import responsecode
+from txweb2.dav.noneprops import NonePropertyStore
+from txweb2.dav.util import allDataFromStream
+from txweb2.http import Response, HTTPError, StatusResponse, JSONResponse
+from txweb2.http_headers import MimeType
+
+from twisted.internet.defer import succeed, returnValue, inlineCallbacks
+
+from twistedcaldav.extensions import DAVResource, \
+    DAVResourceWithoutChildrenMixin
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.scheduling_store.caldav.resource import \
+    deliverSchedulePrivilegeSet
+
+from txdav.xml import element as davxml
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
+from txdav.common.datastore.podding.conduit import FailedCrossPodRequestError
+
+import base64
+import json
+
+__all__ = [
+    "ConduitResource",
+]
+
+class ConduitResource(ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
+    """
+    Podding cross-pod RPC conduit resource.
+
+    Extends L{DAVResource} to provide cross-pod RPC functionality.
+    """
+
+    def __init__(self, parent, store):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        DAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+        self.store = store
+
+
+    def deadProperties(self):
+        if not hasattr(self, "_dead_properties"):
+            self._dead_properties = NonePropertyStore(self)
+        return self._dead_properties
+
+
+    def etag(self):
+        return succeed(None)
+
+
+    def checkPreconditions(self, request):
+        return None
+
+
+    def resourceType(self):
+        return davxml.ResourceType.ischeduleinbox
+
+
+    def contentType(self):
+        return MimeType.fromString("text/html; charset=utf-8")
+
+
+    def isCollection(self):
+        return False
+
+
+    def isCalendarCollection(self):
+        return False
+
+
+    def isPseudoCalendarCollection(self):
+        return False
+
+
+    def principalForCalendarUserAddress(self, address):
+        for principalCollection in self.principalCollections():
+            principal = principalCollection.principalForCalendarUserAddress(address)
+            if principal is not None:
+                return principal
+        return None
+
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Podding Conduit Resource</title>
+</head>
+<body>
+<h1>Podding Conduit Resource.</h1>
+</body
+</html>"""
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The server-to-server POST method.
+        """
+
+        # Check shared secret
+        if not Servers.getThisServer().checkSharedSecret(request.headers):
+            self.log.error("Invalid shared secret header in cross-pod request")
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not authorized to make this request"))
+
+        # Look for XPOD header
+        xpod = request.headers.getRawHeaders("XPOD")
+        contentType = request.headers.getHeader("content-type")
+        if xpod is not None:
+            # Attachments are sent in the request body with the JSON data in a header. We
+            # decode the header and add the request.stream as an attribute of the JSON object.
+            xpod = xpod[0]
+            try:
+                j = json.loads(base64.b64decode(xpod))
+            except (TypeError, ValueError) as e:
+                self.log.error("Invalid JSON header in request: {ex}\n{xpod}", ex=e, xpod=xpod)
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON header in request: {}\n{}".format(e, xpod)))
+            j["stream"] = request.stream
+            j["streamType"] = contentType
+        else:
+            # Check content first
+            if "{}/{}".format(contentType.mediaType, contentType.mediaSubtype) != "application/json":
+                self.log.error("MIME type {mime} not allowed in request", mime=contentType)
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "MIME type {} not allowed in request".format(contentType)))
+
+            body = (yield allDataFromStream(request.stream))
+            try:
+                j = json.loads(body)
+            except ValueError as e:
+                self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
+
+        # Log extended item
+        if not hasattr(request, "extendedLogItems"):
+            request.extendedLogItems = {}
+        request.extendedLogItems["xpod"] = j["action"] if "action" in j else "unknown"
+
+        # Get the conduit to process the data
+        try:
+            result = yield self.store.conduit.processRequest(j)
+        except FailedCrossPodRequestError as e:
+            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
+        except Exception as e:
+            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, str(e)))
+
+        response = JSONResponse(responsecode.OK, result)
+        returnValue(response)
+
+
+    ##
+    # ACL
+    ##
+
+    def supportedPrivileges(self, request):
+        return succeed(deliverSchedulePrivilegeSet)
+
+
+    def defaultAccessControlList(self):
+        privs = (
+            davxml.Privilege(davxml.Read()),
+        )
+
+        return davxml.ACL(
+            # DAV:Read for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(*privs),
+                davxml.Protected(),
+            ),
+        )

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,1082 +0,0 @@
-##
-# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from pycalendar.datetime import DateTime
-from pycalendar.period import Period
-
-from twext.python.clsprop import classproperty
-import twext.web2.dav.test.util
-from twext.web2.http_headers import MimeType
-from twext.web2.stream import MemoryStream
-
-from twisted.internet.defer import inlineCallbacks, succeed, returnValue
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
-from twistedcaldav.ical import Component, normalize_iCalStr
-
-from txdav.caldav.datastore.query.filter import Filter
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
-from txdav.caldav.datastore.sql import ManagedAttachment
-from txdav.caldav.datastore.test.common import CaptureProtocol
-from txdav.caldav.datastore.test.util import buildCalendarStore, \
-    TestCalendarStoreDirectoryRecord
-from txdav.common.datastore.podding.conduit import PoddingConduit, \
-    FailedCrossPodRequestError
-from txdav.common.datastore.podding.resource import ConduitResource
-from txdav.common.datastore.podding.test.util import MultiStoreConduitTest, \
-    FakeConduitRequest
-from txdav.common.datastore.sql_tables import _BIND_STATUS_ACCEPTED
-from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
-from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
-    ObjectResourceNameNotAllowedError
-from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-
-
-class TestConduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
-    class FakeConduit(object):
-
-        def recv_fake(self, j):
-            return succeed({
-                "result": "ok",
-                "back2u": j["echo"],
-                "more": "bits",
-            })
-
-
-    @inlineCallbacks
-    def setUp(self):
-        yield super(TestConduit, self).setUp()
-        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
-        self.directory = self._sqlCalendarStore.directoryService()
-
-        for ctr in range(1, 100):
-            self.directory.addRecord(TestCalendarStoreDirectoryRecord(
-                "puser{:02d}".format(ctr),
-                ("puser{:02d}".format(ctr),),
-                "Puser {:02d}".format(ctr),
-                frozenset((
-                    "urn:uuid:puser{:02d}".format(ctr),
-                    "mailto:puser{:02d}@example.com".format(ctr),
-                )),
-                thisServer=False,
-            ))
-
-        self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
-
-        self.thisServer = Server("A", "http://127.0.0.1", "A", True)
-        Servers.addServer(self.thisServer)
-
-        yield self.populate()
-
-
-    def storeUnderTest(self):
-        """
-        Return a store for testing.
-        """
-        return self._sqlCalendarStore
-
-
-    @inlineCallbacks
-    def populate(self):
-        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
-        self.notifierFactory.reset()
-
-
-    @classproperty(cache=False)
-    def requirements(cls): #@NoSelf
-        return {
-        "user01": {
-            "calendar_1": {
-            },
-            "inbox": {
-            },
-        },
-        "user02": {
-            "calendar_1": {
-            },
-            "inbox": {
-            },
-        },
-        "user03": {
-            "calendar_1": {
-            },
-            "inbox": {
-            },
-        },
-    }
-
-
-    def test_validRequst(self):
-        """
-        Cross-pod request fails when there is no shared secret header present.
-        """
-
-        conduit = PoddingConduit(self.storeUnderTest())
-        r1, r2 = conduit.validRequst("user01", "puser02")
-        self.assertTrue(r1 is not None)
-        self.assertTrue(r2 is not None)
-
-        self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "bogus01", "user02")
-        self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "user01", "bogus02")
-        self.assertRaises(FailedCrossPodRequestError, conduit.validRequst, "user01", "user02")
-
-
-
-class TestConduitToConduit(MultiStoreConduitTest):
-
-    class FakeConduit(PoddingConduit):
-
-        @inlineCallbacks
-        def send_fake(self, txn, ownerUID, shareeUID):
-            _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
-            action = {
-                "action": "fake",
-                "echo": "bravo"
-            }
-
-            result = yield self.sendRequest(txn, sharee, action)
-            returnValue(result)
-
-
-        def recv_fake(self, txn, j):
-            return succeed({
-                "result": "ok",
-                "back2u": j["echo"],
-                "more": "bits",
-            })
-
-
-    def makeConduit(self, store):
-        """
-        Use our own variant.
-        """
-        conduit = self.FakeConduit(store)
-        conduit.conduitRequestClass = FakeConduitRequest
-        return conduit
-
-
-    @inlineCallbacks
-    def test_fake_action(self):
-        """
-        Cross-pod request works when conduit does support the action.
-        """
-
-        txn = self.transactionUnderTest()
-        store1 = self.storeUnderTest()
-        response = yield store1.conduit.send_fake(txn, "user01", "puser01")
-        self.assertTrue("result" in response)
-        self.assertEqual(response["result"], "ok")
-        self.assertTrue("back2u" in response)
-        self.assertEqual(response["back2u"], "bravo")
-        self.assertTrue("more" in response)
-        self.assertEqual(response["more"], "bits")
-        yield txn.commit()
-
-        store2 = self.otherStoreUnderTest()
-        txn = store2.newTransaction()
-        response = yield store2.conduit.send_fake(txn, "puser01", "user01")
-        self.assertTrue("result" in response)
-        self.assertEqual(response["result"], "ok")
-        self.assertTrue("back2u" in response)
-        self.assertEqual(response["back2u"], "bravo")
-        self.assertTrue("more" in response)
-        self.assertEqual(response["more"], "bits")
-        yield txn.commit()
-
-
-
-class TestConduitAPI(MultiStoreConduitTest):
-    """
-    Test that the conduit api works.
-    """
-
-    nowYear = {"now": DateTime.getToday().getYear()}
-
-    caldata1 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid1
-DTSTART:{now:04d}0102T140000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
-    caldata1_changed = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid1
-DTSTART:{now:04d}0102T150000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance changed
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
-    caldata2 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid2
-DTSTART:{now:04d}0102T160000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
-    caldata3 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:uid3
-DTSTART:{now:04d}0102T160000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-RRULE:FREQ=WEEKLY
-SUMMARY:instance
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n").format(**nowYear)
-
-    @inlineCallbacks
-    def test_basic_share(self):
-        """
-        Test that basic invite/uninvite works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        shared = yield  calendar1.shareeView("puser01")
-        self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED)
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        self.assertTrue(shared is not None)
-        self.assertTrue(shared.external())
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield calendar1.uninviteUserFromShare("puser01")
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        self.assertTrue(shared is None)
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_countobjects(self):
-        """
-        Test that action=countobjects works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        count = yield shared.countObjectResources()
-        self.assertEqual(count, 0)
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        count = yield calendar1.countObjectResources()
-        self.assertEqual(count, 1)
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        count = yield shared.countObjectResources()
-        self.assertEqual(count, 1)
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        yield  object1.remove()
-        count = yield calendar1.countObjectResources()
-        self.assertEqual(count, 0)
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        count = yield shared.countObjectResources()
-        self.assertEqual(count, 0)
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_listobjects(self):
-        """
-        Test that action=listobjects works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        objects = yield shared.listObjectResources()
-        self.assertEqual(set(objects), set())
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield  calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
-        objects = yield calendar1.listObjectResources()
-        self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        objects = yield shared.listObjectResources()
-        self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        yield  object1.remove()
-        objects = yield calendar1.listObjectResources()
-        self.assertEqual(set(objects), set(("2.ics",)))
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        objects = yield shared.listObjectResources()
-        self.assertEqual(set(objects), set(("2.ics",)))
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_synctoken(self):
-        """
-        Test that action=synctoken works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        token1_1 = yield calendar1.syncToken()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        token2_1 = yield shared.syncToken()
-        yield self.otherCommit()
-
-        self.assertEqual(token1_1, token2_1)
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        token1_2 = yield calendar1.syncToken()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        token2_2 = yield shared.syncToken()
-        yield self.otherCommit()
-
-        self.assertNotEqual(token1_1, token1_2)
-        self.assertEqual(token1_2, token2_2)
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        yield  object1.remove()
-        count = yield calendar1.countObjectResources()
-        self.assertEqual(count, 0)
-        yield self.commit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        token1_3 = yield calendar1.syncToken()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        token2_3 = yield shared.syncToken()
-        yield self.otherCommit()
-
-        self.assertNotEqual(token1_1, token1_3)
-        self.assertNotEqual(token1_2, token1_3)
-        self.assertEqual(token1_3, token2_3)
-
-
-    @inlineCallbacks
-    def test_resourcenamessincerevision(self):
-        """
-        Test that action=synctoken works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        token1_1 = yield calendar1.syncToken()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        token2_1 = yield shared.syncToken()
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        token1_2 = yield calendar1.syncToken()
-        names1 = yield calendar1.resourceNamesSinceToken(token1_1)
-        self.assertEqual(names1, ([u"1.ics"], [],))
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        token2_2 = yield shared.syncToken()
-        names2 = yield shared.resourceNamesSinceToken(token2_1)
-        self.assertEqual(names2, ([u"1.ics"], [],))
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        yield  object1.remove()
-        count = yield calendar1.countObjectResources()
-        self.assertEqual(count, 0)
-        yield self.commit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        token1_3 = yield calendar1.syncToken()
-        names1 = yield calendar1.resourceNamesSinceToken(token1_2)
-        self.assertEqual(names1, ([], [u"1.ics"],))
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        token2_3 = yield shared.syncToken()
-        names2 = yield shared.resourceNamesSinceToken(token2_2)
-        self.assertEqual(names2, ([], [u"1.ics"],))
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        names1 = yield calendar1.resourceNamesSinceToken(token1_3)
-        self.assertEqual(names1, ([], [],))
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        names2 = yield shared.resourceNamesSinceToken(token2_3)
-        self.assertEqual(names2, ([], [],))
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_resourceuidforname(self):
-        """
-        Test that action=resourceuidforname works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        uid = yield calendar1.resourceUIDForName("1.ics")
-        self.assertEqual(uid, "uid1")
-        uid = yield calendar1.resourceUIDForName("2.ics")
-        self.assertTrue(uid is None)
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        uid = yield shared.resourceUIDForName("1.ics")
-        self.assertEqual(uid, "uid1")
-        uid = yield shared.resourceUIDForName("2.ics")
-        self.assertTrue(uid is None)
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_resourcenameforuid(self):
-        """
-        Test that action=resourcenameforuid works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        name = yield calendar1.resourceNameForUID("uid1")
-        self.assertEqual(name, "1.ics")
-        name = yield calendar1.resourceNameForUID("uid2")
-        self.assertTrue(name is None)
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        name = yield shared.resourceNameForUID("uid1")
-        self.assertEqual(name, "1.ics")
-        name = yield shared.resourceNameForUID("uid2")
-        self.assertTrue(name is None)
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_search(self):
-        """
-        Test that action=resourcenameforuid works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name": "VCALENDAR"}
-            )
-        )
-        filter = Filter(filter)
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        names = [item[0] for item in (yield calendar1.search(filter))]
-        self.assertEqual(names, ["1.ics", ])
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        names = [item[0] for item in (yield shared.search(filter))]
-        self.assertEqual(names, ["1.ics", ])
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_loadallobjects(self):
-        """
-        Test that action=loadallobjects works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        resource1 = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        resource_id1 = resource1.id()
-        resource2 = yield  calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
-        resource_id2 = resource2.id()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resources = yield shared.objectResources()
-        byname = dict([(resource.name(), resource) for resource in resources])
-        byuid = dict([(resource.uid(), resource) for resource in resources])
-        self.assertEqual(len(resources), 2)
-        self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics",)))
-        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid2",)))
-        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id2,)))
-        resource = yield shared.objectResourceWithName("1.ics")
-        self.assertTrue(resource is byname["1.ics"])
-        resource = yield shared.objectResourceWithName("2.ics")
-        self.assertTrue(resource is byname["2.ics"])
-        resource = yield shared.objectResourceWithName("Missing.ics")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithUID("uid1")
-        self.assertTrue(resource is byuid["uid1"])
-        resource = yield shared.objectResourceWithUID("uid2")
-        self.assertTrue(resource is byuid["uid2"])
-        resource = yield shared.objectResourceWithUID("uid-missing")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithID(resource_id1)
-        self.assertTrue(resource is byname["1.ics"])
-        resource = yield shared.objectResourceWithID(resource_id2)
-        self.assertTrue(resource is byname["2.ics"])
-        resource = yield shared.objectResourceWithID(0)
-        self.assertTrue(resource is None)
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        yield  object1.remove()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resources = yield shared.objectResources()
-        byname = dict([(resource.name(), resource) for resource in resources])
-        byuid = dict([(resource.uid(), resource) for resource in resources])
-        self.assertEqual(len(resources), 1)
-        self.assertEqual(set([resource.name() for resource in resources]), set(("2.ics",)))
-        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid2",)))
-        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id2,)))
-        resource = yield shared.objectResourceWithName("1.ics")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithName("2.ics")
-        self.assertTrue(resource is byname["2.ics"])
-        resource = yield shared.objectResourceWithName("Missing.ics")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithUID("uid1")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithUID("uid2")
-        self.assertTrue(resource is byuid["uid2"])
-        resource = yield shared.objectResourceWithUID("uid-missing")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithID(resource_id1)
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithID(resource_id2)
-        self.assertTrue(resource is byname["2.ics"])
-        resource = yield shared.objectResourceWithID(0)
-        self.assertTrue(resource is None)
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_loadallobjectswithnames(self):
-        """
-        Test that action=loadallobjectswithnames works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        resource1 = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        resource_id1 = resource1.id()
-        yield  calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
-        resource3 = yield  calendar1.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3))
-        resource_id3 = resource3.id()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resources = yield shared.objectResources()
-        self.assertEqual(len(resources), 3)
-        yield self.otherCommit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
-        byname = dict([(resource.name(), resource) for resource in resources])
-        byuid = dict([(resource.uid(), resource) for resource in resources])
-        self.assertEqual(len(resources), 2)
-        self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "3.ics",)))
-        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid3",)))
-        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id3,)))
-        resource = yield shared.objectResourceWithName("1.ics")
-        self.assertTrue(resource is byname["1.ics"])
-        resource = yield shared.objectResourceWithName("3.ics")
-        self.assertTrue(resource is byname["3.ics"])
-        resource = yield shared.objectResourceWithName("Missing.ics")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithUID("uid1")
-        self.assertTrue(resource is byuid["uid1"])
-        resource = yield shared.objectResourceWithUID("uid3")
-        self.assertTrue(resource is byuid["uid3"])
-        resource = yield shared.objectResourceWithUID("uid-missing")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithID(resource_id1)
-        self.assertTrue(resource is byname["1.ics"])
-        resource = yield shared.objectResourceWithID(resource_id3)
-        self.assertTrue(resource is byname["3.ics"])
-        resource = yield shared.objectResourceWithID(0)
-        self.assertTrue(resource is None)
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        yield  object1.remove()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
-        byname = dict([(resource.name(), resource) for resource in resources])
-        byuid = dict([(resource.uid(), resource) for resource in resources])
-        self.assertEqual(len(resources), 1)
-        self.assertEqual(set([resource.name() for resource in resources]), set(("3.ics",)))
-        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid3",)))
-        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id3,)))
-        resource = yield shared.objectResourceWithName("1.ics")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithName("3.ics")
-        self.assertTrue(resource is byname["3.ics"])
-        resource = yield shared.objectResourceWithName("Missing.ics")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithUID("uid1")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithUID("uid3")
-        self.assertTrue(resource is byuid["uid3"])
-        resource = yield shared.objectResourceWithUID("uid-missing")
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithID(resource_id1)
-        self.assertTrue(resource is None)
-        resource = yield shared.objectResourceWithID(resource_id3)
-        self.assertTrue(resource is byname["3.ics"])
-        resource = yield shared.objectResourceWithID(0)
-        self.assertTrue(resource is None)
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_objectwith(self):
-        """
-        Test that action=objectwith works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        resource = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        resource_id = resource.id()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.objectResourceWithName("1.ics")
-        self.assertTrue(resource is not None)
-        self.assertEqual(resource.name(), "1.ics")
-        self.assertEqual(resource.uid(), "uid1")
-
-        resource = yield shared.objectResourceWithName("2.ics")
-        self.assertTrue(resource is None)
-
-        yield self.otherCommit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.objectResourceWithUID("uid1")
-        self.assertTrue(resource is not None)
-        self.assertEqual(resource.name(), "1.ics")
-        self.assertEqual(resource.uid(), "uid1")
-
-        resource = yield shared.objectResourceWithUID("uid2")
-        self.assertTrue(resource is None)
-
-        yield self.otherCommit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.objectResourceWithID(resource_id)
-        self.assertTrue(resource is not None)
-        self.assertEqual(resource.name(), "1.ics")
-        self.assertEqual(resource.uid(), "uid1")
-
-        resource = yield shared.objectResourceWithID(0)
-        self.assertTrue(resource is None)
-
-        yield self.otherCommit()
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        yield  object1.remove()
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.objectResourceWithName("1.ics")
-        self.assertTrue(resource is None)
-        yield self.otherCommit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.objectResourceWithUID("uid1")
-        self.assertTrue(resource is None)
-        yield self.otherCommit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.objectResourceWithID(resource_id)
-        self.assertTrue(resource is None)
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_create(self):
-        """
-        Test that action=create works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        resource_id = resource.id()
-        self.assertTrue(resource is not None)
-        self.assertEqual(resource.name(), "1.ics")
-        self.assertEqual(resource.uid(), "uid1")
-        self.assertFalse(resource._componentChanged)
-        yield self.otherCommit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        resource = yield shared.objectResourceWithUID("uid1")
-        self.assertTrue(resource is not None)
-        self.assertEqual(resource.name(), "1.ics")
-        self.assertEqual(resource.uid(), "uid1")
-        self.assertEqual(resource.id(), resource_id)
-        yield self.otherCommit()
-
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        self.assertTrue(object1 is not None)
-        self.assertEqual(object1.name(), "1.ics")
-        self.assertEqual(object1.uid(), "uid1")
-        self.assertEqual(object1.id(), resource_id)
-        yield self.commit()
-
-
-    @inlineCallbacks
-    def test_create_exception(self):
-        """
-        Test that action=create fails when a duplicate name is used.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        yield self.failUnlessFailure(shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError)
-        yield self.otherAbort()
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-        yield self.failUnlessFailure(shared.createCalendarObjectWithName(".2.ics", Component.fromString(self.caldata2)), ObjectResourceNameNotAllowedError)
-        yield self.otherAbort()
-
-
-    @inlineCallbacks
-    def test_setcomponent(self):
-        """
-        Test that action=setcomponent works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        ical = yield shared_object.component()
-        self.assertTrue(isinstance(ical, Component))
-        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
-        yield self.otherCommit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        changed = yield shared_object.setComponent(Component.fromString(self.caldata1_changed))
-        self.assertFalse(changed)
-        ical = yield shared_object.component()
-        self.assertTrue(isinstance(ical, Component))
-        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
-        yield self.otherCommit()
-
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        ical = yield object1.component()
-        self.assertTrue(isinstance(ical, Component))
-        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
-        yield self.commit()
-
-
-    @inlineCallbacks
-    def test_component(self):
-        """
-        Test that action=component works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        ical = yield shared_object.component()
-        self.assertTrue(isinstance(ical, Component))
-        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
-        yield self.otherCommit()
-
-
-    @inlineCallbacks
-    def test_remove(self):
-        """
-        Test that action=create works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        yield shared_object.remove()
-        yield self.otherCommit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        self.assertTrue(shared_object is None)
-        yield self.otherCommit()
-
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        self.assertTrue(object1 is None)
-        yield self.commit()
-
-
-    @inlineCallbacks
-    def test_freebusy(self):
-        """
-        Test that action=component works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
-        fbend = "{now:04d}0103T000000Z".format(**self.nowYear)
-
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
-
-        fbinfo = [[], [], []]
-        matchtotal = yield generateFreeBusyInfo(
-            shared,
-            fbinfo,
-            TimeRange(start=fbstart, end=fbend),
-            0,
-            excludeuid=None,
-            organizer=None,
-            organizerPrincipal=None,
-            same_calendar_user=False,
-            servertoserver=False,
-            event_details=False,
-            logItems=None
-        )
-
-        self.assertEqual(matchtotal, 1)
-        self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
-        self.assertEqual(len(fbinfo[1]), 0)
-        self.assertEqual(len(fbinfo[2]), 0)
-        yield self.otherCommit()
-
-
-    def attachmentToString(self, attachment):
-        """
-        Convenience to convert an L{IAttachment} to a string.
-
-        @param attachment: an L{IAttachment} provider to convert into a string.
-
-        @return: a L{Deferred} that fires with the contents of the attachment.
-
-        @rtype: L{Deferred} firing C{bytes}
-        """
-        capture = CaptureProtocol()
-        attachment.retrieve(capture)
-        return capture.deferred
-
-
-    @inlineCallbacks
-    def test_add_attachment(self):
-        """
-        Test that action=add-attachment works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        resourceID = object1.id()
-        yield self.commit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
-        managedID = attachment.managedID()
-        from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
-        self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
-        self.assertTrue("user01/attachments/test" in location)
-        yield self.otherCommit()
-
-        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
-        self.assertEqual(cobjs, set((resourceID,)))
-        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
-        self.assertEqual(attachment.name(), "test.txt")
-        data = yield self.attachmentToString(attachment)
-        self.assertEqual(data, "Here is some text.")
-        yield self.commit()
-
-
-    @inlineCallbacks
-    def test_update_attachment(self):
-        """
-        Test that action=update-attachment works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        resourceID = object1.id()
-        attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
-        managedID = attachment.managedID()
-        yield self.commit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some more text."))
-        managedID = attachment.managedID()
-        from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
-        self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
-        self.assertTrue("user01/attachments/test" in location)
-        yield self.otherCommit()
-
-        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
-        self.assertEqual(cobjs, set((resourceID,)))
-        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
-        self.assertEqual(attachment.name(), "test.txt")
-        data = yield self.attachmentToString(attachment)
-        self.assertEqual(data, "Here is some more text.")
-        yield self.commit()
-
-
-    @inlineCallbacks
-    def test_remove_attachment(self):
-        """
-        Test that action=remove-attachment works.
-        """
-
-        yield self.createShare("user01", "puser01")
-
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
-
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
-        resourceID = object1.id()
-        attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
-        managedID = attachment.managedID()
-        yield self.commit()
-
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
-        yield shared_object.removeAttachment(None, managedID)
-        yield self.otherCommit()
-
-        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
-        self.assertEqual(cobjs, set())
-        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
-        self.assertTrue(attachment is None)
-        yield self.commit()

Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,1083 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from pycalendar.datetime import DateTime
+from pycalendar.period import Period
+
+from twext.python.clsprop import classproperty
+
+import txweb2.dav.test.util
+from txweb2.http_headers import MimeType
+from txweb2.stream import MemoryStream
+
+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import TimeRange
+from twistedcaldav.ical import Component, normalize_iCalStr
+
+from txdav.caldav.datastore.query.filter import Filter
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
+from txdav.caldav.datastore.sql import ManagedAttachment
+from txdav.caldav.datastore.test.common import CaptureProtocol
+from txdav.caldav.datastore.test.util import buildCalendarStore, \
+    TestCalendarStoreDirectoryRecord
+from txdav.common.datastore.podding.conduit import PoddingConduit, \
+    FailedCrossPodRequestError
+from txdav.common.datastore.podding.resource import ConduitResource
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest, \
+    FakeConduitRequest
+from txdav.common.datastore.sql_tables import _BIND_STATUS_ACCEPTED
+from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
+    ObjectResourceNameNotAllowedError
+from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+
+
+class TestConduit (CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+    class FakeConduit(object):
+
+        def recv_fake(self, j):
+            return succeed({
+                "result": "ok",
+                "back2u": j["echo"],
+                "more": "bits",
+            })
+
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(TestConduit, self).setUp()
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+        self.directory = self._sqlCalendarStore.directoryService()
+
+        for ctr in range(1, 100):
+            self.directory.addRecord(TestCalendarStoreDirectoryRecord(
+                "puser{:02d}".format(ctr),
+                ("puser{:02d}".format(ctr),),
+                "Puser {:02d}".format(ctr),
+                frozenset((
+                    "urn:uuid:puser{:02d}".format(ctr),
+                    "mailto:puser{:02d}@example.com".format(ctr),
+                )),
+                thisServer=False,
+            ))
+
+        self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
+
+        self.thisServer = Server("A", "http://127.0.0.1", "A", True)
+        Servers.addServer(self.thisServer)
+
+        yield self.populate()
+
+
+    def storeUnderTest(self):
+        """
+        Return a store for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        return {
+        "user01": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user02": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user03": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+    }
+
+
+    def test_validRequst(self):
+        """
+        Cross-pod request fails when there is no shared secret header present.
+        """
+
+        conduit = PoddingConduit(self.storeUnderTest())
+        r1, r2 = conduit.validRequst("user01", "puser02")
+        self.assertTrue(r1 is not None)
+        self.assertTrue(r2 is not None)
+
+        self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "bogus01", "user02")
+        self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "user01", "bogus02")
+        self.assertRaises(FailedCrossPodRequestError, conduit.validRequst, "user01", "user02")
+
+
+
+class TestConduitToConduit(MultiStoreConduitTest):
+
+    class FakeConduit(PoddingConduit):
+
+        @inlineCallbacks
+        def send_fake(self, txn, ownerUID, shareeUID):
+            _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+            action = {
+                "action": "fake",
+                "echo": "bravo"
+            }
+
+            result = yield self.sendRequest(txn, sharee, action)
+            returnValue(result)
+
+
+        def recv_fake(self, txn, j):
+            return succeed({
+                "result": "ok",
+                "back2u": j["echo"],
+                "more": "bits",
+            })
+
+
+    def makeConduit(self, store):
+        """
+        Use our own variant.
+        """
+        conduit = self.FakeConduit(store)
+        conduit.conduitRequestClass = FakeConduitRequest
+        return conduit
+
+
+    @inlineCallbacks
+    def test_fake_action(self):
+        """
+        Cross-pod request works when conduit does support the action.
+        """
+
+        txn = self.transactionUnderTest()
+        store1 = self.storeUnderTest()
+        response = yield store1.conduit.send_fake(txn, "user01", "puser01")
+        self.assertTrue("result" in response)
+        self.assertEqual(response["result"], "ok")
+        self.assertTrue("back2u" in response)
+        self.assertEqual(response["back2u"], "bravo")
+        self.assertTrue("more" in response)
+        self.assertEqual(response["more"], "bits")
+        yield txn.commit()
+
+        store2 = self.otherStoreUnderTest()
+        txn = store2.newTransaction()
+        response = yield store2.conduit.send_fake(txn, "puser01", "user01")
+        self.assertTrue("result" in response)
+        self.assertEqual(response["result"], "ok")
+        self.assertTrue("back2u" in response)
+        self.assertEqual(response["back2u"], "bravo")
+        self.assertTrue("more" in response)
+        self.assertEqual(response["more"], "bits")
+        yield txn.commit()
+
+
+
+class TestConduitAPI(MultiStoreConduitTest):
+    """
+    Test that the conduit api works.
+    """
+
+    nowYear = {"now": DateTime.getToday().getYear()}
+
+    caldata1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    caldata1_changed = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T150000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance changed
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    caldata2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid2
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    caldata3 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid3
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    @inlineCallbacks
+    def test_basic_share(self):
+        """
+        Test that basic invite/uninvite works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        shared = yield  calendar1.shareeView("puser01")
+        self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        self.assertTrue(shared is not None)
+        self.assertTrue(shared.external())
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield calendar1.uninviteUserFromShare("puser01")
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_countobjects(self):
+        """
+        Test that action=countobjects works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        count = yield shared.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 1)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        count = yield shared.countObjectResources()
+        self.assertEqual(count, 1)
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        count = yield shared.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_listobjects(self):
+        """
+        Test that action=listobjects works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        objects = yield shared.listObjectResources()
+        self.assertEqual(set(objects), set())
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield  calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        objects = yield calendar1.listObjectResources()
+        self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        objects = yield shared.listObjectResources()
+        self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        objects = yield calendar1.listObjectResources()
+        self.assertEqual(set(objects), set(("2.ics",)))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        objects = yield shared.listObjectResources()
+        self.assertEqual(set(objects), set(("2.ics",)))
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_synctoken(self):
+        """
+        Test that action=synctoken works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_1 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_1 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        self.assertEqual(token1_1, token2_1)
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_2 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_2 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        self.assertNotEqual(token1_1, token1_2)
+        self.assertEqual(token1_2, token2_2)
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_3 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_3 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        self.assertNotEqual(token1_1, token1_3)
+        self.assertNotEqual(token1_2, token1_3)
+        self.assertEqual(token1_3, token2_3)
+
+
+    @inlineCallbacks
+    def test_resourcenamessincerevision(self):
+        """
+        Test that action=synctoken works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_1 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_1 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_2 = yield calendar1.syncToken()
+        names1 = yield calendar1.resourceNamesSinceToken(token1_1)
+        self.assertEqual(names1, ([u"1.ics"], [], [],))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_2 = yield shared.syncToken()
+        names2 = yield shared.resourceNamesSinceToken(token2_1)
+        self.assertEqual(names2, ([u"1.ics"], [], [],))
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_3 = yield calendar1.syncToken()
+        names1 = yield calendar1.resourceNamesSinceToken(token1_2)
+        self.assertEqual(names1, ([], [u"1.ics"], [],))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_3 = yield shared.syncToken()
+        names2 = yield shared.resourceNamesSinceToken(token2_2)
+        self.assertEqual(names2, ([], [u"1.ics"], [],))
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        names1 = yield calendar1.resourceNamesSinceToken(token1_3)
+        self.assertEqual(names1, ([], [], [],))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        names2 = yield shared.resourceNamesSinceToken(token2_3)
+        self.assertEqual(names2, ([], [], [],))
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_resourceuidforname(self):
+        """
+        Test that action=resourceuidforname works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        uid = yield calendar1.resourceUIDForName("1.ics")
+        self.assertEqual(uid, "uid1")
+        uid = yield calendar1.resourceUIDForName("2.ics")
+        self.assertTrue(uid is None)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        uid = yield shared.resourceUIDForName("1.ics")
+        self.assertEqual(uid, "uid1")
+        uid = yield shared.resourceUIDForName("2.ics")
+        self.assertTrue(uid is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_resourcenameforuid(self):
+        """
+        Test that action=resourcenameforuid works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        name = yield calendar1.resourceNameForUID("uid1")
+        self.assertEqual(name, "1.ics")
+        name = yield calendar1.resourceNameForUID("uid2")
+        self.assertTrue(name is None)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        name = yield shared.resourceNameForUID("uid1")
+        self.assertEqual(name, "1.ics")
+        name = yield shared.resourceNameForUID("uid2")
+        self.assertTrue(name is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_search(self):
+        """
+        Test that action=resourcenameforuid works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name": "VCALENDAR"}
+            )
+        )
+        filter = Filter(filter)
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        names = [item[0] for item in (yield calendar1.search(filter))]
+        self.assertEqual(names, ["1.ics", ])
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        names = [item[0] for item in (yield shared.search(filter))]
+        self.assertEqual(names, ["1.ics", ])
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_loadallobjects(self):
+        """
+        Test that action=loadallobjects works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        resource1 = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        resource_id1 = resource1.id()
+        resource2 = yield  calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        resource_id2 = resource2.id()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resources = yield shared.objectResources()
+        byname = dict([(resource.name(), resource) for resource in resources])
+        byuid = dict([(resource.uid(), resource) for resource in resources])
+        self.assertEqual(len(resources), 2)
+        self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics",)))
+        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid2",)))
+        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id2,)))
+        resource = yield shared.objectResourceWithName("1.ics")
+        self.assertTrue(resource is byname["1.ics"])
+        resource = yield shared.objectResourceWithName("2.ics")
+        self.assertTrue(resource is byname["2.ics"])
+        resource = yield shared.objectResourceWithName("Missing.ics")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithUID("uid1")
+        self.assertTrue(resource is byuid["uid1"])
+        resource = yield shared.objectResourceWithUID("uid2")
+        self.assertTrue(resource is byuid["uid2"])
+        resource = yield shared.objectResourceWithUID("uid-missing")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithID(resource_id1)
+        self.assertTrue(resource is byname["1.ics"])
+        resource = yield shared.objectResourceWithID(resource_id2)
+        self.assertTrue(resource is byname["2.ics"])
+        resource = yield shared.objectResourceWithID(0)
+        self.assertTrue(resource is None)
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resources = yield shared.objectResources()
+        byname = dict([(resource.name(), resource) for resource in resources])
+        byuid = dict([(resource.uid(), resource) for resource in resources])
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(set([resource.name() for resource in resources]), set(("2.ics",)))
+        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid2",)))
+        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id2,)))
+        resource = yield shared.objectResourceWithName("1.ics")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithName("2.ics")
+        self.assertTrue(resource is byname["2.ics"])
+        resource = yield shared.objectResourceWithName("Missing.ics")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithUID("uid1")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithUID("uid2")
+        self.assertTrue(resource is byuid["uid2"])
+        resource = yield shared.objectResourceWithUID("uid-missing")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithID(resource_id1)
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithID(resource_id2)
+        self.assertTrue(resource is byname["2.ics"])
+        resource = yield shared.objectResourceWithID(0)
+        self.assertTrue(resource is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_loadallobjectswithnames(self):
+        """
+        Test that action=loadallobjectswithnames works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        resource1 = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        resource_id1 = resource1.id()
+        yield  calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        resource3 = yield  calendar1.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3))
+        resource_id3 = resource3.id()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resources = yield shared.objectResources()
+        self.assertEqual(len(resources), 3)
+        yield self.otherCommit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
+        byname = dict([(resource.name(), resource) for resource in resources])
+        byuid = dict([(resource.uid(), resource) for resource in resources])
+        self.assertEqual(len(resources), 2)
+        self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "3.ics",)))
+        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid3",)))
+        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id3,)))
+        resource = yield shared.objectResourceWithName("1.ics")
+        self.assertTrue(resource is byname["1.ics"])
+        resource = yield shared.objectResourceWithName("3.ics")
+        self.assertTrue(resource is byname["3.ics"])
+        resource = yield shared.objectResourceWithName("Missing.ics")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithUID("uid1")
+        self.assertTrue(resource is byuid["uid1"])
+        resource = yield shared.objectResourceWithUID("uid3")
+        self.assertTrue(resource is byuid["uid3"])
+        resource = yield shared.objectResourceWithUID("uid-missing")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithID(resource_id1)
+        self.assertTrue(resource is byname["1.ics"])
+        resource = yield shared.objectResourceWithID(resource_id3)
+        self.assertTrue(resource is byname["3.ics"])
+        resource = yield shared.objectResourceWithID(0)
+        self.assertTrue(resource is None)
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
+        byname = dict([(resource.name(), resource) for resource in resources])
+        byuid = dict([(resource.uid(), resource) for resource in resources])
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(set([resource.name() for resource in resources]), set(("3.ics",)))
+        self.assertEqual(set([resource.uid() for resource in resources]), set(("uid3",)))
+        self.assertEqual(set([resource.id() for resource in resources]), set((resource_id3,)))
+        resource = yield shared.objectResourceWithName("1.ics")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithName("3.ics")
+        self.assertTrue(resource is byname["3.ics"])
+        resource = yield shared.objectResourceWithName("Missing.ics")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithUID("uid1")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithUID("uid3")
+        self.assertTrue(resource is byuid["uid3"])
+        resource = yield shared.objectResourceWithUID("uid-missing")
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithID(resource_id1)
+        self.assertTrue(resource is None)
+        resource = yield shared.objectResourceWithID(resource_id3)
+        self.assertTrue(resource is byname["3.ics"])
+        resource = yield shared.objectResourceWithID(0)
+        self.assertTrue(resource is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_objectwith(self):
+        """
+        Test that action=objectwith works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        resource = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        resource_id = resource.id()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.objectResourceWithName("1.ics")
+        self.assertTrue(resource is not None)
+        self.assertEqual(resource.name(), "1.ics")
+        self.assertEqual(resource.uid(), "uid1")
+
+        resource = yield shared.objectResourceWithName("2.ics")
+        self.assertTrue(resource is None)
+
+        yield self.otherCommit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.objectResourceWithUID("uid1")
+        self.assertTrue(resource is not None)
+        self.assertEqual(resource.name(), "1.ics")
+        self.assertEqual(resource.uid(), "uid1")
+
+        resource = yield shared.objectResourceWithUID("uid2")
+        self.assertTrue(resource is None)
+
+        yield self.otherCommit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.objectResourceWithID(resource_id)
+        self.assertTrue(resource is not None)
+        self.assertEqual(resource.name(), "1.ics")
+        self.assertEqual(resource.uid(), "uid1")
+
+        resource = yield shared.objectResourceWithID(0)
+        self.assertTrue(resource is None)
+
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.objectResourceWithName("1.ics")
+        self.assertTrue(resource is None)
+        yield self.otherCommit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.objectResourceWithUID("uid1")
+        self.assertTrue(resource is None)
+        yield self.otherCommit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.objectResourceWithID(resource_id)
+        self.assertTrue(resource is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_create(self):
+        """
+        Test that action=create works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        resource_id = resource.id()
+        self.assertTrue(resource is not None)
+        self.assertEqual(resource.name(), "1.ics")
+        self.assertEqual(resource.uid(), "uid1")
+        self.assertFalse(resource._componentChanged)
+        yield self.otherCommit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        resource = yield shared.objectResourceWithUID("uid1")
+        self.assertTrue(resource is not None)
+        self.assertEqual(resource.name(), "1.ics")
+        self.assertEqual(resource.uid(), "uid1")
+        self.assertEqual(resource.id(), resource_id)
+        yield self.otherCommit()
+
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        self.assertTrue(object1 is not None)
+        self.assertEqual(object1.name(), "1.ics")
+        self.assertEqual(object1.uid(), "uid1")
+        self.assertEqual(object1.id(), resource_id)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_create_exception(self):
+        """
+        Test that action=create fails when a duplicate name is used.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        yield self.failUnlessFailure(shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError)
+        yield self.otherAbort()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        yield self.failUnlessFailure(shared.createCalendarObjectWithName(".2.ics", Component.fromString(self.caldata2)), ObjectResourceNameNotAllowedError)
+        yield self.otherAbort()
+
+
+    @inlineCallbacks
+    def test_setcomponent(self):
+        """
+        Test that action=setcomponent works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        ical = yield shared_object.component()
+        self.assertTrue(isinstance(ical, Component))
+        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
+        yield self.otherCommit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        changed = yield shared_object.setComponent(Component.fromString(self.caldata1_changed))
+        self.assertFalse(changed)
+        ical = yield shared_object.component()
+        self.assertTrue(isinstance(ical, Component))
+        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
+        yield self.otherCommit()
+
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        ical = yield object1.component()
+        self.assertTrue(isinstance(ical, Component))
+        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_component(self):
+        """
+        Test that action=component works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        ical = yield shared_object.component()
+        self.assertTrue(isinstance(ical, Component))
+        self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_remove(self):
+        """
+        Test that action=create works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        yield shared_object.remove()
+        yield self.otherCommit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        self.assertTrue(shared_object is None)
+        yield self.otherCommit()
+
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        self.assertTrue(object1 is None)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_freebusy(self):
+        """
+        Test that action=component works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
+        fbend = "{now:04d}0103T000000Z".format(**self.nowYear)
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+
+        fbinfo = [[], [], []]
+        matchtotal = yield generateFreeBusyInfo(
+            shared,
+            fbinfo,
+            TimeRange(start=fbstart, end=fbend),
+            0,
+            excludeuid=None,
+            organizer=None,
+            organizerPrincipal=None,
+            same_calendar_user=False,
+            servertoserver=False,
+            event_details=False,
+            logItems=None
+        )
+
+        self.assertEqual(matchtotal, 1)
+        self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
+        self.assertEqual(len(fbinfo[1]), 0)
+        self.assertEqual(len(fbinfo[2]), 0)
+        yield self.otherCommit()
+
+
+    def attachmentToString(self, attachment):
+        """
+        Convenience to convert an L{IAttachment} to a string.
+
+        @param attachment: an L{IAttachment} provider to convert into a string.
+
+        @return: a L{Deferred} that fires with the contents of the attachment.
+
+        @rtype: L{Deferred} firing C{bytes}
+        """
+        capture = CaptureProtocol()
+        attachment.retrieve(capture)
+        return capture.deferred
+
+
+    @inlineCallbacks
+    def test_add_attachment(self):
+        """
+        Test that action=add-attachment works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        resourceID = object1.id()
+        yield self.commit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
+        managedID = attachment.managedID()
+        from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
+        self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
+        self.assertTrue("user01/attachments/test" in location)
+        yield self.otherCommit()
+
+        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+        self.assertEqual(cobjs, set((resourceID,)))
+        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+        self.assertEqual(attachment.name(), "test.txt")
+        data = yield self.attachmentToString(attachment)
+        self.assertEqual(data, "Here is some text.")
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_update_attachment(self):
+        """
+        Test that action=update-attachment works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        resourceID = object1.id()
+        attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
+        managedID = attachment.managedID()
+        yield self.commit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some more text."))
+        managedID = attachment.managedID()
+        from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
+        self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
+        self.assertTrue("user01/attachments/test" in location)
+        yield self.otherCommit()
+
+        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+        self.assertEqual(cobjs, set((resourceID,)))
+        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+        self.assertEqual(attachment.name(), "test.txt")
+        data = yield self.attachmentToString(attachment)
+        self.assertEqual(data, "Here is some more text.")
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_remove_attachment(self):
+        """
+        Test that action=remove-attachment works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        resourceID = object1.id()
+        attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
+        managedID = attachment.managedID()
+        yield self.commit()
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        yield shared_object.removeAttachment(None, managedID)
+        yield self.otherCommit()
+
+        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+        self.assertEqual(cobjs, set())
+        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+        self.assertTrue(attachment is None)
+        yield self.commit()

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,99 +0,0 @@
-##
-# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twisted.internet.defer import inlineCallbacks
-
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, \
-    Server
-from txdav.caldav.datastore.test.util import buildCalendarStore, \
-    TestCalendarStoreDirectoryRecord
-from txdav.common.datastore.podding.resource import ConduitResource
-from txdav.common.datastore.sql_tables import _HOME_STATUS_NORMAL, \
-    _HOME_STATUS_EXTERNAL
-from txdav.common.datastore.test.util import CommonCommonTests
-from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-
-import twext.web2.dav.test.util
-
-
-class ExternalHome(CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
-    @inlineCallbacks
-    def setUp(self):
-        yield super(ExternalHome, self).setUp()
-        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
-        self.directory = self._sqlCalendarStore.directoryService()
-
-        for ctr in range(1, 100):
-            self.directory.addRecord(TestCalendarStoreDirectoryRecord(
-                "puser{:02d}".format(ctr),
-                ("puser{:02d}".format(ctr),),
-                "Puser {:02d}".format(ctr),
-                frozenset((
-                    "urn:uuid:puser{:02d}".format(ctr),
-                    "mailto:puser{:02d}@example.com".format(ctr),
-                )),
-                thisServer=False,
-            ))
-
-        self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
-
-        self.thisServer = Server("A", "http://127.0.0.1", "A", True)
-        Servers.addServer(self.thisServer)
-
-
-    def storeUnderTest(self):
-        """
-        Return a store for testing.
-        """
-        return self._sqlCalendarStore
-
-
-    @inlineCallbacks
-    def test_validNormalHome(self):
-        """
-        Locally hosted homes are valid.
-        """
-
-        for i in range(1, 100):
-            home = yield self.transactionUnderTest().calendarHomeWithUID("user{:02d}".format(i), create=True)
-            self.assertTrue(home is not None)
-            self.assertEqual(home._status, _HOME_STATUS_NORMAL)
-            calendar = yield home.childWithName("calendar")
-            self.assertTrue(calendar is not None)
-
-
-    @inlineCallbacks
-    def test_validExternalHome(self):
-        """
-        Externally hosted homes are valid.
-        """
-
-        for i in range(1, 100):
-            home = yield self.transactionUnderTest().calendarHomeWithUID("puser{:02d}".format(i), create=True)
-            self.assertTrue(home is not None)
-            self.assertEqual(home._status, _HOME_STATUS_EXTERNAL)
-            calendar = yield home.childWithName("calendar")
-            self.assertTrue(calendar is None)
-
-
-    @inlineCallbacks
-    def test_invalidHome(self):
-        """
-        Homes are invalid.
-        """
-
-        yield self.assertFailure(self.transactionUnderTest().calendarHomeWithUID("buser01", create=True), DirectoryRecordNotFoundError)

Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_external_home.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,99 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twisted.internet.defer import inlineCallbacks
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, \
+    Server
+from txdav.caldav.datastore.test.util import buildCalendarStore, \
+    TestCalendarStoreDirectoryRecord
+from txdav.common.datastore.podding.resource import ConduitResource
+from txdav.common.datastore.sql_tables import _HOME_STATUS_NORMAL, \
+    _HOME_STATUS_EXTERNAL
+from txdav.common.datastore.test.util import CommonCommonTests
+from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+
+import txweb2.dav.test.util
+
+
+class ExternalHome(CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(ExternalHome, self).setUp()
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+        self.directory = self._sqlCalendarStore.directoryService()
+
+        for ctr in range(1, 100):
+            self.directory.addRecord(TestCalendarStoreDirectoryRecord(
+                "puser{:02d}".format(ctr),
+                ("puser{:02d}".format(ctr),),
+                "Puser {:02d}".format(ctr),
+                frozenset((
+                    "urn:uuid:puser{:02d}".format(ctr),
+                    "mailto:puser{:02d}@example.com".format(ctr),
+                )),
+                thisServer=False,
+            ))
+
+        self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
+
+        self.thisServer = Server("A", "http://127.0.0.1", "A", True)
+        Servers.addServer(self.thisServer)
+
+
+    def storeUnderTest(self):
+        """
+        Return a store for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def test_validNormalHome(self):
+        """
+        Locally hosted homes are valid.
+        """
+
+        for i in range(1, 100):
+            home = yield self.transactionUnderTest().calendarHomeWithUID("user{:02d}".format(i), create=True)
+            self.assertTrue(home is not None)
+            self.assertEqual(home._status, _HOME_STATUS_NORMAL)
+            calendar = yield home.childWithName("calendar")
+            self.assertTrue(calendar is not None)
+
+
+    @inlineCallbacks
+    def test_validExternalHome(self):
+        """
+        Externally hosted homes are valid.
+        """
+
+        for i in range(1, 100):
+            home = yield self.transactionUnderTest().calendarHomeWithUID("puser{:02d}".format(i), create=True)
+            self.assertTrue(home is not None)
+            self.assertEqual(home._status, _HOME_STATUS_EXTERNAL)
+            calendar = yield home.childWithName("calendar")
+            self.assertTrue(calendar is None)
+
+
+    @inlineCallbacks
+    def test_invalidHome(self):
+        """
+        Homes are invalid.
+        """
+
+        yield self.assertFailure(self.transactionUnderTest().calendarHomeWithUID("buser01", create=True), DirectoryRecordNotFoundError)

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,275 +0,0 @@
-##
-# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twext.python.clsprop import classproperty
-from twext.web2 import http_headers, responsecode
-import twext.web2.dav.test.util
-from twext.web2.dav.util import allDataFromStream
-from twext.web2.test.test_server import SimpleRequest
-from twisted.internet.defer import inlineCallbacks, succeed
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
-from txdav.caldav.datastore.test.util import buildCalendarStore
-from txdav.common.datastore.podding.resource import ConduitResource
-from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
-import json
-from txdav.common.datastore.podding.conduit import PoddingConduit
-
-class ConduitPOST (CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
-    class FakeConduit(PoddingConduit):
-
-        def recv_fake(self, txn, j):
-            return succeed({
-                "result": "ok",
-                "back2u": j["echo"],
-                "more": "bits",
-            })
-
-
-    @inlineCallbacks
-    def setUp(self):
-        yield super(ConduitPOST, self).setUp()
-        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
-        self.directory = self._sqlCalendarStore.directoryService()
-
-        self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
-
-        self.thisServer = Server("A", "http://127.0.0.1", "A", True)
-        Servers.addServer(self.thisServer)
-
-        yield self.populate()
-
-
-    def storeUnderTest(self):
-        """
-        Return a store for testing.
-        """
-        return self._sqlCalendarStore
-
-
-    @inlineCallbacks
-    def populate(self):
-        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
-        self.notifierFactory.reset()
-
-
-    @classproperty(cache=False)
-    def requirements(cls): #@NoSelf
-        return {
-        "user01": {
-            "calendar_1": {
-            },
-            "inbox": {
-            },
-        },
-        "user02": {
-            "calendar_1": {
-            },
-            "inbox": {
-            },
-        },
-        "user03": {
-            "calendar_1": {
-            },
-            "inbox": {
-            },
-        },
-    }
-
-
-    @inlineCallbacks
-    def test_receive_no_secret(self):
-        """
-        Cross-pod request fails when there is no shared secret header present.
-        """
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/conduit",
-            headers=http_headers.Headers(rawHeaders={
-                "Content-Type": ("text/plain",)
-            }),
-            content="""Hello, World!
-""".replace("\n", "\r\n")
-        )
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.FORBIDDEN)
-
-
-    @inlineCallbacks
-    def test_receive_wrong_mime(self):
-        """
-        Cross-pod request fails when Content-Type header is wrong.
-        """
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/conduit",
-            headers=http_headers.Headers(rawHeaders={
-                "Content-Type": ("text/plain",),
-                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
-            }),
-            content="""Hello, World!
-""".replace("\n", "\r\n")
-        )
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
-    @inlineCallbacks
-    def test_receive_invalid_json(self):
-        """
-        Cross-pod request fails when request data is not JSON.
-        """
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/conduit",
-            headers=http_headers.Headers(rawHeaders={
-                "Content-Type": ("application/json",),
-                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
-            }),
-            content="""Hello, World!
-""".replace("\n", "\r\n")
-        )
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
-    @inlineCallbacks
-    def test_receive_bad_json(self):
-        """
-        Cross-pod request fails when JSON data does not have an "action".
-        """
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/conduit",
-            headers=http_headers.Headers(rawHeaders={
-                "Content-Type": ("application/json",),
-                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
-            }),
-            content="""
-{
-    "foo":"bar"
-}
-""".replace("\n", "\r\n")
-        )
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
-    @inlineCallbacks
-    def test_receive_ping(self):
-        """
-        Cross-pod request works with the "ping" action.
-        """
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/conduit",
-            headers=http_headers.Headers(rawHeaders={
-                "Content-Type": ("application/json",),
-                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
-            }),
-            content="""
-{
-    "action":"ping"
-}
-""".replace("\n", "\r\n")
-        )
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.OK)
-        data = (yield allDataFromStream(response.stream))
-        j = json.loads(data)
-        self.assertTrue("result" in j)
-        self.assertEqual(j["result"], "ok")
-
-
-    @inlineCallbacks
-    def test_receive_fake_conduit_no_action(self):
-        """
-        Cross-pod request fails when conduit does not support the action.
-        """
-
-        store = self.storeUnderTest()
-        self.patch(store, "conduit", self.FakeConduit(store))
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/conduit",
-            headers=http_headers.Headers(rawHeaders={
-                "Content-Type": ("application/json",),
-                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
-            }),
-            content="""
-{
-    "action":"bogus",
-    "echo":"bravo"
-}
-""".replace("\n", "\r\n")
-        )
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.BAD_REQUEST)
-
-
-    @inlineCallbacks
-    def test_receive_fake_conduit(self):
-        """
-        Cross-pod request works when conduit does support the action.
-        """
-
-        store = self.storeUnderTest()
-        self.patch(store, "conduit", self.FakeConduit(store))
-
-        request = SimpleRequest(
-            self.site,
-            "POST",
-            "/conduit",
-            headers=http_headers.Headers(rawHeaders={
-                "Content-Type": ("application/json",),
-                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
-            }),
-            content="""
-{
-    "action":"fake",
-    "echo":"bravo"
-}
-""".replace("\n", "\r\n")
-        )
-
-        response = (yield self.send(request))
-        self.assertEqual(response.code, responsecode.OK)
-        data = (yield allDataFromStream(response.stream))
-        j = json.loads(data)
-        self.assertTrue("result" in j)
-        self.assertEqual(j["result"], "ok")
-        self.assertTrue("back2u" in j)
-        self.assertEqual(j["back2u"], "bravo")
-        self.assertTrue("more" in j)
-        self.assertEqual(j["more"], "bits")

Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_resource.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,278 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twext.python.clsprop import classproperty
+
+import txweb2.dav.test.util
+from txweb2 import http_headers, responsecode
+from txweb2.dav.util import allDataFromStream
+from txweb2.test.test_server import SimpleRequest
+
+from twisted.internet.defer import inlineCallbacks, succeed
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
+from txdav.caldav.datastore.test.util import buildCalendarStore
+from txdav.common.datastore.podding.resource import ConduitResource
+from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+import json
+from txdav.common.datastore.podding.conduit import PoddingConduit
+
+class ConduitPOST (CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+    class FakeConduit(PoddingConduit):
+
+        def recv_fake(self, txn, j):
+            return succeed({
+                "result": "ok",
+                "back2u": j["echo"],
+                "more": "bits",
+            })
+
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(ConduitPOST, self).setUp()
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+        self.directory = self._sqlCalendarStore.directoryService()
+
+        self.site.resource.putChild("conduit", ConduitResource(self.site.resource, self.storeUnderTest()))
+
+        self.thisServer = Server("A", "http://127.0.0.1", "A", True)
+        Servers.addServer(self.thisServer)
+
+        yield self.populate()
+
+
+    def storeUnderTest(self):
+        """
+        Return a store for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        return {
+        "user01": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user02": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user03": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+    }
+
+
+    @inlineCallbacks
+    def test_receive_no_secret(self):
+        """
+        Cross-pod request fails when there is no shared secret header present.
+        """
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/conduit",
+            headers=http_headers.Headers(rawHeaders={
+                "Content-Type": ("text/plain",)
+            }),
+            content="""Hello, World!
+""".replace("\n", "\r\n")
+        )
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.FORBIDDEN)
+
+
+    @inlineCallbacks
+    def test_receive_wrong_mime(self):
+        """
+        Cross-pod request fails when Content-Type header is wrong.
+        """
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/conduit",
+            headers=http_headers.Headers(rawHeaders={
+                "Content-Type": ("text/plain",),
+                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+            }),
+            content="""Hello, World!
+""".replace("\n", "\r\n")
+        )
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+    @inlineCallbacks
+    def test_receive_invalid_json(self):
+        """
+        Cross-pod request fails when request data is not JSON.
+        """
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/conduit",
+            headers=http_headers.Headers(rawHeaders={
+                "Content-Type": ("application/json",),
+                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+            }),
+            content="""Hello, World!
+""".replace("\n", "\r\n")
+        )
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+    @inlineCallbacks
+    def test_receive_bad_json(self):
+        """
+        Cross-pod request fails when JSON data does not have an "action".
+        """
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/conduit",
+            headers=http_headers.Headers(rawHeaders={
+                "Content-Type": ("application/json",),
+                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+            }),
+            content="""
+{
+    "foo":"bar"
+}
+""".replace("\n", "\r\n")
+        )
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+    @inlineCallbacks
+    def test_receive_ping(self):
+        """
+        Cross-pod request works with the "ping" action.
+        """
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/conduit",
+            headers=http_headers.Headers(rawHeaders={
+                "Content-Type": ("application/json",),
+                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+            }),
+            content="""
+{
+    "action":"ping"
+}
+""".replace("\n", "\r\n")
+        )
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.OK)
+        data = (yield allDataFromStream(response.stream))
+        j = json.loads(data)
+        self.assertTrue("result" in j)
+        self.assertEqual(j["result"], "ok")
+
+
+    @inlineCallbacks
+    def test_receive_fake_conduit_no_action(self):
+        """
+        Cross-pod request fails when conduit does not support the action.
+        """
+
+        store = self.storeUnderTest()
+        self.patch(store, "conduit", self.FakeConduit(store))
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/conduit",
+            headers=http_headers.Headers(rawHeaders={
+                "Content-Type": ("application/json",),
+                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+            }),
+            content="""
+{
+    "action":"bogus",
+    "echo":"bravo"
+}
+""".replace("\n", "\r\n")
+        )
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.BAD_REQUEST)
+
+
+    @inlineCallbacks
+    def test_receive_fake_conduit(self):
+        """
+        Cross-pod request works when conduit does support the action.
+        """
+
+        store = self.storeUnderTest()
+        self.patch(store, "conduit", self.FakeConduit(store))
+
+        request = SimpleRequest(
+            self.site,
+            "POST",
+            "/conduit",
+            headers=http_headers.Headers(rawHeaders={
+                "Content-Type": ("application/json",),
+                self.thisServer.secretHeader()[0]: self.thisServer.secretHeader()[1],
+            }),
+            content="""
+{
+    "action":"fake",
+    "echo":"bravo"
+}
+""".replace("\n", "\r\n")
+        )
+
+        response = (yield self.send(request))
+        self.assertEqual(response.code, responsecode.OK)
+        data = (yield allDataFromStream(response.stream))
+        j = json.loads(data)
+        self.assertTrue("result" in j)
+        self.assertEqual(j["result"], "ok")
+        self.assertTrue("back2u" in j)
+        self.assertEqual(j["back2u"], "bravo")
+        self.assertTrue("more" in j)
+        self.assertEqual(j["more"], "bits")

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,237 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Server, \
-    Servers
-from txdav.caldav.datastore.test.util import \
-    TestCalendarStoreDirectoryRecord, TestCalendarStoreDirectoryService
-from txdav.common.datastore.podding.conduit import PoddingConduit
-from txdav.common.datastore.test.util import CommonCommonTests, SQLStoreBuilder,\
-    theStoreBuilder
-
-import twext.web2.dav.test.util
-from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
-from twext.enterprise.ienterprise import AlreadyFinishedError
-import json
-
-class FakeConduitRequest(object):
-    """
-    A conduit request that sends messages internally rather than using HTTP
-    """
-
-    storeMap = {}
-
-    @classmethod
-    def addServerStore(cls, server, store):
-        """
-        Add a store mapped to a server. These mappings are used to "deliver" conduit
-        requests to the appropriate store.
-
-        @param uri: the server
-        @type uri: L{Server}
-        @param store: the store
-        @type store: L{ICommonDataStore}
-        """
-
-        cls.storeMap[server.details()] = store
-
-
-    def __init__(self, server, data, stream=None, stream_type=None):
-
-        self.server = server
-        self.data = json.dumps(data)
-        self.stream = stream
-        self.streamType = stream_type
-
-
-    @inlineCallbacks
-    def doRequest(self, txn):
-
-        # Generate an HTTP client request
-        try:
-            response = (yield self._processRequest())
-            response = json.loads(response)
-        except Exception as e:
-            raise ValueError("Failed cross-pod request: {}".format(e))
-
-        returnValue(response)
-
-
-    @inlineCallbacks
-    def _processRequest(self):
-        """
-        Process the request by sending it to the relevant server.
-
-        @return: the HTTP response.
-        @rtype: L{Response}
-        """
-
-        store = self.storeMap[self.server.details()]
-        j = json.loads(self.data)
-        if self.stream is not None:
-            j["stream"] = self.stream
-            j["streamType"] = self.streamType
-        result = yield store.conduit.processRequest(j)
-        result = json.dumps(result)
-        returnValue(result)
-
-
-
-class MultiStoreConduitTest(CommonCommonTests, twext.web2.dav.test.util.TestCase):
-
-    theStoreBuilder2 = SQLStoreBuilder(secondary=True)
-    otherTransaction = None
-
-    @inlineCallbacks
-    def setUp(self):
-        yield super(MultiStoreConduitTest, self).setUp()
-
-        server1 = Server("A", "http://127.0.0.1:8008", "A", True)
-        Servers.addServer(server1)
-
-        server2 = Server("B", "http://127.0.0.1:8108", "B", False)
-        Servers.addServer(server2)
-
-        self._sqlCalendarStore1 = yield self.makeStore(theStoreBuilder, True, server1, server2)
-        self._sqlCalendarStore2 = yield self.makeStore(self.theStoreBuilder2, False, server1, server2)
-
-        FakeConduitRequest.addServerStore(server1, self._sqlCalendarStore1)
-        FakeConduitRequest.addServerStore(server2, self._sqlCalendarStore2)
-
-
-    def storeUnderTest(self):
-        """
-        Return a store for testing.
-        """
-        return self._sqlCalendarStore1
-
-
-    def otherStoreUnderTest(self):
-        """
-        Return a store for testing.
-        """
-        return self._sqlCalendarStore2
-
-
-    def newOtherTransaction(self):
-        assert self.otherTransaction is None
-        store2 = self.otherStoreUnderTest()
-        txn = store2.newTransaction()
-        @inlineCallbacks
-        def maybeCommitThis():
-            try:
-                yield txn.commit()
-            except AlreadyFinishedError:
-                pass
-        self.addCleanup(maybeCommitThis)
-        self.otherTransaction = txn
-        return self.otherTransaction
-
-
-    def otherTransactionUnderTest(self):
-        if self.otherTransaction is None:
-            self.newOtherTransaction()
-        return self.otherTransaction
-
-
-    @inlineCallbacks
-    def otherCommit(self):
-        assert self.otherTransaction is not None
-        yield self.otherTransaction.commit()
-        self.otherTransaction = None
-
-
-    @inlineCallbacks
-    def otherAbort(self):
-        assert self.otherTransaction is not None
-        yield self.otherTransaction.abort()
-        self.otherTransaction = None
-
-
-    @inlineCallbacks
-    def makeStore(self, builder, internal, server1, server2):
-
-        directory = self.makeDirectory(internal, server1, server2)
-        store = yield builder.buildStore(self, self.notifierFactory, directory)
-        store.queryCacher = None     # Cannot use query caching
-        store.conduit = self.makeConduit(store)
-        returnValue(store)
-
-
-    def makeDirectory(self, internal, server1, server2):
-
-        directory = TestCalendarStoreDirectoryService()
-
-        # User accounts
-        for ctr in range(1, 100):
-            directory.addRecord(TestCalendarStoreDirectoryRecord(
-                "user%02d" % (ctr,),
-                ("user%02d" % (ctr,),),
-                "User %02d" % (ctr,),
-                frozenset((
-                    "urn:uuid:user%02d" % (ctr,),
-                    "mailto:user%02d at example.com" % (ctr,),
-                )),
-                thisServer=internal,
-                server=server1
-            ))
-
-        for ctr in range(1, 100):
-            directory.addRecord(TestCalendarStoreDirectoryRecord(
-                "puser{:02d}".format(ctr),
-                ("puser{:02d}".format(ctr),),
-                "Puser {:02d}".format(ctr),
-                frozenset((
-                    "urn:uuid:puser{:02d}".format(ctr),
-                    "mailto:puser{:02d}@example.com".format(ctr),
-                )),
-                thisServer=not internal,
-                server=server2
-            ))
-
-        return directory
-
-
-    def makeConduit(self, store):
-        conduit = PoddingConduit(store)
-        conduit.conduitRequestClass = FakeConduitRequest
-        return conduit
-
-
-    @inlineCallbacks
-    def createShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
-
-        home = yield self.homeUnderTest(name=ownerGUID, create=True)
-        calendar = yield home.calendarWithName(name)
-        yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
-        yield self.commit()
-
-        home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
-        yield home2.acceptShare("shared-calendar")
-        yield self.otherCommit()
-
-        returnValue("shared-calendar")
-
-
-    @inlineCallbacks
-    def removeShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
-
-        home = yield self.homeUnderTest(name=ownerGUID)
-        calendar = yield home.calendarWithName(name)
-        yield calendar.uninviteUserFromShare(shareeGUID)
-        yield self.commit()

Copied: CalendarServer/trunk/txdav/common/datastore/podding/test/util.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/util.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,239 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Server, \
+    Servers
+from txdav.caldav.datastore.test.util import \
+    TestCalendarStoreDirectoryRecord, TestCalendarStoreDirectoryService
+from txdav.common.datastore.podding.conduit import PoddingConduit
+from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
+from txdav.common.datastore.test.util import CommonCommonTests, SQLStoreBuilder,\
+    theStoreBuilder
+
+import txweb2.dav.test.util
+
+from twext.enterprise.ienterprise import AlreadyFinishedError
+
+import json
+
+class FakeConduitRequest(object):
+    """
+    A conduit request that sends messages internally rather than using HTTP
+    """
+
+    storeMap = {}
+
+    @classmethod
+    def addServerStore(cls, server, store):
+        """
+        Add a store mapped to a server. These mappings are used to "deliver" conduit
+        requests to the appropriate store.
+
+        @param uri: the server
+        @type uri: L{Server}
+        @param store: the store
+        @type store: L{ICommonDataStore}
+        """
+
+        cls.storeMap[server.details()] = store
+
+
+    def __init__(self, server, data, stream=None, stream_type=None):
+
+        self.server = server
+        self.data = json.dumps(data)
+        self.stream = stream
+        self.streamType = stream_type
+
+
+    @inlineCallbacks
+    def doRequest(self, txn):
+
+        # Generate an HTTP client request
+        try:
+            response = (yield self._processRequest())
+            response = json.loads(response)
+        except Exception as e:
+            raise ValueError("Failed cross-pod request: {}".format(e))
+
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def _processRequest(self):
+        """
+        Process the request by sending it to the relevant server.
+
+        @return: the HTTP response.
+        @rtype: L{Response}
+        """
+
+        store = self.storeMap[self.server.details()]
+        j = json.loads(self.data)
+        if self.stream is not None:
+            j["stream"] = self.stream
+            j["streamType"] = self.streamType
+        result = yield store.conduit.processRequest(j)
+        result = json.dumps(result)
+        returnValue(result)
+
+
+
+class MultiStoreConduitTest(CommonCommonTests, txweb2.dav.test.util.TestCase):
+
+    theStoreBuilder2 = SQLStoreBuilder(secondary=True)
+    otherTransaction = None
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(MultiStoreConduitTest, self).setUp()
+
+        server1 = Server("A", "http://127.0.0.1:8008", "A", True)
+        Servers.addServer(server1)
+
+        server2 = Server("B", "http://127.0.0.1:8108", "B", False)
+        Servers.addServer(server2)
+
+        self._sqlCalendarStore1 = yield self.makeStore(theStoreBuilder, True, server1, server2)
+        self._sqlCalendarStore2 = yield self.makeStore(self.theStoreBuilder2, False, server1, server2)
+
+        FakeConduitRequest.addServerStore(server1, self._sqlCalendarStore1)
+        FakeConduitRequest.addServerStore(server2, self._sqlCalendarStore2)
+
+
+    def storeUnderTest(self):
+        """
+        Return a store for testing.
+        """
+        return self._sqlCalendarStore1
+
+
+    def otherStoreUnderTest(self):
+        """
+        Return a store for testing.
+        """
+        return self._sqlCalendarStore2
+
+
+    def newOtherTransaction(self):
+        assert self.otherTransaction is None
+        store2 = self.otherStoreUnderTest()
+        txn = store2.newTransaction()
+        @inlineCallbacks
+        def maybeCommitThis():
+            try:
+                yield txn.commit()
+            except AlreadyFinishedError:
+                pass
+        self.addCleanup(maybeCommitThis)
+        self.otherTransaction = txn
+        return self.otherTransaction
+
+
+    def otherTransactionUnderTest(self):
+        if self.otherTransaction is None:
+            self.newOtherTransaction()
+        return self.otherTransaction
+
+
+    @inlineCallbacks
+    def otherCommit(self):
+        assert self.otherTransaction is not None
+        yield self.otherTransaction.commit()
+        self.otherTransaction = None
+
+
+    @inlineCallbacks
+    def otherAbort(self):
+        assert self.otherTransaction is not None
+        yield self.otherTransaction.abort()
+        self.otherTransaction = None
+
+
+    @inlineCallbacks
+    def makeStore(self, builder, internal, server1, server2):
+
+        directory = self.makeDirectory(internal, server1, server2)
+        store = yield builder.buildStore(self, self.notifierFactory, directory)
+        store.queryCacher = None     # Cannot use query caching
+        store.conduit = self.makeConduit(store)
+        returnValue(store)
+
+
+    def makeDirectory(self, internal, server1, server2):
+
+        directory = TestCalendarStoreDirectoryService()
+
+        # User accounts
+        for ctr in range(1, 100):
+            directory.addRecord(TestCalendarStoreDirectoryRecord(
+                "user%02d" % (ctr,),
+                ("user%02d" % (ctr,),),
+                "User %02d" % (ctr,),
+                frozenset((
+                    "urn:uuid:user%02d" % (ctr,),
+                    "mailto:user%02d at example.com" % (ctr,),
+                )),
+                thisServer=internal,
+                server=server1
+            ))
+
+        for ctr in range(1, 100):
+            directory.addRecord(TestCalendarStoreDirectoryRecord(
+                "puser{:02d}".format(ctr),
+                ("puser{:02d}".format(ctr),),
+                "Puser {:02d}".format(ctr),
+                frozenset((
+                    "urn:uuid:puser{:02d}".format(ctr),
+                    "mailto:puser{:02d}@example.com".format(ctr),
+                )),
+                thisServer=not internal,
+                server=server2
+            ))
+
+        return directory
+
+
+    def makeConduit(self, store):
+        conduit = PoddingConduit(store)
+        conduit.conduitRequestClass = FakeConduitRequest
+        return conduit
+
+
+    @inlineCallbacks
+    def createShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
+
+        home = yield self.homeUnderTest(name=ownerGUID, create=True)
+        calendar = yield home.calendarWithName(name)
+        yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
+        yield self.commit()
+
+        home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
+        yield home2.acceptShare("shared-calendar")
+        yield self.otherCommit()
+
+        returnValue("shared-calendar")
+
+
+    @inlineCallbacks
+    def removeShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
+
+        home = yield self.homeUnderTest(name=ownerGUID)
+        calendar = yield home.calendarWithName(name)
+        yield calendar.uninviteUserFromShare(shareeGUID)
+        yield self.commit()

Deleted: CalendarServer/trunk/txdav/common/datastore/query/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/common/datastore/query/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/common/datastore/query/expression.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/expression.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/expression.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,382 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-"""
-Query Expression Elements. These are used to build a 'generic' query
-expression tree that can then be used by different query language
-generators to produce the actual query syntax required (SQL, xpath
-etc).
-"""
-
-__all__ = [
-    "allExpression",
-    "notExpression",
-    "andExpression",
-    "orExpression",
-    "timerangeExpression",
-    "textcompareExpression",
-    "containsExpression",
-    "notcontainsExpression",
-    "isExpression",
-    "isnotExpression",
-    "startswithExpression",
-    "notstartswithExpression",
-    "endswithExpression",
-    "notendswithExpression",
-    "inExpression",
-    "notinExpression",
-]
-
-class baseExpression(object):
-    """
-    The base class for all types of expression.
-    """
-
-    def __init__(self):
-        pass
-
-
-    def multi(self):
-        """
-        Indicate whether this expression is composed of multiple sub-expressions.
-
-        @return: C{True} if this expressions contains multiple sub-expressions,
-            C{False} otherwise.
-        """
-
-        return False
-
-
-    def _collapsedExpression(self):
-        return self
-
-
-    def andWith(self, other):
-        if isinstance(other, andExpression):
-            return andExpression((self._collapsedExpression(),) + tuple(other.expressions))
-        else:
-            return andExpression((self._collapsedExpression(), other._collapsedExpression(),))
-
-
-    def orWith(self, other):
-        if isinstance(other, orExpression):
-            return orExpression((self._collapsedExpression(),) + tuple(other.expressions))
-        else:
-            return orExpression((self._collapsedExpression(), other._collapsedExpression(),))
-
-
-
-class allExpression(baseExpression):
-    """
-    Match everything.
-    """
-
-    def __init__(self):
-        pass
-
-
-
-class logicExpression(baseExpression):
-    """
-    An expression representing a logical operation (boolean).
-    """
-
-    def __init__(self, expressions):
-        self.expressions = expressions
-
-
-    def __str__(self):
-        """
-        Generate a suitable text descriptor of this expression.
-
-        @return: a C{str} of the text for this expression.
-        """
-
-        result = ""
-        for e in self.expressions:
-            if len(result) != 0:
-                result += " " + self.operator() + " "
-            result += str(e)
-        if len(result):
-            result = "(" + result + ")"
-        return result
-
-
-    def multi(self):
-        """
-        Indicate whether this expression is composed of multiple expressions.
-
-        @return: C{True} if this expressions contains multiple sub-expressions,
-            C{False} otherwise.
-        """
-
-        return True
-
-
-    def _collapsedExpression(self):
-        if self.multi() and len(self.expressions) == 1:
-            return self.expressions[0]._collapsedExpression()
-        else:
-            return self
-
-
-
-class notExpression(logicExpression):
-    """
-    Logical NOT operation.
-    """
-
-    def __init__(self, expression):
-        super(notExpression, self).__init__([expression])
-
-
-    def operator(self):
-        return "NOT"
-
-
-    def __str__(self):
-        result = self.operator() + " " + str(self.expressions[0])
-        return result
-
-
-    def multi(self):
-        """
-        Indicate whether this expression is composed of multiple expressions.
-
-        @return: C{True} if this expressions contains multiple sub-expressions,
-            C{False} otherwise.
-        """
-
-        return False
-
-
-
-class andExpression(logicExpression):
-    """
-    Logical AND operation.
-    """
-
-    def __init__(self, expressions):
-        super(andExpression, self).__init__(expressions)
-
-
-    def operator(self):
-        return "AND"
-
-
-    def andWith(self, other):
-        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
-        return self
-
-
-
-class orExpression(logicExpression):
-    """
-    Logical OR operation.
-    """
-
-    def __init__(self, expressions):
-        super(orExpression, self).__init__(expressions)
-
-
-    def operator(self):
-        return "OR"
-
-
-    def orWith(self, other):
-        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
-        return self
-
-
-
-class timerangeExpression(baseExpression):
-    """
-    CalDAV time-range comparison expression.
-    """
-
-    def __init__(self, start, end, startfloat, endfloat):
-        self.start = start
-        self.end = end
-        self.startfloat = startfloat
-        self.endfloat = endfloat
-
-
-    def __str__(self):
-        return "timerange(" + str(self.start) + ", " + str(self.end) + ")"
-
-
-
-class textcompareExpression(baseExpression):
-    """
-    Base class for text comparison expressions.
-    """
-
-    def __init__(self, field, text, caseless):
-        self.field = field
-        self.text = text
-        self.caseless = caseless
-
-
-    def __str__(self):
-        return self.operator() + "(" + self.field + ", " + self.text + ", " + str(self.caseless) + ")"
-
-
-
-class containsExpression(textcompareExpression):
-    """
-    Text CONTAINS (sub-string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(containsExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "contains"
-
-
-
-class notcontainsExpression(textcompareExpression):
-    """
-    Text NOT CONTAINS (sub-string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(notcontainsExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "does not contain"
-
-
-
-class isExpression(textcompareExpression):
-    """
-    Text IS (exact string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(isExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "is"
-
-
-
-class isnotExpression(textcompareExpression):
-    """
-    Text IS NOT (exact string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(isnotExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "is not"
-
-
-
-class startswithExpression(textcompareExpression):
-    """
-    Text STARTSWITH (sub-string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(startswithExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "starts with"
-
-
-
-class notstartswithExpression(textcompareExpression):
-    """
-    Text NOT STARTSWITH (sub-string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(notstartswithExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "does not start with"
-
-
-
-class endswithExpression(textcompareExpression):
-    """
-    Text STARTSWITH (sub-string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(endswithExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "ends with"
-
-
-
-class notendswithExpression(textcompareExpression):
-    """
-    Text NOT STARTSWITH (sub-string match) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(notendswithExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "does not end with"
-
-
-
-class inExpression(textcompareExpression):
-    """
-    Text IN (exact string match to one of the supplied items) expression.
-    """
-
-    def __init__(self, field, text_list, caseless):
-        super(inExpression, self).__init__(field, text_list, caseless)
-
-
-    def operator(self):
-        return "in"
-
-
-    def __str__(self):
-        return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"
-
-
-
-class notinExpression(textcompareExpression):
-    """
-    Text NOT IN (exact string match to none of the supplied items) expression.
-    """
-
-    def __init__(self, field, text, caseless):
-        super(notinExpression, self).__init__(field, text, caseless)
-
-
-    def operator(self):
-        return "not in"
-
-
-    def __str__(self):
-        return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"

Copied: CalendarServer/trunk/txdav/common/datastore/query/expression.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/expression.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/expression.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/expression.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,382 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+"""
+Query Expression Elements. These are used to build a 'generic' query
+expression tree that can then be used by different query language
+generators to produce the actual query syntax required (SQL, xpath
+etc).
+"""
+
+__all__ = [
+    "allExpression",
+    "notExpression",
+    "andExpression",
+    "orExpression",
+    "timerangeExpression",
+    "textcompareExpression",
+    "containsExpression",
+    "notcontainsExpression",
+    "isExpression",
+    "isnotExpression",
+    "startswithExpression",
+    "notstartswithExpression",
+    "endswithExpression",
+    "notendswithExpression",
+    "inExpression",
+    "notinExpression",
+]
+
+class baseExpression(object):
+    """
+    The base class for all types of expression.
+    """
+
+    def __init__(self):
+        pass
+
+
+    def multi(self):
+        """
+        Indicate whether this expression is composed of multiple sub-expressions.
+
+        @return: C{True} if this expressions contains multiple sub-expressions,
+            C{False} otherwise.
+        """
+
+        return False
+
+
+    def _collapsedExpression(self):
+        return self
+
+
+    def andWith(self, other):
+        if isinstance(other, andExpression):
+            return andExpression((self._collapsedExpression(),) + tuple(other.expressions))
+        else:
+            return andExpression((self._collapsedExpression(), other._collapsedExpression(),))
+
+
+    def orWith(self, other):
+        if isinstance(other, orExpression):
+            return orExpression((self._collapsedExpression(),) + tuple(other.expressions))
+        else:
+            return orExpression((self._collapsedExpression(), other._collapsedExpression(),))
+
+
+
+class allExpression(baseExpression):
+    """
+    Match everything.
+    """
+
+    def __init__(self):
+        pass
+
+
+
+class logicExpression(baseExpression):
+    """
+    An expression representing a logical operation (boolean).
+    """
+
+    def __init__(self, expressions):
+        self.expressions = expressions
+
+
+    def __str__(self):
+        """
+        Generate a suitable text descriptor of this expression.
+
+        @return: a C{str} of the text for this expression.
+        """
+
+        result = ""
+        for e in self.expressions:
+            if len(result) != 0:
+                result += " " + self.operator() + " "
+            result += str(e)
+        if len(result):
+            result = "(" + result + ")"
+        return result
+
+
+    def multi(self):
+        """
+        Indicate whether this expression is composed of multiple expressions.
+
+        @return: C{True} if this expressions contains multiple sub-expressions,
+            C{False} otherwise.
+        """
+
+        return True
+
+
+    def _collapsedExpression(self):
+        if self.multi() and len(self.expressions) == 1:
+            return self.expressions[0]._collapsedExpression()
+        else:
+            return self
+
+
+
+class notExpression(logicExpression):
+    """
+    Logical NOT operation.
+    """
+
+    def __init__(self, expression):
+        super(notExpression, self).__init__([expression])
+
+
+    def operator(self):
+        return "NOT"
+
+
+    def __str__(self):
+        result = self.operator() + " " + str(self.expressions[0])
+        return result
+
+
+    def multi(self):
+        """
+        Indicate whether this expression is composed of multiple expressions.
+
+        @return: C{True} if this expressions contains multiple sub-expressions,
+            C{False} otherwise.
+        """
+
+        return False
+
+
+
+class andExpression(logicExpression):
+    """
+    Logical AND operation.
+    """
+
+    def __init__(self, expressions):
+        super(andExpression, self).__init__(expressions)
+
+
+    def operator(self):
+        return "AND"
+
+
+    def andWith(self, other):
+        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
+        return self
+
+
+
+class orExpression(logicExpression):
+    """
+    Logical OR operation.
+    """
+
+    def __init__(self, expressions):
+        super(orExpression, self).__init__(expressions)
+
+
+    def operator(self):
+        return "OR"
+
+
+    def orWith(self, other):
+        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
+        return self
+
+
+
+class timerangeExpression(baseExpression):
+    """
+    CalDAV time-range comparison expression.
+    """
+
+    def __init__(self, start, end, startfloat, endfloat):
+        self.start = start
+        self.end = end
+        self.startfloat = startfloat
+        self.endfloat = endfloat
+
+
+    def __str__(self):
+        return "timerange(" + str(self.start) + ", " + str(self.end) + ")"
+
+
+
+class textcompareExpression(baseExpression):
+    """
+    Base class for text comparison expressions.
+    """
+
+    def __init__(self, field, text, caseless):
+        self.field = field
+        self.text = text
+        self.caseless = caseless
+
+
+    def __str__(self):
+        return self.operator() + "(" + self.field + ", " + self.text + ", " + str(self.caseless) + ")"
+
+
+
+class containsExpression(textcompareExpression):
+    """
+    Text CONTAINS (sub-string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(containsExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "contains"
+
+
+
+class notcontainsExpression(textcompareExpression):
+    """
+    Text NOT CONTAINS (sub-string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(notcontainsExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "does not contain"
+
+
+
+class isExpression(textcompareExpression):
+    """
+    Text IS (exact string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(isExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "is"
+
+
+
+class isnotExpression(textcompareExpression):
+    """
+    Text IS NOT (exact string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(isnotExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "is not"
+
+
+
+class startswithExpression(textcompareExpression):
+    """
+    Text STARTSWITH (sub-string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(startswithExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "starts with"
+
+
+
+class notstartswithExpression(textcompareExpression):
+    """
+    Text NOT STARTSWITH (sub-string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(notstartswithExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "does not start with"
+
+
+
+class endswithExpression(textcompareExpression):
+    """
+    Text STARTSWITH (sub-string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(endswithExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "ends with"
+
+
+
+class notendswithExpression(textcompareExpression):
+    """
+    Text NOT STARTSWITH (sub-string match) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(notendswithExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "does not end with"
+
+
+
+class inExpression(textcompareExpression):
+    """
+    Text IN (exact string match to one of the supplied items) expression.
+    """
+
+    def __init__(self, field, text_list, caseless):
+        super(inExpression, self).__init__(field, text_list, caseless)
+
+
+    def operator(self):
+        return "in"
+
+
+    def __str__(self):
+        return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"
+
+
+
+class notinExpression(textcompareExpression):
+    """
+    Text NOT IN (exact string match to none of the supplied items) expression.
+    """
+
+    def __init__(self, field, text, caseless):
+        super(notinExpression, self).__init__(field, text, caseless)
+
+
+    def operator(self):
+        return "not in"
+
+
+    def __str__(self):
+        return self.operator() + "(" + self.field + ", " + str(self.text) + ", " + str(self.caseless) + ")"

Deleted: CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/filegenerator.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,322 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from txdav.common.datastore.query import expression
-
-"""
-SQLLite statement generator from query expressions.
-"""
-
-__all__ = [
-    "sqllitegenerator",
-]
-
-import cStringIO as StringIO
-
-class sqllitegenerator(object):
-
-    FROM = " from "
-    WHERE = " where "
-    RESOURCEDB = "RESOURCE"
-    TIMESPANDB = "TIMESPAN"
-    TRANSPARENCYDB = "TRANSPARENCY"
-    PERUSERDB = "PERUSER"
-    NOTOP = "NOT "
-    ANDOP = " AND "
-    OROP = " OR "
-    CONTAINSOP = " GLOB "
-    NOTCONTAINSOP = " NOT GLOB "
-    ISOP = " == "
-    ISNOTOP = " != "
-    STARTSWITHOP = " GLOB "
-    NOTSTARTSWITHOP = " NOT GLOB "
-    ENDSWITHOP = " GLOB "
-    NOTENDSWITHOP = " NOT GLOB "
-    INOP = " IN "
-    NOTINOP = " NOT IN "
-
-    FIELDS = {
-        "TYPE": "RESOURCE.TYPE",
-        "UID": "RESOURCE.UID",
-    }
-
-    TIMESPANTEST = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s))"
-    TIMESPANTEST_NOEND = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.END > %s))"
-    TIMESPANTEST_NOSTART = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s))"
-    TIMESPANTEST_TAIL_PIECE = " AND TIMESPAN.RESOURCEID == RESOURCE.RESOURCEID"
-    TIMESPANTEST_JOIN_ON_PIECE = "TIMESPAN.INSTANCEID == TRANSPARENCY.INSTANCEID AND TRANSPARENCY.PERUSERID == %s"
-
-    def __init__(self, expr, calendarid, userid, freebusy=False):
-        """
-
-        @param expr: the query expression object model
-        @type expr: L{Filter}
-        @param calendarid: resource ID - not used for file-based per-calendar indexes
-        @type calendarid: C{int}
-        @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
-        @type userid: C{str}
-        @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
-        @type freebusy: C{bool}
-        """
-        self.expression = expr
-        self.calendarid = calendarid
-        self.userid = userid if userid else ""
-        self.freebusy = freebusy
-        self.usedtimespan = False
-
-
-    def generate(self):
-        """
-        Generate the actual SQL 'where ...' expression from the passed in expression tree.
-
-        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
-            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
-        """
-
-        # Init state
-        self.sout = StringIO.StringIO()
-        self.arguments = []
-        self.substitutions = []
-        self.usedtimespan = False
-
-        # Generate ' where ...' partial statement
-        self.generateExpression(self.expression)
-
-        # Prefix with ' from ...' partial statement
-        select = self.FROM + self.RESOURCEDB
-        if self.usedtimespan:
-
-            # Free busy needs transparency join
-            if self.freebusy:
-                self.frontArgument(self.userid)
-                select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
-                    self.TIMESPANDB,
-                    self.TRANSPARENCYDB,
-                    self.TIMESPANTEST_JOIN_ON_PIECE
-                )
-            else:
-                select += ", %s" % (
-                    self.TIMESPANDB,
-                )
-        select += self.WHERE
-        if self.usedtimespan:
-            select += "("
-        select += self.sout.getvalue()
-        if self.usedtimespan:
-            if self.calendarid:
-                self.setArgument(self.calendarid)
-            select += ")%s" % (self.TIMESPANTEST_TAIL_PIECE,)
-
-        select = select % tuple(self.substitutions)
-
-        return select, self.arguments
-
-
-    def generateExpression(self, expr):
-        """
-        Generate an expression and all it's subexpressions.
-
-        @param expr: the L{baseExpression} derived class to write out.
-        @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
-        """
-
-        # Generate based on each type of expression we might encounter
-
-        # ALL
-        if isinstance(expr, expression.allExpression):
-            # Wipe out the ' where ...' clause so everything is matched
-            self.sout.truncate(0)
-            self.arguments = []
-            self.substitutions = []
-            self.usedtimespan = False
-
-        # NOT
-        elif isinstance(expr, expression.notExpression):
-            self.sout.write(self.NOTOP)
-            self.generateSubExpression(expr.expressions[0])
-
-        # AND
-        elif isinstance(expr, expression.andExpression):
-            first = True
-            for e in expr.expressions:
-                if first:
-                    first = False
-                else:
-                    self.sout.write(self.ANDOP)
-                self.generateSubExpression(e)
-
-        # OR
-        elif isinstance(expr, expression.orExpression):
-            first = True
-            for e in expr.expressions:
-                if first:
-                    first = False
-                else:
-                    self.sout.write(self.OROP)
-                self.generateSubExpression(e)
-
-        # time-range
-        elif isinstance(expr, expression.timerangeExpression):
-            if expr.start and expr.end:
-                self.setArgument(expr.end)
-                self.setArgument(expr.start)
-                self.setArgument(expr.endfloat)
-                self.setArgument(expr.startfloat)
-                test = self.TIMESPANTEST
-            elif expr.start and expr.end is None:
-                self.setArgument(expr.start)
-                self.setArgument(expr.startfloat)
-                test = self.TIMESPANTEST_NOEND
-            elif not expr.start and expr.end:
-                self.setArgument(expr.end)
-                self.setArgument(expr.endfloat)
-                test = self.TIMESPANTEST_NOSTART
-
-            self.sout.write(test)
-            self.usedtimespan = True
-
-        # CONTAINS
-        elif isinstance(expr, expression.containsExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.CONTAINSOP)
-            self.addArgument(self.containsArgument(expr.text))
-
-        # NOT CONTAINS
-        elif isinstance(expr, expression.notcontainsExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.NOTCONTAINSOP)
-            self.addArgument(self.containsArgument(expr.text))
-
-        # IS
-        elif isinstance(expr, expression.isExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.ISOP)
-            self.addArgument(expr.text)
-
-        # IS NOT
-        elif isinstance(expr, expression.isnotExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.ISNOTOP)
-            self.addArgument(expr.text)
-
-        # STARTSWITH
-        elif isinstance(expr, expression.startswithExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.STARTSWITHOP)
-            self.addArgument(self.startswithArgument(expr.text))
-
-        # NOT STARTSWITH
-        elif isinstance(expr, expression.notstartswithExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.NOTSTARTSWITHOP)
-            self.addArgument(self.startswithArgument(expr.text))
-
-        # ENDSWITH
-        elif isinstance(expr, expression.endswithExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.ENDSWITHOP)
-            self.addArgument(self.endswithArgument(expr.text))
-
-        # NOT ENDSWITH
-        elif isinstance(expr, expression.notendswithExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.NOTENDSWITHOP)
-            self.addArgument(self.endswithArgument(expr.text))
-
-        # IN
-        elif isinstance(expr, expression.inExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.INOP)
-            self.sout.write("(")
-            for count, item in enumerate(expr.text):
-                if count != 0:
-                    self.sout.write(", ")
-                self.addArgument(item)
-            self.sout.write(")")
-
-        # NOT IN
-        elif isinstance(expr, expression.notinExpression):
-            self.sout.write(expr.field)
-            self.sout.write(self.NOTINOP)
-            self.sout.write("(")
-            for count, item in enumerate(expr.text):
-                if count != 0:
-                    self.sout.write(", ")
-                self.addArgument(item)
-            self.sout.write(")")
-
-
-    def generateSubExpression(self, expression):
-        """
-        Generate an SQL expression possibly in parenthesis if its a compound expression.
-
-        @param expression: the L{baseExpression} to write out.
-        @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
-        """
-
-        if expression.multi():
-            self.sout.write("(")
-        self.generateExpression(expression)
-        if expression.multi():
-            self.sout.write(")")
-
-
-    def addArgument(self, arg):
-        """
-
-        @param arg: the C{str} of the argument to add
-        """
-
-        # Append argument to the list and add the appropriate substitution string to the output stream.
-        self.arguments.append(arg)
-        self.substitutions.append(":" + str(len(self.arguments)))
-        self.sout.write("%s")
-
-
-    def setArgument(self, arg):
-        """
-
-        @param arg: the C{str} of the argument to add
-        @return: C{str} for argument substitution text
-        """
-
-        # Append argument to the list and add the appropriate substitution string to the output stream.
-        self.arguments.append(arg)
-        self.substitutions.append(":" + str(len(self.arguments)))
-
-
-    def frontArgument(self, arg):
-        """
-
-        @param arg: the C{str} of the argument to add
-        @return: C{str} for argument substitution text
-        """
-
-        # Append argument to the list and add the appropriate substitution string to the output stream.
-        self.arguments.insert(0, arg)
-        self.substitutions.append(":" + str(len(self.arguments)))
-
-
-    def containsArgument(self, arg):
-        return "*%s*" % (arg,)
-
-
-    def startswithArgument(self, arg):
-        return "%s*" % (arg,)
-
-
-    def endswithArgument(self, arg):
-        return "*%s" % (arg,)

Copied: CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/filegenerator.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/filegenerator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,322 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from txdav.common.datastore.query import expression
+
+"""
+SQLLite statement generator from query expressions.
+"""
+
+__all__ = [
+    "sqllitegenerator",
+]
+
+import cStringIO as StringIO
+
+class sqllitegenerator(object):
+
+    FROM = " from "
+    WHERE = " where "
+    RESOURCEDB = "RESOURCE"
+    TIMESPANDB = "TIMESPAN"
+    TRANSPARENCYDB = "TRANSPARENCY"
+    PERUSERDB = "PERUSER"
+    NOTOP = "NOT "
+    ANDOP = " AND "
+    OROP = " OR "
+    CONTAINSOP = " GLOB "
+    NOTCONTAINSOP = " NOT GLOB "
+    ISOP = " == "
+    ISNOTOP = " != "
+    STARTSWITHOP = " GLOB "
+    NOTSTARTSWITHOP = " NOT GLOB "
+    ENDSWITHOP = " GLOB "
+    NOTENDSWITHOP = " NOT GLOB "
+    INOP = " IN "
+    NOTINOP = " NOT IN "
+
+    FIELDS = {
+        "TYPE": "RESOURCE.TYPE",
+        "UID": "RESOURCE.UID",
+    }
+
+    TIMESPANTEST = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s))"
+    TIMESPANTEST_NOEND = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.END > %s))"
+    TIMESPANTEST_NOSTART = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s))"
+    TIMESPANTEST_TAIL_PIECE = " AND TIMESPAN.RESOURCEID == RESOURCE.RESOURCEID"
+    TIMESPANTEST_JOIN_ON_PIECE = "TIMESPAN.INSTANCEID == TRANSPARENCY.INSTANCEID AND TRANSPARENCY.PERUSERID == %s"
+
+    def __init__(self, expr, calendarid, userid, freebusy=False):
+        """
+
+        @param expr: the query expression object model
+        @type expr: L{Filter}
+        @param calendarid: resource ID - not used for file-based per-calendar indexes
+        @type calendarid: C{int}
+        @param userid: user for whom query is being done - query will be scoped to that user's privileges and their transparency
+        @type userid: C{str}
+        @param freebusy: whether or not a freebusy query is being done - if it is, additional time range and transparency information is returned
+        @type freebusy: C{bool}
+        """
+        self.expression = expr
+        self.calendarid = calendarid
+        self.userid = userid if userid else ""
+        self.freebusy = freebusy
+        self.usedtimespan = False
+
+
+    def generate(self):
+        """
+        Generate the actual SQL 'where ...' expression from the passed in expression tree.
+
+        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+        """
+
+        # Init state
+        self.sout = StringIO.StringIO()
+        self.arguments = []
+        self.substitutions = []
+        self.usedtimespan = False
+
+        # Generate ' where ...' partial statement
+        self.generateExpression(self.expression)
+
+        # Prefix with ' from ...' partial statement
+        select = self.FROM + self.RESOURCEDB
+        if self.usedtimespan:
+
+            # Free busy needs transparency join
+            if self.freebusy:
+                self.frontArgument(self.userid)
+                select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
+                    self.TIMESPANDB,
+                    self.TRANSPARENCYDB,
+                    self.TIMESPANTEST_JOIN_ON_PIECE
+                )
+            else:
+                select += ", %s" % (
+                    self.TIMESPANDB,
+                )
+        select += self.WHERE
+        if self.usedtimespan:
+            select += "("
+        select += self.sout.getvalue()
+        if self.usedtimespan:
+            if self.calendarid:
+                self.setArgument(self.calendarid)
+            select += ")%s" % (self.TIMESPANTEST_TAIL_PIECE,)
+
+        select = select % tuple(self.substitutions)
+
+        return select, self.arguments
+
+
+    def generateExpression(self, expr):
+        """
+        Generate an expression and all it's subexpressions.
+
+        @param expr: the L{baseExpression} derived class to write out.
+        @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
+        """
+
+        # Generate based on each type of expression we might encounter
+
+        # ALL
+        if isinstance(expr, expression.allExpression):
+            # Wipe out the ' where ...' clause so everything is matched
+            self.sout.truncate(0)
+            self.arguments = []
+            self.substitutions = []
+            self.usedtimespan = False
+
+        # NOT
+        elif isinstance(expr, expression.notExpression):
+            self.sout.write(self.NOTOP)
+            self.generateSubExpression(expr.expressions[0])
+
+        # AND
+        elif isinstance(expr, expression.andExpression):
+            first = True
+            for e in expr.expressions:
+                if first:
+                    first = False
+                else:
+                    self.sout.write(self.ANDOP)
+                self.generateSubExpression(e)
+
+        # OR
+        elif isinstance(expr, expression.orExpression):
+            first = True
+            for e in expr.expressions:
+                if first:
+                    first = False
+                else:
+                    self.sout.write(self.OROP)
+                self.generateSubExpression(e)
+
+        # time-range
+        elif isinstance(expr, expression.timerangeExpression):
+            if expr.start and expr.end:
+                self.setArgument(expr.end)
+                self.setArgument(expr.start)
+                self.setArgument(expr.endfloat)
+                self.setArgument(expr.startfloat)
+                test = self.TIMESPANTEST
+            elif expr.start and expr.end is None:
+                self.setArgument(expr.start)
+                self.setArgument(expr.startfloat)
+                test = self.TIMESPANTEST_NOEND
+            elif not expr.start and expr.end:
+                self.setArgument(expr.end)
+                self.setArgument(expr.endfloat)
+                test = self.TIMESPANTEST_NOSTART
+
+            self.sout.write(test)
+            self.usedtimespan = True
+
+        # CONTAINS
+        elif isinstance(expr, expression.containsExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.CONTAINSOP)
+            self.addArgument(self.containsArgument(expr.text))
+
+        # NOT CONTAINS
+        elif isinstance(expr, expression.notcontainsExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.NOTCONTAINSOP)
+            self.addArgument(self.containsArgument(expr.text))
+
+        # IS
+        elif isinstance(expr, expression.isExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.ISOP)
+            self.addArgument(expr.text)
+
+        # IS NOT
+        elif isinstance(expr, expression.isnotExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.ISNOTOP)
+            self.addArgument(expr.text)
+
+        # STARTSWITH
+        elif isinstance(expr, expression.startswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.STARTSWITHOP)
+            self.addArgument(self.startswithArgument(expr.text))
+
+        # NOT STARTSWITH
+        elif isinstance(expr, expression.notstartswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.NOTSTARTSWITHOP)
+            self.addArgument(self.startswithArgument(expr.text))
+
+        # ENDSWITH
+        elif isinstance(expr, expression.endswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.ENDSWITHOP)
+            self.addArgument(self.endswithArgument(expr.text))
+
+        # NOT ENDSWITH
+        elif isinstance(expr, expression.notendswithExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.NOTENDSWITHOP)
+            self.addArgument(self.endswithArgument(expr.text))
+
+        # IN
+        elif isinstance(expr, expression.inExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.INOP)
+            self.sout.write("(")
+            for count, item in enumerate(expr.text):
+                if count != 0:
+                    self.sout.write(", ")
+                self.addArgument(item)
+            self.sout.write(")")
+
+        # NOT IN
+        elif isinstance(expr, expression.notinExpression):
+            self.sout.write(expr.field)
+            self.sout.write(self.NOTINOP)
+            self.sout.write("(")
+            for count, item in enumerate(expr.text):
+                if count != 0:
+                    self.sout.write(", ")
+                self.addArgument(item)
+            self.sout.write(")")
+
+
+    def generateSubExpression(self, expression):
+        """
+        Generate an SQL expression possibly in parenthesis if its a compound expression.
+
+        @param expression: the L{baseExpression} to write out.
+        @return: C{True} if the TIMESPAN table is used, C{False} otherwise.
+        """
+
+        if expression.multi():
+            self.sout.write("(")
+        self.generateExpression(expression)
+        if expression.multi():
+            self.sout.write(")")
+
+
+    def addArgument(self, arg):
+        """
+
+        @param arg: the C{str} of the argument to add
+        """
+
+        # Append argument to the list and add the appropriate substitution string to the output stream.
+        self.arguments.append(arg)
+        self.substitutions.append(":" + str(len(self.arguments)))
+        self.sout.write("%s")
+
+
+    def setArgument(self, arg):
+        """
+
+        @param arg: the C{str} of the argument to add
+        @return: C{str} for argument substitution text
+        """
+
+        # Append argument to the list and add the appropriate substitution string to the output stream.
+        self.arguments.append(arg)
+        self.substitutions.append(":" + str(len(self.arguments)))
+
+
+    def frontArgument(self, arg):
+        """
+
+        @param arg: the C{str} of the argument to add
+        @return: C{str} for argument substitution text
+        """
+
+        # Append argument to the list and add the appropriate substitution string to the output stream.
+        self.arguments.insert(0, arg)
+        self.substitutions.append(":" + str(len(self.arguments)))
+
+
+    def containsArgument(self, arg):
+        return "*%s*" % (arg,)
+
+
+    def startswithArgument(self, arg):
+        return "%s*" % (arg,)
+
+
+    def endswithArgument(self, arg):
+        return "*%s" % (arg,)

Deleted: CalendarServer/trunk/txdav/common/datastore/query/generator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/generator.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/generator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,164 +0,0 @@
-##
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twext.enterprise.dal.syntax import Select, Parameter, Not
-from txdav.common.datastore.query import expression
-
-"""
-SQL statement generator from query expressions.
-"""
-
-__all__ = [
-    "SQLQueryGenerator",
-]
-
-class SQLQueryGenerator(object):
-
-    def __init__(self, expr, collection, whereid):
-        """
-
-        @param expr: the query expression object model
-        @type expr: L{expression}
-        @param collection: the resource targeted by the query
-        @type collection: L{CommonHomeChild}
-        """
-        self.expression = expr
-        self.collection = collection
-        self.whereid = whereid
-
-
-    def generate(self):
-        """
-        Generate the actual SQL statement from the passed in expression tree.
-
-        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
-            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
-        """
-
-        # Init state
-        self.arguments = {}
-        self.argcount = 0
-        obj = self.collection._objectSchema
-
-        columns = [obj.RESOURCE_NAME, obj.UID]
-
-        # For SQL data DB we need to restrict the query to just the targeted collection resource-id if provided
-        if self.whereid:
-            # AND the whole thing
-            test = expression.isExpression(obj.PARENT_RESOURCE_ID, self.whereid, True)
-            self.expression = test if isinstance(self.expression, expression.allExpression) else test.andWith(self.expression)
-
-        # Generate ' where ...' partial statement
-        where = self.generateExpression(self.expression)
-
-        select = Select(
-            columns,
-            From=obj,
-            Where=where,
-            Distinct=True,
-        )
-
-        return select, self.arguments
-
-
-    def generateExpression(self, expr):
-        """
-        Generate an expression and all it's subexpressions.
-
-        @param expr: the L{baseExpression} derived class to write out.
-        """
-
-        # Generate based on each type of expression we might encounter
-        partial = None
-
-        # ALL
-        if isinstance(expr, expression.allExpression):
-            # Everything is matched
-            partial = None
-            self.arguments = {}
-
-        # NOT
-        elif isinstance(expr, expression.notExpression):
-            partial = Not(self.generateExpression(expr.expressions[0]))
-
-        # AND
-        elif isinstance(expr, expression.andExpression):
-            for e in expr.expressions:
-                next = self.generateExpression(e)
-                partial = partial.And(next) if partial is not None else next
-
-        # OR
-        elif isinstance(expr, expression.orExpression):
-            for e in expr.expressions:
-                next = self.generateExpression(e)
-                partial = partial.Or(next) if partial is not None else next
-
-        # CONTAINS
-        elif isinstance(expr, expression.containsExpression):
-            partial = expr.field.Contains(expr.text)
-
-        # NOT CONTAINS
-        elif isinstance(expr, expression.notcontainsExpression):
-            partial = expr.field.NotContains(expr.text)
-
-        # IS
-        elif isinstance(expr, expression.isExpression):
-            partial = expr.field == expr.text
-
-        # IS NOT
-        elif isinstance(expr, expression.isnotExpression):
-            partial = expr.field != expr.text
-
-        # STARTSWITH
-        elif isinstance(expr, expression.startswithExpression):
-            partial = expr.field.StartsWith(expr.text)
-
-        # NOT STARTSWITH
-        elif isinstance(expr, expression.notstartswithExpression):
-            partial = expr.field.NotStartsWith(expr.text)
-
-        # ENDSWITH
-        elif isinstance(expr, expression.endswithExpression):
-            partial = expr.field.EndsWith(expr.text)
-
-        # NOT ENDSWITH
-        elif isinstance(expr, expression.notendswithExpression):
-            partial = expr.field.NotEndsWith(expr.text)
-
-        # IN
-        elif isinstance(expr, expression.inExpression):
-            argname = self.addArgument(expr.text)
-            partial = expr.field.In(Parameter(argname, len(expr.text)))
-
-        # NOT IN
-        elif isinstance(expr, expression.notinExpression):
-            argname = self.addArgument(expr.text)
-            partial = expr.field.NotIn(Parameter(argname, len(expr.text)))
-
-        return partial
-
-
-    def addArgument(self, arg):
-        """
-
-        @param arg: the C{str} of the argument to add
-        """
-
-        # Append argument to the list and add the appropriate substitution string to the output stream.
-        self.argcount += 1
-        argname = "arg{}".format(self.argcount)
-        self.arguments[argname] = arg
-        return argname

Copied: CalendarServer/trunk/txdav/common/datastore/query/generator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/generator.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/generator.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/generator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,164 @@
+##
+# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twext.enterprise.dal.syntax import Select, Parameter, Not
+from txdav.common.datastore.query import expression
+
+"""
+SQL statement generator from query expressions.
+"""
+
+__all__ = [
+    "SQLQueryGenerator",
+]
+
+class SQLQueryGenerator(object):
+
+    def __init__(self, expr, collection, whereid):
+        """
+
+        @param expr: the query expression object model
+        @type expr: L{expression}
+        @param collection: the resource targeted by the query
+        @type collection: L{CommonHomeChild}
+        """
+        self.expression = expr
+        self.collection = collection
+        self.whereid = whereid
+
+
+    def generate(self):
+        """
+        Generate the actual SQL statement from the passed in expression tree.
+
+        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the partial SQL statement,
+            and the C{list} is the list of argument substitutions to use with the SQL API execute method.
+        """
+
+        # Init state
+        self.arguments = {}
+        self.argcount = 0
+        obj = self.collection._objectSchema
+
+        columns = [obj.RESOURCE_NAME, obj.UID]
+
+        # For SQL data DB we need to restrict the query to just the targeted collection resource-id if provided
+        if self.whereid:
+            # AND the whole thing
+            test = expression.isExpression(obj.PARENT_RESOURCE_ID, self.whereid, True)
+            self.expression = test if isinstance(self.expression, expression.allExpression) else test.andWith(self.expression)
+
+        # Generate ' where ...' partial statement
+        where = self.generateExpression(self.expression)
+
+        select = Select(
+            columns,
+            From=obj,
+            Where=where,
+            Distinct=True,
+        )
+
+        return select, self.arguments
+
+
+    def generateExpression(self, expr):
+        """
+        Generate an expression and all it's subexpressions.
+
+        @param expr: the L{baseExpression} derived class to write out.
+        """
+
+        # Generate based on each type of expression we might encounter
+        partial = None
+
+        # ALL
+        if isinstance(expr, expression.allExpression):
+            # Everything is matched
+            partial = None
+            self.arguments = {}
+
+        # NOT
+        elif isinstance(expr, expression.notExpression):
+            partial = Not(self.generateExpression(expr.expressions[0]))
+
+        # AND
+        elif isinstance(expr, expression.andExpression):
+            for e in expr.expressions:
+                next = self.generateExpression(e)
+                partial = partial.And(next) if partial is not None else next
+
+        # OR
+        elif isinstance(expr, expression.orExpression):
+            for e in expr.expressions:
+                next = self.generateExpression(e)
+                partial = partial.Or(next) if partial is not None else next
+
+        # CONTAINS
+        elif isinstance(expr, expression.containsExpression):
+            partial = expr.field.Contains(expr.text)
+
+        # NOT CONTAINS
+        elif isinstance(expr, expression.notcontainsExpression):
+            partial = expr.field.NotContains(expr.text)
+
+        # IS
+        elif isinstance(expr, expression.isExpression):
+            partial = expr.field == expr.text
+
+        # IS NOT
+        elif isinstance(expr, expression.isnotExpression):
+            partial = expr.field != expr.text
+
+        # STARTSWITH
+        elif isinstance(expr, expression.startswithExpression):
+            partial = expr.field.StartsWith(expr.text)
+
+        # NOT STARTSWITH
+        elif isinstance(expr, expression.notstartswithExpression):
+            partial = expr.field.NotStartsWith(expr.text)
+
+        # ENDSWITH
+        elif isinstance(expr, expression.endswithExpression):
+            partial = expr.field.EndsWith(expr.text)
+
+        # NOT ENDSWITH
+        elif isinstance(expr, expression.notendswithExpression):
+            partial = expr.field.NotEndsWith(expr.text)
+
+        # IN
+        elif isinstance(expr, expression.inExpression):
+            argname = self.addArgument(expr.text)
+            partial = expr.field.In(Parameter(argname, len(expr.text)))
+
+        # NOT IN
+        elif isinstance(expr, expression.notinExpression):
+            argname = self.addArgument(expr.text)
+            partial = expr.field.NotIn(Parameter(argname, len(expr.text)))
+
+        return partial
+
+
+    def addArgument(self, arg):
+        """
+
+        @param arg: the C{str} of the argument to add
+        """
+
+        # Append argument to the list and add the appropriate substitution string to the output stream.
+        self.argcount += 1
+        argname = "arg{}".format(self.argcount)
+        self.arguments[argname] = arg
+        return argname

Deleted: CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/__init__.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2013 Apple Inc. All rights reserved.
-#
-# 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.
-##

Copied: CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/__init__.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##

Deleted: CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_expression.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,167 +0,0 @@
-##
-# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from txdav.common.datastore.query import expression
-from twisted.trial.unittest import TestCase
-
-class Tests(TestCase):
-
-    def test_andWith(self):
-
-        tests = (
-            (
-                expression.isExpression("A", "1", True),
-                expression.isExpression("B", "2", True),
-                "(is(A, 1, True) AND is(B, 2, True))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.andExpression((
-                    expression.isExpression("B", "2", True),
-                )),
-                "(is(A, 1, True) AND is(B, 2, True))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.andExpression((
-                    expression.isExpression("B", "2", True),
-                    expression.isExpression("C", "3", True),
-                )),
-                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.orExpression((
-                    expression.isExpression("B", "2", True),
-                )),
-                "(is(A, 1, True) AND is(B, 2, True))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.orExpression((
-                    expression.isExpression("B", "2", True),
-                    expression.isExpression("C", "3", True),
-                )),
-                "(is(A, 1, True) AND (is(B, 2, True) OR is(C, 3, True)))"
-            ),
-            (
-                expression.andExpression((
-                    expression.isExpression("A", "1", True),
-                )),
-                expression.isExpression("B", "2", True),
-                "(is(A, 1, True) AND is(B, 2, True))"
-            ),
-            (
-                expression.andExpression((
-                    expression.isExpression("A", "1", True),
-                    expression.isExpression("B", "2", True),
-                )),
-                expression.isExpression("C", "3", True),
-                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
-            ),
-            (
-                expression.orExpression((
-                    expression.isExpression("A", "1", True),
-                )),
-                expression.isExpression("B", "2", True),
-                "(is(A, 1, True) AND is(B, 2, True))"
-            ),
-            (
-                expression.orExpression((
-                    expression.isExpression("A", "1", True),
-                    expression.isExpression("B", "2", True),
-                )),
-                expression.isExpression("C", "3", True),
-                "((is(A, 1, True) OR is(B, 2, True)) AND is(C, 3, True))"
-            ),
-        )
-
-        for expr1, expr2, result in tests:
-            self.assertEqual(str(expr1.andWith(expr2)), result, msg="Failed on %s" % (result,))
-
-
-    def test_orWith(self):
-
-        tests = (
-            (
-                expression.isExpression("A", "1", True),
-                expression.isExpression("B", "2", True),
-                "(is(A, 1, True) OR is(B, 2, True))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.andExpression((
-                    expression.isExpression("B", "2", True),
-                )),
-                "(is(A, 1, True) OR is(B, 2, True))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.andExpression((
-                    expression.isExpression("B", "2", True),
-                    expression.isExpression("C", "3", True),
-                )),
-                "(is(A, 1, True) OR (is(B, 2, True) AND is(C, 3, True)))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.orExpression((
-                    expression.isExpression("B", "2", True),
-                )),
-                "(is(A, 1, True) OR is(B, 2, True))"
-            ),
-            (
-                expression.isExpression("A", "1", True),
-                expression.orExpression((
-                    expression.isExpression("B", "2", True),
-                    expression.isExpression("C", "3", True),
-                )),
-                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
-            ),
-            (
-                expression.andExpression((
-                    expression.isExpression("A", "1", True),
-                )),
-                expression.isExpression("B", "2", True),
-                "(is(A, 1, True) OR is(B, 2, True))"
-            ),
-            (
-                expression.andExpression((
-                    expression.isExpression("A", "1", True),
-                    expression.isExpression("B", "2", True),
-                )),
-                expression.isExpression("C", "3", True),
-                "((is(A, 1, True) AND is(B, 2, True)) OR is(C, 3, True))"
-            ),
-            (
-                expression.orExpression((
-                    expression.isExpression("A", "1", True),
-                )),
-                expression.isExpression("B", "2", True),
-                "(is(A, 1, True) OR is(B, 2, True))"
-            ),
-            (
-                expression.orExpression((
-                    expression.isExpression("A", "1", True),
-                    expression.isExpression("B", "2", True),
-                )),
-                expression.isExpression("C", "3", True),
-                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
-            ),
-        )
-
-        for expr1, expr2, result in tests:
-            self.assertEqual(str(expr1.orWith(expr2)), result, msg="Failed on %s" % (result,))

Copied: CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_expression.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_expression.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,167 @@
+##
+# Copyright (c) 2011-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from txdav.common.datastore.query import expression
+from twisted.trial.unittest import TestCase
+
+class Tests(TestCase):
+
+    def test_andWith(self):
+
+        tests = (
+            (
+                expression.isExpression("A", "1", True),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) AND (is(B, 2, True) OR is(C, 3, True)))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "((is(A, 1, True) OR is(B, 2, True)) AND is(C, 3, True))"
+            ),
+        )
+
+        for expr1, expr2, result in tests:
+            self.assertEqual(str(expr1.andWith(expr2)), result, msg="Failed on %s" % (result,))
+
+
+    def test_orWith(self):
+
+        tests = (
+            (
+                expression.isExpression("A", "1", True),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) OR (is(B, 2, True) AND is(C, 3, True)))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "((is(A, 1, True) AND is(B, 2, True)) OR is(C, 3, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
+            ),
+        )
+
+        for expr1, expr2, result in tests:
+            self.assertEqual(str(expr1.orWith(expr2)), result, msg="Failed on %s" % (result,))

Deleted: CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_generator.py	2013-12-23 16:55:28 UTC (rev 12191)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,127 +0,0 @@
-##
-# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-from twext.enterprise.dal.syntax import SQLFragment, Parameter
-from txdav.common.datastore.query.generator import SQLQueryGenerator
-from txdav.common.datastore.query import expression
-
-"""
-Tests for L{txdav.common.datastore.sql}.
-"""
-
-from twisted.trial.unittest import TestCase
-
-from txdav.common.datastore.sql_tables import schema
-
-class SQLQueryGeneratorTests(TestCase):
-    """
-    Tests for shared functionality in L{txdav.common.datastore.sql}.
-    """
-
-    class FakeHomeChild(object):
-        _objectSchema = schema.CALENDAR_OBJECT
-
-        def id(self):
-            return 1234
-
-
-    def test_all_query(self):
-
-        expr = expression.allExpression()
-        resource = self.FakeHomeChild()
-        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
-        self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ?", [1234]))
-        self.assertEqual(args, {})
-
-
-    def test_uid_query(self):
-
-        resource = self.FakeHomeChild()
-        obj = resource._objectSchema
-        expr = expression.isExpression(obj.UID, 5678, False)
-        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
-        self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ?", [1234, 5678]))
-        self.assertEqual(args, {})
-
-
-    def test_or_query(self):
-
-        resource = self.FakeHomeChild()
-        obj = resource._objectSchema
-        expr = expression.orExpression((
-            expression.isExpression(obj.UID, 5678, False),
-            expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
-        ))
-        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
-        self.assertEqual(
-            select.toSQL(),
-            SQLFragment(
-                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and (ICALENDAR_UID = ? or RESOURCE_NAME != ?)",
-                [1234, 5678, "foobar.ics"]
-            )
-        )
-        self.assertEqual(args, {})
-
-
-    def test_and_query(self):
-
-        resource = self.FakeHomeChild()
-        obj = resource._objectSchema
-        expr = expression.andExpression((
-            expression.isExpression(obj.UID, 5678, False),
-            expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
-        ))
-        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
-        self.assertEqual(
-            select.toSQL(),
-            SQLFragment(
-                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ? and RESOURCE_NAME != ?",
-                [1234, 5678, "foobar.ics"]
-            )
-        )
-        self.assertEqual(args, {})
-
-
-    def test_not_query(self):
-
-        resource = self.FakeHomeChild()
-        obj = resource._objectSchema
-        expr = expression.notExpression(expression.isExpression(obj.UID, 5678, False))
-        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
-        self.assertEqual(
-            select.toSQL(),
-            SQLFragment(
-                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and not ICALENDAR_UID = ?",
-                [1234, 5678]
-            )
-        )
-        self.assertEqual(args, {})
-
-
-    def test_in_query(self):
-
-        resource = self.FakeHomeChild()
-        obj = resource._objectSchema
-        expr = expression.inExpression(obj.RESOURCE_NAME, ["1.ics", "2.ics", "3.ics"], False)
-        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
-        self.assertEqual(
-            select.toSQL(),
-            SQLFragment(
-                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and RESOURCE_NAME in (?, ?, ?)",
-                [1234, Parameter('arg1', 3)]
-            )
-        )
-        self.assertEqual(args, {"arg1": ["1.ics", "2.ics", "3.ics"]})

Copied: CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/query/test/test_generator.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/query/test/test_generator.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,127 @@
+##
+# Copyright (c) 2012-2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+
+from twext.enterprise.dal.syntax import SQLFragment, Parameter
+from txdav.common.datastore.query.generator import SQLQueryGenerator
+from txdav.common.datastore.query import expression
+
+"""
+Tests for L{txdav.common.datastore.sql}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from txdav.common.datastore.sql_tables import schema
+
+class SQLQueryGeneratorTests(TestCase):
+    """
+    Tests for shared functionality in L{txdav.common.datastore.sql}.
+    """
+
+    class FakeHomeChild(object):
+        _objectSchema = schema.CALENDAR_OBJECT
+
+        def id(self):
+            return 1234
+
+
+    def test_all_query(self):
+
+        expr = expression.allExpression()
+        resource = self.FakeHomeChild()
+        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+        self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ?", [1234]))
+        self.assertEqual(args, {})
+
+
+    def test_uid_query(self):
+
+        resource = self.FakeHomeChild()
+        obj = resource._objectSchema
+        expr = expression.isExpression(obj.UID, 5678, False)
+        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+        self.assertEqual(select.toSQL(), SQLFragment("select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ?", [1234, 5678]))
+        self.assertEqual(args, {})
+
+
+    def test_or_query(self):
+
+        resource = self.FakeHomeChild()
+        obj = resource._objectSchema
+        expr = expression.orExpression((
+            expression.isExpression(obj.UID, 5678, False),
+            expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
+        ))
+        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+        self.assertEqual(
+            select.toSQL(),
+            SQLFragment(
+                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and (ICALENDAR_UID = ? or RESOURCE_NAME != ?)",
+                [1234, 5678, "foobar.ics"]
+            )
+        )
+        self.assertEqual(args, {})
+
+
+    def test_and_query(self):
+
+        resource = self.FakeHomeChild()
+        obj = resource._objectSchema
+        expr = expression.andExpression((
+            expression.isExpression(obj.UID, 5678, False),
+            expression.isnotExpression(obj.RESOURCE_NAME, "foobar.ics", False),
+        ))
+        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+        self.assertEqual(
+            select.toSQL(),
+            SQLFragment(
+                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and ICALENDAR_UID = ? and RESOURCE_NAME != ?",
+                [1234, 5678, "foobar.ics"]
+            )
+        )
+        self.assertEqual(args, {})
+
+
+    def test_not_query(self):
+
+        resource = self.FakeHomeChild()
+        obj = resource._objectSchema
+        expr = expression.notExpression(expression.isExpression(obj.UID, 5678, False))
+        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+        self.assertEqual(
+            select.toSQL(),
+            SQLFragment(
+                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and not ICALENDAR_UID = ?",
+                [1234, 5678]
+            )
+        )
+        self.assertEqual(args, {})
+
+
+    def test_in_query(self):
+
+        resource = self.FakeHomeChild()
+        obj = resource._objectSchema
+        expr = expression.inExpression(obj.RESOURCE_NAME, ["1.ics", "2.ics", "3.ics"], False)
+        select, args = SQLQueryGenerator(expr, resource, resource.id()).generate()
+        self.assertEqual(
+            select.toSQL(),
+            SQLFragment(
+                "select distinct RESOURCE_NAME, ICALENDAR_UID from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = ? and RESOURCE_NAME in (?, ?, ?)",
+                [1234, Parameter('arg1', 3)]
+            )
+        )
+        self.assertEqual(args, {"arg1": ["1.ics", "2.ics", "3.ics"]})

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -14,9 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from collections import namedtuple
-from txdav.xml import element
-from txdav.base.propertystore.base import PropertyName
 
 """
 SQL data store.
@@ -57,32 +54,40 @@
 
 from txdav.base.datastore.util import QueryCacher
 from txdav.base.datastore.util import normalizeUUIDOrNot
+from txdav.base.propertystore.base import PropertyName
 from txdav.base.propertystore.none import PropertyStore as NonePropertyStore
 from txdav.base.propertystore.sql import PropertyStore
 from txdav.caldav.icalendarstore import ICalendarTransaction, ICalendarStore
 from txdav.carddav.iaddressbookstore import IAddressBookTransaction
 from txdav.common.datastore.common import HomeChildBase
+from txdav.common.datastore.podding.conduit import PoddingConduit
 from txdav.common.datastore.sql_tables import _BIND_MODE_OWN, \
     _BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, _BIND_STATUS_INVALID, \
     _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_DELETED, \
-    _BIND_MODE_INDIRECT
+    _BIND_MODE_INDIRECT, _HOME_STATUS_NORMAL, _HOME_STATUS_EXTERNAL
 from txdav.common.datastore.sql_tables import schema, splitSQLString
-from txdav.common.icommondatastore import ConcurrentModification
+from txdav.common.icommondatastore import ConcurrentModification, \
+    RecordNotAllowedError, ExternalShareFailed, ShareNotAllowed, \
+    IndexedSearchException
 from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
     HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
     ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError, \
     NoSuchObjectResourceError, AllRetriesFailed, InvalidSubscriptionValues, \
     InvalidIMIPTokenValues, TooManyObjectResourcesError, \
     SyncTokenValidException
-from txdav.common.idirectoryservice import IStoreDirectoryService
+from txdav.common.idirectoryservice import IStoreDirectoryService, \
+    DirectoryRecordNotFoundError
 from txdav.common.inotifications import INotificationCollection, \
     INotificationObject
 from txdav.idav import ChangeCategory
+from txdav.xml import element
 
 from uuid import uuid4, UUID
 
 from zope.interface import implements, directlyProvides
 
+from collections import namedtuple
+import itertools
 import json
 import sys
 import time
@@ -189,6 +194,8 @@
         else:
             self.queryCacher = None
 
+        self.conduit = PoddingConduit(self)
+
         # Always import these here to trigger proper "registration" of the calendar and address book
         # home classes
         __import__("txdav.caldav.datastore.sql")
@@ -210,16 +217,18 @@
 
 
     @inlineCallbacks
-    def _withEachHomeDo(self, homeTable, homeFromTxn, action, batchSize): #@UnusedVariable
+    def _withEachHomeDo(self, homeTable, homeFromTxn, action, batchSize, processExternal=False):
         """
         Implementation of L{ICalendarStore.withEachCalendarHomeDo} and
         L{IAddressbookStore.withEachAddressbookHomeDo}.
         """
         txn = yield self.newTransaction()
         try:
-            allUIDs = yield (Select([homeTable.OWNER_UID], From=homeTable)
-                             .on(txn))
+            allUIDs = yield (Select([homeTable.OWNER_UID], From=homeTable).on(txn))
             for [uid] in allUIDs:
+                home = yield homeFromTxn(txn, uid)
+                if not processExternal and home.external():
+                    continue
                 yield action(txn, (yield homeFromTxn(txn, uid)))
         except:
             a, b, c = sys.exc_info()
@@ -229,25 +238,25 @@
             yield txn.commit()
 
 
-    def withEachCalendarHomeDo(self, action, batchSize=None):
+    def withEachCalendarHomeDo(self, action, batchSize=None, processExternal=False):
         """
         Implementation of L{ICalendarStore.withEachCalendarHomeDo}.
         """
         return self._withEachHomeDo(
             schema.CALENDAR_HOME,
             lambda txn, uid: txn.calendarHomeWithUID(uid),
-            action, batchSize
+            action, batchSize, processExternal
         )
 
 
-    def withEachAddressbookHomeDo(self, action, batchSize=None):
+    def withEachAddressbookHomeDo(self, action, batchSize=None, processExternal=False):
         """
         Implementation of L{IAddressbookStore.withEachAddressbookHomeDo}.
         """
         return self._withEachHomeDo(
             schema.ADDRESSBOOK_HOME,
             lambda txn, uid: txn.addressbookHomeWithUID(uid),
-            action, batchSize
+            action, batchSize, processExternal
         )
 
 
@@ -473,7 +482,7 @@
         self._label = label
         self._migrating = migrating
         self._primaryHomeType = None
-        self._disableCache = disableCache
+        self._disableCache = disableCache or not store.queryCachingEnabled()
         if disableCache:
             self._queryCacher = None
         else:
@@ -1467,10 +1476,108 @@
         if shareeView is not None:
             yield shareeView.declineShare()
 
-        returnValue(shareeView)
+        returnValue(shareeView is not None)
 
 
+    #
+    # External (cross-pod) sharing - entry point is the sharee's home collection.
+    #
+    @inlineCallbacks
+    def processExternalInvite(self, ownerUID, ownerRID, ownerName, shareUID, bindMode, summary, copy_invite_properties, supported_components=None):
+        """
+        External invite received.
+        """
 
+        # Get the owner home - create external one if not present
+        ownerHome = yield self._txn.homeWithUID(self._homeType, ownerUID, create=True)
+        if ownerHome is None or not ownerHome.external():
+            raise ExternalShareFailed("Invalid owner UID: {}".format(ownerUID))
+
+        # Try to find owner calendar via its external id
+        ownerView = yield ownerHome.childWithExternalID(ownerRID)
+        if ownerView is None:
+            try:
+                ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+            except HomeChildNameAlreadyExistsError:
+                # This is odd - it means we possibly have a left over sharer collection which the sharer likely removed
+                # and re-created with the same name but now it has a different externalID and is not found by the initial
+                # query. What we do is check to see whether any shares still reference the old ID - if they do we are hosed.
+                # If not, we can remove the old item and create a new one.
+                oldOwnerView = yield ownerHome.childWithName(ownerName)
+                invites = yield oldOwnerView.sharingInvites()
+                if len(invites) != 0:
+                    log.error("External invite collection name is present with a different externalID and still has shares")
+                    raise
+                log.error("External invite collection name is present with a different externalID - trying to fix")
+                yield ownerHome.removeExternalChild(oldOwnerView)
+                ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+
+            if supported_components is not None and hasattr(ownerView, "setSupportedComponents"):
+                yield ownerView.setSupportedComponents(supported_components)
+
+        # Now carry out the share operation
+        if bindMode == _BIND_MODE_DIRECT:
+            shareeView = yield ownerView.directShareWithUser(self.uid(), shareName=shareUID)
+        else:
+            shareeView = yield ownerView.inviteUserToShare(self.uid(), bindMode, summary, shareName=shareUID)
+
+        shareeView.setInviteCopyProperties(copy_invite_properties)
+
+
+    @inlineCallbacks
+    def processExternalUninvite(self, ownerUID, ownerRID, shareUID):
+        """
+        External invite received.
+        """
+
+        # Get the owner home
+        ownerHome = yield self._txn.homeWithUID(self._homeType, ownerUID)
+        if ownerHome is None or not ownerHome.external():
+            raise ExternalShareFailed("Invalid owner UID: {}".format(ownerUID))
+
+        # Try to find owner calendar via its external id
+        ownerView = yield ownerHome.childWithExternalID(ownerRID)
+        if ownerView is None:
+            raise ExternalShareFailed("Invalid share ID: {}".format(shareUID))
+
+        # Now carry out the share operation
+        yield ownerView.uninviteUserFromShare(self.uid())
+
+        # See if there are any references to the external share - if not remove it
+        invites = yield ownerView.sharingInvites()
+        if len(invites) == 0:
+            yield ownerHome.removeExternalChild(ownerView)
+
+
+    @inlineCallbacks
+    def processExternalReply(self, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
+        """
+        External invite received.
+        """
+
+        # Make sure the shareeUID and shareUID match
+
+        # Get the owner home - create external one if not present
+        shareeHome = yield self._txn.homeWithUID(self._homeType, shareeUID)
+        if shareeHome is None or not shareeHome.external():
+            raise ExternalShareFailed("Invalid sharee UID: {}".format(shareeUID))
+
+        # Try to find owner calendar via its external id
+        shareeView = yield shareeHome.anyObjectWithShareUID(shareUID)
+        if shareeView is None:
+            raise ExternalShareFailed("Invalid share UID: {}".format(shareUID))
+
+        # Now carry out the share operation
+        if bindStatus == _BIND_STATUS_ACCEPTED:
+            yield shareeHome.acceptShare(shareUID, summary)
+        elif bindStatus == _BIND_STATUS_DECLINED:
+            if shareeView.direct():
+                yield shareeView.deleteShare()
+            else:
+                yield shareeHome.declineShare(shareUID)
+
+
+
 class CommonHome(SharingHomeMixIn):
     log = Logger()
 
@@ -1478,6 +1585,7 @@
     _homeType = None
     _homeTable = None
     _homeMetaDataTable = None
+    _externalClass = None
     _childClass = None
     _childTable = None
     _notifierPrefix = None
@@ -1487,10 +1595,30 @@
 
     _cacher = None  # Initialize in derived classes
 
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, transaction, ownerUID, no_cache=False):
+        """
+        Build the actual home class taking into account the possibility that we might need to
+        switch in the external version of the class.
+
+        @param transaction: transaction
+        @type transaction: L{CommonStoreTransaction}
+        @param ownerUID: owner UID of home to load
+        @type ownerUID: C{str}
+        @param no_cache: should cached query be used
+        @type no_cache: C{bool}
+        """
+        home = cls(transaction, ownerUID)
+        actualHome = yield home.initFromStore(no_cache)
+        returnValue(actualHome)
+
+
     def __init__(self, transaction, ownerUID):
         self._txn = transaction
         self._ownerUID = ownerUID
         self._resourceID = None
+        self._status = _HOME_STATUS_NORMAL
         self._dataVersion = None
         self._childrenLoaded = False
         self._children = {}
@@ -1554,6 +1682,7 @@
         return (
             cls._homeSchema.RESOURCE_ID,
             cls._homeSchema.OWNER_UID,
+            cls._homeSchema.STATUS,
         )
 
 
@@ -1568,6 +1697,7 @@
         return (
             "_resourceID",
             "_ownerUID",
+            "_status",
         )
 
 
@@ -1612,39 +1742,57 @@
         """
         result = yield self._cacher.get(self._ownerUID)
         if result is None:
-            result = yield self._homeColumnsFromOwnerQuery.on(
-                self._txn, ownerUID=self._ownerUID)
-            if result and not no_cache:
-                yield self._cacher.set(self._ownerUID, result)
+            result = yield self._homeColumnsFromOwnerQuery.on(self._txn, ownerUID=self._ownerUID)
+            if result:
+                result = result[0]
+                if not no_cache:
+                    yield self._cacher.set(self._ownerUID, result)
 
         if result:
-            for attr, value in zip(self.homeAttributes(), result[0]):
+            for attr, value in zip(self.homeAttributes(), result):
                 setattr(self, attr, value)
 
-            queryCacher = self._txn._queryCacher
-            if queryCacher:
-                # Get cached copy
-                cacheKey = queryCacher.keyForHomeMetaData(self._resourceID)
-                data = yield queryCacher.get(cacheKey)
+            # STOP! If the status is external we need to convert this object to a CommonHomeExternal class which will
+            # have the right behavior for non-hosted external users.
+            if self._status == _HOME_STATUS_EXTERNAL:
+                actualHome = self._externalClass(self._txn, self._ownerUID, self._resourceID)
             else:
-                data = None
-            if data is None:
-                # Don't have a cached copy
-                data = (yield self._metaDataQuery.on(
-                    self._txn, resourceID=self._resourceID))[0]
-                if queryCacher:
-                    # Cache the data
-                    yield queryCacher.setAfterCommit(self._txn, cacheKey, data)
+                actualHome = self
+            yield actualHome.initMetaDataFromStore()
+            yield actualHome._loadPropertyStore()
 
-            for attr, value in zip(self.metadataAttributes(), data):
-                setattr(self, attr, value)
+            for factory_type, factory in self._txn._notifierFactories.items():
+                actualHome.addNotifier(factory_type, factory.newNotifier(actualHome))
 
-            yield self._loadPropertyStore()
-            returnValue(self)
+            returnValue(actualHome)
         else:
             returnValue(None)
 
 
+    @inlineCallbacks
+    def initMetaDataFromStore(self):
+        """
+        Load up the metadata and property store
+        """
+
+        queryCacher = self._txn._queryCacher
+        if queryCacher:
+            # Get cached copy
+            cacheKey = queryCacher.keyForHomeMetaData(self._resourceID)
+            data = yield queryCacher.get(cacheKey)
+        else:
+            data = None
+        if data is None:
+            # Don't have a cached copy
+            data = (yield self._metaDataQuery.on(self._txn, resourceID=self._resourceID))[0]
+            if queryCacher:
+                # Cache the data
+                yield queryCacher.setAfterCommit(self._txn, cacheKey, data)
+
+        for attr, value in zip(self.metadataAttributes(), data):
+            setattr(self, attr, value)
+
+
     @classmethod
     @inlineCallbacks
     def listHomes(cls, txn):
@@ -1664,16 +1812,20 @@
     @classmethod
     @inlineCallbacks
     def homeWithUID(cls, txn, uid, create=False):
-        homeObject = cls(txn, uid)
-        for factory_type, factory in txn._notifierFactories.items():
-            homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
-        homeObject = (yield homeObject.initFromStore())
+        homeObject = yield cls.makeClass(txn, uid)
         if homeObject is not None:
             returnValue(homeObject)
         else:
             if not create:
                 returnValue(None)
 
+            # Determine if the user is local or external
+            record = txn.directoryService().recordWithUID(uid)
+            if record is None:
+                raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
+
+            state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
+
             # Use savepoint so we can do a partial rollback if there is a race condition
             # where this row has already been inserted
             savepoint = SavepointAction("homeWithUID")
@@ -1685,19 +1837,17 @@
                 resourceid = (yield Insert(
                     {
                         cls._homeSchema.OWNER_UID: uid,
+                        cls._homeSchema.STATUS: state,
                         cls._homeSchema.DATAVERSION: cls._dataVersionValue,
                     },
-                    Return=cls._homeSchema.RESOURCE_ID).on(txn))[0][0]
-                yield Insert(
-                    {cls._homeMetaDataSchema.RESOURCE_ID: resourceid}).on(txn)
+                    Return=cls._homeSchema.RESOURCE_ID
+                ).on(txn))[0][0]
+                yield Insert({cls._homeMetaDataSchema.RESOURCE_ID: resourceid}).on(txn)
             except Exception:  # FIXME: Really want to trap the pg.DatabaseError but in a non-DB specific manner
                 yield savepoint.rollback(txn)
 
                 # Retry the query - row may exist now, if not re-raise
-                homeObject = cls(txn, uid)
-                for factory_type, factory in txn._notifierFactories.items():
-                    homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
-                homeObject = (yield homeObject.initFromStore())
+                homeObject = yield cls.makeClass(txn, uid)
                 if homeObject:
                     returnValue(homeObject)
                 else:
@@ -1708,10 +1858,7 @@
                 # Note that we must not cache the owner_uid->resource_id
                 # mapping in _cacher when creating as we don't want that to appear
                 # until AFTER the commit
-                home = cls(txn, uid)
-                for factory_type, factory in txn._notifierFactories.items():
-                    home.addNotifier(factory_type, factory.newNotifier(home))
-                home = (yield home.initFromStore(no_cache=True))
+                home = yield cls.makeClass(txn, uid, no_cache=True)
                 yield home.createdHome()
                 returnValue(home)
 
@@ -1727,7 +1874,7 @@
 
 
     def __repr__(self):
-        return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+        return "<%s: %s, %s>" % (self.__class__.__name__, self._resourceID, self._ownerUID)
 
 
     def id(self):
@@ -1749,6 +1896,15 @@
         return self._ownerUID
 
 
+    def external(self):
+        """
+        Is this an external home.
+
+        @return: a string.
+        """
+        return False
+
+
     def transaction(self):
         return self._txn
 
@@ -1866,6 +2022,17 @@
         return self._childClass.objectWithID(self, resourceID)
 
 
+    def childWithExternalID(self, externalID):
+        """
+        Retrieve the child with the given C{externalID} contained in this
+        home.
+
+        @param name: a string.
+        @return: an L{ICalendar} or C{None} if no such child exists.
+        """
+        return self._childClass.objectWithExternalID(self, externalID)
+
+
     def allChildWithID(self, resourceID):
         """
         Retrieve the child with the given C{resourceID} contained in this
@@ -1878,12 +2045,11 @@
 
 
     @inlineCallbacks
-    def createChildWithName(self, name):
+    def createChildWithName(self, name, externalID=None):
         if name.startswith("."):
             raise HomeChildNameNotAllowedError(name)
 
-        yield self._childClass.create(self, name)
-        child = (yield self.childWithName(name))
+        child = yield self._childClass.create(self, name, externalID=externalID)
         returnValue(child)
 
 
@@ -2025,6 +2191,10 @@
         record a revision for the sharee home and sharee collection name with the "deleted" flag set. That way
         the shared collection can be reported as removed.
 
+        For external shared collections we need to report them as invalid as we cannot aggregate the sync token
+        for this home with the sync token from the external share which is under the control of the other pod.
+        Reporting it as invalid means that clients should do requests directly on the share itself to sync it.
+
         @param revision: the sync revision to compare to
         @type revision: C{str}
         @param depth: depth for determine what changed
@@ -2043,6 +2213,7 @@
 
         changed = set()
         deleted = set()
+        invalid = set()
         deleted_collections = set()
         for path, name, wasdeleted in results:
             if wasdeleted:
@@ -2071,13 +2242,17 @@
         # TODO: think about whether this can be done in one query rather than looping over each share
         for share in (yield self.children()):
             if not share.owned():
-                sharedChanged, sharedDeleted = yield share.sharedChildResourceNamesSinceRevision(revision, depth)
+                sharedChanged, sharedDeleted, sharedInvalid = yield share.sharedChildResourceNamesSinceRevision(revision, depth)
                 changed |= sharedChanged
+                changed -= sharedInvalid
                 deleted |= sharedDeleted
+                deleted -= sharedInvalid
+                invalid |= sharedInvalid
 
         changed = sorted(changed)
         deleted = sorted(deleted)
-        returnValue((changed, deleted))
+        invalid = sorted(invalid)
+        returnValue((changed, deleted, invalid,))
 
 
     @inlineCallbacks
@@ -2400,7 +2575,7 @@
     def revisionFromToken(self, token):
         if token is None:
             return 0
-        elif isinstance(token, str):
+        elif isinstance(token, str) or isinstance(token, unicode):
             _ignore_uuid, revision = token.split("_", 1)
             return int(revision)
         else:
@@ -2466,6 +2641,7 @@
 
         changed = []
         deleted = []
+        invalid = []
         for name, wasdeleted in results:
             if name:
                 if wasdeleted:
@@ -2474,7 +2650,7 @@
                 else:
                     changed.append(name)
 
-        returnValue((changed, deleted))
+        returnValue((changed, deleted, invalid))
 
 
     @classproperty
@@ -2759,6 +2935,7 @@
         return Insert({
             bind.HOME_RESOURCE_ID: Parameter("homeID"),
             bind.RESOURCE_ID: Parameter("resourceID"),
+            bind.EXTERNAL_ID: Parameter("externalID"),
             bind.RESOURCE_NAME: Parameter("name"),
             bind.BIND_MODE: Parameter("mode"),
             bind.BIND_STATUS: Parameter("bindStatus"),
@@ -2840,11 +3017,21 @@
         """
         bind = cls._bindSchema
         return cls._bindFor((bind.RESOURCE_ID == Parameter("resourceID"))
-                               .And(bind.HOME_RESOURCE_ID == Parameter("homeID"))
-                               )
+                            .And(bind.HOME_RESOURCE_ID == Parameter("homeID")))
 
 
     @classproperty
+    def _bindForExternalIDAndHomeID(cls): #@NoSelf
+        """
+        DAL query that looks up home bind rows by home child
+        resource ID and home resource ID.
+        """
+        bind = cls._bindSchema
+        return cls._bindFor((bind.EXTERNAL_ID == Parameter("externalID"))
+                            .And(bind.HOME_RESOURCE_ID == Parameter("homeID")))
+
+
+    @classproperty
     def _bindForNameAndHomeID(cls): #@NoSelf
         """
         DAL query that looks up any bind rows by home child
@@ -2852,15 +3039,14 @@
         """
         bind = cls._bindSchema
         return cls._bindFor((bind.RESOURCE_NAME == Parameter("name"))
-                               .And(bind.HOME_RESOURCE_ID == Parameter("homeID"))
-                               )
+                            .And(bind.HOME_RESOURCE_ID == Parameter("homeID")))
 
 
     #
     # Higher level API
     #
     @inlineCallbacks
-    def inviteUserToShare(self, shareeUID, mode, summary):
+    def inviteUserToShare(self, shareeUID, mode, summary, shareName=None):
         """
         Invite a user to share this collection - either create the share if it does not exist, or
         update the existing share with new values. Make sure a notification is sent as well.
@@ -2879,15 +3065,19 @@
             status = _BIND_STATUS_INVITED if shareeView.shareStatus() in (_BIND_STATUS_DECLINED, _BIND_STATUS_INVALID) else None
             yield self.updateShare(shareeView, mode=mode, status=status, summary=summary)
         else:
-            shareeView = yield self.createShare(shareeUID=shareeUID, mode=mode, summary=summary)
+            shareeView = yield self.createShare(shareeUID=shareeUID, mode=mode, summary=summary, shareName=shareName)
 
-        # Send invite notification
-        yield self._sendInviteNotification(shareeView)
+        # Check for external
+        if shareeView.viewerHome().external():
+            yield self._sendExternalInvite(shareeView)
+        else:
+            # Send invite notification
+            yield self._sendInviteNotification(shareeView)
         returnValue(shareeView)
 
 
     @inlineCallbacks
-    def directShareWithUser(self, shareeUID):
+    def directShareWithUser(self, shareeUID, shareName=None):
         """
         Create a direct share with the specified user. Note it is currently up to the app layer
         to enforce access control - this is not ideal as we really should have control of that in
@@ -2902,8 +3092,13 @@
         # Ignore if it already exists
         shareeView = yield self.shareeView(shareeUID)
         if shareeView is None:
-            shareeView = yield self.createShare(shareeUID=shareeUID, mode=_BIND_MODE_DIRECT)
+            shareeView = yield self.createShare(shareeUID=shareeUID, mode=_BIND_MODE_DIRECT, shareName=shareName)
             yield shareeView.newShare()
+
+            # Check for external
+            if shareeView.viewerHome().external():
+                yield self._sendExternalInvite(shareeView)
+
         returnValue(shareeView)
 
 
@@ -2919,13 +3114,16 @@
 
         shareeView = yield self.shareeView(shareeUID)
         if shareeView is not None:
-            # If current user state is accepted then we send an invite with the new state, otherwise
-            # we cancel any existing invites for the user
-            if not shareeView.direct():
-                if shareeView.shareStatus() != _BIND_STATUS_ACCEPTED:
-                    yield self._removeInviteNotification(shareeView)
-                else:
-                    yield self._sendInviteNotification(shareeView, notificationState=_BIND_STATUS_DELETED)
+            if shareeView.viewerHome().external():
+                yield self._sendExternalUninvite(shareeView)
+            else:
+                # If current user state is accepted then we send an invite with the new state, otherwise
+                # we cancel any existing invites for the user
+                if not shareeView.direct():
+                    if shareeView.shareStatus() != _BIND_STATUS_ACCEPTED:
+                        yield self._removeInviteNotification(shareeView)
+                    else:
+                        yield self._sendInviteNotification(shareeView, notificationState=_BIND_STATUS_DELETED)
 
             # Remove the bind
             yield self.removeShare(shareeView)
@@ -2938,10 +3136,13 @@
         """
 
         if not self.direct() and self.shareStatus() != _BIND_STATUS_ACCEPTED:
+            if self.external():
+                yield self._replyExternalInvite(_BIND_STATUS_ACCEPTED, summary)
             ownerView = yield self.ownerView()
             yield ownerView.updateShare(self, status=_BIND_STATUS_ACCEPTED)
             yield self.newShare(displayname=summary)
-            yield self._sendReplyNotification(ownerView, summary)
+            if not ownerView.external():
+                yield self._sendReplyNotification(ownerView, summary)
 
 
     @inlineCallbacks
@@ -2951,24 +3152,43 @@
         """
 
         if not self.direct() and self.shareStatus() != _BIND_STATUS_DECLINED:
+            if self.external():
+                yield self._replyExternalInvite(_BIND_STATUS_DECLINED)
             ownerView = yield self.ownerView()
             yield ownerView.updateShare(self, status=_BIND_STATUS_DECLINED)
-            yield self._sendReplyNotification(ownerView)
+            if not ownerView.external():
+                yield self._sendReplyNotification(ownerView)
 
 
     @inlineCallbacks
     def deleteShare(self):
         """
-        This share is being deleted - either decline or remove (for direct shares).
+        This share is being deleted (by the sharee) - either decline or remove (for direct shares).
         """
 
         ownerView = yield self.ownerView()
         if self.direct():
             yield ownerView.removeShare(self)
+            if ownerView.external():
+                yield self._replyExternalInvite(_BIND_STATUS_DECLINED)
         else:
             yield self.declineShare()
 
 
+    @inlineCallbacks
+    def ownerDeleteShare(self):
+        """
+        This share is being deleted (by the owner) - either decline or remove (for direct shares).
+        """
+
+        # Change status on store object
+        yield self.setShared(False)
+
+        # Remove all sharees (direct and invited)
+        for invitation in (yield self.sharingInvites()):
+            yield self.uninviteUserFromShare(invitation.shareeUID)
+
+
     def newShare(self, displayname=None):
         """
         Override in derived classes to do any specific operations needed when a share
@@ -3068,10 +3288,57 @@
 
 
     #
-    # Lower level API
+    # External/cross-pod API
     #
+    @inlineCallbacks
+    def _sendExternalInvite(self, shareeView):
 
+        yield self._txn.store().conduit.send_shareinvite(
+            self._txn,
+            shareeView.ownerHome()._homeType,
+            shareeView.ownerHome().uid(),
+            self.id(),
+            self.shareName(),
+            shareeView.viewerHome().uid(),
+            shareeView.shareUID(),
+            shareeView.shareMode(),
+            shareeView.shareMessage(),
+            self.getInviteCopyProperties(),
+            supported_components=self.getSupportedComponents() if hasattr(self, "getSupportedComponents") else None,
+        )
+
+
     @inlineCallbacks
+    def _sendExternalUninvite(self, shareeView):
+
+        yield self._txn.store().conduit.send_shareuninvite(
+            self._txn,
+            shareeView.ownerHome()._homeType,
+            shareeView.ownerHome().uid(),
+            self.id(),
+            shareeView.viewerHome().uid(),
+            shareeView.shareUID(),
+        )
+
+
+    @inlineCallbacks
+    def _replyExternalInvite(self, status, summary=None):
+
+        yield self._txn.store().conduit.send_sharereply(
+            self._txn,
+            self.viewerHome()._homeType,
+            self.ownerHome().uid(),
+            self.viewerHome().uid(),
+            self.shareUID(),
+            status,
+            summary,
+        )
+
+
+    #
+    # Lower level API
+    #
+    @inlineCallbacks
     def ownerView(self):
         """
         Return the owner resource counterpart of this shared resource.
@@ -3094,7 +3361,7 @@
 
 
     @inlineCallbacks
-    def shareWith(self, shareeHome, mode, status=None, summary=None):
+    def shareWith(self, shareeHome, mode, status=None, summary=None, shareName=None):
         """
         Share this (owned) L{CommonHomeChild} with another home.
 
@@ -3122,11 +3389,12 @@
 
         @inlineCallbacks
         def doInsert(subt):
-            newName = self.newShareName()
+            newName = shareName if shareName is not None else self.newShareName()
             yield self._bindInsertQuery.on(
                 subt,
                 homeID=shareeHome._resourceID,
                 resourceID=self._resourceID,
+                externalID=self._externalID,
                 name=newName,
                 mode=mode,
                 bindStatus=status,
@@ -3160,7 +3428,7 @@
 
 
     @inlineCallbacks
-    def createShare(self, shareeUID, mode, summary=None):
+    def createShare(self, shareeUID, mode, summary=None, shareName=None):
         """
         Create a new shared resource. If the mode is direct, the share is created in accepted state,
         otherwise the share is created in invited state.
@@ -3172,6 +3440,7 @@
             mode=mode,
             status=_BIND_STATUS_INVITED if mode != _BIND_MODE_DIRECT else _BIND_STATUS_ACCEPTED,
             summary=summary,
+            shareName=shareName,
         )
         shareeView = yield self.shareeView(shareeUID)
         returnValue(shareeView)
@@ -3232,12 +3501,7 @@
             if summary is not None:
                 shareeView._bindMessage = columnMap[bind.MESSAGE]
 
-            queryCacher = self._txn._queryCacher
-            if queryCacher:
-                cacheKey = queryCacher.keyForObjectWithName(shareeView._home._resourceID, shareeView._name)
-                yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-                cacheKey = queryCacher.keyForObjectWithResourceID(shareeView._home._resourceID, shareeView._resourceID)
-                yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
+            yield shareeView.invalidateQueryCache()
 
             # Must send notification to ensure cache invalidation occurs
             yield self.notifyPropertyChanged()
@@ -3291,12 +3555,7 @@
             homeID=shareeHome._resourceID,
         )
 
-        queryCacher = self._txn._queryCacher
-        if queryCacher:
-            cacheKey = queryCacher.keyForObjectWithName(shareeHome._resourceID, shareeView._name)
-            yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-            cacheKey = queryCacher.keyForObjectWithResourceID(shareeHome._resourceID, shareeView._resourceID)
-            yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
+        yield shareeView.invalidateQueryCache()
 
 
     @inlineCallbacks
@@ -3496,6 +3755,24 @@
         return self._bindMessage
 
 
+    def getInviteCopyProperties(self):
+        """
+        Get a dictionary of property name/values (as strings) for properties that are shadowable and
+        need to be copied to a sharee's collection when an external (cross-pod) share is created.
+        Sub-classes should override to expose the properties they care about.
+        """
+        return {}
+
+
+    def setInviteCopyProperties(self, props):
+        """
+        Copy a set of shadowable properties (as name/value strings) onto this shared resource when
+        a cross-pod invite is processed. Sub-classes should override to expose the properties they
+        care about.
+        """
+        pass
+
+
     @classmethod
     def metadataColumns(cls):
         """
@@ -3540,13 +3817,14 @@
             cls._bindSchema.BIND_MODE,
             cls._bindSchema.HOME_RESOURCE_ID,
             cls._bindSchema.RESOURCE_ID,
+            cls._bindSchema.EXTERNAL_ID,
             cls._bindSchema.RESOURCE_NAME,
             cls._bindSchema.BIND_STATUS,
             cls._bindSchema.BIND_REVISION,
             cls._bindSchema.MESSAGE
         )
 
-    bindColumnCount = 7
+    bindColumnCount = 8
 
     @classmethod
     def additionalBindColumns(cls):
@@ -3606,6 +3884,7 @@
             yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForHomeChildMetaData(self._resourceID))
             yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithName(self._home._resourceID, self._name))
             yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithResourceID(self._home._resourceID, self._resourceID))
+            yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithExternalID(self._home._resourceID, self._externalID))
 
 
 
@@ -3621,6 +3900,7 @@
         "_resourceID",
     )
 
+    _externalClass = None
     _objectResourceClass = None
 
     _bindSchema = None
@@ -3631,11 +3911,148 @@
     _objectSchema = None
 
 
-    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None):
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, home, bindData, additionalBindData, metadataData, propstore=None, ownerHome=None):
+        """
+        Given the various database rows, build the actual class.
 
+        @param home: the parent home object
+        @type home: L{CommonHome}
+        @param bindData: the standard set of bind columns
+        @type bindData: C{list}
+        @param additionalBindData: additional bind data specific to sub-classes
+        @type additionalBindData: C{list}
+        @param metadataData: metadata data
+        @type metadataData: C{list}
+        @param propstore: a property store to use, or C{None} to load it automatically
+        @type propstore: L{PropertyStore}
+        @param ownerHome: the home of the owner, or C{None} to figure it out automatically
+        @type ownerHome: L{CommonHome}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        """
+
+        bindMode, _ignore_homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = bindData
+
+        if ownerHome is None:
+            if bindMode == _BIND_MODE_OWN:
+                ownerHome = home
+                ownerName = name
+            else:
+                ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
+        else:
+            ownerName = None
+
+        c = cls._externalClass if ownerHome.external() else cls
+        child = c(
+            home=home,
+            name=name,
+            resourceID=resourceID,
+            mode=bindMode,
+            status=bindStatus,
+            revision=bindRevision,
+            message=bindMessage,
+            ownerHome=ownerHome,
+            ownerName=ownerName,
+            externalID=externalID,
+        )
+
+        if additionalBindData:
+            for attr, value in zip(child.additionalBindAttributes(), additionalBindData):
+                setattr(child, attr, value)
+
+        if metadataData:
+            for attr, value in zip(child.metadataAttributes(), metadataData):
+                setattr(child, attr, value)
+
+        # We have to re-adjust the property store object to account for possible shared
+        # collections as previously we loaded them all as if they were owned
+        if propstore and bindMode != _BIND_MODE_OWN:
+            propstore._setDefaultUserUID(ownerHome.uid())
+        yield child._loadPropertyStore(propstore)
+
+        returnValue(child)
+
+
+    @classmethod
+    @inlineCallbacks
+    def _getDBData(cls, home, name, resourceID, externalID):
+        """
+        Given a set of identifying information, load the data rows for the object. Only one of
+        L{name}, L{resourceID} or L{externalID} is specified - others are C{None}.
+
+        @param home: the parent home object
+        @type home: L{CommonHome}
+        @param name: the resource name
+        @type name: C{str}
+        @param resourceID: the resource ID
+        @type resourceID: C{int}
+        @param externalID: the resource ID of the external (cross-pod) referenced item
+        @type externalID: C{int}
+        """
+
+        # Get the bind row data
+        row = None
+        queryCacher = home._txn._queryCacher
+
+        if queryCacher:
+            # Retrieve data from cache
+            if name:
+                cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
+            elif resourceID:
+                cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
+            elif externalID:
+                cacheKey = queryCacher.keyForObjectWithExternalID(home._resourceID, externalID)
+            row = yield queryCacher.get(cacheKey)
+
+        if row is None:
+            # No cached copy
+            if name:
+                rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
+            elif resourceID:
+                rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
+            elif externalID:
+                rows = yield cls._bindForExternalIDAndHomeID.on(home._txn, externalID=externalID, homeID=home._resourceID)
+            row = rows[0] if rows else None
+
+        if not row:
+            returnValue(None)
+
+        if queryCacher:
+            # Cache the result
+            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), row)
+            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), row)
+            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithExternalID(home._resourceID, externalID), row)
+
+        bindData = row[:cls.bindColumnCount]
+        additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+        resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+
+        # Get the matching metadata data
+        metadataData = None
+        if queryCacher:
+            # Retrieve from cache
+            cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+            metadataData = yield queryCacher.get(cacheKey)
+
+        if metadataData is None:
+            # No cached copy
+            metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+            if queryCacher:
+                # Cache the results
+                yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+        returnValue((bindData, additionalBindData, metadataData,))
+
+
+    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
+
         self._home = home
         self._name = name
         self._resourceID = resourceID
+        self._externalID = externalID
         self._bindMode = mode
         self._bindStatus = status
         self._bindRevision = revision
@@ -3657,9 +4074,7 @@
         else:
             self._notifiers = None
 
-        self._index = None  # Derived classes need to set this
 
-
     def memoMe(self, key, memo): #@UnusedVariable
         """
         Add this object to the memo dictionary in whatever fashion is appropriate.
@@ -3685,7 +4100,7 @@
         rows = yield cls._acceptedBindForHomeID.on(
             home._txn, homeID=home._resourceID
         )
-        names = [row[3] for row in rows]
+        names = [row[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] for row in rows]
         returnValue(names)
 
 
@@ -3718,119 +4133,66 @@
 
         # Create the actual objects merging in properties
         for dataRow in dataRows:
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
-            additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
-            metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+            bindData = dataRow[:cls.bindColumnCount] #@UnusedVariable
+            resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+            additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+            metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+            propstore = propertyStores.get(resourceID, None)
 
-            if bindMode == _BIND_MODE_OWN:
-                ownerHome = home
-                ownerName = bindName
-            else:
-                #TODO: get all ownerHomeIDs at once
-                ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
-            child = cls(
-                home=home,
-                name=bindName,
-                resourceID=resourceID,
-                mode=bindMode,
-                status=bindStatus,
-                revision=bindRevision,
-                message=bindMessage,
-                ownerHome=ownerHome,
-                ownerName=ownerName,
-            )
-            for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
-                setattr(child, attr, value)
-            for attr, value in zip(cls.metadataAttributes(), metadata):
-                setattr(child, attr, value)
+            child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore)
             child._syncTokenRevision = revisions[resourceID]
-            propstore = propertyStores.get(resourceID, None)
-            # We have to re-adjust the property store object to account for possible shared
-            # collections as previously we loaded them all as if they were owned
-            if propstore and bindMode != _BIND_MODE_OWN:
-                propstore._setDefaultUserUID(ownerHome.uid())
-            yield child._loadPropertyStore(propstore)
             results.append(child)
+
         returnValue(results)
 
 
     @classmethod
     def objectWithName(cls, home, name, accepted=True):
-        return cls._objectWithNameOrID(home, name=name, accepted=accepted)
+        return cls.objectWith(home, name=name, accepted=accepted)
 
 
     @classmethod
     def objectWithID(cls, home, resourceID, accepted=True):
-        return cls._objectWithNameOrID(home, resourceID=resourceID, accepted=accepted)
+        return cls.objectWith(home, resourceID=resourceID, accepted=accepted)
 
 
     @classmethod
+    def objectWithExternalID(cls, home, externalID, accepted=True):
+        return cls.objectWith(home, externalID=externalID, accepted=accepted)
+
+
+    @classmethod
     @inlineCallbacks
-    def _objectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
-        # replaces objectWithName()
+    def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
         """
-        Retrieve the child with the given C{name} or C{resourceID} contained in the given
-        C{home}.
+        Create the object using one of the specified arguments as the key to load it. One
+        and only one of the keyword arguments must be set.
 
-        @param home: a L{CommonHome}.
+        @param parent: parent collection
+        @type parent: L{CommonHomeChild}
+        @param name: name of the resource, or C{None}
+        @type name: C{str}
+        @param uid: resource data UID, or C{None}
+        @type uid: C{str}
+        @param resourceID: resource id
+        @type resourceID: C{int}
+        @param accepted: if C{True} only load owned or accepted share items
+        @type accepted: C{bool}
 
-        @param name: a string; the name of the L{CommonHomeChild} to retrieve.
-
-        @return: an L{CommonHomeChild} or C{None} if no such child
-            exists.
+        @return: the new object or C{None} if not found
+        @rtype: C{CommonHomeChild}
         """
-        rows = None
-        queryCacher = home._txn._queryCacher
 
-        if queryCacher:
-            # Retrieve data from cache
-            if name:
-                cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
-            else:
-                cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
-            rows = yield queryCacher.get(cacheKey)
-
-        if rows is None:
-            # No cached copy
-            if name:
-                rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
-            else:
-                rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
-
-        if not rows:
+        dbData = yield cls._getDBData(home, name, resourceID, externalID)
+        if dbData is None:
             returnValue(None)
+        bindData, additionalBindData, metadataData = dbData
 
-        row = rows[0]
-        bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
-
-        if queryCacher:
-            # Cache the result
-            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), rows)
-            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), rows)
-
+        bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
         if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
             returnValue(None)
-        additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
 
-        if bindMode == _BIND_MODE_OWN:
-            ownerHome = home
-            ownerName = name
-        else:
-            ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
-        child = cls(
-            home=home,
-            name=name,
-            resourceID=resourceID,
-            mode=bindMode,
-            status=bindStatus,
-            revision=bindRevision,
-            message=bindMessage,
-            ownerHome=ownerHome,
-            ownerName=ownerName
-        )
-        yield child.initFromStore(additionalBind)
+        child = yield cls.makeClass(home, bindData, additionalBindData, metadataData)
         returnValue(child)
 
 
@@ -3858,35 +4220,28 @@
 
     @classmethod
     @inlineCallbacks
-    def create(cls, home, name):
+    def create(cls, home, name, externalID=None):
 
-        if (yield cls._bindForNameAndHomeID.on(home._txn,
-            name=name, homeID=home._resourceID)):
+        if (yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)):
             raise HomeChildNameAlreadyExistsError(name)
 
         if name.startswith("."):
             raise HomeChildNameNotAllowedError(name)
 
         # Create this object
-        resourceID = (
-            yield cls._insertHomeChild.on(home._txn))[0][0]
+        resourceID = (yield cls._insertHomeChild.on(home._txn))[0][0]
 
         # Initialize this object
-        _created, _modified = (
-            yield cls._insertHomeChildMetaData.on(home._txn,
-                                                  resourceID=resourceID))[0]
+        _created, _modified = (yield cls._insertHomeChildMetaData.on(home._txn, resourceID=resourceID))[0]
         # Bind table needs entry
         yield cls._bindInsertQuery.on(
-            home._txn, homeID=home._resourceID, resourceID=resourceID,
+            home._txn, homeID=home._resourceID, resourceID=resourceID, externalID=externalID,
             name=name, mode=_BIND_MODE_OWN, bindStatus=_BIND_STATUS_ACCEPTED,
             message=None,
         )
 
         # Initialize other state
-        child = cls(home, name, resourceID, _BIND_MODE_OWN, _BIND_STATUS_ACCEPTED)
-        child._created = _created
-        child._modified = _modified
-        yield child._loadPropertyStore()
+        child = yield cls.objectWithID(home, resourceID)
 
         yield child._initSyncToken()
 
@@ -3907,44 +4262,32 @@
                       Where=child.RESOURCE_ID == Parameter("resourceID"))
 
 
-    @inlineCallbacks
-    def initFromStore(self, additionalBind=None):
+    def id(self):
         """
-        Initialise this object from the store, based on its already-populated
-        resource ID. We read in and cache all the extra metadata from the DB to
-        avoid having to do DB queries for those individually later.
+        Retrieve the store identifier for this collection.
+
+        @return: store identifier.
+        @rtype: C{int}
         """
-        queryCacher = self._txn._queryCacher
-        if queryCacher:
-            # Retrieve from cache
-            cacheKey = queryCacher.keyForHomeChildMetaData(self._resourceID)
-            dataRows = yield queryCacher.get(cacheKey)
-        else:
-            dataRows = None
-        if dataRows is None:
-            # No cached copy
-            dataRows = (yield self._metadataByIDQuery.on(self._txn, resourceID=self._resourceID))[0]
-            if queryCacher:
-                # Cache the results
-                yield queryCacher.setAfterCommit(self._txn, cacheKey, dataRows)
+        return self._resourceID
 
-        if additionalBind:
-            for attr, value in zip(self.additionalBindAttributes(), additionalBind):
-                setattr(self, attr, value)
 
-        for attr, value in zip(self.metadataAttributes(), dataRows):
-            setattr(self, attr, value)
-        yield self._loadPropertyStore()
+    def external_id(self):
+        """
+        Retrieve the external store identifier for this collection.
 
+        @return: a string.
+        """
+        return self._externalID
 
-    def id(self):
+
+    def external(self):
         """
-        Retrieve the store identifier for this collection.
+        Is this an external home.
 
-        @return: store identifier.
-        @rtype: C{int}
+        @return: a string.
         """
-        return self._resourceID
+        return self.ownerHome().external()
 
 
     @property
@@ -3956,10 +4299,6 @@
         return self._txn.store().directoryService()
 
 
-    def retrieveOldIndex(self):
-        return self._index
-
-
     def __repr__(self):
         return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
 
@@ -3994,14 +4333,13 @@
 
         @return: a L{Deferred} which fires when the modification is complete.
         """
+
+        if self.isShared() or self.external():
+            raise ShareNotAllowed("Cannot rename a shared collection")
+
         oldName = self._name
 
-        queryCacher = self._home._txn._queryCacher
-        if queryCacher:
-            cacheKey = queryCacher.keyForObjectWithName(self._home._resourceID, oldName)
-            yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
-            cacheKey = queryCacher.keyForObjectWithResourceID(self._home._resourceID, self._resourceID)
-            yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
+        yield self.invalidateQueryCache()
 
         yield self._renameQuery.on(self._txn, name=name,
                                    resourceID=self._resourceID,
@@ -4028,19 +4366,16 @@
     @inlineCallbacks
     def remove(self):
 
+        # Stop sharing first
+        yield self.ownerDeleteShare()
+
         # Do before setting _resourceID making changes
         yield self.notifyPropertyChanged()
 
-        queryCacher = self._home._txn._queryCacher
-        if queryCacher:
-            cacheKey = queryCacher.keyForObjectWithName(self._home._resourceID, self._name)
-            yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
-            cacheKey = queryCacher.keyForObjectWithResourceID(self._home._resourceID, self._resourceID)
-            yield queryCacher.invalidateAfterCommit(self._home._txn, cacheKey)
+        yield self.invalidateQueryCache()
 
         yield self._deletedSyncToken()
-        yield self._deleteQuery.on(self._txn, NoSuchHomeChildError,
-                                   resourceID=self._resourceID)
+        yield self._deleteQuery.on(self._txn, NoSuchHomeChildError, resourceID=self._resourceID)
         yield self.properties()._removeResource()
 
         # Set to non-existent state
@@ -4092,6 +4427,7 @@
         for result in results:
             self._objects[result.name()] = result
             self._objects[result.uid()] = result
+            self._objects[result.id()] = result
         self._objectNames = sorted([result.name() for result in results])
         returnValue(results)
 
@@ -4105,6 +4441,7 @@
         for result in results:
             self._objects[result.name()] = result
             self._objects[result.uid()] = result
+            self._objects[result.id()] = result
         self._objectNames = sorted([result.name() for result in results])
         returnValue(results)
 
@@ -4141,8 +4478,7 @@
     @inlineCallbacks
     def countObjectResources(self):
         if self._objectNames is None:
-            rows = yield self._objectCountQuery.on(
-                self._txn, resourceID=self._resourceID)
+            rows = yield self._objectCountQuery.on(self._txn, resourceID=self._resourceID)
             returnValue(rows[0][0])
         returnValue(len(self._objectNames))
 
@@ -4174,18 +4510,13 @@
         We create the empty object first then have it initialize itself from the
         store.
         """
-        if resourceID:
-            objectResource = (
-                yield self._objectResourceClass.objectWithID(self, resourceID)
-            )
-        else:
-            objectResource = (
-                yield self._objectResourceClass.objectWithName(self, name, uid)
-            )
+        objectResource = (
+            yield self._objectResourceClass.objectWith(self, name=name, uid=uid, resourceID=resourceID)
+        )
         if objectResource:
             self._objects[objectResource.name()] = objectResource
             self._objects[objectResource.uid()] = objectResource
-            self._objects[objectResource._resourceID] = objectResource
+            self._objects[objectResource.id()] = objectResource
         else:
             if resourceID:
                 self._objects[resourceID] = None
@@ -4232,7 +4563,7 @@
         obj = cls._objectSchema
         return Select(
             [obj.UID], From=obj,
-            Where=(obj.UID == Parameter("name")
+            Where=(obj.RESOURCE_NAME == Parameter("name")
                   ).And(obj.PARENT_RESOURCE_ID == Parameter("resourceID")))
 
 
@@ -4275,6 +4606,7 @@
         )
         self._objects[objectResource.name()] = objectResource
         self._objects[objectResource.uid()] = objectResource
+        self._objects[objectResource.id()] = objectResource
 
         # Note: create triggers a notification when the component is set, so we
         # don't need to call notify() here like we do for object removal.
@@ -4285,6 +4617,7 @@
     def removedObjectResource(self, child):
         self._objects.pop(child.name(), None)
         self._objects.pop(child.uid(), None)
+        self._objects.pop(child.id(), None)
         if self._objectNames and child.name() in self._objectNames:
             self._objectNames.remove(child.name())
         yield self._deleteRevision(child.name())
@@ -4317,10 +4650,9 @@
 
 
     @inlineCallbacks
-    def moveObjectResource(self, child, newparent, newname=None):
+    def _validObjectResource(self, child, newparent, newname=None):
         """
-        Move a child of this collection into another collection without actually removing/re-inserting the data.
-        Make sure sync and cache details for both collections are updated.
+        Check that the move operation is valid
 
         TODO: check that the resource name does not exist in the new parent, or that the UID
         does not exist there too.
@@ -4334,7 +4666,6 @@
         """
 
         name = child.name()
-        uid = child.uid()
 
         if newname is None:
             newname = name
@@ -4353,10 +4684,34 @@
             if child_count >= config.MaxResourcesPerCollection:
                 raise TooManyObjectResourcesError()
 
+        returnValue(newname)
+
+
+    @inlineCallbacks
+    def moveObjectResource(self, child, newparent, newname=None):
+        """
+        Move a child of this collection into another collection without actually removing/re-inserting the data.
+        Make sure sync and cache details for both collections are updated.
+
+        TODO: check that the resource name does not exist in the new parent, or that the UID
+        does not exist there too.
+
+        @param child: the child resource to move
+        @type child: L{CommonObjectResource}
+        @param newparent: the parent to move to
+        @type newparent: L{CommonHomeChild}
+        @param newname: new name to use in new parent
+        @type newname: C{str} or C{None} for existing name
+        """
+
+        name = child.name()
+        newname = yield self._validObjectResource(child, newparent, newname)
+        uid = child.uid()
+
         # Clean this collections cache and signal sync change
         self._objects.pop(name, None)
         self._objects.pop(uid, None)
-        self._objects.pop(child._resourceID, None)
+        self._objects.pop(child.id(), None)
         yield self._deleteRevision(name)
         yield self.notifyChanged()
 
@@ -4387,11 +4742,68 @@
         # Signal sync change on new collection
         newparent._objects.pop(name, None)
         newparent._objects.pop(uid, None)
-        newparent._objects.pop(child._resourceID, None)
+        newparent._objects.pop(child.id(), None)
         yield newparent._insertRevision(newname)
         yield newparent.notifyChanged()
 
 
+    @inlineCallbacks
+    def moveObjectResourceCreateDelete(self, child, newparent, newname=None):
+        """
+        Move a child of this collection into another collection by doing a create/delete.
+
+        TODO: check that the resource name does not exist in the new parent, or that the UID
+        does not exist there too.
+
+        @param child: the child resource to move
+        @type child: L{CommonObjectResource}
+        @param newparent: the parent to move to
+        @type newparent: L{CommonHomeChild}
+        @param newname: new name to use in new parent
+        @type newname: C{str} or C{None} for existing name
+        """
+
+        name = child.name()
+        newname = yield self._validObjectResource(child, newparent, newname)
+
+        # Do a move as a create/delete
+        component = yield child.component()
+        yield newparent.moveObjectResourceHere(name, component)
+        yield self.moveObjectResourceAway(child.id(), child)
+
+
+    @inlineCallbacks
+    def moveObjectResourceHere(self, name, component):
+        """
+        Create a new child in this collection as part of a move operation. This needs to be split out because
+        behavior differs for sub-classes and cross-pod operations.
+
+        @param name: new name to use in new parent
+        @type name: C{str} or C{None} for existing name
+        @param component: data for new resource
+        @type component: L{Component}
+        """
+
+        yield self.createObjectResourceWithName(name, component)
+
+
+    @inlineCallbacks
+    def moveObjectResourceAway(self, rid, child=None):
+        """
+        Remove the child as the result of a move operation. This needs to be split out because
+        behavior differs for sub-classes and cross-pod operations.
+
+        @param rid: the child resource-id to move
+        @type rid: C{int}
+        @param child: the child resource to move - might be C{None} for cross-pod
+        @type child: L{CommonObjectResource}
+        """
+
+        if child is None:
+            child = yield self.objectResourceWithID(rid)
+        yield child.remove()
+
+
     def objectResourcesHaveProperties(self):
         return False
 
@@ -4407,12 +4819,26 @@
         @type revision: C{int}
         """
 
-        if revision != 0 and revision < self._bindRevision:
+        if revision != 0 and revision < self._bindRevision and not self.external():
             raise SyncTokenValidException
-
         return super(CommonHomeChild, self).resourceNamesSinceRevision(revision)
 
 
+    def search(self, filter):
+        """
+        Do a query of the contents of this collection.
+
+        @param filter: the query filter to use
+        @type filter: L{Filter}
+
+        @return: the names of the matching resources
+        @rtype: C{list}
+        """
+
+        # This implementation raises - sub-classes override to do the actual query
+        raise IndexedSearchException()
+
+
     @inlineCallbacks
     def sharedChildResourceNamesSinceRevision(self, revision, depth):
         """
@@ -4436,47 +4862,56 @@
         """
         assert not self.owned()
 
-        if revision != 0 and revision < self._bindRevision:
-            if depth != '1':
-                raise SyncTokenValidException
-            else:
-                revision = 0
-
         changed = set()
         deleted = set()
-        rev = self._revisionsSchema
-        results = [
-            (
-                self.name(),
-                name if name else "",
-                wasdeleted
-            )
-            for name, wasdeleted in
-            (yield Select([rev.RESOURCE_NAME, rev.DELETED],
-                             From=rev,
-                            Where=(rev.REVISION > revision).And(
-                            rev.RESOURCE_ID == self._resourceID)).on(self._txn))
-            if name
-        ]
+        invalid = set()
+        if self.external():
+            if depth == "1":
+                pass
+            else:
+                name = self.name() + "/"
+                invalid.add(name)
+        else:
+            if revision != 0 and revision < self._bindRevision:
+                if depth != "1":
+                    raise SyncTokenValidException
+                else:
+                    revision = 0
 
-        for path, name, wasdeleted in results:
-            if wasdeleted:
-                if revision:
-                    if depth == "1":
-                        changed.add("%s/" % (path,))
-                    else:
-                        deleted.add("%s/%s" % (path, name,))
+            rev = self._revisionsSchema
+            results = [
+                (
+                    self.name(),
+                    name if name else "",
+                    wasdeleted
+                )
+                for name, wasdeleted in
+                (yield Select([rev.RESOURCE_NAME, rev.DELETED],
+                                 From=rev,
+                                Where=(rev.REVISION > revision).And(
+                                rev.RESOURCE_ID == self._resourceID)).on(self._txn))
+                if name
+            ]
 
-            # Always report collection as changed
-            changed.add("%s/" % (path,))
+            for path, name, wasdeleted in results:
+                if wasdeleted:
+                    if revision:
+                        if depth == "1":
+                            changed.add("%s/" % (path,))
+                        else:
+                            deleted.add("%s/%s" % (path, name,))
 
-            # Resource changed - for depth "infinity" report resource as changed
-            if depth != "1":
-                changed.add("%s/%s" % (path, name,))
+                # Always report collection as changed
+                changed.add("%s/" % (path,))
 
-        returnValue((changed, deleted))
+                if name:
+                    # Resource changed - for depth "infinity" report resource as changed
+                    if depth != "1":
+                        changed.add("%s/%s" % (path, name,))
 
+        returnValue((changed, deleted, invalid,))
 
+
     @inlineCallbacks
     def _loadPropertyStore(self, props=None):
         if props is None:
@@ -4656,10 +5091,85 @@
         "_parentCollection",
     )
 
+    _externalClass = None
     _objectSchema = None
+    _componentClass = None
 
     BATCH_LOAD_SIZE = 50
 
+
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, parent, objectData, propstore=None):
+        """
+        Given the various database rows, build the actual class.
+
+        @param parent: the parent collection object
+        @type parent: L{CommonHomeChild}
+        @param objectData: the standard set of object columns
+        @type objectData: C{list}
+        @param propstore: a property store to use, or C{None} to load it automatically
+        @type propstore: L{PropertyStore}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        """
+
+        c = cls._externalClass if parent.external() else cls
+        child = c(
+            parent,
+            objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+            objectData[cls._allColumns().index(cls._objectSchema.UID)],
+        )
+
+        for attr, value in zip(child._rowAttributes(), objectData):
+            setattr(child, attr, value)
+
+        yield child._loadPropertyStore(propstore)
+
+        returnValue(child)
+
+
+    @classmethod
+    @inlineCallbacks
+    def _getDBData(cls, parent, name, uid, resourceID):
+        """
+        Given a set of identifying information, load the data rows for the object. Only one of
+        L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+        @param parent: the parent collection object
+        @type parent: L{CommonHomeChild}
+        @param name: the resource name
+        @type name: C{str}
+        @param uid: the UID of the data
+        @type uid: C{str}
+        @param resourceID: the resource ID
+        @type resourceID: C{int}
+        """
+
+        rows = None
+        if name:
+            rows = yield cls._allColumnsWithParentAndName.on(
+                parent._txn,
+                name=name,
+                parentID=parent._resourceID
+            )
+        elif uid:
+            rows = yield cls._allColumnsWithParentAndUID.on(
+                parent._txn,
+                uid=uid,
+                parentID=parent._resourceID
+            )
+        elif resourceID:
+            rows = yield cls._allColumnsWithParentAndID.on(
+                parent._txn,
+                resourceID=resourceID,
+                parentID=parent._resourceID
+            )
+
+        returnValue(rows[0] if rows else None)
+
+
     def __init__(self, parent, name, uid, resourceID=None, options=None): #@UnusedVariable
         self._parentCollection = parent
         self._resourceID = resourceID
@@ -4669,7 +5179,8 @@
         self._size = None
         self._created = None
         self._modified = None
-        self._notificationData = None
+        self._textData = None
+        self._cachedComponent = None
 
         self._locked = False
 
@@ -4677,7 +5188,7 @@
     @classproperty
     def _allColumnsWithParentQuery(cls): #@NoSelf
         obj = cls._objectSchema
-        return Select(cls._allColumns, From=obj,
+        return Select(cls._allColumns(), From=obj,
                       Where=obj.PARENT_RESOURCE_ID == Parameter("parentID"))
 
 
@@ -4718,11 +5229,10 @@
 
         # Create the actual objects merging in properties
         for row in dataRows:
-            child = cls(parent, "", None)
-            child._initFromRow(tuple(row))
-            yield child._loadPropertyStore(
-                props=propertyStores.get(child._resourceID, None)
-            )
+            resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+            propstore = propertyStores.get(resourceID, None)
+
+            child = yield cls.makeClass(parent, row, propstore=propstore)
             results.append(child)
 
         returnValue(results)
@@ -4732,7 +5242,9 @@
     @inlineCallbacks
     def loadAllObjectsWithNames(cls, parent, names):
         """
-        Load all child objects with the specified names, doing so in batches.
+        Load all child objects with the specified names, doing so in batches (because we need to match
+        using SQL "resource_name in (...)" where there might be a character length limit on the number
+        of items in the set).
         """
         names = tuple(names)
         results = []
@@ -4747,7 +5259,7 @@
     @classmethod
     def _allColumnsWithParentAndNamesQuery(cls, names):
         obj = cls._objectSchema
-        return Select(cls._allColumns, From=obj,
+        return Select(cls._allColumns(), From=obj,
                       Where=(obj.PARENT_RESOURCE_ID == Parameter("parentID")).And(
                           obj.RESOURCE_NAME.In(Parameter("names", len(names)))))
 
@@ -4771,7 +5283,7 @@
 
         # Optimize case of single name to load
         if len(names) == 1:
-            obj = yield cls.objectWithName(parent, names[0], None)
+            obj = yield cls.objectWithName(parent, names[0])
             returnValue([obj] if obj else [])
 
         results = []
@@ -4792,30 +5304,61 @@
 
         # Create the actual objects merging in properties
         for row in dataRows:
-            child = cls(parent, "", None)
-            child._initFromRow(tuple(row))
-            yield child._loadPropertyStore(
-                props=propertyStores.get(child._resourceID, None)
-            )
+            resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+            propstore = propertyStores.get(resourceID, None)
+
+            child = yield cls.makeClass(parent, row, propstore=propstore)
             results.append(child)
 
         returnValue(results)
 
 
     @classmethod
-    def objectWithName(cls, parent, name, uid):
-        objectResource = cls(parent, name, uid, None)
-        return objectResource.initFromStore()
+    def objectWithName(cls, parent, name):
+        return cls.objectWith(parent, name=name)
 
 
     @classmethod
+    def objectWithUID(cls, parent, uid):
+        return cls.objectWith(parent, uid=uid)
+
+
+    @classmethod
     def objectWithID(cls, parent, resourceID):
-        objectResource = cls(parent, None, None, resourceID)
-        return objectResource.initFromStore()
+        return cls.objectWith(parent, resourceID=resourceID)
 
 
     @classmethod
     @inlineCallbacks
+    def objectWith(cls, parent, name=None, uid=None, resourceID=None):
+        """
+        Create the object using one of the specified arguments as the key to load it. One
+        and only one of the keyword arguments must be set.
+
+        @param parent: parent collection
+        @type parent: L{CommonHomeChild}
+        @param name: name of the resource, or C{None}
+        @type name: C{str}
+        @param uid: resource data UID, or C{None}
+        @type uid: C{str}
+        @param resourceID: resource id
+        @type resourceID: C{int}
+
+        @return: the new object or C{None} if not found
+        @rtype: C{CommonObjectResource}
+        """
+
+        row = yield cls._getDBData(parent, name, uid, resourceID)
+
+        if row:
+            child = yield cls.makeClass(parent, row)
+            returnValue(child)
+        else:
+            returnValue(None)
+
+
+    @classmethod
+    @inlineCallbacks
     def create(cls, parent, name, component, options=None):
 
         child = (yield parent.objectResourceWithName(name))
@@ -4825,13 +5368,13 @@
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
 
-        objectResource = cls(parent, name, None, None, options=options)
+        c = cls._externalClass if parent.external() else cls
+        objectResource = c(parent, name, None, None, options=options)
         yield objectResource.setComponent(component, inserting=True)
         yield objectResource._loadPropertyStore(created=True)
 
         # Note: setComponent triggers a notification, so we don't need to
         # call notify( ) here like we do for object removal.
-
         returnValue(objectResource)
 
 
@@ -4842,7 +5385,7 @@
         parameter and a given instance column matches a given parameter name.
         """
         return Select(
-            cls._allColumns, From=cls._objectSchema,
+            cls._allColumns(), From=cls._objectSchema,
             Where=(column == Parameter(paramName)).And(
                 cls._objectSchema.PARENT_RESOURCE_ID == Parameter("parentID"))
         )
@@ -4863,38 +5406,7 @@
         return cls._allColumnsWithParentAnd(cls._objectSchema.RESOURCE_ID, "resourceID")
 
 
-    @inlineCallbacks
-    def initFromStore(self):
-        """
-        Initialise this object from the store. We read in and cache all the
-        extra metadata from the DB to avoid having to do DB queries for those
-        individually later. Either the name or uid is present, so we have to
-        tweak the query accordingly.
-
-        @return: L{self} if object exists in the DB, else C{None}
-        """
-
-        if self._name:
-            rows = yield self._allColumnsWithParentAndName.on(
-                self._txn, name=self._name,
-                parentID=self._parentCollection._resourceID)
-        elif self._uid:
-            rows = yield self._allColumnsWithParentAndUID.on(
-                self._txn, uid=self._uid,
-                parentID=self._parentCollection._resourceID)
-        elif self._resourceID:
-            rows = yield self._allColumnsWithParentAndID.on(
-                self._txn, resourceID=self._resourceID,
-                parentID=self._parentCollection._resourceID)
-        if rows:
-            self._initFromRow(tuple(rows[0]))
-            yield self._loadPropertyStore()
-            returnValue(self)
-        else:
-            returnValue(None)
-
-
-    @classproperty
+    @classmethod
     def _allColumns(cls): #@NoSelf
         """
         Full set of columns in the object table that need to be loaded to
@@ -4912,21 +5424,52 @@
         ]
 
 
-    def _initFromRow(self, row):
+    @classmethod
+    def _rowAttributes(cls): #@NoSelf
+        return (
+            "_resourceID",
+            "_name",
+            "_uid",
+            "_md5",
+            "_size",
+            "_created",
+            "_modified",
+        )
+
+
+    @classmethod
+    def _otherSerializedAttributes(cls): #@NoSelf
+        return (
+            "_componentChanged",
+        )
+
+
+    def externalize(self):
         """
-        Given a select result using the columns from L{_allColumns}, initialize
-        the object resource state.
+        Create a dictionary mapping key attributes so this object can be sent over a cross-pod call
+        and reconstituted at the other end. Note that the other end may have a different schema so
+        the attributes may not match exactly and will need to be processed accordingly.
         """
-        (self._resourceID,
-         self._name,
-         self._uid,
-         self._md5,
-         self._size,
-         self._created,
-         self._modified,) = tuple(row)
+        return dict([(attr[1:], getattr(self, attr, None)) for attr in itertools.chain(self._rowAttributes(), self._otherSerializedAttributes())])
 
 
+    @classmethod
     @inlineCallbacks
+    def internalize(cls, parent, mapping):
+        """
+        Given a mapping generated by L{externalize}, convert the values into an array of database
+        like items that conforms to the ordering of L{_allColumns} so it can be fed into L{makeClass}.
+        Note that there may be a schema mismatch with the external data, so treat missing items as
+        C{None} and ignore extra items.
+        """
+
+        child = yield cls.makeClass(parent, [mapping.get(row[1:]) for row in cls._rowAttributes()])
+        for attr in cls._otherSerializedAttributes():
+            setattr(child, attr, mapping.get(attr[1:]))
+        returnValue(child)
+
+
+    @inlineCallbacks
     def _loadPropertyStore(self, props=None, created=False):
         if props is None:
             if self._parentCollection.objectResourcesHaveProperties():
@@ -5036,7 +5579,7 @@
         self._locked = True
 
 
-    def setComponent(self, component, inserting=False, options=None):
+    def setComponent(self, component, inserting=False):
         raise NotImplementedError
 
 
@@ -5069,9 +5612,15 @@
         """
 
         yield self.moveValidation(destination, name)
-        yield self._parentCollection.moveObjectResource(self, destination, name)
 
+        # If possible we do a "fast" move by simply fixing up the database information directly rather than
+        # re-writing any data. That is only possible when the source and destination are on this pod.
+        if not self._parentCollection.external() and not destination.external():
+            yield self._parentCollection.moveObjectResource(self, destination, name)
+        else:
+            yield self._parentCollection.moveObjectResourceCreateDelete(self, destination, name)
 
+
     def moveValidation(self, destination, name):
         raise NotImplementedError
 
@@ -5092,7 +5641,8 @@
         self._size = None
         self._created = None
         self._modified = None
-        self._notificationData = None
+        self._textData = None
+        self._cachedComponent = None
 
 
     def removeNotifyCategory(self):
@@ -5148,19 +5698,19 @@
 
     @inlineCallbacks
     def _text(self):
-        if self._notificationData is None:
+        if self._textData is None:
             texts = (
                 yield self._textByIDQuery.on(self._txn,
                                              resourceID=self._resourceID)
             )
             if texts:
                 text = texts[0][0]
-                self._notificationData = text
+                self._textData = text
                 returnValue(text)
             else:
                 raise ConcurrentModification()
         else:
-            returnValue(self._notificationData)
+            returnValue(self._textData)
 
 
 
@@ -5224,6 +5774,15 @@
             resourceID = rows[0][0]
             created = False
         elif create:
+            # Determine if the user is local or external
+            record = txn.directoryService().recordWithUID(uid)
+            if record is None:
+                raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
+
+            state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
+            if state == _HOME_STATUS_EXTERNAL:
+                raise RecordNotAllowedError("Cannot store notifications for external user: {}".format(uid))
+
             # Use savepoint so we can do a partial rollback if there is a race
             # condition where this row has already been inserted
             savepoint = SavepointAction("notificationsWithUID")

Copied: CalendarServer/trunk/txdav/common/datastore/sql_external.py (from rev 12191, CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py)
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_external.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_external.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,470 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# 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.
+##
+"""
+SQL data store.
+"""
+
+from twext.internet.decorate import memoizedKey
+from twext.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+from txdav.base.propertystore.sql import PropertyStore
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
+    CommonObjectResource
+from txdav.common.datastore.sql_tables import _HOME_STATUS_EXTERNAL
+from txdav.common.icommondatastore import NonExistentExternalShare, \
+    ExternalShareFailed
+
+
+log = Logger()
+
+class CommonHomeExternal(CommonHome):
+    """
+    A CommonHome for a user not hosted on this system, but on another pod. This is needed to provide a
+    "reference" to the external user so we can share with them. Actual operations to list child resources, etc
+    are all stubbed out since no data for the user is actually hosted in this store.
+    """
+
+    def __init__(self, transaction, ownerUID, resourceID):
+        super(CommonHomeExternal, self).__init__(transaction, ownerUID)
+        self._resourceID = resourceID
+        self._status = _HOME_STATUS_EXTERNAL
+
+
+    def initFromStore(self, no_cache=False):
+        """
+        Never called - this should be done by CommonHome.initFromStore only.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def external(self):
+        """
+        Is this an external home.
+
+        @return: a string.
+        """
+        return True
+
+
+    def children(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def loadChildren(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def listChildren(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def objectWithShareUID(self, shareUID):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def invitedObjectWithShareUID(self, shareUID):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    @memoizedKey("name", "_children")
+    @inlineCallbacks
+    def createChildWithName(self, name, externalID=None):
+        """
+        No real children - only external ones.
+        """
+        if externalID is None:
+            raise AssertionError("CommonHomeExternal: not supported")
+        child = yield super(CommonHomeExternal, self).createChildWithName(name, externalID)
+        returnValue(child)
+
+
+    def removeChildWithName(self, name):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    @inlineCallbacks
+    def removeExternalChild(self, child):
+        """
+        Remove an external child. Check that it is invalid or unused before calling this because if there
+        are valid references to it, removing will break things.
+        """
+        if child._externalID is None:
+            raise AssertionError("CommonHomeExternal: not supported")
+        yield super(CommonHomeExternal, self).removeChildWithName(child.name())
+
+
+    def syncToken(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def resourceNamesSinceRevision(self, revision, depth):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    @inlineCallbacks
+    def _loadPropertyStore(self):
+        """
+        No property store - stub to a NonePropertyStore.
+        """
+        props = yield PropertyStore.load(
+            self.uid(),
+            self.uid(),
+            self._txn,
+            self._resourceID,
+            notifyCallback=self.notifyChanged
+        )
+        self._propertyStore = props
+
+
+    def properties(self):
+        return self._propertyStore
+
+
+    def objectResourcesWithUID(self, uid, ignore_children=[], allowShared=True):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def objectResourceWithID(self, rid):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def notifyChanged(self):
+        """
+        Notifications are not handled for external homes - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def bumpModified(self):
+        """
+        No changes recorded for external homes - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def removeUnacceptedShares(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+#    def ownerHomeAndChildNameForChildID(self, resourceID):
+#        """
+#        No children.
+#        """
+#        raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CommonHomeChildExternal(CommonHomeChild):
+    """
+    A CommonHomeChild for a collection not hosted on this system, but on another pod. This will forward
+    specific apis to the other pod using cross-pod requests.
+    """
+
+    def external(self):
+        """
+        Is this an external home.
+
+        @return: a string.
+        """
+        return True
+
+
+    def fixNonExistentExternalShare(self):
+        """
+        An external request has returned and indicates the external share no longer exists. That
+        means this shared resource is an "orphan" and needs to be remove (uninvited) to clean things up.
+        """
+        log.error("Non-existent share detected and removed for {share}", share=self)
+        ownerView = yield self.ownerView()
+        yield ownerView.removeShare(self)
+
+
+    @inlineCallbacks
+    def remove(self):
+        """
+        External shares are never removed directly - instead they must be "uninvited". However,
+        the owner's external calendar can be removed.
+        """
+        if self.owned():
+            yield super(CommonHomeChildExternal, self).remove()
+        else:
+            raise AssertionError("CommonHomeChildExternal: not supported")
+
+
+    @inlineCallbacks
+    def listObjectResources(self):
+        if self._objectNames is None:
+            try:
+                self._objectNames = yield self._txn.store().conduit.send_listobjects(self)
+            except NonExistentExternalShare:
+                yield self.fixNonExistentExternalShare()
+                raise ExternalShareFailed("External share does not exist")
+
+        returnValue(self._objectNames)
+
+
+    @inlineCallbacks
+    def countObjectResources(self):
+        if self._objectNames is None:
+            try:
+                count = yield self._txn.store().conduit.send_countobjects(self)
+            except NonExistentExternalShare:
+                yield self.fixNonExistentExternalShare()
+                raise ExternalShareFailed("External share does not exist")
+            returnValue(count)
+        returnValue(len(self._objectNames))
+
+
+    @inlineCallbacks
+    def resourceNameForUID(self, uid):
+        try:
+            resource = self._objects[uid]
+            returnValue(resource.name() if resource else None)
+        except KeyError:
+            pass
+
+        try:
+            name = yield self._txn.store().conduit.send_resourcenameforuid(self, uid)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+
+        if name:
+            returnValue(name)
+        else:
+            self._objects[uid] = None
+            returnValue(None)
+
+
+    @inlineCallbacks
+    def resourceUIDForName(self, name):
+        try:
+            resource = self._objects[name]
+            returnValue(resource.uid() if resource else None)
+        except KeyError:
+            pass
+
+        try:
+            uid = yield self._txn.store().conduit.send_resourceuidforname(self, name)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+
+        if uid:
+            returnValue(uid)
+        else:
+            self._objects[name] = None
+            returnValue(None)
+
+
+    @inlineCallbacks
+    def moveObjectResource(self, child, newparent, newname=None):
+        """
+        The base class does an optimization to avoid removing/re-creating
+        the actual object resource data. That might not always be possible
+        with external shares if the shared resource is moved to a collection
+        that is not shared or shared by someone else on a different (third)
+        pod. The best bet here is to treat the move as a delete/create.
+        """
+        raise NotImplementedError("TODO: external resource")
+
+
+    @inlineCallbacks
+    def moveObjectResourceHere(self, name, component):
+        """
+        Create a new child in this collection as part of a move operation. This needs to be split out because
+        behavior differs for sub-classes and cross-pod operations.
+
+        @param name: new name to use in new parent
+        @type name: C{str} or C{None} for existing name
+        @param component: data for new resource
+        @type component: L{Component}
+        """
+
+        try:
+            result = yield self._txn.store().conduit.send_movehere(self, name, str(component))
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def moveObjectResourceAway(self, rid, child=None):
+        """
+        Remove the child as the result of a move operation. This needs to be split out because
+        behavior differs for sub-classes and cross-pod operations.
+
+        @param rid: the child resource-id to move
+        @type rid: C{int}
+        @param child: the child resource to move - might be C{None} for cross-pod
+        @type child: L{CommonObjectResource}
+        """
+
+        try:
+            result = yield self._txn.store().conduit.send_moveaway(self, rid)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def syncToken(self):
+        if self._syncTokenRevision is None:
+            try:
+                token = yield self._txn.store().conduit.send_synctoken(self)
+                self._syncTokenRevision = self.revisionFromToken(token)
+            except NonExistentExternalShare:
+                yield self.fixNonExistentExternalShare()
+                raise ExternalShareFailed("External share does not exist")
+        returnValue(("%s_%s" % (self._externalID, self._syncTokenRevision,)))
+
+
+    @inlineCallbacks
+    def resourceNamesSinceRevision(self, revision):
+        try:
+            names = yield self._txn.store().conduit.send_resourcenamessincerevision(self, revision)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+
+        returnValue(names)
+
+
+    @inlineCallbacks
+    def search(self, filter, **kwargs):
+        try:
+            results = yield self._txn.store().conduit.send_search(self, filter.serialize(), **kwargs)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+
+        returnValue(results)
+
+
+
+class CommonObjectResourceExternal(CommonObjectResource):
+    """
+    A CommonObjectResource for a resource not hosted on this system, but on another pod. This will forward
+    specific apis to the other pod using cross-pod requests.
+    """
+
+    @classmethod
+    @inlineCallbacks
+    def loadAllObjects(cls, parent):
+        mapping_list = yield parent._txn.store().conduit.send_loadallobjects(parent, None)
+
+        results = []
+        if mapping_list:
+            for mapping in mapping_list:
+                child = yield cls.internalize(parent, mapping)
+                results.append(child)
+        returnValue(results)
+
+
+    @classmethod
+    @inlineCallbacks
+    def loadAllObjectsWithNames(cls, parent, names):
+        mapping_list = yield parent._txn.store().conduit.send_loadallobjectswithnames(parent, None, names)
+
+        results = []
+        if mapping_list:
+            for mapping in mapping_list:
+                child = yield cls.internalize(parent, mapping)
+                results.append(child)
+        returnValue(results)
+
+
+    @classmethod
+    @inlineCallbacks
+    def objectWith(cls, parent, name=None, uid=None, resourceID=None):
+        mapping = yield parent._txn.store().conduit.send_objectwith(parent, None, name, uid, resourceID)
+
+        if mapping:
+            child = yield cls.internalize(parent, mapping)
+            returnValue(child)
+        else:
+            returnValue(None)
+
+
+    @classmethod
+    @inlineCallbacks
+    def create(cls, parent, name, component, options=None):
+        mapping = yield parent._txn.store().conduit.send_create(parent, None, name, str(component), options=options)
+
+        if mapping:
+            child = yield cls.internalize(parent, mapping)
+            returnValue(child)
+        else:
+            returnValue(None)
+
+
+    @inlineCallbacks
+    def setComponent(self, component, **kwargs):
+        self._componentChanged = yield self._txn.store().conduit.send_setcomponent(self.parentCollection(), self, str(component), **kwargs)
+        self._cachedComponent = None
+        returnValue(self._componentChanged)
+
+
+    @inlineCallbacks
+    def component(self):
+        if self._cachedComponent is None:
+            text = yield self._txn.store().conduit.send_component(self.parentCollection(), self)
+            self._cachedComponent = self._componentClass.fromString(text)
+
+        returnValue(self._cachedComponent)
+
+
+    @inlineCallbacks
+    def remove(self):
+        yield self._txn.store().conduit.send_remove(self.parentCollection(), self)

Deleted: CalendarServer/trunk/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_legacy.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_legacy.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -1,886 +0,0 @@
-# -*- test-case-name: twistedcaldav.test.test_sharing,twistedcaldav.test.test_calendarquery -*-
-##
-# Copyright (c) 2010-2013 Apple Inc. All rights reserved.
-#
-# 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.
-##
-
-
-"""
-PostgreSQL data store.
-"""
-
-import StringIO
-
-
-from twisted.python import hashlib
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-
-from twistedcaldav.config import config
-from twistedcaldav.dateops import normalizeForIndex, pyCalendarTodatetime
-from twistedcaldav.memcachepool import CachePoolUserMixIn
-from twistedcaldav.query import \
-    calendarqueryfilter, calendarquery, addressbookquery, expression, \
-    addressbookqueryfilter
-from twistedcaldav.query.sqlgenerator import sqlgenerator
-
-from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
-from txdav.common.icommondatastore import IndexedSearchException, \
-    ReservationError, NoSuchObjectResourceError
-
-from txdav.common.datastore.sql_tables import schema
-from twext.enterprise.dal.syntax import Parameter, Select
-from twext.python.clsprop import classproperty
-from twext.python.log import Logger
-
-from pycalendar.datetime import DateTime
-from pycalendar.duration import Duration
-
-log = Logger()
-
-indexfbtype_to_icalfbtype = {
-    0: '?',
-    1: 'F',
-    2: 'B',
-    3: 'U',
-    4: 'T',
-}
-
-class MemcachedUIDReserver(CachePoolUserMixIn):
-    log = Logger()
-
-    def __init__(self, index, cachePool=None):
-        self.index = index
-        self._cachePool = cachePool
-
-
-    def _key(self, uid):
-        return 'reservation:%s' % (
-            hashlib.md5('%s:%s' % (uid,
-                                   self.index.resource._resourceID)).hexdigest())
-
-
-    def reserveUID(self, uid):
-        self.log.debug("Reserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        def _handleFalse(result):
-            if result is False:
-                raise ReservationError(
-                    "UID %s already reserved for calendar collection %s."
-                    % (uid, self.index.resource._name)
-                    )
-
-        d = self.getCachePool().add(self._key(uid),
-                                    'reserved',
-                                    expireTime=config.UIDReservationTimeOut)
-        d.addCallback(_handleFalse)
-        return d
-
-
-    def unreserveUID(self, uid):
-        self.log.debug("Unreserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        def _handleFalse(result):
-            if result is False:
-                raise ReservationError(
-                    "UID %s is not reserved for calendar collection %s."
-                    % (uid, self.index.resource._resourceID)
-                    )
-
-        d = self.getCachePool().delete(self._key(uid))
-        d.addCallback(_handleFalse)
-        return d
-
-
-    def isReservedUID(self, uid):
-        self.log.debug("Is reserved UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        def _checkValue((flags, value)):
-            if value is None:
-                return False
-            else:
-                return True
-
-        d = self.getCachePool().get(self._key(uid))
-        d.addCallback(_checkValue)
-        return d
-
-
-
-class DummyUIDReserver(object):
-    log = Logger()
-
-    def __init__(self, index):
-        self.index = index
-        self.reservations = set()
-
-
-    def _key(self, uid):
-        return 'reservation:%s' % (
-            hashlib.md5('%s:%s' % (uid,
-                                   self.index.resource._resourceID)).hexdigest())
-
-
-    def reserveUID(self, uid):
-        self.log.debug("Reserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        key = self._key(uid)
-        if key in self.reservations:
-            raise ReservationError(
-                "UID %s already reserved for calendar collection %s."
-                % (uid, self.index.resource._name)
-                )
-        self.reservations.add(key)
-        return succeed(None)
-
-
-    def unreserveUID(self, uid):
-        self.log.debug("Unreserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        key = self._key(uid)
-        if key in self.reservations:
-            self.reservations.remove(key)
-        return succeed(None)
-
-
-    def isReservedUID(self, uid):
-        self.log.debug("Is reserved UID %r @ %r" % (
-                uid,
-                self.index.resource))
-        key = self._key(uid)
-        return succeed(key in self.reservations)
-
-
-
-class RealSQLBehaviorMixin(object):
-    """
-    Class attributes for 'real' SQL behavior; avoid idiosyncracies of SQLite,
-    use standard SQL constructions, and depend on the full schema in
-    sql_schema/current.sql rather than the partial one in twistedcaldav which depends
-    on the placement of the database in the filesystem for some information.
-    """
-
-    ISOP = " = "
-    STARTSWITHOP = ENDSWITHOP = CONTAINSOP = " LIKE "
-    NOTSTARTSWITHOP = NOTENDSWITHOP = NOTCONTAINSOP = " NOT LIKE "
-
-    def containsArgument(self, arg):
-        return "%%%s%%" % (arg,)
-
-
-    def startswithArgument(self, arg):
-        return "%s%%" % (arg,)
-
-
-    def endswithArgument(self, arg):
-        return "%%%s" % (arg,)
-
-
-
-class CalDAVSQLBehaviorMixin(RealSQLBehaviorMixin):
-    """
-    Query generator for CalDAV indexed searches.
-    """
-
-    FIELDS = {
-        "TYPE": "CALENDAR_OBJECT.ICALENDAR_TYPE",
-        "UID": "CALENDAR_OBJECT.ICALENDAR_UID",
-    }
-    RESOURCEDB = "CALENDAR_OBJECT"
-    TIMESPANDB = "TIME_RANGE"
-
-    TIMESPANTEST = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
-    TIMESPANTEST_NOEND = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.END_DATE > %s))"
-    TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s))"
-    TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID = CALENDAR_OBJECT.RESOURCE_ID AND TIME_RANGE.CALENDAR_RESOURCE_ID = %s"
-    TIMESPANTEST_JOIN_ON_PIECE = "TIME_RANGE.INSTANCE_ID = TRANSPARENCY.TIME_RANGE_INSTANCE_ID AND TRANSPARENCY.USER_ID = %s"
-
-    def generate(self):
-        """
-        Generate the actual SQL 'where ...' expression from the passed in
-        expression tree.
-
-        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
-            partial SQL statement, and the C{list} is the list of argument
-            substitutions to use with the SQL API execute method.
-        """
-
-        # Init state
-        self.sout = StringIO.StringIO()
-        self.arguments = []
-        self.substitutions = []
-        self.usedtimespan = False
-
-        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
-        if self.calendarid:
-
-            test = expression.isExpression("CALENDAR_OBJECT.CALENDAR_RESOURCE_ID", str(self.calendarid), True)
-
-            # Since timerange expression already have the calendar resource-id test in them, do not
-            # add the additional term to those. When the additional term is added, add it as the first
-            # component in the AND expression to hopefully get the DB to use its index first
-
-            # Top-level timerange expression already has calendar resource-id restriction in it
-            if isinstance(self.expression, expression.timerangeExpression):
-                pass
-
-            # Top-level OR - check each component
-            elif isinstance(self.expression, expression.orExpression):
-
-                def _hasTopLevelTimerange(testexpr):
-                    if isinstance(testexpr, expression.timerangeExpression):
-                        return True
-                    elif isinstance(testexpr, expression.andExpression):
-                        return any([isinstance(expr, expression.timerangeExpression) for expr in testexpr.expressions])
-                    else:
-                        return False
-
-                hasTimerange = any([_hasTopLevelTimerange(expr) for expr in self.expression.expressions])
-
-                if hasTimerange:
-                    # timerange expression forces a join on calendarid
-                    pass
-                else:
-                    # AND the whole thing with calendarid
-                    self.expression = test.andWith(self.expression)
-
-            # Top-level AND - only add additional expression if timerange not present
-            elif isinstance(self.expression, expression.andExpression):
-                hasTimerange = any([isinstance(expr, expression.timerangeExpression) for expr in self.expression.expressions])
-                if not hasTimerange:
-                    # AND the whole thing
-                    self.expression = test.andWith(self.expression)
-
-            # Just AND the entire thing
-            else:
-                self.expression = test.andWith(self.expression)
-
-        # Generate ' where ...' partial statement
-        self.generateExpression(self.expression)
-
-        # Prefix with ' from ...' partial statement
-        select = self.FROM + self.RESOURCEDB
-        if self.usedtimespan:
-
-            # Free busy needs transparency join
-            if self.freebusy:
-                self.frontArgument(self.userid)
-                select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
-                    self.TIMESPANDB,
-                    self.TRANSPARENCYDB,
-                    self.TIMESPANTEST_JOIN_ON_PIECE
-                )
-            else:
-                select += ", %s" % (
-                    self.TIMESPANDB,
-                )
-        select += self.WHERE
-        if self.usedtimespan:
-            select += "("
-        select += self.sout.getvalue()
-        if self.usedtimespan:
-            if self.calendarid:
-                self.setArgument(self.calendarid)
-            select += ")%s" % (self.TIMESPANTEST_TAIL_PIECE,)
-
-        select = select % tuple(self.substitutions)
-
-        return select, self.arguments
-
-
-
-class FormatParamStyleMixin(object):
-    """
-    Mixin for overriding methods on sqlgenerator that generate arguments
-    according to format/pyformat rules rather than the base class's 'numeric'
-    rules.
-    """
-
-    def addArgument(self, arg):
-        self.arguments.append(arg)
-        self.substitutions.append("%s")
-        self.sout.write("%s")
-
-
-    def setArgument(self, arg):
-        self.arguments.append(arg)
-        self.substitutions.append("%s")
-
-
-    def frontArgument(self, arg):
-        self.arguments.insert(0, arg)
-        self.substitutions.insert(0, "%s")
-
-
-
-class postgresqlgenerator(FormatParamStyleMixin, CalDAVSQLBehaviorMixin,
-                          sqlgenerator):
-    """
-    Query generator for PostgreSQL indexed searches.
-    """
-
-
-
-def fixbools(sqltext):
-    return sqltext.replace("TRUE", "1").replace("FALSE", "0")
-
-
-
-class oraclesqlgenerator(CalDAVSQLBehaviorMixin, sqlgenerator):
-    """
-    Query generator for Oracle indexed searches.
-    """
-    TIMESPANTEST = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST)
-    TIMESPANTEST_NOEND = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST_NOEND)
-    TIMESPANTEST_NOSTART = fixbools(CalDAVSQLBehaviorMixin.TIMESPANTEST_NOSTART)
-    TIMESPANTEST_TAIL_PIECE = fixbools(
-        CalDAVSQLBehaviorMixin.TIMESPANTEST_TAIL_PIECE)
-    TIMESPANTEST_JOIN_ON_PIECE = fixbools(
-        CalDAVSQLBehaviorMixin.TIMESPANTEST_JOIN_ON_PIECE)
-
-
-
-class LegacyIndexHelper(object):
-    log = Logger()
-
-    @inlineCallbacks
-    def isAllowedUID(self, uid, *names):
-        """
-        Checks to see whether to allow an operation which would add the
-        specified UID to the index.  Specifically, the operation may not
-        violate the constraint that UIDs must be unique.
-        @param uid: the UID to check
-        @param names: the names of resources being replaced or deleted by the
-            operation; UIDs associated with these resources are not checked.
-        @return: True if the UID is not in the index and is not reserved,
-            False otherwise.
-        """
-        rname = yield self.resourceNameForUID(uid)
-        returnValue(rname is None or rname in names)
-
-
-    def reserveUID(self, uid):
-        return self.reserver.reserveUID(uid)
-
-
-    def unreserveUID(self, uid):
-        return self.reserver.unreserveUID(uid)
-
-
-    def isReservedUID(self, uid):
-        return self.reserver.isReservedUID(uid)
-
-
-
-class PostgresLegacyIndexEmulator(LegacyIndexHelper):
-    """
-    Emulator for L{twistedcaldv.index.Index} and
-    L{twistedcaldv.index.IndexSchedule}.
-    """
-
-    def __init__(self, calendar):
-        self.resource = self.calendar = calendar
-        if (
-            hasattr(config, "Memcached") and
-            config.Memcached.Pools.Default.ClientEnabled
-        ):
-            self.reserver = MemcachedUIDReserver(self)
-        else:
-            # This is only used with unit tests
-            self.reserver = DummyUIDReserver(self)
-
-    _objectSchema = schema.CALENDAR_OBJECT
-
-    @property
-    def _txn(self):
-        return self.calendar._txn
-
-
-    @inlineCallbacks
-    def isAllowedUID(self, uid, *names):
-        """
-        Checks to see whether to allow an operation which would add the
-        specified UID to the index.  Specifically, the operation may not
-        violate the constraint that UIDs must be unique.
-        @param uid: the UID to check
-        @param names: the names of resources being replaced or deleted by the
-            operation; UIDs associated with these resources are not checked.
-        @return: True if the UID is not in the index and is not reserved,
-            False otherwise.
-        """
-        rname = yield self.resourceNameForUID(uid)
-        returnValue(rname is None or rname in names)
-
-
-    @inlineCallbacks
-    def resourceUIDForName(self, name):
-        uid = yield self.calendar.resourceUIDForName(name)
-        returnValue(uid)
-
-
-    @inlineCallbacks
-    def resourceNameForUID(self, uid):
-        name = yield self.calendar.resourceNameForUID(uid)
-        returnValue(name)
-
-
-    @classproperty
-    def _notExpandedWithinQuery(cls): #@NoSelf
-        """
-        DAL query to satisfy L{PostgresLegacyIndexEmulator.notExpandedBeyond}.
-        """
-        co = schema.CALENDAR_OBJECT
-        return Select(
-            [co.RESOURCE_NAME],
-            From=co,
-            Where=((co.RECURRANCE_MIN > Parameter("minDate"))
-                .Or(co.RECURRANCE_MAX < Parameter("maxDate")))
-                .And(co.CALENDAR_RESOURCE_ID == Parameter("resourceID"))
-        )
-
-
-    @inlineCallbacks
-    def notExpandedWithin(self, minDate, maxDate):
-        """
-        Gives all resources which have not been expanded beyond a given date
-        in the database.  (Unused; see above L{postgresqlgenerator}.
-        """
-        returnValue([row[0] for row in (
-            yield self._notExpandedWithinQuery.on(
-                self._txn,
-                minDate=pyCalendarTodatetime(normalizeForIndex(minDate)) if minDate is not None else None,
-                maxDate=pyCalendarTodatetime(normalizeForIndex(maxDate)),
-                resourceID=self.calendar._resourceID))]
-        )
-
-
-    @inlineCallbacks
-    def reExpandResource(self, name, expand_start, expand_end):
-        """
-        Given a resource name, remove it from the database and re-add it
-        with a longer expansion.
-        """
-        obj = yield self.calendar.calendarObjectWithName(name)
-
-        # Use a new transaction to do this update quickly without locking the row for too long. However, the original
-        # transaction may have the row locked, so use wait=False and if that fails, fall back to using the original txn.
-
-        newTxn = obj.transaction().store().newTransaction()
-        try:
-            yield obj.lock(wait=False, txn=newTxn)
-        except NoSuchObjectResourceError:
-            yield newTxn.commit()
-            returnValue(None)
-        except:
-            yield newTxn.abort()
-            newTxn = None
-
-        # Now do the re-expand using the appropriate transaction
-        try:
-            doExpand = False
-            if newTxn is None:
-                doExpand = True
-            else:
-                # We repeat this check because the resource may have been re-expanded by someone else
-                rmin, rmax = (yield obj.recurrenceMinMax(txn=newTxn))
-
-                # If the resource is not fully expanded, see if within the required range or not.
-                # Note that expand_start could be None if no lower limit is applied, but expand_end will
-                # never be None
-                if rmax is not None and rmax < expand_end:
-                    doExpand = True
-                if rmin is not None and expand_start is not None and rmin > expand_start:
-                    doExpand = True
-
-            if doExpand:
-                yield obj.updateDatabase(
-                    (yield obj.component()),
-                    expand_until=expand_end,
-                    reCreate=True,
-                    txn=newTxn,
-                )
-        finally:
-            if newTxn is not None:
-                yield newTxn.commit()
-
-
-    @inlineCallbacks
-    def testAndUpdateIndex(self, minDate, maxDate):
-        # Find out if the index is expanded far enough
-        names = yield self.notExpandedWithin(minDate, maxDate)
-
-        # Actually expand recurrence max
-        for name in names:
-            self.log.info("Search falls outside range of index for %s %s to %s" %
-                          (name, minDate, maxDate))
-            yield self.reExpandResource(name, minDate, maxDate)
-
-
-    @inlineCallbacks
-    def indexedSearch(self, filter, useruid='', fbtype=False):
-        """
-        Finds resources matching the given qualifiers.
-
-        @param filter: the L{Filter} for the calendar-query to execute.
-
-        @return: a L{Deferred} which fires with an iterable of tuples for each
-            resource matching the given C{qualifiers}. The tuples are C{(name,
-            uid, type)}, where C{name} is the resource name, C{uid} is the
-            resource UID, and C{type} is the resource iCalendar component type.
-        """
-        # Detect which style of parameter-generation we're using.  Naming is a
-        # little off here, because the reason we're using the numeric one is
-        # that it happens to be used by the oracle binding that we're using,
-        # whereas the postgres binding happens to use the 'pyformat' (e.g. %s)
-        # parameter style.
-        if self.calendar._txn.paramstyle == 'numeric':
-            generator = oraclesqlgenerator
-        else:
-            generator = postgresqlgenerator
-        # Make sure we have a proper Filter element and get the partial SQL
-        # statement to use.
-        if isinstance(filter, calendarqueryfilter.Filter):
-            qualifiers = calendarquery.sqlcalendarquery(
-                filter, self.calendar._resourceID, useruid, fbtype,
-                generator=generator
-            )
-            if qualifiers is not None:
-
-                today = DateTime.getToday()
-
-                # Determine how far we need to extend the current expansion of
-                # events. If we have an open-ended time-range we will expand
-                # one year past the start. That should catch bounded
-                # recurrences - unbounded will have been indexed with an
-                # "infinite" value always included.
-                maxDate, isStartDate = filter.getmaxtimerange()
-                if maxDate:
-                    maxDate = maxDate.duplicate()
-                    maxDate.offsetDay(1)
-                    maxDate.setDateOnly(True)
-                    upperLimit = today + Duration(days=config.FreeBusyIndexExpandMaxDays)
-                    if maxDate > upperLimit:
-                        raise TimeRangeUpperLimit(upperLimit)
-                    if isStartDate:
-                        maxDate += Duration(days=365)
-
-                # Determine if the start date is too early for the restricted range we
-                # are applying. If it is today or later we don't need to worry about truncation
-                # in the past.
-                minDate, _ignore_isEndDate = filter.getmintimerange()
-                if minDate >= today:
-                    minDate = None
-                if minDate is not None and config.FreeBusyIndexLowerLimitDays:
-                    truncateLowerLimit = today - Duration(days=config.FreeBusyIndexLowerLimitDays)
-                    if minDate < truncateLowerLimit:
-                        raise TimeRangeLowerLimit(truncateLowerLimit)
-
-                if maxDate is not None or minDate is not None:
-                    yield self.testAndUpdateIndex(minDate, maxDate)
-
-            else:
-                # We cannot handle this filter in an indexed search
-                raise IndexedSearchException()
-        else:
-            qualifiers = None
-
-        # Perform the search
-        if qualifiers is None:
-            rowiter = yield self.bruteForceSearch()
-        else:
-            if fbtype:
-                # For a free-busy time-range query we return all instances
-                rowiter = yield self._txn.execSQL(
-                    """
-                    select DISTINCT
-                        CALENDAR_OBJECT.RESOURCE_NAME,
-                        CALENDAR_OBJECT.ICALENDAR_UID,
-                        CALENDAR_OBJECT.ICALENDAR_TYPE,
-                        CALENDAR_OBJECT.ORGANIZER,
-                        TIME_RANGE.FLOATING, TIME_RANGE.START_DATE,
-                        TIME_RANGE.END_DATE, TIME_RANGE.FBTYPE,
-                        TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT
-                    """ +
-                    qualifiers[0],
-                    qualifiers[1]
-                )
-            else:
-                rowiter = yield self._txn.execSQL(
-                    """
-                    select
-                        DISTINCT CALENDAR_OBJECT.RESOURCE_NAME,
-                        CALENDAR_OBJECT.ICALENDAR_UID,
-                        CALENDAR_OBJECT.ICALENDAR_TYPE
-                    """ +
-                    qualifiers[0],
-                    qualifiers[1]
-                )
-
-        # Check result for missing resources
-
-        results = []
-        for row in rowiter:
-            if fbtype:
-                row = list(row)
-                row[4] = 'Y' if row[4] else 'N'
-                row[7] = indexfbtype_to_icalfbtype[row[7]]
-                if row[9] is not None:
-                    row[8] = row[9]
-                row[8] = 'T' if row[8] else 'F'
-                del row[9]
-            results.append(row)
-        returnValue(results)
-
-
-    @classproperty
-    def _bruteForceQuery(cls): #@NoSelf
-        """
-        DAL query for all C{CALENDAR_OBJECT} rows in the calendar represented by
-        this index.
-        """
-        obj = cls._objectSchema
-        return Select(
-            [obj.RESOURCE_NAME, obj.ICALENDAR_UID, obj.ICALENDAR_TYPE],
-            From=obj, Where=obj.PARENT_RESOURCE_ID == Parameter("resourceID")
-        )
-
-
-    def bruteForceSearch(self):
-        return self._bruteForceQuery.on(
-            self._txn, resourceID=self.resource._resourceID)
-
-
-    @inlineCallbacks
-    def resourcesExist(self, names):
-        returnValue(list(set(names).intersection(
-            set((yield self.calendar.listCalendarObjects())))))
-
-
-    @classproperty
-    def _resourceExistsQuery(cls): #@NoSelf
-        """
-        DAL query to determine whether a calendar object exists in the
-        collection represented by this index.
-        """
-        obj = cls._objectSchema
-        return Select(
-            [obj.RESOURCE_NAME], From=obj,
-            Where=(obj.RESOURCE_NAME == Parameter("name"))
-            .And(obj.PARENT_RESOURCE_ID == Parameter("resourceID"))
-        )
-
-
-    @inlineCallbacks
-    def resourceExists(self, name):
-        returnValue((bool(
-            (yield self._resourceExistsQuery.on(
-                self._txn, name=name, resourceID=self.resource._resourceID))
-        )))
-
-
-
-class PostgresLegacyInboxIndexEmulator(PostgresLegacyIndexEmulator):
-    """
-    UIDs need not be unique in the 'inbox' calendar, so override those
-    behaviors intended to ensure that.
-    """
-
-    def isAllowedUID(self):
-        return succeed(True)
-
-
-    def reserveUID(self, uid):
-        return succeed(None)
-
-
-    def unreserveUID(self, uid):
-        return succeed(None)
-
-
-    def isReservedUID(self, uid):
-        return succeed(False)
-
-
-
-# CARDDAV
-
-class CardDAVSQLBehaviorMixin(RealSQLBehaviorMixin):
-    """
-    Query generator for CardDAV indexed searches.
-    """
-
-    FIELDS = {
-        "UID": "ADDRESSBOOK_OBJECT.VCARD_UID",
-    }
-    RESOURCEDB = "ADDRESSBOOK_OBJECT"
-
-    def generate(self):
-        """
-        Generate the actual SQL 'where ...' expression from the passed in
-        expression tree.
-
-        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
-            partial SQL statement, and the C{list} is the list of argument
-            substitutions to use with the SQL API execute method.
-        """
-
-        # Init state
-        self.sout = StringIO.StringIO()
-        self.arguments = []
-        self.substitutions = []
-
-        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
-        if self.calendarid:
-
-            # AND the whole thing
-            test = expression.isExpression("ADDRESSBOOK_OBJECT.ADDRESSBOOK_HOME_RESOURCE_ID", str(self.calendarid), True)
-            self.expression = test.andWith(self.expression)
-
-        # Generate ' where ...' partial statement
-        self.sout.write(self.WHERE)
-        self.generateExpression(self.expression)
-
-        # Prefix with ' from ...' partial statement
-        select = self.FROM + self.RESOURCEDB
-        select += self.sout.getvalue()
-
-        select = select % tuple(self.substitutions)
-
-        return select, self.arguments
-
-
-
-class postgresqladbkgenerator(FormatParamStyleMixin, CardDAVSQLBehaviorMixin, sqlgenerator):
-    """
-    Query generator for PostgreSQL indexed searches.
-    """
-
-
-
-class oraclesqladbkgenerator(CardDAVSQLBehaviorMixin, sqlgenerator):
-    """
-    Query generator for Oracle indexed searches.
-    """
-
-
-
-class PostgresLegacyABIndexEmulator(LegacyIndexHelper):
-    """
-    Emulator for L{twistedcaldv.index.Index} and
-    L{twistedcaldv.index.IndexSchedule}.
-    """
-
-    _objectSchema = schema.ADDRESSBOOK_OBJECT
-
-    def __init__(self, addressbook):
-        self.resource = self.addressbook = addressbook
-        if (
-            hasattr(config, "Memcached") and
-            config.Memcached.Pools.Default.ClientEnabled
-        ):
-            self.reserver = MemcachedUIDReserver(self)
-        else:
-            # This is only used with unit tests
-            self.reserver = DummyUIDReserver(self)
-
-
-    @property
-    def _txn(self):
-        return self.addressbook._txn
-
-
-    @inlineCallbacks
-    def resourceUIDForName(self, name):
-        obj = yield self.addressbook.addressbookObjectWithName(name)
-        if obj is None:
-            returnValue(None)
-        returnValue(obj.uid())
-
-
-    @inlineCallbacks
-    def resourceNameForUID(self, uid):
-        obj = yield self.addressbook.addressbookObjectWithUID(uid)
-        if obj is None:
-            returnValue(None)
-        returnValue(obj.name())
-
-
-    def searchValid(self, filter):
-        if isinstance(filter, addressbookqueryfilter.Filter):
-            qualifiers = addressbookquery.sqladdressbookquery(filter)
-        else:
-            qualifiers = None
-
-        return qualifiers is not None
-
-
-    @inlineCallbacks
-    def search(self, filter):
-        """
-        Finds resources matching the given qualifiers.
-        @param filter: the L{Filter} for the addressbook-query to execute.
-        @return: an iterable of tuples for each resource matching the
-            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
-            C{name} is the resource name, C{uid} is the resource UID, and
-            C{type} is the resource iCalendar component type.x
-        """
-        if self.addressbook._txn.paramstyle == 'numeric':
-            generator = oraclesqladbkgenerator
-        else:
-            generator = postgresqladbkgenerator
-        # Make sure we have a proper Filter element and get the partial SQL statement to use.
-        if isinstance(filter, addressbookqueryfilter.Filter):
-            qualifiers = addressbookquery.sqladdressbookquery(
-                filter, self.addressbook._resourceID, generator=generator)
-        else:
-            qualifiers = None
-        if qualifiers is not None:
-            rowiter = yield self._txn.execSQL(
-                "select DISTINCT ADDRESSBOOK_OBJECT.RESOURCE_NAME, ADDRESSBOOK_OBJECT.VCARD_UID" +
-                qualifiers[0],
-                qualifiers[1]
-            )
-        else:
-            rowiter = yield Select(
-                [self._objectSchema.RESOURCE_NAME,
-                 self._objectSchema.VCARD_UID],
-                From=self._objectSchema,
-                Where=self._objectSchema.ADDRESSBOOK_HOME_RESOURCE_ID ==
-                self.addressbook._resourceID
-            ).on(self.addressbook._txn)
-
-        returnValue(list(rowiter))
-
-
-    def indexedSearch(self, filter, useruid='', fbtype=False):
-        """
-        Always raise L{IndexedSearchException}, since these indexes are not
-        fully implemented yet.
-        """
-        raise IndexedSearchException()
-
-
-    @inlineCallbacks
-    def resourcesExist(self, names):
-        returnValue(list(set(names).intersection(
-            set((yield self.addressbook.listAddressBookObjects())))))

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2014-01-02 17:19:18 UTC (rev 12211)
@@ -18,9 +18,17 @@
 create table CALENDAR_HOME (
     "RESOURCE_ID" integer primary key,
     "OWNER_UID" nvarchar2(255) unique,
+    "STATUS" integer default 0 not null,
     "DATAVERSION" integer default 0 not null
 );
 
+create table HOME_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
 create table CALENDAR (
     "RESOURCE_ID" integer primary key
 );
@@ -50,6 +58,7 @@
 create table NOTIFICATION_HOME (
     "RESOURCE_ID" integer primary key,
     "OWNER_UID" nvarchar2(255) unique,
+    "STATUS" integer default 0 not null,
     "DATAVERSION" integer default 0 not null
 );
 
@@ -210,6 +219,7 @@
     "RESOURCE_ID" integer primary key,
     "ADDRESSBOOK_PROPERTY_STORE_ID" integer not null,
     "OWNER_UID" nvarchar2(255) unique,
+    "STATUS" integer default 0 not null,
     "DATAVERSION" integer default 0 not null
 );
 
@@ -373,7 +383,7 @@
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '30');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '31');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '5');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2014-01-02 17:19:18 UTC (rev 12211)
@@ -53,10 +53,22 @@
 
 create table CALENDAR_HOME (
   RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
-  OWNER_UID        varchar(255) not null unique,                                 -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                -- implicit index
+  STATUS           integer      default 0 not null,                             -- enum HOME_STATUS
   DATAVERSION      integer      default 0 not null
 );
 
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+
+
 --------------
 -- Calendar --
 --------------
@@ -65,6 +77,7 @@
   RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
 );
 
+
 ----------------------------
 -- Calendar Home Metadata --
 ----------------------------
@@ -91,6 +104,7 @@
 create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
 	CALENDAR_HOME_METADATA(DEFAULT_POLLS);
 
+
 -----------------------
 -- Calendar Metadata --
 -----------------------
@@ -110,6 +124,7 @@
 create table NOTIFICATION_HOME (
   RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
   OWNER_UID   varchar(255) not null unique,                                -- implicit index
+  STATUS      integer      default 0 not null,                             -- enum HOME_STATUS
   DATAVERSION integer      default 0 not null
 );
 
@@ -139,6 +154,7 @@
 create table CALENDAR_BIND (
   CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
   CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  EXTERNAL_ID			    integer      default null,
   CALENDAR_RESOURCE_NAME    varchar(255) not null,
   BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
   BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
@@ -380,6 +396,7 @@
   RESOURCE_ID      				integer			primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
   ADDRESSBOOK_PROPERTY_STORE_ID	integer      	default nextval('RESOURCE_ID_SEQ') not null, 	-- implicit index
   OWNER_UID        				varchar(255) 	not null unique,                                -- implicit index
+  STATUS           				integer      	default 0 not null,                             -- enum HOME_STATUS
   DATAVERSION      				integer      	default 0 not null
 );
 
@@ -405,6 +422,7 @@
 create table SHARED_ADDRESSBOOK_BIND (
   ADDRESSBOOK_HOME_RESOURCE_ID			integer			not null references ADDRESSBOOK_HOME,
   OWNER_HOME_RESOURCE_ID    			integer      	not null references ADDRESSBOOK_HOME on delete cascade,
+  EXTERNAL_ID			                integer         default null,
   ADDRESSBOOK_RESOURCE_NAME    			varchar(255) 	not null,
   BIND_MODE                    			integer      	not null,	-- enum CALENDAR_BIND_MODE
   BIND_STATUS                  			integer      	not null,	-- enum CALENDAR_BIND_STATUS
@@ -503,6 +521,7 @@
 create table SHARED_GROUP_BIND (	
   ADDRESSBOOK_HOME_RESOURCE_ID 		integer      not null references ADDRESSBOOK_HOME,
   GROUP_RESOURCE_ID      			integer      not null references ADDRESSBOOK_OBJECT on delete cascade,
+  EXTERNAL_ID			            integer      default null,
   GROUP_ADDRESSBOOK_NAME			varchar(255) not null,
   BIND_MODE                    		integer      not null, -- enum CALENDAR_BIND_MODE
   BIND_STATUS                  		integer      not null, -- enum CALENDAR_BIND_STATUS
@@ -711,7 +730,7 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '30');
+insert into CALENDARSERVER values ('VERSION', '31');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '5');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v30.sql	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,508 @@
+create sequence RESOURCE_ID_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
+create table NODE_INFO (
+    "HOSTNAME" nvarchar2(255),
+    "PID" integer not null,
+    "PORT" integer not null,
+    "TIME" timestamp default CURRENT_TIMESTAMP at time zone 'UTC' not null, 
+    primary key("HOSTNAME", "PORT")
+);
+
+create table NAMED_LOCK (
+    "LOCK_NAME" nvarchar2(255) primary key
+);
+
+create table CALENDAR_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table CALENDAR (
+    "RESOURCE_ID" integer primary key
+);
+
+create table CALENDAR_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "DEFAULT_EVENTS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_TASKS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_POLLS" integer default null references CALENDAR on delete set null,
+    "ALARM_VEVENT_TIMED" nclob default null,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "AVAILABILITY" nclob default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
+    "SUPPORTED_COMPONENTS" nvarchar2(255) default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table NOTIFICATION_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table NOTIFICATION (
+    "RESOURCE_ID" integer primary key,
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+    "NOTIFICATION_UID" nvarchar2(255),
+    "NOTIFICATION_TYPE" nvarchar2(255),
+    "NOTIFICATION_DATA" nclob,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("NOTIFICATION_UID", "NOTIFICATION_HOME_RESOURCE_ID")
+);
+
+create table CALENDAR_BIND (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "CALENDAR_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
+    "MESSAGE" nclob,
+    "TRANSP" integer default 0 not null,
+    "ALARM_VEVENT_TIMED" nclob default null,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "TIMEZONE" nclob default null, 
+    primary key("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID"), 
+    unique("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_NAME")
+);
+
+create table CALENDAR_BIND_MODE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('own', 0);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('write', 2);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('direct', 3);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
+create table CALENDAR_BIND_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invited', 0);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('accepted', 1);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('declined', 2);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invalid', 3);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('deleted', 4);
+create table CALENDAR_TRANSP (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('opaque', 0);
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('transparent', 1);
+create table CALENDAR_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob,
+    "ICALENDAR_UID" nvarchar2(255),
+    "ICALENDAR_TYPE" nvarchar2(255),
+    "ATTACHMENTS_MODE" integer default 0 not null,
+    "DROPBOX_ID" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "RECURRANCE_MIN" date,
+    "RECURRANCE_MAX" date,
+    "ACCESS" integer default 0 not null,
+    "SCHEDULE_OBJECT" integer default 0,
+    "SCHEDULE_TAG" nvarchar2(36) default null,
+    "SCHEDULE_ETAGS" nclob default null,
+    "PRIVATE_COMMENTS" integer default 0 not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MO (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(32) unique
+);
+
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('', 0);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('public', 1);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('private', 2);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('confidential', 3);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('restricted', 4);
+create table TIME_RANGE (
+    "INSTANCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "FLOATING" integer not null,
+    "START_DATE" timestamp not null,
+    "END_DATE" timestamp not null,
+    "FBTYPE" integer not null,
+    "TRANSPARENT" integer not null
+);
+
+create table FREE_BUSY_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('unknown', 0);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('free', 1);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy', 2);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-unavailable', 3);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-tentative', 4);
+create table TRANSPARENCY (
+    "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+    "USER_ID" nvarchar2(255),
+    "TRANSPARENT" integer not null
+);
+
+create table ATTACHMENT (
+    "ATTACHMENT_ID" integer primary key,
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "DROPBOX_ID" nvarchar2(255),
+    "CONTENT_TYPE" nvarchar2(255),
+    "SIZE" integer not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PATH" nvarchar2(1024)
+);
+
+create table ATTACHMENT_CALENDAR_OBJECT (
+    "ATTACHMENT_ID" integer not null references ATTACHMENT on delete cascade,
+    "MANAGED_ID" nvarchar2(255),
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade, 
+    primary key("ATTACHMENT_ID", "CALENDAR_OBJECT_RESOURCE_ID"), 
+    unique("MANAGED_ID", "CALENDAR_OBJECT_RESOURCE_ID")
+);
+
+create table RESOURCE_PROPERTY (
+    "RESOURCE_ID" integer not null,
+    "NAME" nvarchar2(255),
+    "VALUE" nclob,
+    "VIEWER_UID" nvarchar2(255), 
+    primary key("RESOURCE_ID", "NAME", "VIEWER_UID")
+);
+
+create table ADDRESSBOOK_HOME (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_PROPERTY_STORE_ID" integer not null,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table ADDRESSBOOK_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references ADDRESSBOOK_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table SHARED_ADDRESSBOOK_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
+    "MESSAGE" nclob, 
+    primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID"), 
+    unique("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "VCARD_TEXT" nclob,
+    "VCARD_UID" nvarchar2(255),
+    "KIND" integer not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("ADDRESSBOOK_HOME_RESOURCE_ID", "RESOURCE_NAME"), 
+    unique("ADDRESSBOOK_HOME_RESOURCE_ID", "VCARD_UID")
+);
+
+create table ADDRESSBOOK_OBJECT_KIND (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('person', 0);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('group', 1);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('resource', 2);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('location', 3);
+create table ABO_MEMBERS (
+    "GROUP_ID" integer not null,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ID" integer not null,
+    "REVISION" integer not null,
+    "REMOVED" integer default 0 not null, 
+    primary key("GROUP_ID", "MEMBER_ID", "REVISION")
+);
+
+create table ABO_FOREIGN_MEMBERS (
+    "GROUP_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ADDRESS" nvarchar2(255), 
+    primary key("GROUP_ID", "MEMBER_ADDRESS")
+);
+
+create table SHARED_GROUP_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "GROUP_RESOURCE_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "GROUP_ADDRESSBOOK_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
+    "MESSAGE" nclob, 
+    primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_RESOURCE_ID"), 
+    unique("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_ADDRESSBOOK_NAME")
+);
+
+create table CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer references CALENDAR,
+    "CALENDAR_NAME" nvarchar2(255) default null,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+    "OBJECT_RESOURCE_ID" integer default 0,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null
+);
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null, 
+    unique("NOTIFICATION_HOME_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table APN_SUBSCRIPTIONS (
+    "TOKEN" nvarchar2(255),
+    "RESOURCE_KEY" nvarchar2(255),
+    "MODIFIED" integer not null,
+    "SUBSCRIBER_GUID" nvarchar2(255),
+    "USER_AGENT" nvarchar2(255) default null,
+    "IP_ADDR" nvarchar2(255) default null, 
+    primary key("TOKEN", "RESOURCE_KEY")
+);
+
+create table IMIP_TOKENS (
+    "TOKEN" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALUID" nvarchar2(255),
+    "ACCESSED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key("ORGANIZER", "ATTENDEE", "ICALUID")
+);
+
+create table IMIP_INVITATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PUSH_ID" nvarchar2(255),
+    "PRIORITY" integer not null
+);
+
+create table GROUP_CACHER_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table CALENDARSERVER (
+    "NAME" nvarchar2(255) primary key,
+    "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '30');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '5');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
+create index CALENDAR_HOME_METADAT_3cb9049e on CALENDAR_HOME_METADATA (
+    DEFAULT_EVENTS
+);
+
+create index CALENDAR_HOME_METADAT_d55e5548 on CALENDAR_HOME_METADATA (
+    DEFAULT_TASKS
+);
+
+create index CALENDAR_HOME_METADAT_910264ce on CALENDAR_HOME_METADATA (
+    DEFAULT_POLLS
+);
+
+create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
+    NOTIFICATION_HOME_RESOURCE_ID
+);
+
+create index CALENDAR_BIND_RESOURC_e57964d4 on CALENDAR_BIND (
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_CALEN_a9a453a9 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_CALEN_96e83b73 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    RECURRANCE_MAX
+);
+
+create index CALENDAR_OBJECT_ICALE_82e731d5 on CALENDAR_OBJECT (
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_DROPB_de041d80 on CALENDAR_OBJECT (
+    DROPBOX_ID
+);
+
+create index TIME_RANGE_CALENDAR_R_beb6e7eb on TIME_RANGE (
+    CALENDAR_RESOURCE_ID
+);
+
+create index TIME_RANGE_CALENDAR_O_acf37bd1 on TIME_RANGE (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index TRANSPARENCY_TIME_RAN_5f34467f on TRANSPARENCY (
+    TIME_RANGE_INSTANCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    CALENDAR_HOME_RESOURCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_O_81508484 on ATTACHMENT_CALENDAR_OBJECT (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
+    OWNER_HOME_RESOURCE_ID
+);
+
+create index ABO_MEMBERS_ADDRESSBO_4effa879 on ABO_MEMBERS (
+    ADDRESSBOOK_ID
+);
+
+create index ABO_MEMBERS_MEMBER_ID_8d66adcf on ABO_MEMBERS (
+    MEMBER_ID
+);
+
+create index ABO_FOREIGN_MEMBERS_A_1fd2c5e9 on ABO_FOREIGN_MEMBERS (
+    ADDRESSBOOK_ID
+);
+
+create index SHARED_GROUP_BIND_RES_cf52f95d on SHARED_GROUP_BIND (
+    GROUP_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_3a3956c4 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_HOME_RESOURCE_ID,
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_6d9d929c on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    RESOURCE_NAME,
+    DELETED,
+    REVISION
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_2bfcf757 on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_HOME_RESOURCE_ID,
+    OWNER_HOME_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_OBJECT_RE_00fe8288 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_HOME_RESOURCE_ID,
+    RESOURCE_NAME,
+    DELETED,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_45004780 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+    NOTIFICATION_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+    RESOURCE_KEY
+);
+
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+    TOKEN
+);
+
+create index CALENDAR_OBJECT_SPLIT_af71dcda on CALENDAR_OBJECT_SPLITTER_WORK (
+    RESOURCE_ID
+);
+

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v30.sql	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,717 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2013 Apple Inc. All rights reserved.
+--
+-- 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.
+----
+
+
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+
+-------------------------
+-- Cluster Bookkeeping --
+-------------------------
+
+-- Information about a process connected to this database.
+
+-- Note that this must match the node info schema in twext.enterprise.queue.
+create table NODE_INFO (
+  HOSTNAME  varchar(255) not null,
+  PID       integer      not null,
+  PORT      integer      not null,
+  TIME      timestamp    not null default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (HOSTNAME, PORT)
+);
+
+-- Unique named locks.  This table should always be empty, but rows are
+-- temporarily created in order to prevent undesirable concurrency.
+create table NAMED_LOCK (
+    LOCK_NAME varchar(255) primary key
+);
+
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                 -- implicit index
+  DATAVERSION      integer      default 0 not null
+);
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID              integer     primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES         integer     default 0 not null,
+  DEFAULT_EVENTS           integer     default null references CALENDAR on delete set null,
+  DEFAULT_TASKS            integer     default null references CALENDAR on delete set null,
+  DEFAULT_POLLS            integer     default null references CALENDAR on delete set null,
+  ALARM_VEVENT_TIMED       text        default null,
+  ALARM_VEVENT_ALLDAY      text        default null,
+  ALARM_VTODO_TIMED        text        default null,
+  ALARM_VTODO_ALLDAY       text        default null,
+  AVAILABILITY             text        default null,
+  CREATED                  timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                 timestamp   default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index CALENDAR_HOME_METADATA_DEFAULT_EVENTS on
+	CALENDAR_HOME_METADATA(DEFAULT_EVENTS);
+create index CALENDAR_HOME_METADATA_DEFAULT_TASKS on
+	CALENDAR_HOME_METADATA(DEFAULT_TASKS);
+create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
+	CALENDAR_HOME_METADATA(DEFAULT_POLLS);
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer      primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CREATED               timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null unique,                                -- implicit index
+  DATAVERSION integer      default 0 not null
+);
+
+create table NOTIFICATION (
+  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID              varchar(255) not null,
+  NOTIFICATION_TYPE             varchar(255) not null,
+  NOTIFICATION_DATA             text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+	NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  CALENDAR_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION				integer      default 0 not null,
+  MESSAGE                   text,
+  TRANSP                    integer      default 0 not null, -- enum CALENDAR_TRANSP
+  ALARM_VEVENT_TIMED        text         default null,
+  ALARM_VEVENT_ALLDAY       text         default null,
+  ALARM_VTODO_TIMED         text         default null,
+  ALARM_VTODO_ALLDAY        text         default null,
+  TIMEZONE                  text         default null,
+
+  primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on
+	CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+insert into CALENDAR_BIND_MODE values (4, 'indirect');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+insert into CALENDAR_BIND_STATUS values (4, 'deleted');
+
+
+-- Enumeration of transparency
+
+create table CALENDAR_TRANSP (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_TRANSP values (0, 'opaque' );
+insert into CALENDAR_TRANSP values (1, 'transparent');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+
+  -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+  -- calendar objects, this constraint has to be selectively enforced by the
+  -- application layer.
+
+  -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_AND_ICALENDAR_UID on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_RECURRANCE_MAX on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX);
+
+create index CALENDAR_OBJECT_ICALENDAR_UID on
+  CALENDAR_OBJECT(ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null
+);
+
+create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
+  TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+create index ATTACHMENT_CALENDAR_OBJECT_CALENDAR_OBJECT_RESOURCE_ID on
+	ATTACHMENT_CALENDAR_OBJECT(CALENDAR_OBJECT_RESOURCE_ID);
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID      				integer			primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  ADDRESSBOOK_PROPERTY_STORE_ID	integer      	default nextval('RESOURCE_ID_SEQ') not null, 	-- implicit index
+  OWNER_UID        				varchar(255) 	not null unique,                                -- implicit index
+  DATAVERSION      				integer      	default 0 not null
+);
+
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+-----------------------------
+-- Shared AddressBook Bind --
+-----------------------------
+
+-- Joins sharee ADDRESSBOOK_HOME and owner ADDRESSBOOK_HOME
+
+create table SHARED_ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID			integer			not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID    			integer      	not null references ADDRESSBOOK_HOME on delete cascade,
+  ADDRESSBOOK_RESOURCE_NAME    			varchar(255) 	not null,
+  BIND_MODE                    			integer      	not null,	-- enum CALENDAR_BIND_MODE
+  BIND_STATUS                  			integer      	not null,	-- enum CALENDAR_BIND_STATUS
+  BIND_REVISION				   			integer      	default 0 not null,
+  MESSAGE                      			text,                  		-- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index SHARED_ADDRESSBOOK_BIND_RESOURCE_ID on
+  SHARED_ADDRESSBOOK_BIND(OWNER_HOME_RESOURCE_ID);
+
+
+------------------------
+-- AddressBook Object --
+------------------------
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID             		integer   		primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_HOME_RESOURCE_ID 	integer      	not null references ADDRESSBOOK_HOME on delete cascade,
+  RESOURCE_NAME           		varchar(255) 	not null,
+  VCARD_TEXT              		text         	not null,
+  VCARD_UID               		varchar(255) 	not null,
+  KIND 			  		  		integer      	not null,  -- enum ADDRESSBOOK_OBJECT_KIND
+  MD5                     		char(32)     	not null,
+  CREATED                 		timestamp    	default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                		timestamp    	default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+
+-----------------------------
+-- AddressBook Object kind --
+-----------------------------
+
+create table ADDRESSBOOK_OBJECT_KIND (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND values (0, 'person');
+insert into ADDRESSBOOK_OBJECT_KIND values (1, 'group' );
+insert into ADDRESSBOOK_OBJECT_KIND values (2, 'resource');
+insert into ADDRESSBOOK_OBJECT_KIND values (3, 'location');
+
+
+----------------------------------
+-- Revisions, forward reference --
+----------------------------------
+
+create sequence REVISION_SEQ;
+
+---------------------------------
+-- Address Book Object Members --
+---------------------------------
+
+create table ABO_MEMBERS (
+    GROUP_ID              integer      not null, -- references ADDRESSBOOK_OBJECT on delete cascade,	-- AddressBook Object's (kind=='group') RESOURCE_ID
+ 	ADDRESSBOOK_ID		  integer      not null references ADDRESSBOOK_HOME on delete cascade,
+    MEMBER_ID             integer      not null, -- references ADDRESSBOOK_OBJECT,						-- member AddressBook Object's RESOURCE_ID
+  	REVISION              integer      default nextval('REVISION_SEQ') not null,
+  	REMOVED               boolean      default false not null,
+
+    primary key (GROUP_ID, MEMBER_ID, REVISION) -- implicit index
+);
+
+create index ABO_MEMBERS_ADDRESSBOOK_ID on
+	ABO_MEMBERS(ADDRESSBOOK_ID);
+create index ABO_MEMBERS_MEMBER_ID on
+	ABO_MEMBERS(MEMBER_ID);
+
+------------------------------------------
+-- Address Book Object Foreign Members  --
+------------------------------------------
+
+create table ABO_FOREIGN_MEMBERS (
+    GROUP_ID              integer      not null references ADDRESSBOOK_OBJECT on delete cascade,	-- AddressBook Object's (kind=='group') RESOURCE_ID
+ 	ADDRESSBOOK_ID		  integer      not null references ADDRESSBOOK_HOME on delete cascade,
+    MEMBER_ADDRESS  	  varchar(255) not null, 													-- member AddressBook Object's 'calendar' address
+
+    primary key (GROUP_ID, MEMBER_ADDRESS) -- implicit index
+);
+
+create index ABO_FOREIGN_MEMBERS_ADDRESSBOOK_ID on
+	ABO_FOREIGN_MEMBERS(ADDRESSBOOK_ID);
+
+-----------------------
+-- Shared Group Bind --
+-----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK_OBJECT (kind == group)
+
+create table SHARED_GROUP_BIND (	
+  ADDRESSBOOK_HOME_RESOURCE_ID 		integer      not null references ADDRESSBOOK_HOME,
+  GROUP_RESOURCE_ID      			integer      not null references ADDRESSBOOK_OBJECT on delete cascade,
+  GROUP_ADDRESSBOOK_NAME			varchar(255) not null,
+  BIND_MODE                    		integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                  		integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION				   		integer      default 0 not null,
+  MESSAGE                      		text,                  -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_ADDRESSBOOK_NAME)  -- implicit index
+);
+
+create index SHARED_GROUP_BIND_RESOURCE_ID on
+  SHARED_GROUP_BIND(GROUP_RESOURCE_ID);
+
+
+---------------
+-- Revisions --
+---------------
+
+-- create sequence REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null
+);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_CALENDAR_RESOURCE_ID
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+
+----------------------------------
+-- AddressBook Object Revisions --
+----------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID 			integer			not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID    			integer     	references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_NAME             			varchar(255) 	default null,
+  OBJECT_RESOURCE_ID					integer			default 0,
+  RESOURCE_NAME                			varchar(255),
+  REVISION                     			integer     	default nextval('REVISION_SEQ') not null,
+  DELETED                      			boolean      	not null
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_HOME_RESOURCE_ID_OWNER_HOME_RESOURCE_ID
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, REVISION);
+
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+
+  unique(NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer      not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+   on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+   
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+   on IMIP_TOKENS(TOKEN);
+
+   
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  PUSH_ID                       varchar(255) not null,
+  PRIORITY                      integer      not null -- 1:low 5:medium 10:high
+);
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+--------------------------
+-- Object Splitter Work --
+--------------------------
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_SPLITTER_WORK_RESOURCE_ID on
+	CALENDAR_OBJECT_SPLITTER_WORK(RESOURCE_ID);
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '30');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '5');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_30_to_31.sql	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,53 @@
+----
+-- Copyright (c) 2012-2013 Apple Inc. All rights reserved.
+--
+-- 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 30 to 31 --
+---------------------------------------------------
+
+-- Home related updates
+
+alter table CALENDAR_HOME
+ add ("STATUS" integer default 0 not null);
+
+alter table NOTIFICATION_HOME
+ add ("STATUS" integer default 0 not null);
+
+alter table ADDRESSBOOK_HOME
+ add ("STATUS" integer default 0 not null);
+
+create table HOME_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
+
+-- Bind changes
+alter table CALENDAR_BIND
+ add ("EXTERNAL_ID" integer default null);
+
+alter table SHARED_ADDRESSBOOK_BIND
+ add ("EXTERNAL_ID" integer default null);
+
+alter table SHARED_GROUP_BIND
+ add ("EXTERNAL_ID" integer default null);
+
+
+-- Now update the version
+-- No data upgrades
+update CALENDARSERVER set VALUE = '31' where NAME = 'VERSION';

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_30_to_31.sql	2014-01-02 17:19:18 UTC (rev 12211)
@@ -0,0 +1,55 @@
+----
+-- Copyright (c) 2012-2013 Apple Inc. All rights reserved.
+--
+-- 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 30 to 31 --
+---------------------------------------------------
+
+-- Home related updates
+
+alter table CALENDAR_HOME
+ add column STATUS integer default 0 not null;
+
+alter table NOTIFICATION_HOME
+ add column STATUS integer default 0 not null;
+
+alter table ADDRESSBOOK_HOME
+ add column STATUS integer default 0 not null;
+
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+
+-- Bind changes
+alter table CALENDAR_BIND
+ add column EXTERNAL_ID integer default null;
+
+alter table SHARED_ADDRESSBOOK_BIND
+ add column EXTERNAL_ID integer default null;
+
+alter table SHARED_GROUP_BIND
+ add column EXTERNAL_ID integer default null;
+
+
+-- Now update the version
+-- No data upgrades
+update CALENDARSERVER set VALUE = '31' where NAME = 'VERSION';

Modified: CalendarServer/trunk/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_tables.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/sql_tables.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -133,6 +133,15 @@
 
 # Various constants
 
+_homeStatus = _schemaConstants(
+    schema.HOME_STATUS.DESCRIPTION,
+    schema.HOME_STATUS.ID
+)
+
+
+_HOME_STATUS_NORMAL = _homeStatus('normal')
+_HOME_STATUS_EXTERNAL = _homeStatus('external')
+
 _bindStatus = _schemaConstants(
     schema.CALENDAR_BIND_STATUS.DESCRIPTION,
     schema.CALENDAR_BIND_STATUS.ID

Modified: CalendarServer/trunk/txdav/common/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/test_sql.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/test/test_sql.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -19,21 +19,28 @@
 """
 
 from twext.enterprise.dal.syntax import Select
-from txdav.xml import element as davxml
+from twext.enterprise.dal.syntax import Insert
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.task import Clock
 from twisted.trial.unittest import TestCase
 from twisted.internet.defer import Deferred
 
+from txdav.caldav.datastore.test.util import buildDirectoryRecord
 from txdav.common.datastore.sql import log, CommonStoreTransactionMonitor, \
     CommonHome, CommonHomeChild, ECALENDARTYPE
 from txdav.common.datastore.sql_tables import schema
 from txdav.common.datastore.test.util import CommonCommonTests, buildStore
 from txdav.common.icommondatastore import AllRetriesFailed
-from twext.enterprise.dal.syntax import Insert
 from txdav.common.datastore.sql import fixUUIDNormalization
+from txdav.xml import element as davxml
 
+from uuid import UUID
+
+exampleUID = UUID("a" * 32)
+denormalizedUID = str(exampleUID)
+normalizedUID = denormalizedUID.upper()
+
 class CommonSQLStoreTests(CommonCommonTests, TestCase):
     """
     Tests for shared functionality in L{txdav.common.datastore.sql}.
@@ -46,6 +53,9 @@
         """
         yield super(CommonSQLStoreTests, self).setUp()
         self._sqlStore = yield buildStore(self, self.notifierFactory)
+        self._sqlStore.directoryService().addRecord(buildDirectoryRecord(denormalizedUID))
+        self._sqlStore.directoryService().addRecord(buildDirectoryRecord(normalizedUID))
+        self._sqlStore.directoryService().addRecord(buildDirectoryRecord("uid"))
 
 
     def storeUnderTest(self):
@@ -308,31 +318,31 @@
         token = yield homeChild.syncToken()
         yield homeChild._changeRevision("insert", "C")
         changed = yield homeChild.resourceNamesSinceToken(token)
-        self.assertEqual(changed, (["C"], [],))
+        self.assertEqual(changed, (["C"], [], [],))
 
         # update test
         token = yield homeChild.syncToken()
         yield homeChild._changeRevision("update", "C")
         changed = yield homeChild.resourceNamesSinceToken(token)
-        self.assertEqual(changed, (["C"], [],))
+        self.assertEqual(changed, (["C"], [], [],))
 
         # delete test
         token = yield homeChild.syncToken()
         yield homeChild._changeRevision("delete", "C")
         changed = yield homeChild.resourceNamesSinceToken(token)
-        self.assertEqual(changed, ([], ["C"],))
+        self.assertEqual(changed, ([], ["C"], [],))
 
         # missing update test
         token = yield homeChild.syncToken()
         yield homeChild._changeRevision("update", "D")
         changed = yield homeChild.resourceNamesSinceToken(token)
-        self.assertEqual(changed, (["D"], [],))
+        self.assertEqual(changed, (["D"], [], [],))
 
         # missing delete test
         token = yield homeChild.syncToken()
         yield homeChild._changeRevision("delete", "E")
         changed = yield homeChild.resourceNamesSinceToken(token)
-        self.assertEqual(changed, ([], [],))
+        self.assertEqual(changed, ([], [], [],))
 
         yield txn.abort()
 
@@ -421,10 +431,3 @@
         yield fixUUIDNormalization(self.storeUnderTest())
         self.assertEqual((yield self.allHomeUIDs(schema.ADDRESSBOOK_HOME)),
                          [[normalizedUID]])
-
-
-
-from uuid import UUID
-exampleUID = UUID("a" * 32)
-denormalizedUID = str(exampleUID)
-normalizedUID = denormalizedUID.upper()

Modified: CalendarServer/trunk/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/util.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/test/util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -112,12 +112,14 @@
     def recordWithUID(self, uid):
         return self.records.get(uid)
 
+
     def recordWithGUID(self, guid):
         for record in self.records.itervalues():
             if record.guid == guid:
                 return record
         return None
 
+
     def addRecord(self, record):
         self.records[record.uid] = record
 
@@ -127,32 +129,70 @@
 
     implements(IStoreDirectoryRecord)
 
-    def __init__(self, uid, shortNames, fullName, extras={}):
+    def __init__(self, uid, shortNames, fullName, thisServer=True, server=None, extras={}):
         self.uid = uid
         self.guid = uid
         self.shortNames = shortNames
         self.fullName = fullName
         self.displayName = self.fullName if self.fullName else self.shortNames[0]
+        self._thisServer = thisServer
+        self._server = server
         self.extras = extras
 
 
+    def thisServer(self):
+        return self._thisServer
 
+
+    def server(self):
+        return self._server
+
+
+
+def buildDirectory(homes=None):
+
+    directory = TestStoreDirectoryService()
+
+    # User accounts
+    for ctr in range(1, 100):
+        directory.addRecord(TestStoreDirectoryRecord(
+            "user%02d" % (ctr,),
+            ("user%02d" % (ctr,),),
+            "User %02d" % (ctr,),
+        ))
+
+    homes = set(homes) if homes is not None else set()
+    for uid in homes:
+        directory.addRecord(buildDirectoryRecord(uid))
+
+    return directory
+
+
+
+def buildDirectoryRecord(uid):
+    return TestStoreDirectoryRecord(
+        uid,
+        (uid,),
+        uid.capitalize(),
+    )
+
+
+
 class SQLStoreBuilder(object):
     """
     Test-fixture-builder which can construct a PostgresStore.
     """
-    sharedService = None
-    currentTestID = None
+    def __init__(self, secondary=False):
+        self.sharedService = None
+        self.currentTestID = None
+        self.sharedDBPath = "_test_sql_db" + str(os.getpid()) + ("-2" if secondary else "")
 
-    SHARED_DB_PATH = "_test_sql_db" + str(os.getpid())
 
-
-    @classmethod
-    def createService(cls, serviceFactory):
+    def createService(self, serviceFactory):
         """
         Create a L{PostgresService} to use for building a store.
         """
-        dbRoot = CachingFilePath(cls.SHARED_DB_PATH)
+        dbRoot = CachingFilePath(self.sharedDBPath)
         return PostgresService(
             dbRoot, serviceFactory, current_sql_schema, resetSchema=True,
             databaseName="caldav",
@@ -168,17 +208,15 @@
         )
 
 
-    @classmethod
-    def childStore(cls):
+    def childStore(self):
         """
         Create a store suitable for use in a child process, that is hooked up
         to the store that a parent test process is managing.
         """
         disableMemcacheForTest(TestCase())
         staticQuota = 3000
-        attachmentRoot = (CachingFilePath(cls.SHARED_DB_PATH)
-                          .child("attachments"))
-        stubsvc = cls.createService(lambda cf: Service())
+        attachmentRoot = (CachingFilePath(self.sharedDBPath).child("attachments"))
+        stubsvc = self.createService(lambda cf: Service())
 
         cp = ConnectionPool(stubsvc.produceConnection, maxConnections=1)
         # Attach the service to the running reactor.
@@ -194,17 +232,17 @@
         return cds
 
 
-    def buildStore(self, testCase, notifierFactory, directoryService=None):
+    def buildStore(self, testCase, notifierFactory, directoryService=None, homes=None):
         """
         Do the necessary work to build a store for a particular test case.
 
         @return: a L{Deferred} which fires with an L{IDataStore}.
         """
         disableMemcacheForTest(testCase)
-        dbRoot = CachingFilePath(self.SHARED_DB_PATH)
+        dbRoot = CachingFilePath(self.sharedDBPath)
         attachmentRoot = dbRoot.child("attachments")
         if directoryService is None:
-            directoryService = TestStoreDirectoryService()
+            directoryService = buildDirectory(homes=homes)
         if self.sharedService is None:
             ready = Deferred()
             def getReady(connectionFactory, storageService):
@@ -251,8 +289,7 @@
         attachmentRoot.createDirectory()
 
         currentTestID = testCase.id()
-        cp = ConnectionPool(self.sharedService.produceConnection,
-                            maxConnections=5)
+        cp = ConnectionPool(self.sharedService.produceConnection, maxConnections=5)
         quota = deriveQuota(testCase)
         store = CommonDataStore(
             cp.connection,
@@ -314,6 +351,7 @@
 
 theStoreBuilder = SQLStoreBuilder()
 buildStore = theStoreBuilder.buildStore
+cleanStore = theStoreBuilder.cleanStore
 
 
 _notSet = object()
@@ -685,13 +723,13 @@
 
 
     @inlineCallbacks
-    def homeUnderTest(self, txn=None, name="home1"):
+    def homeUnderTest(self, txn=None, name="home1", create=False):
         """
         Get the calendar home detailed by C{requirements['home1']}.
         """
         if txn is None:
             txn = self.transactionUnderTest()
-        returnValue((yield txn.calendarHomeWithUID(name)))
+        returnValue((yield txn.calendarHomeWithUID(name, create=create)))
 
 
     @inlineCallbacks

Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/util.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -19,7 +19,7 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 from txdav.base.propertystore.base import PropertyName
 from txdav.base.propertystore.sql import PropertyStore
-from txdav.common.datastore.sql_tables import schema
+from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_EXTERNAL
 from twisted.python.failure import Failure
 
 log = Logger()
@@ -134,7 +134,7 @@
 
 
 @inlineCallbacks
-def doToEachHomeNotAtVersion(store, homeSchema, version, doIt, logStr, filterOwnerUID=None):
+def doToEachHomeNotAtVersion(store, homeSchema, version, doIt, logStr, filterOwnerUID=None, processExternal=False):
     """
     Do something to each home whose version column indicates it is older
     than the specified version. Do this in batches as there may be a lot of work to do. Also,
@@ -161,7 +161,7 @@
         txn = store.newTransaction("updateDataVersion")
         try:
             rows = yield Select(
-                [homeSchema.RESOURCE_ID, homeSchema.OWNER_UID, ],
+                [homeSchema.RESOURCE_ID, homeSchema.OWNER_UID, homeSchema.STATUS, ],
                 From=homeSchema,
                 Where=where,
                 OrderBy=homeSchema.OWNER_UID,
@@ -173,9 +173,10 @@
                 logUpgradeStatus("End {}".format(logStr), count, total)
                 returnValue(None)
 
-            # Apply to the home
-            homeResourceID, _ignore_owner_uid = rows[0]
-            yield doIt(txn, homeResourceID)
+            # Apply to the home if not external
+            homeResourceID, _ignore_owner_uid, homeStatus = rows[0]
+            if homeStatus != _HOME_STATUS_EXTERNAL or processExternal:
+                yield doIt(txn, homeResourceID)
 
             # Update the home to the current version
             yield Update(

Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -143,7 +143,17 @@
             self.filesPath, {"push": StubNotifierFactory()}, TestStoreDirectoryService(), True, True
         )
         self.sqlStore = yield theStoreBuilder.buildStore(
-            self, StubNotifierFactory()
+            self,
+            StubNotifierFactory(),
+            homes=(
+                "home1",
+                "home2",
+                "home3",
+                "home_defaults",
+                "home_no_splits",
+                "home_splits",
+                "home_splits_shared",
+            )
         )
         self.upgrader = UpgradeToDatabaseStep(self.fileStore, self.sqlStore)
 

Modified: CalendarServer/trunk/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/trunk/txdav/common/icommondatastore.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/icommondatastore.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -51,6 +51,13 @@
 
 
 
+class RecordNotAllowedError(CommonStoreError):
+    """
+    User not allowed.
+    """
+
+
+
 class NameNotAllowedError(CommonStoreError):
     """
     Attempt to create an object with a name that is not allowed.
@@ -205,6 +212,29 @@
     """
 
 
+
+class ShareNotAllowed(CommonStoreError):
+    """
+    An operation on a shared resource is not allowed.
+    """
+
+
+
+class ExternalShareFailed(CommonStoreError):
+    """
+    An external sharee operation failed.
+    """
+
+
+
+class NonExistentExternalShare(CommonStoreError):
+    """
+    An external sharee operation failed because the share does not exist on the
+    other pod. The caller of the external request receiving this exception should
+    remove the local external share to "heal" this mismatch.
+    """
+
+
 # Indexing / sync tokens
 
 

Modified: CalendarServer/trunk/txdav/common/idirectoryservice.py
===================================================================
--- CalendarServer/trunk/txdav/common/idirectoryservice.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txdav/common/idirectoryservice.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -26,6 +26,20 @@
     "IStoreDirectoryRecord",
 ]
 
+class IStoreDirectoryError(Exception):
+    """
+    Base class for directory related errors.
+    """
+
+
+
+class DirectoryRecordNotFoundError(Exception):
+    """
+    Directory record not found.
+    """
+
+
+
 class IStoreDirectoryService(Interface):
     """
     Directory Service for looking up users.
@@ -48,6 +62,7 @@
         """
 
 
+
 class IStoreDirectoryRecord(Interface):
     """
     Directory record object
@@ -62,3 +77,27 @@
     fullName = Attribute("Full name for the entity associated with the record: C{str}")
 
     displayName = Attribute("Display name for entity associated with the record: C{str}")
+
+    def serverURI(): #@NoSelf
+        """
+        Return the URI for the record's server "pod".
+
+        @return: a URI.
+        @rtype: C{str}
+        """
+
+    def server(): #@NoSelf
+        """
+        Return the L{txdav.caldav.datastore.scheduling.localservers.Server} for the record's server "pod".
+
+        @return: a pod server record.
+        @rtype: L{txdav.caldav.datastore.scheduling.localservers.Server}
+        """
+
+    def thisServer(): #@NoSelf
+        """
+        Indicates whether the record is hosted on this server "pod".
+
+        @return: C{True} if hosted by this service.
+        @rtype: C{bool}
+        """

Modified: CalendarServer/trunk/txweb2/http.py
===================================================================
--- CalendarServer/trunk/txweb2/http.py	2014-01-02 17:02:37 UTC (rev 12210)
+++ CalendarServer/trunk/txweb2/http.py	2014-01-02 17:19:18 UTC (rev 12211)
@@ -558,7 +558,7 @@
     """
     def __init__(self, code, jobj):
         """
-        @param xml_responses: an iterable of davxml.Response objects.
+        @param jobj: a Python object that can be serialized to JSON.
         """
         Response.__init__(self, code, stream=json.dumps(jobj))
         self.headers.setHeader("content-type", http_headers.MimeType("application", "json"))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/4bb7e9a5/attachment.html>


More information about the calendarserver-changes mailing list