diff --git a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java index 75d4530df..071d7fbc3 100644 --- a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java +++ b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java @@ -27,6 +27,7 @@ import jakarta.persistence.EntityTransaction; import jakarta.persistence.OptimisticLockException; import jakarta.persistence.Persistence; +import jakarta.persistence.PersistenceException; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -34,6 +35,7 @@ import java.net.URLClassLoader; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -58,6 +60,7 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrincipalSecrets; +import org.apache.polaris.core.exceptions.AlreadyExistsException; import org.apache.polaris.core.persistence.PolarisMetaStoreManagerImpl; import org.apache.polaris.core.persistence.PolarisMetaStoreSession; import org.apache.polaris.core.persistence.RetryOnConcurrencyException; @@ -261,6 +264,12 @@ public T runInTransaction( } finally { localSession.remove(); } + } catch (PersistenceException e) { + if (e.toString().toLowerCase(Locale.ROOT).contains("duplicate key")) { + throw new AlreadyExistsException("Duplicate key error when persisting entity", e); + } else { + throw e; + } } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/exceptions/AlreadyExistsException.java b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/AlreadyExistsException.java new file mode 100644 index 000000000..a941a549c --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/AlreadyExistsException.java @@ -0,0 +1,33 @@ +/* + * 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.polaris.core.exceptions; + +/** + * A {@link PolarisException} implementation for when Polaris is unable to create an entity that + * already exists. + */ +public class AlreadyExistsException extends PolarisException { + public AlreadyExistsException(String message) { + super(message); + } + + public AlreadyExistsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/exceptions/PolarisException.java b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/PolarisException.java new file mode 100644 index 000000000..96ef9cf7c --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/PolarisException.java @@ -0,0 +1,35 @@ +/* + * 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.polaris.core.exceptions; + +/** + * Base class for Polaris-specific runtime exceptions. + * + *

All custom exceptions in Polaris should extend this class to provide specific error details. + */ +public abstract class PolarisException extends RuntimeException { + + public PolarisException(String message) { + super(message); + } + + public PolarisException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/polaris-service/src/main/java/org/apache/polaris/service/PolarisApplication.java b/polaris-service/src/main/java/org/apache/polaris/service/PolarisApplication.java index e62e87125..c782c6a91 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/PolarisApplication.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/PolarisApplication.java @@ -100,6 +100,10 @@ import org.apache.polaris.service.context.CallContextResolver; import org.apache.polaris.service.context.PolarisCallContextCatalogFactory; import org.apache.polaris.service.context.RealmContextResolver; +import org.apache.polaris.service.exception.IcebergExceptionMapper; +import org.apache.polaris.service.exception.IcebergJerseyViolationExceptionMapper; +import org.apache.polaris.service.exception.IcebergJsonProcessingExceptionMapper; +import org.apache.polaris.service.exception.PolarisExceptionMapper; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.apache.polaris.service.ratelimiter.RateLimiterFilter; import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; @@ -277,6 +281,7 @@ public void run(PolarisApplicationConfig configuration, Environment environment) } environment.jersey().register(new IcebergRestOAuth2Api(oauth2Service)); environment.jersey().register(new IcebergExceptionMapper()); + environment.jersey().register(new PolarisExceptionMapper()); PolarisServiceImpl polarisService = new PolarisServiceImpl(entityManagerFactory, authorizer); environment.jersey().register(new PolarisCatalogsApi(polarisService)); environment.jersey().register(new PolarisPrincipalsApi(polarisService)); diff --git a/polaris-service/src/main/java/org/apache/polaris/service/IcebergExceptionMapper.java b/polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java similarity index 99% rename from polaris-service/src/main/java/org/apache/polaris/service/IcebergExceptionMapper.java rename to polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java index b829a39c1..565b1a707 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/IcebergExceptionMapper.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.service; +package org.apache.polaris.service.exception; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; diff --git a/polaris-service/src/main/java/org/apache/polaris/service/IcebergJerseyViolationExceptionMapper.java b/polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergJerseyViolationExceptionMapper.java similarity index 97% rename from polaris-service/src/main/java/org/apache/polaris/service/IcebergJerseyViolationExceptionMapper.java rename to polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergJerseyViolationExceptionMapper.java index 2ffbdbbb2..e26e89a2b 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/IcebergJerseyViolationExceptionMapper.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergJerseyViolationExceptionMapper.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.service; +package org.apache.polaris.service.exception; import io.dropwizard.jersey.validation.JerseyViolationException; import jakarta.ws.rs.core.MediaType; diff --git a/polaris-service/src/main/java/org/apache/polaris/service/IcebergJsonProcessingExceptionMapper.java b/polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergJsonProcessingExceptionMapper.java similarity index 98% rename from polaris-service/src/main/java/org/apache/polaris/service/IcebergJsonProcessingExceptionMapper.java rename to polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergJsonProcessingExceptionMapper.java index 729920085..929453ae1 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/IcebergJsonProcessingExceptionMapper.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/exception/IcebergJsonProcessingExceptionMapper.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.service; +package org.apache.polaris.service.exception; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonParseException; diff --git a/polaris-service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java b/polaris-service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java new file mode 100644 index 000000000..489b37351 --- /dev/null +++ b/polaris-service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java @@ -0,0 +1,58 @@ +/* + * 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.polaris.service.exception; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import org.apache.iceberg.rest.responses.ErrorResponse; +import org.apache.polaris.core.exceptions.AlreadyExistsException; +import org.apache.polaris.core.exceptions.PolarisException; + +/** + * An {@link ExceptionMapper} implementation for {@link PolarisException}s modeled after {@link + * IcebergExceptionMapper} + */ +@Provider +public class PolarisExceptionMapper implements ExceptionMapper { + + private Response.Status getStatus(PolarisException exception) { + if (exception instanceof AlreadyExistsException) { + return Response.Status.CONFLICT; + } else { + return Response.Status.INTERNAL_SERVER_ERROR; + } + } + + @Override + public Response toResponse(PolarisException exception) { + Response.Status status = getStatus(exception); + ErrorResponse errorResponse = + ErrorResponse.builder() + .responseCode(status.getStatusCode()) + .withType(exception.getClass().getSimpleName()) + .withMessage(exception.getMessage()) + .build(); + return Response.status(status) + .entity(errorResponse) + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } +}