001: /*
002: * Copyright 2004 Outerthought bvba and Schaubroeck nv
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.outerj.daisy.repository.serverimpl;
017:
018: import org.outerj.daisy.repository.*;
019: import org.outerj.daisy.repository.spi.local.PreSaveHook;
020: import org.outerj.daisy.repository.spi.ExtensionProvider;
021: import org.outerj.daisy.repository.namespace.NamespaceNotFoundException;
022: import org.outerj.daisy.repository.user.Role;
023: import org.outerj.daisy.repository.serverimpl.model.LocalSchemaStrategy;
024: import org.outerj.daisy.repository.serverimpl.user.LocalUserManagementStrategy;
025: import org.outerj.daisy.repository.serverimpl.acl.LocalAclStrategy;
026: import org.outerj.daisy.repository.serverimpl.variant.LocalVariantStrategy;
027: import org.outerj.daisy.repository.serverimpl.comment.LocalCommentStrategy;
028: import org.outerj.daisy.repository.commonimpl.*;
029: import org.outerj.daisy.repository.commonimpl.schema.CommonRepositorySchema;
030: import org.outerj.daisy.blobstore.BlobStore;
031: import org.outerj.daisy.cache.DocumentCache;
032: import org.outerj.daisy.query.QueryFactory;
033: import org.outerj.daisy.authentication.UserAuthenticator;
034: import org.outerj.daisy.ftindex.FullTextIndex;
035: import org.outerj.daisy.summary.DocumentSummarizer;
036: import org.outerj.daisy.jdbcutil.JdbcHelper;
037: import org.outerj.daisy.jdbcutil.SqlCounter;
038: import org.outerj.daisy.linkextraction.LinkExtractor;
039: import org.outerj.daisy.linkextraction.LinkExtractorManager;
040: import org.outerj.daisy.plugin.PluginUser;
041: import org.outerj.daisy.plugin.PluginRegistry;
042: import org.outerj.daisy.plugin.PluginHandle;
043: import org.apache.avalon.framework.configuration.Configuration;
044: import org.apache.avalon.framework.configuration.ConfigurationException;
045: import org.apache.commons.logging.Log;
046: import org.apache.commons.logging.LogFactory;
047:
048: import javax.sql.DataSource;
049: import javax.annotation.PreDestroy;
050:
051: import java.sql.*;
052: import java.util.*;
053: import java.util.concurrent.ConcurrentHashMap;
054: import java.util.concurrent.CopyOnWriteArrayList;
055:
056: public class LocalRepositoryManager implements RepositoryManager {
057: private PluginRegistry pluginRegistry;
058: private CommonRepository commonRepository;
059: private DataSource dataSource;
060: private DocumentCache documentCache;
061: private BlobStore blobStore;
062: private UserAuthenticator userAuthenticator;
063: private QueryFactory queryFactory;
064: private AuthenticatedUser systemUser;
065: private FullTextIndex fullTextIndex;
066: private DocumentSummarizer documentSummarizer;
067: private LinkExtractorManager linkExtractorManager;
068: private JdbcHelper jdbcHelper;
069: private String repositoryNamespace;
070: private Map<String, ExtensionProvider> extensions = new ConcurrentHashMap<String, ExtensionProvider>(
071: 16, .75f, 1);
072: private PluginUser<ExtensionProvider> extensionPluginUser = new ExtensionPluginUser();
073: private List<PluginHandle<PreSaveHook>> preSaveHooks = new CopyOnWriteArrayList<PluginHandle<PreSaveHook>>();
074: private PluginUser<PreSaveHook> preSaveHookPluginUser = new PreSaveHookPluginUser();
075: private long expiredLockJanitorInterval;
076: private Thread expiredLockJanitorThread;
077: private final Log log = LogFactory.getLog(getClass());
078: /**
079: * The database schema version number. Not necessarily the same as the Daisy version number.
080: * This number only gets augmented on releases where the schema actually changes.
081: * If changed here, also needs to be changed in daisy-data.xml and the database
082: * upgrade script.
083: */
084: private final static String DB_SCHEMA_VERSION = "2.2";
085:
086: public LocalRepositoryManager(Configuration configuration,
087: DataSource dataSource, BlobStore blobStore,
088: DocumentCache documentCache,
089: UserAuthenticator userAuthenticator,
090: QueryFactory queryFactory, FullTextIndex fullTextIndex,
091: DocumentSummarizer documentSummarizer,
092: LinkExtractorManager linkExtractorManager,
093: PluginRegistry pluginRegistry) throws Exception {
094: this .dataSource = dataSource;
095: this .blobStore = blobStore;
096: this .documentCache = documentCache;
097: this .userAuthenticator = userAuthenticator;
098: this .queryFactory = queryFactory;
099: this .fullTextIndex = fullTextIndex;
100: this .documentSummarizer = documentSummarizer;
101: this .linkExtractorManager = linkExtractorManager;
102: this .pluginRegistry = pluginRegistry;
103: this .configure(configuration);
104: this .initialize();
105: this .start();
106: }
107:
108: @PreDestroy
109: public void destroy() throws Exception {
110: this .stop();
111: pluginRegistry.unsetPluginUser(PreSaveHook.class,
112: preSaveHookPluginUser);
113: pluginRegistry.unsetPluginUser(ExtensionProvider.class,
114: extensionPluginUser);
115: }
116:
117: private void configure(Configuration configuration)
118: throws ConfigurationException {
119: this .expiredLockJanitorInterval = configuration.getChild(
120: "expiredLockJanitorInterval").getValueAsLong(60000);
121: this .repositoryNamespace = configuration.getChild("namespace")
122: .getValue();
123: }
124:
125: private void initialize() throws Exception {
126: Context context = new Context();
127:
128: // The sytem user is a built-in user which has ID 1
129: this .systemUser = new AuthenticatedUserImpl(1, null,
130: new long[] { Role.ADMINISTRATOR },
131: new long[] { Role.ADMINISTRATOR }, "$system");
132:
133: // Get the database-specific JdbcHelper
134: jdbcHelper = JdbcHelper.getInstance(dataSource, log);
135:
136: RepositoryStrategy repositoryStrategy = new LocalRepositoryStrategy(
137: context, jdbcHelper);
138: DocumentStrategy documentStrategy = new LocalDocumentStrategy(
139: context, systemUser, jdbcHelper);
140: LocalSchemaStrategy schemaStrategy = new LocalSchemaStrategy(
141: context, jdbcHelper);
142: LocalAclStrategy aclStrategy = new LocalAclStrategy(context,
143: systemUser, jdbcHelper);
144: LocalUserManagementStrategy userManagementStrategy = new LocalUserManagementStrategy(
145: context, jdbcHelper, systemUser);
146: LocalVariantStrategy variantStrategy = new LocalVariantStrategy(
147: context, jdbcHelper);
148: LocalCollectionStrategy collectionStrategy = new LocalCollectionStrategy(
149: context, systemUser, jdbcHelper);
150: LocalCommentStrategy commentStrategy = new LocalCommentStrategy(
151: context, systemUser, jdbcHelper);
152: commonRepository = new LocalCommonRepository(
153: repositoryStrategy, documentStrategy, schemaStrategy,
154: aclStrategy, userManagementStrategy, variantStrategy,
155: collectionStrategy, commentStrategy, context,
156: systemUser, documentCache, extensions, jdbcHelper);
157: commonRepository.addListener(new DocumentCacheInvalidator());
158:
159: // supply the user
160: userAuthenticator.setUserManager(new RepositoryImpl(
161: commonRepository, systemUser).getUserManager());
162:
163: checkDatabaseSchema();
164:
165: assureRepositoryNamespaceRegistered();
166:
167: pluginRegistry.setPluginUser(PreSaveHook.class,
168: preSaveHookPluginUser);
169: pluginRegistry.setPluginUser(ExtensionProvider.class,
170: extensionPluginUser);
171: }
172:
173: private void start() throws Exception {
174: expiredLockJanitorThread = new Thread(new ExpiredLockJanitor(),
175: "Daisy Expired Lock Janitor");
176: expiredLockJanitorThread.start();
177: }
178:
179: private void stop() throws Exception {
180: log.info("Waiting for expired lock janitor thread to end.");
181: expiredLockJanitorThread.interrupt();
182: try {
183: expiredLockJanitorThread.join();
184: } catch (InterruptedException e) {
185: // ignore
186: }
187: }
188:
189: public Repository getRepository(final Credentials credentials)
190: throws RepositoryException {
191: AuthenticatedUser user = userAuthenticator
192: .authenticate(credentials);
193: return new RepositoryImpl(commonRepository, user);
194: }
195:
196: private class ExtensionPluginUser implements
197: PluginUser<ExtensionProvider> {
198:
199: public void pluginAdded(
200: PluginHandle<ExtensionProvider> pluginHandle) {
201: extensions.put(pluginHandle.getName(), pluginHandle
202: .getPlugin());
203: }
204:
205: public void pluginRemoved(
206: PluginHandle<ExtensionProvider> pluginHandle) {
207: extensions.remove(pluginHandle.getName());
208: }
209: }
210:
211: private class PreSaveHookPluginUser implements
212: PluginUser<PreSaveHook> {
213:
214: public void pluginAdded(PluginHandle<PreSaveHook> plugin) {
215: preSaveHooks.add(plugin);
216: }
217:
218: public void pluginRemoved(PluginHandle<PreSaveHook> plugin) {
219: preSaveHooks.remove(plugin);
220: }
221: }
222:
223: /**
224: * Context information for the document implementation
225: */
226: public class Context {
227: private SqlCounter docIdCounter = new SqlCounter(
228: "document_sequence", dataSource, log);
229: private SqlCounter collectionIdCounter = new SqlCounter(
230: "collection_sequence", dataSource, log);
231: private SqlCounter docTypeCounter = new SqlCounter(
232: "documenttype_sequence", dataSource, log);
233: private SqlCounter partTypeCounter = new SqlCounter(
234: "parttype_sequence", dataSource, log);
235: private SqlCounter fieldTypeCounter = new SqlCounter(
236: "fieldtype_sequence", dataSource, log);
237: private SqlCounter localizedStringCounter = new SqlCounter(
238: "localizedstring_sequence", dataSource, log);
239: private SqlCounter commentCounter = new SqlCounter(
240: "comment_sequence", dataSource, log);
241: private SqlCounter eventCounter = new SqlCounter(
242: "event_sequence", dataSource, log);
243: private SqlCounter userCounter = new SqlCounter(
244: "user_sequence", dataSource, log);
245: private SqlCounter roleCounter = new SqlCounter(
246: "role_sequence", dataSource, log);
247: private SqlCounter namespaceCounter = new SqlCounter(
248: "namespace_sequence", dataSource, log);
249:
250: private Context() {
251: // private constructor to make sure no-one else can create this
252: }
253:
254: public DataSource getDataSource() {
255: return dataSource;
256: }
257:
258: public QueryFactory getQueryFactory() {
259: return queryFactory;
260: }
261:
262: public BlobStore getBlobStore() {
263: return blobStore;
264: }
265:
266: public FullTextIndex getFullTextIndex() {
267: return fullTextIndex;
268: }
269:
270: public UserAuthenticator getUserAuthenticator() {
271: return userAuthenticator;
272: }
273:
274: public DocumentSummarizer getDocumentSummarizer() {
275: return documentSummarizer;
276: }
277:
278: public CommonRepositorySchema getRepositorySchema() {
279: return commonRepository.getRepositorySchema();
280: }
281:
282: public CommonRepository getCommonRepository() {
283: return commonRepository;
284: }
285:
286: public Log getLogger() {
287: return log;
288: }
289:
290: public long getNextDocumentId() throws SQLException {
291: return docIdCounter.getNextId();
292: }
293:
294: public long getNextCollectionId() throws SQLException {
295: return collectionIdCounter.getNextId();
296: }
297:
298: public long getNextDocumentTypeId() throws SQLException {
299: return docTypeCounter.getNextId();
300: }
301:
302: public long getNextPartTypeId() throws SQLException {
303: return partTypeCounter.getNextId();
304: }
305:
306: public long getNextFieldTypeId() throws SQLException {
307: return fieldTypeCounter.getNextId();
308: }
309:
310: public long getNextLocalizedStringId() throws SQLException {
311: return localizedStringCounter.getNextId();
312: }
313:
314: public long getNextCommentId() throws SQLException {
315: return commentCounter.getNextId();
316: }
317:
318: public long getNextEventId() throws SQLException {
319: return eventCounter.getNextId();
320: }
321:
322: public long getNextUserId() throws SQLException {
323: return userCounter.getNextId();
324: }
325:
326: public long getNextRoleId() throws SQLException {
327: return roleCounter.getNextId();
328: }
329:
330: public long getNextNamespaceId() throws SQLException {
331: return namespaceCounter.getNextId();
332: }
333:
334: public LinkExtractor getLinkExtractor(String name) {
335: return linkExtractorManager.getLinkExtractor(name);
336: }
337:
338: public LinkExtractorInfo[] getLinkExtractors() {
339: LinkExtractor[] extractors = linkExtractorManager
340: .getLinkExtractors();
341: LinkExtractorInfo[] extractorInfos = new LinkExtractorInfo[extractors.length];
342: for (int i = 0; i < extractorInfos.length; i++) {
343: extractorInfos[i] = new LinkExtractorInfoImpl(
344: extractors[i].getName(), extractors[i]
345: .getDescription());
346: }
347: return extractorInfos;
348: }
349:
350: public List<PluginHandle<PreSaveHook>> getPreSaveHooks() {
351: return preSaveHooks;
352: }
353:
354: public String getRepositoryNamespace() {
355: return repositoryNamespace;
356: }
357: }
358:
359: /**
360: * Event listener that removes a document from the cache when it has been updated.
361: */
362: private class DocumentCacheInvalidator implements
363: RepositoryListener {
364: public void repositoryEvent(RepositoryEventType eventType,
365: Object id, long updateCount) {
366: if (eventType == RepositoryEventType.DOCUMENT_UPDATED
367: || eventType == RepositoryEventType.DOCUMENT_DELETED) {
368: documentCache.remove((String) id);
369: documentCache.removeAvailableVariants((String) id);
370: } else if (eventType == RepositoryEventType.COLLECTION_DELETED
371: || eventType == RepositoryEventType.COLLECTION_UPDATED) {
372: // If a collection is deleted (or its name is changed), we would
373: // need to remove/update all cached document objects referencing
374: // that collection.
375: // To avoid this complexity, we simply clear the entire cache.
376: documentCache.clear();
377: } else if (eventType == RepositoryEventType.VERSION_UPDATED
378: || eventType == RepositoryEventType.DOCUMENT_VARIANT_UPDATED
379: || eventType == RepositoryEventType.DOCUMENT_VARIANT_CREATED
380: || eventType == RepositoryEventType.DOCUMENT_VARIANT_DELETED) {
381: VariantKey variantKey = (VariantKey) id;
382: // If a variant is deleted we would need to remove/update all cached
383: // objects referencing this variant through document.referenceLanguageId,
384: // variant.(live|last)SyncedWith or version.syncedWith.
385: // To avoid this complexity, we simply remove the whole document.
386: documentCache.remove(variantKey.getDocumentId());
387: // removing available variants is needed in all of these event types because
388: // availableVariants contains the liveVersionId and the retired flag.
389: documentCache.removeAvailableVariants(variantKey
390: .getDocumentId());
391: } else if (eventType == RepositoryEventType.LOCK_CHANGE) {
392: VariantKey variantKey = (VariantKey) id;
393: DocumentImpl document = documentCache.get(variantKey
394: .getDocumentId(), variantKey.getBranchId(),
395: variantKey.getLanguageId());
396: if (document != null) {
397: document.clearLockInfo();
398: }
399: }
400: }
401: }
402:
403: private class ExpiredLockJanitor implements Runnable {
404: public void run() {
405: try {
406: while (true) {
407: Thread.sleep(expiredLockJanitorInterval);
408: Connection conn = null;
409: PreparedStatement stmt = null;
410: try {
411: conn = dataSource.getConnection();
412: stmt = conn
413: .prepareStatement("select doc_id, ns_id, ns.name_ as ns_name, branch_id, lang_id from locks left join daisy_namespaces ns on (locks.ns_id = ns.id) where time_expires is not null and time_expires < ?");
414: stmt.setTimestamp(1, new Timestamp(System
415: .currentTimeMillis()));
416: ResultSet rs = stmt.executeQuery();
417: while (rs.next()) {
418: if (Thread.interrupted()) {
419: return;
420: }
421: String documentId = rs.getLong("doc_id")
422: + "-" + rs.getString("ns_name");
423: long branchId = rs.getLong("branch_id");
424: long languageId = rs.getLong("lang_id");
425: try {
426: commonRepository.getDocument(
427: documentId, branchId,
428: languageId, false, systemUser)
429: .getLockInfo(true);
430: } catch (DocumentVariantNotFoundException e) {
431: // ignore
432: } catch (DocumentNotFoundException e) {
433: // ignore
434: } catch (RepositoryException e) {
435: log
436: .error("Error trying to update expired lock info for document "
437: + documentId
438: + ", branch "
439: + branchId
440: + ", language "
441: + languageId);
442: }
443: }
444: } catch (Throwable e) {
445: log
446: .error(
447: "Exception occured in ExpiredLockJanitor.",
448: e);
449: } finally {
450: jdbcHelper.closeStatement(stmt);
451: jdbcHelper.closeConnection(conn);
452: }
453: }
454: } catch (InterruptedException e) {
455: // ignore
456: } finally {
457: log.info("Expired lock janitor thread ended.");
458: }
459: }
460: }
461:
462: private void checkDatabaseSchema() throws RepositoryException {
463: Connection conn = null;
464: PreparedStatement stmt = null;
465: try {
466: conn = dataSource.getConnection();
467: stmt = conn
468: .prepareStatement("select propvalue from daisy_system where propname = 'schema_version'");
469: ResultSet rs = stmt.executeQuery();
470: if (!rs.next())
471: throw new RepositoryException(
472: "No schema_version found in daisy_system table.");
473: String version = rs.getString(1);
474: if (!DB_SCHEMA_VERSION.equals(version))
475: throw new RepositoryException(
476: "The repository database schema is not at the correct version: found :\""
477: + version + "\", expected: \""
478: + DB_SCHEMA_VERSION + "\".");
479: } catch (SQLException e) {
480: throw new RepositoryException(
481: "Error getting schema version information.", e);
482: } catch (RepositoryException e) {
483: throw e;
484: } finally {
485: jdbcHelper.closeStatement(stmt);
486: jdbcHelper.closeConnection(conn);
487: }
488: }
489:
490: public String getRepositoryServerVersion() {
491: return commonRepository.getServerVersion(systemUser);
492: }
493:
494: private void assureRepositoryNamespaceRegistered()
495: throws RepositoryException {
496: try {
497: commonRepository.getNamespaceManager().getNamespace(
498: repositoryNamespace);
499: } catch (NamespaceNotFoundException e) {
500: log
501: .info("The repository's namespace was not yet registered, doing that now.");
502: commonRepository.getNamespaceManager().registerNamespace(
503: repositoryNamespace, systemUser);
504: }
505: }
506: }
|