This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 83804338aa0ba1ff67f2dc8d88b25b8349e76b3d Author: Maksim Meliashchuk <[email protected]> AuthorDate: Sun Sep 10 23:59:15 2023 +0300 JAMES-2156 Add configuration option to enable attachment storage for JPA --- mailbox/jpa/pom.xml | 23 +++ .../jpa/JPAMailboxSessionMapperFactory.java | 10 +- .../james/mailbox/jpa/mail/JPAMessageMapper.java | 27 +++- .../model/openjpa/AbstractJPAMailboxMessage.java | 46 +++--- .../JPAMailboxMessageWithAttachmentStorage.java | 155 +++++++++++++++++++++ .../james/mailbox/jpa/JPAMailboxFixture.java | 4 +- .../mailbox/jpa/JPASubscriptionManagerTest.java | 10 +- .../mailbox/jpa/JpaMailboxManagerProvider.java | 10 +- .../mailbox/jpa/mail/JPAAttachmentMapperTest.java | 5 +- .../james/mailbox/jpa/mail/JPAMapperProvider.java | 10 +- .../mail/JPAMessageWithAttachmentMapperTest.java | 2 +- .../task/JPARecomputeCurrentQuotasServiceTest.java | 10 +- .../james-database-mariadb.properties | 5 + .../sample-configuration/james-database.properties | 5 + .../james-database-mariadb.properties | 5 + .../sample-configuration/james-database.properties | 5 + .../src/main/resources/james-database.properties | 6 + .../james/modules/data/JPAConfiguration.java | 30 +++- .../james/modules/data/JPAEntityManagerModule.java | 14 +- .../james/modules/data/JPAConfigurationTest.java | 4 + src/site/xdoc/server/config-system.xml | 7 + 21 files changed, 346 insertions(+), 47 deletions(-) diff --git a/mailbox/jpa/pom.xml b/mailbox/jpa/pom.xml index b3020ba999..558c5ca266 100644 --- a/mailbox/jpa/pom.xml +++ b/mailbox/jpa/pom.xml @@ -31,6 +31,18 @@ <packaging>jar</packaging> <name>Apache James :: Mailbox :: JPA</name> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-guice</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + <dependencies> <dependency> <groupId>${james.groupId}</groupId> @@ -89,6 +101,17 @@ <artifactId>james-server-data-jpa</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-jpa-common-guice</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-jpa-common-guice</artifactId> + <scope>compile</scope> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>james-server-testing</artifactId> diff --git a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java index 935623ef6d..96612081f1 100644 --- a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java +++ b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/JPAMailboxSessionMapperFactory.java @@ -42,6 +42,7 @@ import org.apache.james.mailbox.store.mail.MessageMapper; import org.apache.james.mailbox.store.mail.ModSeqProvider; import org.apache.james.mailbox.store.mail.UidProvider; import org.apache.james.mailbox.store.user.SubscriptionMapper; +import org.apache.james.modules.data.JPAConfiguration; /** * JPA implementation of {@link MailboxSessionMapperFactory} @@ -53,16 +54,19 @@ public class JPAMailboxSessionMapperFactory extends MailboxSessionMapperFactory private final JPAUidProvider uidProvider; private final JPAModSeqProvider modSeqProvider; private final AttachmentMapper attachmentMapper; + private final JPAConfiguration jpaConfiguration; @Inject - public JPAMailboxSessionMapperFactory(EntityManagerFactory entityManagerFactory, JPAUidProvider uidProvider, JPAModSeqProvider modSeqProvider) { + public JPAMailboxSessionMapperFactory(EntityManagerFactory entityManagerFactory, JPAUidProvider uidProvider, + JPAModSeqProvider modSeqProvider, JPAConfiguration jpaConfiguration) { this.entityManagerFactory = entityManagerFactory; this.uidProvider = uidProvider; this.modSeqProvider = modSeqProvider; EntityManagerUtils.safelyClose(createEntityManager()); this.attachmentMapper = new JPAAttachmentMapper(entityManagerFactory); + this.jpaConfiguration = jpaConfiguration; } - + @Override public MailboxMapper createMailboxMapper(MailboxSession session) { return new JPAMailboxMapper(entityManagerFactory); @@ -70,7 +74,7 @@ public class JPAMailboxSessionMapperFactory extends MailboxSessionMapperFactory @Override public MessageMapper createMessageMapper(MailboxSession session) { - return new JPAMessageMapper(uidProvider, modSeqProvider, entityManagerFactory); + return new JPAMessageMapper(uidProvider, modSeqProvider, entityManagerFactory, jpaConfiguration); } @Override diff --git a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/JPAMessageMapper.java b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/JPAMessageMapper.java index 28cc9f207e..a0c0ef3a4b 100644 --- a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/JPAMessageMapper.java +++ b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/JPAMessageMapper.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -42,6 +43,7 @@ import org.apache.james.mailbox.jpa.mail.model.JPAMailbox; import org.apache.james.mailbox.jpa.mail.model.openjpa.AbstractJPAMailboxMessage; import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAEncryptedMailboxMessage; import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAMailboxMessage; +import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAMailboxMessageWithAttachmentStorage; import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAStreamingMailboxMessage; import org.apache.james.mailbox.model.Mailbox; import org.apache.james.mailbox.model.MailboxCounters; @@ -54,6 +56,7 @@ import org.apache.james.mailbox.model.UpdatedFlags; import org.apache.james.mailbox.store.FlagsUpdateCalculator; import org.apache.james.mailbox.store.mail.MessageMapper; import org.apache.james.mailbox.store.mail.model.MailboxMessage; +import org.apache.james.modules.data.JPAConfiguration; import org.apache.openjpa.persistence.ArgumentException; import com.github.fge.lambdas.Throwing; @@ -73,12 +76,15 @@ public class JPAMessageMapper extends JPATransactionalMapper implements MessageM private final MessageUtils messageMetadataMapper; private final JPAUidProvider uidProvider; private final JPAModSeqProvider modSeqProvider; + private final JPAConfiguration jpaConfiguration; - public JPAMessageMapper(JPAUidProvider uidProvider, JPAModSeqProvider modSeqProvider, EntityManagerFactory entityManagerFactory) { + public JPAMessageMapper(JPAUidProvider uidProvider, JPAModSeqProvider modSeqProvider, EntityManagerFactory entityManagerFactory, + JPAConfiguration jpaConfiguration) { super(entityManagerFactory); this.messageMetadataMapper = new MessageUtils(uidProvider, modSeqProvider); this.uidProvider = uidProvider; this.modSeqProvider = modSeqProvider; + this.jpaConfiguration = jpaConfiguration; } @Override @@ -361,6 +367,8 @@ public class JPAMessageMapper extends JPATransactionalMapper implements MessageM copy = new JPAStreamingMailboxMessage(currentMailbox, uid, modSeq, original); } else if (original instanceof JPAEncryptedMailboxMessage) { copy = new JPAEncryptedMailboxMessage(currentMailbox, uid, modSeq, original); + } else if (original instanceof JPAMailboxMessageWithAttachmentStorage) { + copy = new JPAMailboxMessageWithAttachmentStorage(currentMailbox, uid, modSeq, original); } else { copy = new JPAMailboxMessage(currentMailbox, uid, modSeq, original); } @@ -375,13 +383,19 @@ public class JPAMessageMapper extends JPATransactionalMapper implements MessageM // org.apache.openjpa.persistence.ArgumentException. JPAId mailboxId = (JPAId) mailbox.getMailboxId(); JPAMailbox currentMailbox = getEntityManager().find(JPAMailbox.class, mailboxId.getRawId()); + + boolean isAttachmentStorage = false; + if (Objects.nonNull(jpaConfiguration)) { + isAttachmentStorage = jpaConfiguration.isAttachmentStorageEnabled().orElse(false); + } + if (message instanceof AbstractJPAMailboxMessage) { ((AbstractJPAMailboxMessage) message).setMailbox(currentMailbox); getEntityManager().persist(message); return message.metaData(); - } else { - JPAMailboxMessage persistData = new JPAMailboxMessage(currentMailbox, message.getUid(), message.getModSeq(), message); + } else if (isAttachmentStorage) { + JPAMailboxMessageWithAttachmentStorage persistData = new JPAMailboxMessageWithAttachmentStorage(currentMailbox, message.getUid(), message.getModSeq(), message); persistData.setFlags(message.createFlags()); if (message.getAttachments().isEmpty()) { @@ -399,6 +413,11 @@ public class JPAMessageMapper extends JPATransactionalMapper implements MessageM } } return persistData.metaData(); + } else { + JPAMailboxMessage persistData = new JPAMailboxMessage(currentMailbox, message.getUid(), message.getModSeq(), message); + persistData.setFlags(message.createFlags()); + getEntityManager().persist(persistData); + return persistData.metaData(); } } catch (PersistenceException | ArgumentException e) { @@ -468,7 +487,7 @@ public class JPAMessageMapper extends JPATransactionalMapper implements MessageM private List<MessageUid> getUidList(List<MailboxMessage> messages) { return messages.stream() - .map(message -> message.getUid()) + .map(MailboxMessage::getUid) .collect(ImmutableList.toImmutableList()); } diff --git a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/model/openjpa/AbstractJPAMailboxMessage.java b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/model/openjpa/AbstractJPAMailboxMessage.java index 73fa322478..8b9f1fe0a9 100644 --- a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/model/openjpa/AbstractJPAMailboxMessage.java +++ b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/model/openjpa/AbstractJPAMailboxMessage.java @@ -26,7 +26,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.concurrent.atomic.AtomicInteger; import javax.mail.Flags; import javax.persistence.Basic; @@ -46,26 +46,29 @@ import org.apache.james.mailbox.MessageUid; import org.apache.james.mailbox.ModSeq; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.jpa.JPAId; -import org.apache.james.mailbox.jpa.mail.model.JPAAttachment; import org.apache.james.mailbox.jpa.mail.model.JPAMailbox; import org.apache.james.mailbox.jpa.mail.model.JPAProperty; import org.apache.james.mailbox.jpa.mail.model.JPAUserFlag; +import org.apache.james.mailbox.model.AttachmentId; import org.apache.james.mailbox.model.ComposedMessageId; import org.apache.james.mailbox.model.ComposedMessageIdWithMetaData; import org.apache.james.mailbox.model.MessageAttachmentMetadata; import org.apache.james.mailbox.model.MessageId; +import org.apache.james.mailbox.model.ParsedAttachment; import org.apache.james.mailbox.model.ThreadId; import org.apache.james.mailbox.store.mail.model.DefaultMessageId; import org.apache.james.mailbox.store.mail.model.DelegatingMailboxMessage; import org.apache.james.mailbox.store.mail.model.FlagsFactory; import org.apache.james.mailbox.store.mail.model.MailboxMessage; import org.apache.james.mailbox.store.mail.model.Property; +import org.apache.james.mailbox.store.mail.model.impl.MessageParser; import org.apache.james.mailbox.store.mail.model.impl.Properties; import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder; import org.apache.openjpa.persistence.jdbc.ElementJoinColumn; import org.apache.openjpa.persistence.jdbc.ElementJoinColumns; import org.apache.openjpa.persistence.jdbc.Index; +import com.github.fge.lambdas.Throwing; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; @@ -274,14 +277,6 @@ public abstract class AbstractJPAMailboxMessage implements MailboxMessage { @ElementJoinColumn(name = "MAIL_UID", referencedColumnName = "MAIL_UID")}) private List<JPAUserFlag> userFlags; - /** - * Metadata for attachments - */ - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) - @OrderBy("attachmentId") - @ElementJoinColumns({@ElementJoinColumn(name = "MAILBOX_ID", referencedColumnName = "MAILBOX_ID"), - @ElementJoinColumn(name = "MAIL_UID", referencedColumnName = "MAIL_UID")}) - private List<JPAAttachment> attachments; protected AbstractJPAMailboxMessage() { } @@ -291,7 +286,6 @@ public abstract class AbstractJPAMailboxMessage implements MailboxMessage { this.mailbox = mailbox; this.internalDate = internalDate; userFlags = new ArrayList<>(); - attachments = new ArrayList<>(); setFlags(flags); this.contentOctets = contentOctets; @@ -344,7 +338,6 @@ public abstract class AbstractJPAMailboxMessage implements MailboxMessage { for (Property property : properties) { this.properties.add(new JPAProperty(property, order++)); } - this.attachments = new ArrayList<>(); } @Override @@ -561,17 +554,26 @@ public abstract class AbstractJPAMailboxMessage implements MailboxMessage { + " )"; } - /** - * Utility attachments' setter. - */ - public void setAttachments(List<JPAAttachment> attachments) { - this.attachments = attachments; - } - @Override public List<MessageAttachmentMetadata> getAttachments() { - return this.attachments.stream() - .map(JPAAttachment::toMessageAttachmentMetadata) - .collect(Collectors.toList()); + try { + AtomicInteger counter = new AtomicInteger(0); + MessageParser.ParsingResult parsingResult = new MessageParser().retrieveAttachments(getFullContent()); + ImmutableList<MessageAttachmentMetadata> result = parsingResult + .getAttachments() + .stream() + .map(Throwing.<ParsedAttachment, MessageAttachmentMetadata>function( + attachmentMetadata -> attachmentMetadata.asMessageAttachment(generateFixedAttachmentId(counter.incrementAndGet()), getMessageId())) + .sneakyThrow()) + .collect(ImmutableList.toImmutableList()); + parsingResult.dispose(); + return result; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private AttachmentId generateFixedAttachmentId(int position) { + return AttachmentId.from(getMailboxId().serialize() + "-" + getUid().asLong() + "-" + position); } } diff --git a/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/model/openjpa/JPAMailboxMessageWithAttachmentStorage.java b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/model/openjpa/JPAMailboxMessageWithAttachmentStorage.java new file mode 100644 index 0000000000..2e4e1a969e --- /dev/null +++ b/mailbox/jpa/src/main/java/org/apache/james/mailbox/jpa/mail/model/openjpa/JPAMailboxMessageWithAttachmentStorage.java @@ -0,0 +1,155 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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. * + ****************************************************************/ +package org.apache.james.mailbox.jpa.mail.model.openjpa; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import javax.mail.Flags; +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Lob; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BoundedInputStream; +import org.apache.james.mailbox.MessageUid; +import org.apache.james.mailbox.ModSeq; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.jpa.mail.model.JPAAttachment; +import org.apache.james.mailbox.jpa.mail.model.JPAMailbox; +import org.apache.james.mailbox.model.Content; +import org.apache.james.mailbox.model.Mailbox; +import org.apache.james.mailbox.model.MessageAttachmentMetadata; +import org.apache.james.mailbox.store.mail.model.MailboxMessage; +import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder; +import org.apache.openjpa.persistence.jdbc.ElementJoinColumn; +import org.apache.openjpa.persistence.jdbc.ElementJoinColumns; + +@Entity(name = "MailboxMessage") +@Table(name = "JAMES_MAIL") +public class JPAMailboxMessageWithAttachmentStorage extends AbstractJPAMailboxMessage { + + private static final byte[] EMPTY_ARRAY = new byte[] {}; + + /** The value for the body field. Lazy loaded */ + /** We use a max length to represent 1gb data. Thats prolly overkill, but who knows */ + @Basic(optional = false, fetch = FetchType.LAZY) + @Column(name = "MAIL_BYTES", length = 1048576000, nullable = false) + @Lob + private byte[] body; + + /** The value for the header field. Lazy loaded */ + /** We use a max length to represent 10mb data. Thats prolly overkill, but who knows */ + @Basic(optional = false, fetch = FetchType.LAZY) + @Column(name = "HEADER_BYTES", length = 10485760, nullable = false) + @Lob private byte[] header; + + /** + * Metadata for attachments + */ + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @OrderBy("attachmentId") + @ElementJoinColumns({@ElementJoinColumn(name = "MAILBOX_ID", referencedColumnName = "MAILBOX_ID"), + @ElementJoinColumn(name = "MAIL_UID", referencedColumnName = "MAIL_UID")}) + private List<JPAAttachment> attachments; + + + public JPAMailboxMessageWithAttachmentStorage() { + + } + + public JPAMailboxMessageWithAttachmentStorage(JPAMailbox mailbox, Date internalDate, int size, Flags flags, Content content, int bodyStartOctet, PropertyBuilder propertyBuilder) throws MailboxException { + super(mailbox, internalDate, flags, size, bodyStartOctet, propertyBuilder); + try { + int headerEnd = bodyStartOctet; + if (headerEnd < 0) { + headerEnd = 0; + } + InputStream stream = content.getInputStream(); + this.header = IOUtils.toByteArray(new BoundedInputStream(stream, headerEnd)); + this.body = IOUtils.toByteArray(stream); + + } catch (IOException e) { + throw new MailboxException("Unable to parse message",e); + } + attachments = new ArrayList<>(); + } + + /** + * Create a copy of the given message + */ + public JPAMailboxMessageWithAttachmentStorage(JPAMailbox mailbox, MessageUid uid, ModSeq modSeq, MailboxMessage message) throws MailboxException { + super(mailbox, uid, modSeq, message); + try { + this.body = IOUtils.toByteArray(message.getBodyContent()); + this.header = IOUtils.toByteArray(message.getHeaderContent()); + } catch (IOException e) { + throw new MailboxException("Unable to parse message",e); + } + attachments = new ArrayList<>(); + + } + + @Override + public InputStream getBodyContent() throws IOException { + if (body == null) { + return new ByteArrayInputStream(EMPTY_ARRAY); + } + return new ByteArrayInputStream(body); + } + + @Override + public InputStream getHeaderContent() throws IOException { + if (header == null) { + return new ByteArrayInputStream(EMPTY_ARRAY); + } + return new ByteArrayInputStream(header); + } + + @Override + public MailboxMessage copy(Mailbox mailbox) throws MailboxException { + return new JPAMailboxMessage(JPAMailbox.from(mailbox), getUid(), getModSeq(), this); + } + + /** + * Utility attachments' setter. + */ + public void setAttachments(List<JPAAttachment> attachments) { + this.attachments = attachments; + } + + @Override + public List<MessageAttachmentMetadata> getAttachments() { + + return this.attachments.stream() + .map(JPAAttachment::toMessageAttachmentMetadata) + .collect(Collectors.toList()); + } +} diff --git a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPAMailboxFixture.java b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPAMailboxFixture.java index 6dc6d62d5b..25a96d93ca 100644 --- a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPAMailboxFixture.java +++ b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPAMailboxFixture.java @@ -28,6 +28,7 @@ import org.apache.james.mailbox.jpa.mail.model.JPAProperty; import org.apache.james.mailbox.jpa.mail.model.JPAUserFlag; import org.apache.james.mailbox.jpa.mail.model.openjpa.AbstractJPAMailboxMessage; import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAMailboxMessage; +import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAMailboxMessageWithAttachmentStorage; import org.apache.james.mailbox.jpa.quota.model.JpaCurrentQuota; import org.apache.james.mailbox.jpa.quota.model.MaxDomainMessageCount; import org.apache.james.mailbox.jpa.quota.model.MaxDomainStorage; @@ -49,7 +50,8 @@ public interface JPAMailboxFixture { JPAUserFlag.class, JPAMailboxAnnotation.class, JPASubscription.class, - JPAAttachment.class + JPAAttachment.class, + JPAMailboxMessageWithAttachmentStorage.class ); List<Class<?>> QUOTA_PERSISTANCE_CLASSES = ImmutableList.of( diff --git a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPASubscriptionManagerTest.java b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPASubscriptionManagerTest.java index de8112a3e4..6f4510e852 100644 --- a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPASubscriptionManagerTest.java +++ b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JPASubscriptionManagerTest.java @@ -31,6 +31,8 @@ import org.apache.james.mailbox.jpa.mail.JPAModSeqProvider; import org.apache.james.mailbox.jpa.mail.JPAUidProvider; import org.apache.james.mailbox.store.StoreSubscriptionManager; import org.apache.james.metrics.tests.RecordingMetricFactory; +import org.apache.james.modules.data.JPAConfiguration; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -49,9 +51,15 @@ class JPASubscriptionManagerTest implements SubscriptionManagerContract { void setUp() { EntityManagerFactory entityManagerFactory = JPA_TEST_CLUSTER.getEntityManagerFactory(); + JPAConfiguration jpaConfiguration = JPAConfiguration.builder() + .driverName("driverName") + .driverURL("driverUrl") + .build(); + JPAMailboxSessionMapperFactory mapperFactory = new JPAMailboxSessionMapperFactory(entityManagerFactory, new JPAUidProvider(entityManagerFactory), - new JPAModSeqProvider(entityManagerFactory)); + new JPAModSeqProvider(entityManagerFactory), + jpaConfiguration); InVMEventBus eventBus = new InVMEventBus(new InVmEventDelivery(new RecordingMetricFactory()), EventBusTestFixture.RETRY_BACKOFF_CONFIGURATION, new MemoryEventDeadLetters()); subscriptionManager = new StoreSubscriptionManager(mapperFactory, mapperFactory, eventBus); } diff --git a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JpaMailboxManagerProvider.java b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JpaMailboxManagerProvider.java index b951986ab2..afe26b1119 100644 --- a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JpaMailboxManagerProvider.java +++ b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/JpaMailboxManagerProvider.java @@ -46,6 +46,7 @@ import org.apache.james.mailbox.store.quota.QuotaComponents; import org.apache.james.mailbox.store.search.MessageSearchIndex; import org.apache.james.mailbox.store.search.SimpleMessageSearchIndex; import org.apache.james.metrics.tests.RecordingMetricFactory; +import org.apache.james.modules.data.JPAConfiguration; import org.apache.james.utils.UpdatableTickingClock; public class JpaMailboxManagerProvider { @@ -55,7 +56,14 @@ public class JpaMailboxManagerProvider { public static OpenJPAMailboxManager provideMailboxManager(JpaTestCluster jpaTestCluster) { EntityManagerFactory entityManagerFactory = jpaTestCluster.getEntityManagerFactory(); - JPAMailboxSessionMapperFactory mf = new JPAMailboxSessionMapperFactory(entityManagerFactory, new JPAUidProvider(entityManagerFactory), new JPAModSeqProvider(entityManagerFactory)); + + JPAConfiguration jpaConfiguration = JPAConfiguration.builder() + .driverName("driverName") + .driverURL("driverUrl") + .attachmentStorage(true) + .build(); + + JPAMailboxSessionMapperFactory mf = new JPAMailboxSessionMapperFactory(entityManagerFactory, new JPAUidProvider(entityManagerFactory), new JPAModSeqProvider(entityManagerFactory), jpaConfiguration); MailboxACLResolver aclResolver = new UnionMailboxACLResolver(); MessageParser messageParser = new MessageParser(); diff --git a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAAttachmentMapperTest.java b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAAttachmentMapperTest.java index 552d50a9e9..d4dc4282a5 100644 --- a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAAttachmentMapperTest.java +++ b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAAttachmentMapperTest.java @@ -1,4 +1,4 @@ -/*************************************************************** +/************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * * or more contributor license agreements. See the NOTICE file * * distributed with this work for additional information * @@ -15,7 +15,8 @@ * KIND, either express or implied. See the License for the * * specific language governing permissions and limitations * * under the License. * - ***************************************************************/ + **************************************************************/ + package org.apache.james.mailbox.jpa.mail; diff --git a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMapperProvider.java b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMapperProvider.java index 58e735840f..9388bd60c1 100644 --- a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMapperProvider.java +++ b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMapperProvider.java @@ -39,6 +39,7 @@ import org.apache.james.mailbox.store.mail.MessageIdMapper; import org.apache.james.mailbox.store.mail.MessageMapper; import org.apache.james.mailbox.store.mail.model.DefaultMessageId; import org.apache.james.mailbox.store.mail.model.MapperProvider; +import org.apache.james.modules.data.JPAConfiguration; import com.google.common.collect.ImmutableList; @@ -59,9 +60,16 @@ public class JPAMapperProvider implements MapperProvider { public MessageMapper createMessageMapper() { EntityManagerFactory entityManagerFactory = jpaTestCluster.getEntityManagerFactory(); + JPAConfiguration jpaConfiguration = JPAConfiguration.builder() + .driverName("driverName") + .driverURL("driverUrl") + .attachmentStorage(true) + .build(); + JPAMessageMapper messageMapper = new JPAMessageMapper(new JPAUidProvider(entityManagerFactory), new JPAModSeqProvider(entityManagerFactory), - entityManagerFactory); + entityManagerFactory, + jpaConfiguration); return new TransactionalMessageMapper(messageMapper); } diff --git a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMessageWithAttachmentMapperTest.java b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMessageWithAttachmentMapperTest.java index a744d806bd..7383b55b71 100644 --- a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMessageWithAttachmentMapperTest.java +++ b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/JPAMessageWithAttachmentMapperTest.java @@ -1,4 +1,4 @@ -/*************************************************************** +/************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * * or more contributor license agreements. See the NOTICE file * * distributed with this work for additional information * diff --git a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/task/JPARecomputeCurrentQuotasServiceTest.java b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/task/JPARecomputeCurrentQuotasServiceTest.java index 8fcb1300bc..f044028ac3 100644 --- a/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/task/JPARecomputeCurrentQuotasServiceTest.java +++ b/mailbox/jpa/src/test/java/org/apache/james/mailbox/jpa/mail/task/JPARecomputeCurrentQuotasServiceTest.java @@ -41,6 +41,7 @@ import org.apache.james.mailbox.quota.task.RecomputeMailboxCurrentQuotasService; import org.apache.james.mailbox.store.StoreMailboxManager; import org.apache.james.mailbox.store.quota.CurrentQuotaCalculator; import org.apache.james.mailbox.store.quota.DefaultUserQuotaRootResolver; +import org.apache.james.modules.data.JPAConfiguration; import org.apache.james.user.api.UsersRepository; import org.apache.james.user.jpa.JPAUsersRepository; import org.apache.james.user.jpa.model.JPAUser; @@ -71,9 +72,16 @@ class JPARecomputeCurrentQuotasServiceTest implements RecomputeCurrentQuotasServ @BeforeEach void setUp() throws Exception { EntityManagerFactory entityManagerFactory = JPA_TEST_CLUSTER.getEntityManagerFactory(); + + JPAConfiguration jpaConfiguration = JPAConfiguration.builder() + .driverName("driverName") + .driverURL("driverUrl") + .build(); + JPAMailboxSessionMapperFactory mapperFactory = new JPAMailboxSessionMapperFactory(entityManagerFactory, new JPAUidProvider(entityManagerFactory), - new JPAModSeqProvider(entityManagerFactory)); + new JPAModSeqProvider(entityManagerFactory), + jpaConfiguration); usersRepository = new JPAUsersRepository(NO_DOMAIN_LIST); usersRepository.setEntityManagerFactory(JPA_TEST_CLUSTER.getEntityManagerFactory()); diff --git a/server/apps/jpa-app/sample-configuration/james-database-mariadb.properties b/server/apps/jpa-app/sample-configuration/james-database-mariadb.properties index cf091319a4..52ca423635 100644 --- a/server/apps/jpa-app/sample-configuration/james-database-mariadb.properties +++ b/server/apps/jpa-app/sample-configuration/james-database-mariadb.properties @@ -42,3 +42,8 @@ openjpa.streaming=false # datasource.validationQuery=select 1 # The maximum number of active connections that can be allocated from this pool at the same time, or negative for no limit. # datasource.maxTotal=8 + +# Attachment storage +# *WARNING*: Is not made to store large binary content +# Optional, Allowed values are: true, false, defaults to false +# attachmentStorage.enabled=false diff --git a/server/apps/jpa-app/sample-configuration/james-database.properties b/server/apps/jpa-app/sample-configuration/james-database.properties index d569d6d5d6..1336fe5a1c 100644 --- a/server/apps/jpa-app/sample-configuration/james-database.properties +++ b/server/apps/jpa-app/sample-configuration/james-database.properties @@ -46,3 +46,8 @@ openjpa.streaming=false # datasource.validationQuery=select 1 # The maximum number of active connections that can be allocated from this pool at the same time, or negative for no limit. # datasource.maxTotal=8 + +# Attachment storage +# *WARNING*: Is not made to store large binary content +# Optional, Allowed values are: true, false, defaults to false +# attachmentStorage.enabled=false diff --git a/server/apps/jpa-smtp-app/sample-configuration/james-database-mariadb.properties b/server/apps/jpa-smtp-app/sample-configuration/james-database-mariadb.properties index cf091319a4..52ca423635 100644 --- a/server/apps/jpa-smtp-app/sample-configuration/james-database-mariadb.properties +++ b/server/apps/jpa-smtp-app/sample-configuration/james-database-mariadb.properties @@ -42,3 +42,8 @@ openjpa.streaming=false # datasource.validationQuery=select 1 # The maximum number of active connections that can be allocated from this pool at the same time, or negative for no limit. # datasource.maxTotal=8 + +# Attachment storage +# *WARNING*: Is not made to store large binary content +# Optional, Allowed values are: true, false, defaults to false +# attachmentStorage.enabled=false diff --git a/server/apps/jpa-smtp-app/sample-configuration/james-database.properties b/server/apps/jpa-smtp-app/sample-configuration/james-database.properties index d569d6d5d6..1336fe5a1c 100644 --- a/server/apps/jpa-smtp-app/sample-configuration/james-database.properties +++ b/server/apps/jpa-smtp-app/sample-configuration/james-database.properties @@ -46,3 +46,8 @@ openjpa.streaming=false # datasource.validationQuery=select 1 # The maximum number of active connections that can be allocated from this pool at the same time, or negative for no limit. # datasource.maxTotal=8 + +# Attachment storage +# *WARNING*: Is not made to store large binary content +# Optional, Allowed values are: true, false, defaults to false +# attachmentStorage.enabled=false diff --git a/server/apps/spring-app/src/main/resources/james-database.properties b/server/apps/spring-app/src/main/resources/james-database.properties index 22770d00d2..5dc74806b1 100644 --- a/server/apps/spring-app/src/main/resources/james-database.properties +++ b/server/apps/spring-app/src/main/resources/james-database.properties @@ -44,3 +44,9 @@ openjpa.streaming=false # datasource.validationQueryTimeoutSec=2 # This is different per database. See https://stackoverflow.com/questions/10684244/dbcp-validationquery-for-different-databases#10684260 # datasource.validationQuery=select 1 + +# Attachment storage +# *WARNING*: Is not made to store large binary content +# Optional, Allowed values are: true, false, defaults to false +# attachmentStorage.enabled=false + diff --git a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAConfiguration.java b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAConfiguration.java index 62419c1bb2..07733020b1 100644 --- a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAConfiguration.java +++ b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAConfiguration.java @@ -21,6 +21,7 @@ package org.apache.james.modules.data; import static org.apache.james.modules.data.JPAConfiguration.Credential.NO_CREDENTIAL; import static org.apache.james.modules.data.JPAConfiguration.ReadyToBuild.CUSTOM_DATASOURCE_PROPERTIES; import static org.apache.james.modules.data.JPAConfiguration.ReadyToBuild.CUSTOM_OPENJPA_PROPERTIES; +import static org.apache.james.modules.data.JPAConfiguration.ReadyToBuild.NO_ATTACHMENT_STORAGE; import static org.apache.james.modules.data.JPAConfiguration.ReadyToBuild.NO_MAX_CONNECTIONS; import static org.apache.james.modules.data.JPAConfiguration.ReadyToBuild.NO_MULTITHREADED; import static org.apache.james.modules.data.JPAConfiguration.ReadyToBuild.NO_TEST_ON_BORROW; @@ -54,6 +55,8 @@ public class JPAConfiguration { static final String DATASOURCE_MAX_TOTAL = "datasource.maxTotal"; static List<String> DEFAULT_DATASOURCE_PROPERTIES = List.of(DATASOURCE_TEST_ON_BORROW, DATASOURCE_VALIDATION_QUERY_TIMEOUT_SEC, DATASOURCE_VALIDATION_QUERY, DATASOURCE_MAX_TOTAL); + static final String ATTACHMENT_STORAGE = "attachmentStorage.enabled"; + static { } @@ -107,6 +110,7 @@ public class JPAConfiguration { static final Optional<Integer> NO_MAX_CONNECTIONS = Optional.empty(); static final Map<String,String> CUSTOM_OPENJPA_PROPERTIES = Map.of(); static final Map<String,String> CUSTOM_DATASOURCE_PROPERTIES = Map.of(); + static final Optional<Boolean> NO_ATTACHMENT_STORAGE = Optional.empty(); private final String driverName; private final String driverURL; @@ -119,12 +123,14 @@ public class JPAConfiguration { private Optional<Integer> maxConnections; private Map<String,String> customDatasourceProperties; private Map<String,String> customOpenjpaProperties; + private Optional<Boolean> attachmentStorage; private ReadyToBuild(String driverName, String driverURL, Optional<Credential> credential, Optional<Boolean> testOnBorrow, Optional<Boolean> multithreaded, Optional<Integer> validationQueryTimeoutSec, Optional<String> validationQuery,Optional<Integer> maxConnections, - Map<String,String> customDatasourceProperties, Map<String,String> customOpenjpaProperties + Map<String,String> customDatasourceProperties, Map<String,String> customOpenjpaProperties, + Optional<Boolean> attachmentStorage ) { this.driverName = driverName; this.driverURL = driverURL; @@ -136,15 +142,16 @@ public class JPAConfiguration { this.maxConnections = maxConnections; this.customDatasourceProperties = customDatasourceProperties; this.customOpenjpaProperties = customOpenjpaProperties; + this.attachmentStorage = attachmentStorage; } public JPAConfiguration build() { - return new JPAConfiguration(driverName, driverURL, credential, testOnBorrow, multithreaded, validationQueryTimeoutSec, validationQuery, maxConnections, customDatasourceProperties, customOpenjpaProperties); + return new JPAConfiguration(driverName, driverURL, credential, testOnBorrow, multithreaded, validationQueryTimeoutSec, validationQuery, maxConnections, customDatasourceProperties, customOpenjpaProperties, attachmentStorage); } public RequirePassword username(String username) { return password -> new ReadyToBuild(driverName, driverURL, Credential.of(username, password), - testOnBorrow, multithreaded, validationQueryTimeoutSec, validationQuery, maxConnections, customDatasourceProperties, customOpenjpaProperties); + testOnBorrow, multithreaded, validationQueryTimeoutSec, validationQuery, maxConnections, customDatasourceProperties, customOpenjpaProperties, attachmentStorage); } public ReadyToBuild testOnBorrow(Boolean testOnBorrow) { @@ -172,6 +179,11 @@ public class JPAConfiguration { return this; } + public ReadyToBuild attachmentStorage(Boolean attachmentStorage) { + this.attachmentStorage = Optional.ofNullable(attachmentStorage); + return this; + } + public ReadyToBuild setCustomDatasourceProperties(Map<String, String> customDatasourceProperties) { this.customDatasourceProperties = new HashMap<>(customDatasourceProperties); DEFAULT_DATASOURCE_PROPERTIES.forEach(this.customDatasourceProperties::remove); @@ -193,7 +205,7 @@ public class JPAConfiguration { public static RequireDriverName builder() { return driverName -> driverURL -> new ReadyToBuild(driverName, driverURL, NO_CREDENTIAL, NO_TEST_ON_BORROW, NO_MULTITHREADED, - NO_VALIDATION_QUERY_TIMEOUT_SEC, NO_VALIDATION_QUERY, NO_MAX_CONNECTIONS, CUSTOM_DATASOURCE_PROPERTIES, CUSTOM_OPENJPA_PROPERTIES); + NO_VALIDATION_QUERY_TIMEOUT_SEC, NO_VALIDATION_QUERY, NO_MAX_CONNECTIONS, CUSTOM_DATASOURCE_PROPERTIES, CUSTOM_OPENJPA_PROPERTIES, NO_ATTACHMENT_STORAGE); } private final String driverName; @@ -206,11 +218,13 @@ public class JPAConfiguration { private final Optional<Integer> maxConnections; private Map<String,String> customDatasourceProperties; private Map<String,String> customOpenjpaProperties; + private final Optional<Boolean> attachmentStorage; @VisibleForTesting JPAConfiguration(String driverName, String driverURL, Optional<Credential> credential, Optional<Boolean> testOnBorrow, Optional<Boolean> multithreaded, - Optional<Integer> validationQueryTimeoutSec, Optional<String> validationQuery, Optional<Integer> maxConnections, Map<String,String> customDatasourceProperties, Map<String,String> customOpenjpaProperties) { + Optional<Integer> validationQueryTimeoutSec, Optional<String> validationQuery, Optional<Integer> maxConnections, Map<String,String> customDatasourceProperties, Map<String,String> customOpenjpaProperties, + Optional<Boolean> attachmentStorage) { Preconditions.checkNotNull(driverName, "driverName cannot be null"); Preconditions.checkNotNull(driverURL, "driverURL cannot be null"); validationQueryTimeoutSec.ifPresent(timeoutInSec -> @@ -228,6 +242,7 @@ public class JPAConfiguration { this.maxConnections = maxConnections; this.customDatasourceProperties = customDatasourceProperties; this.customOpenjpaProperties = customOpenjpaProperties; + this.attachmentStorage = attachmentStorage; } @@ -267,8 +282,11 @@ public class JPAConfiguration { return customDatasourceProperties; } - public Optional<Integer> getMaxConnections() { return maxConnections; } + + public Optional<Boolean> isAttachmentStorageEnabled() { + return attachmentStorage; + } } diff --git a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAEntityManagerModule.java b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAEntityManagerModule.java index efcc300c11..758e3aa87e 100644 --- a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAEntityManagerModule.java +++ b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPAEntityManagerModule.java @@ -65,9 +65,15 @@ public class JPAEntityManagerModule extends AbstractModule { properties.put(JPAConfiguration.JPA_CONNECTION_PROPERTIES, Joiner.on(",").join(connectionProperties)); properties.putAll(jpaConfiguration.getCustomOpenjpaProperties()); - if (jpaConfiguration.isMultithreaded().isPresent()) { - properties.put(JPAConfiguration.JPA_MULTITHREADED, jpaConfiguration.isMultithreaded().get().toString()); - } + jpaConfiguration.isMultithreaded() + .ifPresent(isMultiThread -> + properties.put(JPAConfiguration.JPA_MULTITHREADED, jpaConfiguration.isMultithreaded().toString()) + ); + + jpaConfiguration.isAttachmentStorageEnabled() + .ifPresent(isMultiThread -> + properties.put(JPAConfiguration.ATTACHMENT_STORAGE, jpaConfiguration.isAttachmentStorageEnabled().toString()) + ); return Persistence.createEntityManagerFactory("Global", properties); } @@ -80,7 +86,6 @@ public class JPAEntityManagerModule extends AbstractModule { Map<String, String> openjpaProperties = getKeysForPrefix(dataSource, "openjpa", false); Map<String, String> datasourceProperties = getKeysForPrefix(dataSource, "datasource", true); - return JPAConfiguration.builder() .driverName(dataSource.getString("database.driverClassName")) .driverURL(dataSource.getString("database.url")) @@ -93,6 +98,7 @@ public class JPAEntityManagerModule extends AbstractModule { .password(dataSource.getString("database.password")) .setCustomOpenjpaProperties(openjpaProperties) .setCustomDatasourceProperties(datasourceProperties) + .attachmentStorage(dataSource.getBoolean(JPAConfiguration.ATTACHMENT_STORAGE, false)) .build(); } diff --git a/server/container/guice/jpa-common/src/test/java/org/apache/james/modules/data/JPAConfigurationTest.java b/server/container/guice/jpa-common/src/test/java/org/apache/james/modules/data/JPAConfigurationTest.java index b81fd7fb52..6758163475 100644 --- a/server/container/guice/jpa-common/src/test/java/org/apache/james/modules/data/JPAConfigurationTest.java +++ b/server/container/guice/jpa-common/src/test/java/org/apache/james/modules/data/JPAConfigurationTest.java @@ -36,6 +36,7 @@ class JPAConfigurationTest { private static final String USER_NAME = "username"; private static final String PASSWORD = "password"; private static final String EMPTY_STRING = ""; + private static final boolean ATTACHMENT_STORAGE = true; @Test void buildShouldReturnCorrespondingProperties() { @@ -48,6 +49,7 @@ class JPAConfigurationTest { .username(USER_NAME) .password(PASSWORD) .maxConnections(MAX_CONNECTIONS) + .attachmentStorage(ATTACHMENT_STORAGE) .build(); SoftAssertions.assertSoftly(softly -> { @@ -61,6 +63,7 @@ class JPAConfigurationTest { softly.assertThat(credential.getUsername()).isEqualTo(USER_NAME); }); softly.assertThat(configuration.getMaxConnections()).contains(MAX_CONNECTIONS); + softly.assertThat(configuration.isAttachmentStorageEnabled()).contains(ATTACHMENT_STORAGE); }); } @@ -80,6 +83,7 @@ class JPAConfigurationTest { softly.assertThat(configuration.getValidationQueryTimeoutSec()).isEmpty(); softly.assertThat(configuration.getCredential()).isEmpty(); softly.assertThat(configuration.getMaxConnections()).isEmpty(); + softly.assertThat(configuration.isAttachmentStorageEnabled()).isEmpty(); }); } diff --git a/src/site/xdoc/server/config-system.xml b/src/site/xdoc/server/config-system.xml index 89a45c50c4..3e33f17c0f 100644 --- a/src/site/xdoc/server/config-system.xml +++ b/src/site/xdoc/server/config-system.xml @@ -107,6 +107,13 @@ t0.mailbox_uid_validity, t0.user_name FROM public.james_mailbox t0 WHERE (t0.mailbox_name LIKE ? ESCAPE '\\' AND t0.user_name = ? AND t0.mailbox_namespace = ?) [params=?, ?, ?]} [code=0, state=22025]"</code></p> + <p>Attachment storage configuration</p> + <dl> + <dt><strong>attachmentStorage.enabled</strong></dt> + <dd>(Guice only). The attachment storage configuration for JPA is used to enable the implementation of the attachment storage mechanism. + *WARNING*: this configuration is not designed to store large binary content, as it is not optimized for this purpose. + Optional, Allowed values are: `true` or `false`, defaults to `false`</dd> + </dl> </subsection> <subsection name="META-INF/persistence.xml"> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
