001/*
002 $Id: DbControl.java 4413 2013-02-20 14:04:44Z olle $
003
004 Copyright (C) 2006 Gregory Vincic, Olle Mansson
005 Copyright (C) 2007 Gregory Vincic
006
007 This file is part of Proteios.
008 Available at http://www.proteios.org/
009
010 Proteios is free software; you can redistribute it and/or modify it
011 under the terms of the GNU General Public License as published by
012 the Free Software Foundation; either version 2 of the License, or
013 (at your option) any later version.
014
015 Proteios is distributed in the hope that it will be useful, but
016 WITHOUT ANY WARRANTY; without even the implied warranty of
017 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
018 General Public License for more details.
019
020 You should have received a copy of the GNU General Public License
021 along with this program; if not, write to the Free Software
022 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
023 02111-1307, USA.
024 */
025package org.proteios.core;
026
027import org.proteios.core.data.BasicData;
028import org.proteios.core.data.DiskConsumableData;
029import org.proteios.core.data.DiskUsageData;
030import org.proteios.core.data.GroupData;
031import org.proteios.core.data.QuotaData;
032import org.proteios.core.data.QuotaIndex;
033import org.proteios.core.data.QuotaTypeData;
034import org.proteios.core.data.UserData;
035import org.proteios.util.ClassUtil;
036import java.lang.reflect.Constructor;
037import java.util.ArrayList;
038import java.util.HashMap;
039import java.util.Iterator;
040import java.util.LinkedHashMap;
041import java.util.List;
042import java.util.Map;
043
044/**
045 * A <code>DbControl</code> object is the main object used for communicating
046 * with the database. It contains methods for saving and deleting items,
047 * handling transactions, etc. <code>DbControl</code> objects are created by
048 * the {@link SessionControl#newDbControl()} method.
049 * 
050 * @author Nicklas, Samuel
051 * @version 2.0
052 * @base.modified $Date: 2013-02-20 15:04:44 +0100 (Wed, 20 Feb 2013) $
053 */
054public final class DbControl
055{
056        /**
057         * Log core events.
058         */
059        private static final org.apache.log4j.Logger log = org.apache.log4j.LogManager
060                .getLogger("org.proteios.core");
061        /*
062         * The <code>SessionControl</code> that created this object.
063         */
064        private final SessionControl sc;
065        /**
066         * The Hibernate session.
067         */
068        private org.hibernate.Session hSession;
069        /**
070         * A Hibernate stateless session.
071         */
072        private org.hibernate.StatelessSession hStatelessSession;
073        /**
074         * The Hibernate transaction.
075         */
076        private org.hibernate.Transaction hTransaction;
077        /**
078         * A cache of already loaded items. Maps from data object --> item object
079         */
080        private Map<BasicData, BasicItem> itemCache;
081        /**
082         * A list of items that need special handling when commit is called.
083         */
084        private Map<BasicItem, Transactional.Action> commitQueue;
085        /**
086         * A list of batchers that will be closed when <code>DbControl</code> is
087         * closed and will be flushed when commit is called.
088         */
089        private List<Batcher> batchers;
090        /**
091         * Is the connection closed or not?
092         */
093        private boolean isClosed;
094        /**
095         * If Hibernate is connected to the database or not.
096         */
097        private boolean isConnected;
098
099
100        /**
101         * Create a new object.
102         */
103        DbControl(SessionControl sc) throws BaseException
104        {
105                this.sc = sc;
106                hSession = HibernateUtil.newSession();
107                hTransaction = HibernateUtil.newTransaction(hSession);
108                itemCache = new HashMap<BasicData, BasicItem>();
109                commitQueue = new LinkedHashMap<BasicItem, Transactional.Action>();
110                isClosed = false;
111                isConnected = true;
112        }
113
114
115        /**
116         * Check if the connection to the database has been closed. To get a new
117         * connection, you must create a new <code>DbControl</code> object.
118         * 
119         * @return TRUE if the connection is closed, FALSE otherwise
120         * @see SessionControl#newDbControl()
121         */
122        public boolean isClosed()
123        {
124                return isClosed;
125        }
126
127
128        /**
129         * Get the {@link SessionControl} object that owns this
130         * <code>DbControl</code> object.
131         * 
132         * @return a <code>SessionControl</code> object
133         */
134        public SessionControl getSessionControl()
135        {
136                return sc;
137        }
138
139
140        /**
141         * Get the underlying Hibernate session.
142         */
143        org.hibernate.Session getHibernateSession()
144        {
145                return hSession;
146        }
147
148
149        /**
150         * Get a stateless Hibernate session using the same database connection as
151         * the regular session. If no stateless session exists a new one is created.
152         */
153        org.hibernate.StatelessSession getStatelessSession()
154        {
155                if (hSession != null && hStatelessSession == null)
156                {
157                        hStatelessSession = HibernateUtil.newStatelessSession(hSession);
158                }
159                return hStatelessSession;
160        }
161
162
163        /**
164         * Temporarily disconnect from the database. Uncommitted changes of regular
165         * items are not lost, but you must {@link #reconnect()} before calling
166         * {@link #commit()}. If you have used batch items, you should call
167         * {@link #commit()} or you will lose everything that has already been
168         * {@link BasicBatcher#flush()}:ed to the database.
169         */
170        public void disconnect()
171                        throws BaseException
172        {
173                if (isClosed())
174                        throw new ConnectionClosedException();
175                sc.updateLastAccess();
176                HibernateUtil.rollback(hTransaction);
177                hSession.disconnect();
178                isConnected = false;
179        }
180
181
182        /**
183         * Reconnects to the database after a {@link #disconnect()}.
184         */
185        public void reconnect()
186                        throws BaseException
187        {
188                if (isClosed())
189                        throw new ConnectionClosedException();
190                sc.updateLastAccess();
191                hTransaction = HibernateUtil.newTransaction(hSession);
192                isConnected = true;
193        }
194
195
196        /**
197         * Checks if there is an active connection to the database or not.
198         */
199        public boolean isConnected()
200        {
201                return isConnected;
202        }
203
204
205        /**
206         * Close the connection to the database. Uncommitted changes will be rolled
207         * back. If the connection is already closed this method does nothing. Any
208         * errors in this method are written to the log file. After calling this
209         * method the <code>DbControl</code> cannot be used anymore.
210         */
211        public void close()
212        {
213                if (isClosed())
214                        return;
215                rollback();
216                cleanUp();
217        }
218
219
220        /**
221         * Rollback the transaction.
222         */
223        private void rollback()
224        {
225                // Close up batchers
226                // Make copy because close will remove it from the queue
227                if (batchers != null)
228                {
229                        ArrayList<Batcher> batchCopy = new ArrayList<Batcher>(batchers);
230                        for (Batcher batcher : batchCopy)
231                        {
232                                try
233                                {
234                                        batcher.close();
235                                }
236                                catch (BaseException ex)
237                                {
238                                        log
239                                                .warn("Exception during rollback in batcher.close()",
240                                                        ex);
241                                }
242                        }
243                }
244                try
245                {
246                        HibernateUtil.rollback(hTransaction);
247                        HibernateUtil.close(hSession);
248                        if (hStatelessSession != null)
249                                HibernateUtil.close(hStatelessSession);
250                }
251                catch (BaseException ex)
252                {
253                        log.warn("Exception during rollback in Hibernate", ex);
254                }
255                hStatelessSession = null;
256                hSession = null;
257                hTransaction = null;
258                for (Map.Entry<BasicItem, Transactional.Action> entry : commitQueue
259                        .entrySet())
260                {
261                        BasicItem item = entry.getKey();
262                        Transactional.Action action = entry.getValue();
263                        if (item instanceof Transactional)
264                                item.onRollback(action);
265                }
266        }
267
268
269        /**
270         * Clear and close all objects.
271         */
272        private void cleanUp()
273        {
274                commitQueue.clear();
275                itemCache.clear();
276                if (batchers != null)
277                        batchers.clear();
278                commitQueue = null;
279                itemCache = null;
280                batchers = null;
281                isClosed = true;
282                isConnected = false;
283        }
284
285
286        /**
287         * Commit all changes made to items and close the connection to the
288         * database. If there is an error, a rollback is issued and the connection
289         * is closed.
290         * 
291         * @throws BaseException If there is an error
292         */
293        public void commit()
294                        throws BaseException
295        {
296                if (isClosed())
297                        throw new ConnectionClosedException();
298                sc.updateLastAccess();
299                Map<BasicItem, Transactional.Action> tempQueue = null;
300                Map<BasicItem, Transactional.Action> afterCommitQueue = new HashMap<BasicItem, Transactional.Action>();
301                try
302                {
303                        // Flush and close all batchers
304                        // Make copy because close will remove it from the queue
305                        if (batchers != null)
306                        {
307                                ArrayList<Batcher> batchCopy = new ArrayList<Batcher>(batchers);
308                                for (Batcher batcher : batchCopy)
309                                {
310                                        batcher.close();
311                                }
312                                batchers = null;
313                        }
314                        /*
315                         * We must make a copy of and clear the commitQueue since the
316                         * onBeforeCommit may add new items to it. Therefore we must also
317                         * check if new items have been added to the queue and process them
318                         * as well.
319                         */
320                        while (commitQueue.size() > 0)
321                        {
322                                tempQueue = commitQueue;
323                                commitQueue = new HashMap<BasicItem, Transactional.Action>();
324                                Iterator<Map.Entry<BasicItem, Transactional.Action>> iterator = tempQueue
325                                        .entrySet().iterator();
326                                while (iterator.hasNext())
327                                {
328                                        Map.Entry<BasicItem, Transactional.Action> entry = iterator
329                                                .next();
330                                        BasicItem item = entry.getKey();
331                                        BasicData data = item.getData();
332                                        Transactional.Action action = entry.getValue();
333                                        boolean controlled = item instanceof Controlled;
334                                        boolean transactional = controlled && item instanceof Transactional;
335                                        if (controlled && (item instanceof Validatable))
336                                                item.validate();
337                                        if (action != Transactional.Action.UPDATE || transactional)
338                                        {
339                                                item.onBeforeCommit(action);
340                                        }
341                                        if (controlled && (item instanceof DiskConsumable))
342                                                updateDiskUsage(item);
343                                        if (action == Transactional.Action.CREATE)
344                                        {
345                                                HibernateUtil.saveData(hSession, data);
346                                                itemCache.put(data, item);
347                                                item.onAfterInsert();
348                                        }
349                                        else if (action == Transactional.Action.DELETE)
350                                        {
351                                                if (item.isUsed())
352                                                        throw new ItemInUseException(item.toString());
353                                                HibernateUtil.deleteData(hSession, data);
354                                                itemCache.remove(data);
355                                        }
356                                        if (action != Transactional.Action.UPDATE || transactional)
357                                                afterCommitQueue.put(item, action);
358                                }
359                        }
360                        HibernateUtil.commit(hTransaction);
361                        HibernateUtil.close(hSession);
362                        if (hStatelessSession != null)
363                                HibernateUtil.close(hStatelessSession);
364                        hSession = null;
365                        hTransaction = null;
366                }
367                catch (BaseException ex)
368                {
369                        if (afterCommitQueue != null)
370                                commitQueue.putAll(afterCommitQueue);
371                        if (tempQueue != null)
372                                commitQueue.putAll(tempQueue);
373                        close();
374                        throw ex;
375                }
376                cleanUp();
377                for (Map.Entry<BasicItem, Transactional.Action> entry : afterCommitQueue
378                        .entrySet())
379                {
380                        BasicItem item = entry.getKey();
381                        Transactional.Action action = entry.getValue();
382                        item.onAfterCommit(action);
383                }
384        }
385
386
387        /**
388         * Update the disk usage information for a DiskConsumable item. It is
389         * expected that the <code>item</code> parameter impements the
390         * <code>DiskConsumable</code> interface and that it's data object
391         * implements the <code>DiskConsumableData</code> interface.
392         * 
393         * @throws QuotaException If the quota is exceeded
394         * @throws BaseException If there is another error
395         */
396        private void updateDiskUsage(BasicItem item)
397                        throws QuotaException, BaseException
398        {
399                DiskConsumable dcItem = (DiskConsumable) item; // The item
400                DiskConsumableData dcData = (DiskConsumableData) item.getData(); // The
401                // data
402                // object
403                // for
404                // the
405                // item
406                DiskUsageData currentDiskUsage = dcData.getDiskUsage(); // Current disk
407                // usage
408                // information
409                QuotaTypeData quotaType = HibernateUtil.loadData(hSession,
410                        QuotaTypeData.class, SystemItems.getId(dcItem
411                                .getQuotaTypeSystemId()));
412                Location location = dcItem.getLocation();
413                long bytes = dcItem.getSizeInBytes();
414                // Validate that correct values are returned by the DiskConsumable item
415                if (quotaType == null)
416                        throw new InvalidUseOfNullException(item + ".quotaType");
417                if (location == null)
418                        throw new InvalidUseOfNullException(item + ".location");
419                if (bytes < 0)
420                        throw new NumberOutOfRangeException(item + ".bytes", bytes, 0,
421                                false);
422                if (quotaType.getId() == SystemItems.getId(QuotaType.TOTAL))
423                {
424                        throw new InvalidDataException(
425                                "QuotaType.TOTAL is not a valid quota type for item " + item + ".");
426                }
427                if (location == Location.SECONDARY && !quotaType.getSecondaryLocation())
428                {
429                        throw new InvalidDataException(
430                                quotaType.getName() + " cannot use the secondary location");
431                }
432                // Ignore quota check if new location is OFFLINE
433                if (dcItem.getLocation() != Location.OFFLINE)
434                {
435                        long deltaBytes = bytes;
436                        if (location.getValue() == currentDiskUsage.getLocation())
437                        {
438                                deltaBytes = deltaBytes - currentDiskUsage.getBytes();
439                        }
440                        // Ignore quota check if the item now takes less space than before
441                        // (on the same location)
442                        if (deltaBytes > 0)
443                        {
444                                QuotaTypeData typeTotal = HibernateUtil.loadData(hSession,
445                                        QuotaTypeData.class, SystemItems.getId(QuotaType.TOTAL));
446                                QuotaTypeData typeSpecific = quotaType;
447                                /*
448                                 * Check the owner's quota We use the loadData method since it
449                                 * is possible that the dcData item has been loaded in another
450                                 * Hibernate session, which would cause a
451                                 * LazyInitialisationException when we call getQuotaValues
452                                 * below.
453                                 */
454                                UserData owner = HibernateUtil.loadData(hSession,
455                                        UserData.class, dcData.getOwner().getId());
456                                if (owner.getQuota() != null)
457                                {
458                                        QuotaData ownerQuota = owner.getQuota();
459                                        // Check the owner's total quota
460                                        Long maxTotalQuota = ownerQuota.getQuotaValues().get(
461                                                new QuotaIndex(typeTotal, location.getValue()));
462                                        if (maxTotalQuota == null)
463                                        {
464                                                maxTotalQuota = Quota.UNDEFINED;
465                                        }
466                                        if (maxTotalQuota != Quota.UNLIMITED)
467                                        {
468                                                long usedTotalQuota = getDiskUsage(owner, typeTotal,
469                                                        location);
470                                                if (usedTotalQuota >= maxTotalQuota)
471                                                {
472                                                        throw new QuotaException(location, typeTotal,
473                                                                maxTotalQuota, usedTotalQuota);
474                                                }
475                                        }
476                                        // Check the owner's type specific quota if it exists
477                                        Long maxSpecificQuota = ownerQuota.getQuotaValues().get(
478                                                new QuotaIndex(typeSpecific, location.getValue()));
479                                        if (maxSpecificQuota != null && maxSpecificQuota != Quota.UNLIMITED)
480                                        {
481                                                long usedSpecificQuota = getDiskUsage(owner,
482                                                        typeSpecific, location);
483                                                if (usedSpecificQuota >= maxSpecificQuota)
484                                                {
485                                                        throw new QuotaException(location, typeSpecific,
486                                                                maxSpecificQuota, usedSpecificQuota);
487                                                }
488                                        }
489                                }
490                                // If the user belongs to a quota group, check the groups quota
491                                GroupData quotaGroup = owner.getQuotaGroup();
492                                if (quotaGroup != null && quotaGroup.getQuota() != null)
493                                {
494                                        QuotaData groupQuota = quotaGroup.getQuota();
495                                        // Check the group's total quota
496                                        long maxTotalQuota = groupQuota.getQuotaValues().get(
497                                                new QuotaIndex(typeTotal, location.getValue()));
498                                        if (maxTotalQuota != Quota.UNLIMITED)
499                                        {
500                                                long usedTotalQuota = getDiskUsage(quotaGroup,
501                                                        typeTotal, location);
502                                                if (usedTotalQuota >= maxTotalQuota)
503                                                {
504                                                        throw new QuotaException(location, typeTotal,
505                                                                maxTotalQuota, usedTotalQuota);
506                                                }
507                                        }
508                                        // Check the group's type specific quota if it exists
509                                        long maxSpecificQuota = groupQuota.getQuotaValues().get(
510                                                new QuotaIndex(typeSpecific, location.getValue()));
511                                        if (maxSpecificQuota != Quota.UNLIMITED && maxSpecificQuota != Quota.UNDEFINED)
512                                        {
513                                                long usedSpecificQuota = getDiskUsage(quotaGroup,
514                                                        typeSpecific, location);
515                                                if (usedSpecificQuota >= maxSpecificQuota)
516                                                {
517                                                        throw new QuotaException(location, typeSpecific,
518                                                                maxSpecificQuota, usedSpecificQuota);
519                                                }
520                                        }
521                                }
522                        }
523                }
524                // Update the disk usage information
525                currentDiskUsage.setBytes(bytes);
526                currentDiskUsage.setLocation(location.getValue());
527                currentDiskUsage.setQuotaType(quotaType);
528                currentDiskUsage.setUser(dcData.getOwner());
529                currentDiskUsage.setGroup(dcData.getOwner().getQuotaGroup());
530        }
531
532
533        /**
534         * Get the disk usage for the specified user at the specified location and
535         * quota type.
536         */
537        long getDiskUsage(UserData user, QuotaTypeData quotaType, Location location)
538                        throws BaseException
539        {
540                assert user != null : "user == null";
541                assert location != null : "location == null";
542                assert quotaType != null : "quotaType == null";
543                org.hibernate.Query query;
544                if (quotaType.getId() == SystemItems.getId(QuotaType.TOTAL))
545                {
546                        query = HibernateUtil.getPredefinedQuery(hSession,
547                                "GET_TOTAL_DISKUSAGE_FOR_USER");
548                        /*
549                         * SELECT SUM(du.bytes) FROM DiskUsageData du WHERE du.user = :user
550                         * AND du.location = :location
551                         */
552                }
553                else
554                {
555                        query = HibernateUtil.getPredefinedQuery(hSession,
556                                "GET_SPECIFIC_DISKUSAGE_FOR_USER");
557                        /*
558                         * SELECT SUM(du.bytes) FROM DiskUsageData du WHERE du.user = :user
559                         * AND du.location = :location AND du.quotaType = :quotaType
560                         */
561                        query.setEntity("quotaType", quotaType);
562                }
563                query.setEntity("user", user);
564                query.setInteger("location", location.getValue());
565                Long bytes = HibernateUtil.loadData(Long.class, query);
566                return bytes == null ? 0 : bytes;
567        }
568
569
570        /**
571         * Get the disk usage for the specified group at the specified location and
572         * quota type.
573         */
574        long getDiskUsage(GroupData group, QuotaTypeData quotaType,
575                        Location location)
576                        throws BaseException
577        {
578                assert group != null : "group == null";
579                assert location != null : "location == null";
580                assert quotaType != null : "quotaType == null";
581                org.hibernate.Query query;
582                if (quotaType.getId() == SystemItems.getId(QuotaType.TOTAL))
583                {
584                        query = HibernateUtil.getPredefinedQuery(hSession,
585                                "GET_TOTAL_DISKUSAGE_FOR_GROUP");
586                        /*
587                         * SELECT SUM(du.bytes) FROM DiskUsageData du WHERE du.group =
588                         * :group AND du.location = :location
589                         */
590                }
591                else
592                {
593                        query = HibernateUtil.getPredefinedQuery(hSession,
594                                "GET_SPECIFIC_DISKUSAGE_FOR_GROUP");
595                        /*
596                         * SELECT SUM(du.bytes) FROM DiskUsageData du WHERE du.group =
597                         * :group AND du.location = :location AND du.quotaType = :quotaType
598                         */
599                        query.setEntity("quotaType", quotaType);
600                }
601                query.setEntity("group", group);
602                query.setInteger("location", location.getValue());
603                Long bytes = HibernateUtil.loadData(Long.class, query);
604                return bytes == null ? 0 : bytes;
605        }
606
607
608        /**
609         * Create a new item.
610         * 
611         * @param itemClass The class of the item
612         * @return An object of the <code>itemClass</code> class
613         *         item is not defined in the Item class
614         * @throws BaseException If there is an error
615         */
616        @SuppressWarnings("unchecked")
617        <I extends BasicItem> I newItem(Class<I> itemClass,
618                        Object... extraParameters)
619        {
620                if (isClosed())
621                        throw new ConnectionClosedException();
622                sc.updateLastAccess();
623                Item fromItemClass = Item.fromItemClass(itemClass);
624                if (fromItemClass == null)
625                        throw new BaseException(
626                                "The itemClass[" + itemClass.toString() + "] is not defined in the Item class.");
627                Class<? extends BasicData> dataClass = fromItemClass.getDataClass();
628                assert dataClass != null : "dataClass == null";
629                Item itemType = Item.fromDataClass(dataClass);
630                Class<? extends I> realItemClass = (Class<? extends I>) itemType
631                        .getItemClass();
632                Constructor<? extends I> itemConstructor;
633                Object[] constructorParams;
634                try
635                {
636                        BasicData data = dataClass.newInstance();
637                        if (extraParameters != null && extraParameters.length > 0)
638                        {
639                                constructorParams = new Object[1 + extraParameters.length];
640                                for (int i = 0; i < extraParameters.length; i++)
641                                {
642                                        constructorParams[1 + i] = extraParameters[i];
643                                }
644                        }
645                        else
646                        {
647                                constructorParams = new Object[1];
648                        }
649                        constructorParams[0] = data;
650                        itemConstructor = ClassUtil.findConstructor(realItemClass,
651                                constructorParams);
652                        assert itemConstructor != null : "itemConstructor == null";
653                        I item = itemConstructor.newInstance(constructorParams);
654                        item.setDbControl(this);
655                        return item;
656                }
657                catch (Exception ex)
658                {
659                        throw new BaseException(ex);
660                }
661        }
662
663
664        /**
665         * Load an item from the database when you now the id. If the item has
666         * already been loaded the item in the cache will be used.
667         * 
668         * @param itemClass The <code>Class</code> of the item object
669         * @param id The id of the object
670         * @return An object of the <code>itemClass</code> class
671         * @throws PermissionDeniedException If the logged in user doesn't have read
672         *         permission
673         * @throws BaseException If there is another error
674         */
675        <I extends BasicItem> I loadItem(Class<I> itemClass, int id)
676                        throws PermissionDeniedException, BaseException
677        {
678                if (isClosed())
679                        throw new ConnectionClosedException();
680                sc.updateLastAccess();
681                // Find out which data class to use for the specified item class
682                Class<? extends BasicData> dataClass = Item.fromItemClass(itemClass)
683                        .getDataClass();
684                BasicData data = HibernateUtil.loadData(hSession, dataClass, id);
685                return getItem(itemClass, data);
686        }
687
688
689        /**
690         * Get an item object for a known data object, using the cache if possible.
691         * If the item already exists in the cache that object is returned,
692         * otherwise a new item object is created assuming that a constructor that
693         * takes a single data object as parameter exists.
694         * 
695         * @param itemClass The <code>Class</code> of the item object
696         * @param data The data object, or null
697         * @return An object of the <code>itemClass</code> class, or null if null
698         *         was passed
699         * @throws PermissionDeniedException If the logged in user doesn't have read
700         *         permission
701         * @throws ItemNotFoundException If the data object is a Hibernate proxy
702         *         which points to a non-existing row in the database
703         * @throws BaseException If there is another error
704         */
705        @SuppressWarnings(
706                {
707                        "unchecked"
708                })
709        <I extends BasicItem> I getItem(Class<I> itemClass, BasicData data,
710                        Object... extraParameters)
711                        throws PermissionDeniedException, ItemNotFoundException,
712                        BaseException
713        {
714                if (isClosed())
715                        throw new ConnectionClosedException();
716                sc.updateLastAccess();
717                if (data == null)
718                        return null;
719                I item = null;
720                try
721                {
722                        Class<? extends BasicData> realDataClass = null;
723                        // The 'data' object may be a proxy, we need the real data class
724                        if (data instanceof org.hibernate.proxy.HibernateProxy)
725                        {
726                                // We are dealing with a proxy instead of the real data class
727                                // To get the real instance we...
728                                // 1. Find out the real data-layer class
729                                realDataClass = org.hibernate.proxy.HibernateProxyHelper
730                                        .getClassWithoutInitializingProxy(data);
731                                // 2. Initialise the proxy using the stateful session (the proxy
732                                // may come from the stateless session)
733                                int id = data.getId();
734                                org.hibernate.proxy.HibernateProxy hp = (org.hibernate.proxy.HibernateProxy) data;
735                                org.hibernate.proxy.LazyInitializer li = hp
736                                        .getHibernateLazyInitializer();
737                                if (li.isUninitialized())
738                                {
739                                        li.setSession((org.hibernate.impl.SessionImpl) hSession);
740                                }
741                                // 3. Get the real instance from the proxy, but it may have
742                                // pointed to a non-existing row in the database
743                                try
744                                {
745                                        data = (BasicData) li.getImplementation();
746                                }
747                                catch (org.hibernate.ObjectNotFoundException ex)
748                                {
749                                        throw new ItemNotFoundException(HibernateUtil
750                                                .getShortEntityName(ex.getEntityName()) + "[id=" + ex
751                                                .getIdentifier() + "]");
752                                }
753                        }
754                        else
755                        {
756                                realDataClass = data.getClass();
757                        }
758                        // Check if the item is in the cache.
759                        item = itemClass.cast(itemCache.get(data));
760                        if (item == null)
761                        {
762                                // The actual item class may be a subclass of the one specified
763                                // We use the real data class to find out which real item class
764                                // to create
765                                Item itemType = Item.fromDataClass(realDataClass);
766                                Class<? extends I> realItemClass = (Class<? extends I>) itemType
767                                        .getItemClass();
768                                // The constructor always take a BasicData as first parameter
769                                // but may take additional parameters passed in the
770                                // extraParameters array
771                                Object[] constructorParams;
772                                Constructor<? extends I> c;
773                                if (extraParameters != null && extraParameters.length > 0)
774                                {
775                                        constructorParams = new Object[1 + extraParameters.length];
776                                        for (int i = 0; i < extraParameters.length; ++i)
777                                        {
778                                                constructorParams[i + 1] = extraParameters[i];
779                                        }
780                                        c = ClassUtil.findConstructor(realItemClass,
781                                                constructorParams);
782                                }
783                                else
784                                {
785                                        constructorParams = new Object[1];
786                                        c = (Constructor<? extends I>) itemType.getConstructor();
787                                }
788                                constructorParams[0] = data;
789                                try
790                                {
791                                        item = c.newInstance(constructorParams);
792                                }
793                                catch (Exception ex)
794                                {
795                                        throw new BaseException(ex);
796                                }
797                        }
798                }
799                catch (org.hibernate.HibernateException hex)
800                {
801                        throw new BaseException(hex);
802                }
803                // Initialise the new item and check permissions
804                item.setDbControl(this);
805                item.initPermissions(0, 0);
806                item.checkPermission(Permission.READ);
807                itemCache.put(data, item);
808                if (item instanceof Controlled)
809                {
810                        commitQueue.put(item, Transactional.Action.UPDATE);
811                }
812                return item;
813        }
814
815
816        /**
817         * Schedule a new item to be saved in the database. The item is saved the
818         * next time the {@link #commit()} method is called.
819         * 
820         * @param item The item to be saved
821         * @throws PermissionDeniedException If the logged in user doesn't have
822         *         create permission
823         * @throws ItemAlreadyExistsException If the item already exists in the
824         *         database
825         * @throws BaseException If there is another error
826         */
827        public void saveItem(BasicItem item)
828                        throws PermissionDeniedException, ItemAlreadyExistsException,
829                        BaseException
830        {
831                if (isClosed())
832                        throw new ConnectionClosedException();
833                sc.updateLastAccess();
834                if (item.isInDatabase())
835                {
836                        throw new ItemAlreadyExistsException(item.toString());
837                }
838                item.setDbControl(this);
839                item.initPermissions(0, 0);
840                item.checkPermission(Permission.CREATE);
841                commitQueue.put(item, Transactional.Action.CREATE);
842        }
843
844
845        /**
846         * Schedule an existing item to be deleted from the database. The item is
847         * deleted the next time the {@link #commit()} method is called.
848         * 
849         * @param item The item to be deleted
850         * @throws PermissionDeniedException If the logged in user doesn't have
851         *         delete permission
852         * @throws ItemNotFoundException If the item doesn't exist in the database
853         * @throws BaseException If there is another error
854         */
855        public void deleteItem(BasicItem item)
856                        throws PermissionDeniedException, ItemNotFoundException,
857                        BaseException
858        {
859                if (isClosed())
860                        throw new ConnectionClosedException();
861                sc.updateLastAccess();
862                if (!item.isInDatabase())
863                {
864                        throw new ItemNotFoundException(item.toString());
865                }
866                item.setDbControl(this);
867                item.initPermissions(0, 0);
868                item.checkPermission(Permission.DELETE);
869                commitQueue.put(item, Transactional.Action.DELETE);
870        }
871
872
873        /**
874         * Detach an item from this <code>DbControl</code>. The detached item
875         * will no longer be managed and changes will not be saved to the database.
876         * For some items, certain operations are forbidden for detached items. For
877         * example you are not allowed to upload files to {@link File} items.
878         * 
879         * @param item The item to be detached
880         * @throws BaseException If there is an error
881         * @see #reattachItem(BasicItem)
882         */
883        public void detachItem(BasicItem item)
884                        throws BaseException
885        {
886                if (isClosed())
887                        throw new ConnectionClosedException();
888                sc.updateLastAccess();
889                HibernateUtil.evictData(hSession, item.getData());
890                commitQueue.remove(item);
891                itemCache.remove(item.getData());
892                item.setDbControl(null);
893        }
894
895
896        /**
897         * Reattach a detached item from this <code>DbControl</code>. If the
898         * reattached already exists in the database it will be managed and changes
899         * will be saved to the database.
900         * 
901         * @param item The item to be reattached
902         * @throws PermissionDeniedException If the logged in user doesn't have read
903         *         permission
904         * @throws BaseException If there is another error
905         * @see #detachItem(BasicItem)
906         */
907        public void reattachItem(BasicItem item)
908                        throws PermissionDeniedException, ItemNotFoundException,
909                        BaseException
910        {
911                if (isClosed())
912                        throw new ConnectionClosedException();
913                sc.updateLastAccess();
914                item.setDbControl(this);
915                if (item.isInDatabase())
916                {
917                        item.initPermissions(0, 0);
918                        if (item.hasPermission(Permission.RESTRICTED_WRITE))
919                        {
920                                HibernateUtil.updateData(hSession, item.getData());
921                        }
922                        else if (item.hasPermission(Permission.READ))
923                        {
924                                HibernateUtil.lockData(hSession, item.getData(),
925                                        org.hibernate.LockOptions.NONE);
926                        }
927                        else
928                        {
929                                throw new PermissionDeniedException(Permission.READ, item
930                                        .toString());
931                        }
932                        itemCache.put(item.getData(), item);
933                        if (item instanceof Controlled)
934                        {
935                                commitQueue.put(item, Transactional.Action.UPDATE);
936                        }
937                }
938        }
939
940
941        /**
942         * Check if an item is attached to this <code>DbControl</code>. An item
943         * is attached if it is found in either the item cache or commit queue.
944         */
945        boolean isAttached(BasicItem item)
946        {
947                assert item != null : "item == null";
948                return itemCache.containsKey(item.getData()) || commitQueue
949                        .containsKey(item);
950        }
951
952
953        /**
954         * Initialise a collection on an item. This method is useful if you detach
955         * an item but needs to use a collection while it is detached. Since
956         * collections are not loaded until they are needed, you will get an
957         * exception if you try to access an unitialised collection of a detached
958         * item. One example is if you have detach a {@link Quota} object and calls
959         * the {@link Quota#getQuotaValue(QuotaType, Location)} method.
960         * 
961         * @param item The item
962         * @param collectionName The name of the collection
963         * @throws BaseException If there is an error
964         * @see #detachItem(BasicItem)
965         * @see #reattachItem(BasicItem)
966         */
967        public void initCollection(BasicItem item, String collectionName)
968                        throws BaseException
969        {
970                if (isClosed())
971                        throw new ConnectionClosedException();
972                sc.updateLastAccess();
973                HibernateUtil.initCollection(hSession, item.getData(), collectionName);
974        }
975
976
977        /**
978         * Add a <code>Batcher</code> to the batcher queue.
979         * 
980         * @param batcher The <code>Batcher</code> to add
981         */
982        void addBatcher(Batcher batcher)
983        {
984                if (isClosed())
985                        throw new ConnectionClosedException();
986                if (batchers == null)
987                {
988                        batchers = new ArrayList<Batcher>();
989                }
990                assert batcher != null : "batcher is null";
991                batchers.add(batcher);
992        }
993
994
995        /**
996         * Remove a <code>Batcher</code> from the batcher queue.
997         * 
998         * @param batcher The <code>Batcher</code> to remove
999         */
1000        void removeBatcher(Batcher batcher)
1001        {
1002                if (batcher != null)
1003                {
1004                        batchers.remove(batcher);
1005                }
1006        }
1007
1008
1009        /**
1010         * Clean up if a bad client application forgot to close the connection.
1011         */
1012        @Override
1013        protected void finalize()
1014                        throws Throwable
1015        {
1016                if (!isClosed())
1017                {
1018                        SessionControl sc = getSessionControl();
1019                        log
1020                                .warn("Unclosed DbControl during finalize" + "; isLoggedIn = " + sc
1021                                        .isLoggedIn() + "; clientId = " + sc.getClientId() + "; userId = " + sc
1022                                        .getLoggedInUserId());
1023                }
1024                close();
1025                super.finalize();
1026        }
1027}