This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
plone.dexterity/plone/dexterity/schema.py

210 lines
7.0 KiB
Python

import new
from threading import RLock
from plone.synchronize import synchronized
from zope.interface import implements, alsoProvides
from zope.interface.interface import InterfaceClass
from zope.component import adapter
from zope.component import queryUtility
from plone.behavior.interfaces import IBehavior
from plone.supermodel.parser import ISchemaPolicy
from plone.supermodel.utils import syncSchema
from plone.alterego.interfaces import IDynamicObjectFactory
from plone.dexterity.interfaces import IContentType
from plone.dexterity.interfaces import IDexteritySchema
from plone.dexterity.interfaces import IDexterityFTI
from plone.dexterity.interfaces import ISchemaInvalidatedEvent
from plone.dexterity import utils
from plone.alterego import dynamic
# Dynamic modules
generated = dynamic.create('plone.dexterity.schema.generated')
transient = new.module("transient")
# Schema cache
class SchemaCache(object):
"""Simple schema cache.
This cache will store a Python object reference to the schema, as returned
by fti.lookupSchema(), for any number of portal types. The value will
be cached until the server is restarted or the cache is invalidated or
cleared.
You should only use this if you require bare-metal speed. For almost all
operations, it's safer and easier to do:
>>> fti = getUtility(IDexterityFTI, name=portal_type)
>>> schema = fti.lookupSchema()
The lookupSchema() call is probably as fast as this cache. However, if
you need to avoid the utility lookup, you can use the cache like so:
>>> from plone.dexterity.schema import SCHEMA_CACHE
>>> my_schema = SCHEMA_CACHE.get(portal_type)
Invalidate the cache by calling invalidate() (for one portal_type) or
clear() (for all cached values), or simply raise a SchemaInvalidatedEvent.
"""
lock = RLock()
cache = {}
subtypes_cache = {}
counter_values = {}
@synchronized(lock)
def get(self, portal_type):
cached = self.cache.get(portal_type, None)
if cached is None:
fti = queryUtility(IDexterityFTI, name=portal_type)
if fti is not None:
try:
cached = self.cache[portal_type] = fti.lookupSchema()
except (AttributeError, ValueError):
pass
return cached
@synchronized(lock)
def subtypes(self, portal_type):
cached = self.subtypes_cache.get(portal_type, None)
if cached is None:
subtypes = []
fti = queryUtility(IDexterityFTI, name=portal_type)
if fti is None:
return ()
for behavior_name in fti.behaviors:
behavior = queryUtility(IBehavior, name=behavior_name)
if behavior is not None and behavior.marker is not None:
subtypes.append(behavior.marker)
cached = self.subtypes_cache[portal_type] = tuple(subtypes)
return cached
@synchronized(lock)
def counter(self, portal_type):
counter = self.counter_values.get(portal_type, None)
if counter is None:
counter = self.counter_values[portal_type] = 0
return counter
@synchronized(lock)
def invalidate(self, portal_type):
self.cache[portal_type] = None
self.subtypes_cache[portal_type] = None
if portal_type in self.counter_values:
self.counter_values[portal_type] += 1
else:
self.counter_values[portal_type] = 0
@synchronized(lock)
def clear(self):
self.cache.clear()
self.subtypes_cache.clear()
SCHEMA_CACHE = SchemaCache()
class SchemaInvalidatedEvent(object):
implements(ISchemaInvalidatedEvent)
def __init__(self, portal_type):
self.portal_type = portal_type
@adapter(ISchemaInvalidatedEvent)
def invalidate_schema(event):
if event.portal_type:
SCHEMA_CACHE.invalidate(event.portal_type)
else:
SCHEMA_CACHE.clear()
# Dynamic module factory
class SchemaModuleFactory(object):
"""Create dynamic schema interfaces on the fly
"""
implements(IDynamicObjectFactory)
lock = RLock()
_transient_SCHEMA_CACHE = {}
@synchronized(lock)
def __call__(self, name, module):
"""Someone tried to load a dynamic interface that has not yet been
created yet. We will attempt to load it from the FTI if we can. If
the FTI doesn't exist, create a temporary marker interface that we
can fill later.
The goal here is to ensure that we create exactly one interface
instance for each name. If we can't find an FTI, we'll cache the
interface so that we don't get a new one with a different id later.
This cache is global, so we synchronise the method with a thread
lock.
Once we have a properly populated interface, we set it onto the
module using setattr(). This means that the factory will not be
invoked again.
"""
try:
prefix, portal_type, schemaName = utils.splitSchemaName(name)
except ValueError:
return None
if name in self._transient_SCHEMA_CACHE:
schema = self._transient_SCHEMA_CACHE[name]
else:
bases = ()
is_default_schema = not schemaName
if is_default_schema:
bases += (IDexteritySchema,)
schema = InterfaceClass(name, bases, __module__=module.__name__)
if is_default_schema:
alsoProvides(schema, IContentType)
fti = queryUtility(IDexterityFTI, name=portal_type)
if fti is None and name not in self._transient_SCHEMA_CACHE:
self._transient_SCHEMA_CACHE[name] = schema
elif fti is not None:
model = fti.lookupModel()
syncSchema(model.schemata[schemaName], schema, sync_bases=True)
# Save this schema in the module - this factory will not be
# called again for this name
if name in self._transient_SCHEMA_CACHE:
del self._transient_SCHEMA_CACHE[name]
setattr(module, name, schema)
return schema
class DexteritySchemaPolicy(object):
"""Determines how and where imported dynamic interfaces are created.
Note that these schemata are never used directly. Rather, they are merged
into a schema with a proper name and module, either dynamically or
in code.
"""
implements(ISchemaPolicy)
def module(self, schemaName, tree):
return 'plone.dexterity.schema.transient'
def bases(self, schemaName, tree):
return ()
def name(self, schemaName, tree):
# We use a temporary name whilst the interface is being generated;
# when it's first used, we know the portal_type and site, and can
# thus update it
return '__tmp__' + schemaName