Jakiś czas temu, chciałem sprawić by zapis uploadowanych plików był procesem automatycznym. Czyli jak zwykle... zrób więcej, pisząc mniej kodu.. ahhh yeah. Udało się. Wszystko odbywa się za pomocą odpowiedniej encji, która przechowuje MultipartFile, oraz EventListener-ów w Hibernate. Definiujemy więc interfejs, który będzie identyfikował encje plikowe, musi być on dostępny na poziomie backingObject formularzy uploadujących pliki.

import java.io.IOException;
import java.io.InputStream;

import org.springframework.web.multipart.MultipartFile;

public interface FileResourceBean {
  InputStream getInputStream() throws IOException;

  MultipartFile getMultipartFile();

  void setMultipartFile(MultipartFile multipartfile);
}

Teraz skonfigurujemy eventListener, który będzie reagował na odpowiednie zdarzenia. Jak widać chcę także obsłużyć usuwanie encji z bazy, więc cała funkcjonalność działa w obie strony :)

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean" p:schemaUpdate="true" p:dataSource-ref="dataSource">
  <!-- ... -->
  <property name="eventListeners">
    <map>
      <entry key="post-insert" value-ref="fileStateSyncInterceptor" />
      <entry key="save-update" value-ref="fileStateSyncInterceptor" />
      <entry key="post-delete" value-ref="fileStateSyncInterceptor" />
    </map>
  </property>
</bean>

Pora na najważniejsze. Implementacja EventListener-a, obsługuje zdarzenia post-insert, save-update oraz post-delete. Korzysta z ResourceManager-a (który nie zostanie tutaj opisany). Problemem niewątpliwie bolesnym, jest brak obsługi transakcji plikowej.

package com.gigacube.storage.event;

import java.io.IOException;

import org.apache.log4j.Logger;
import org.hibernate.event.PostDeleteEvent;
import org.hibernate.event.PostDeleteEventListener;
import org.hibernate.event.PostInsertEvent;
import org.hibernate.event.PostInsertEventListener;
import org.hibernate.event.SaveOrUpdateEvent;
import org.hibernate.event.def.DefaultSaveOrUpdateEventListener;
import org.springframework.beans.factory.InitializingBean;

import com.gigacube.storage.IdentifiedFileResourceBean;
import com.gigacube.storage.manager.ResourceManagerFactory;
import com.gigacube.util.Assertions;

public class FileStateSyncInterceptor extends DefaultSaveOrUpdateEventListener implements
PostDeleteEventListener, PostInsertEventListener, InitializingBean {
 private static final Logger logger = Logger.getLogger(FileStateSyncInterceptor.class);
 private static final long serialVersionUID = -4354252351354429127 L;

 private ResourceManagerFactory resourceManagerFactory;

 public void afterPropertiesSet() throws Exception {
  Assertions.notNull(resourceManagerFactory, "resourceManagerFactory");
 }

 public void onPostDelete(PostDeleteEvent event) {
  if (IdentifiedFileResourceBean.class.isAssignableFrom(event.getEntity().getClass())) {
   if (logger.isDebugEnabled()) {
    logger.debug("onPostDelete " + event.getEntity().getClass() + " with id " + event.getId());
   }
   resourceManagerFactory.delete((IdentifiedFileResourceBean) event.getEntity());
  }
 }

 public void onPostInsert(PostInsertEvent event) {
  if (IdentifiedFileResourceBean.class.isAssignableFrom(event.getEntity().getClass())) {
   if (logger.isDebugEnabled()) {
    logger.debug("onPostInsert " + event.getEntity().getClass() + " with id " + event.getId());
   }
   try {
    resourceManagerFactory.save((IdentifiedFileResourceBean) event.getEntity());
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }

 @Override
 public void onSaveOrUpdate(SaveOrUpdateEvent event) {
  if (IdentifiedFileResourceBean.class.isAssignableFrom(event.getObject().getClass())) {
   resourceManagerFactory.assignExtraIdentifier((IdentifiedFileResourceBean) event.getObject());
  }
  super.onSaveOrUpdate(event);
 }

 public void setResourceManagerFactory(ResourceManagerFactory resourceManagerFactory) {
  this.resourceManagerFactory = resourceManagerFactory;
 }
}


Łatwo zauważyć, że gdyby zamiast zdarzenia PostInsert obsłużyć PreInsert, wówczas mógłbym wycofać transakcję na bazie gdyby nie powiódł się zapis pliku na dysku, jednak ze względu na mapowanie plików do nazw, często potrzebuje w nazwie pliku umieścić identyfikator encji, a jest on dostępny dopiero w zdarzeniu PostInsert :( Inna kwestia to usability takiego rozwiązania, bowiem cały kod nadaje się tylko do śmietnika, gdy zastosujemy inne podejście i zastosujemy rozwiązania klasy BigTable lub usługi dostępne w Amazon AWS.