| /* |
| * 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.felix.http.base.internal.registry; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.regex.Pattern; |
| |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.annotations.NotNull; |
| |
| import org.apache.felix.http.base.internal.handler.ServletHandler; |
| import org.apache.felix.http.base.internal.runtime.ServletInfo; |
| import org.apache.felix.http.base.internal.runtime.dto.BuilderConstants; |
| import org.apache.felix.http.base.internal.runtime.dto.ErrorPageDTOBuilder; |
| import org.osgi.service.http.runtime.dto.DTOConstants; |
| import org.osgi.service.http.runtime.dto.ErrorPageDTO; |
| import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; |
| import org.osgi.service.http.runtime.dto.ServletContextDTO; |
| |
| /** |
| * The error page registry keeps tracks of the active/inactive servlets handling |
| * error pages (error code and/or exception). |
| * This registry is per servlet context. |
| */ |
| public final class ErrorPageRegistry |
| { |
| private static final String CLIENT_ERROR = "4xx"; |
| private static final String SERVER_ERROR = "5xx"; |
| private static final Pattern ERROR_CODE_PATTERN = Pattern.compile("\\d{3}"); |
| |
| private static final List<Long> CLIENT_ERROR_CODES = hundredOf(400); |
| private static final List<Long> SERVER_ERROR_CODES = hundredOf(500); |
| |
| private final Map<String, List<ServletHandler>> errorMapping = new ConcurrentHashMap<String, List<ServletHandler>>(); |
| |
| private volatile List<ErrorRegistrationStatus> status = Collections.emptyList(); |
| |
| public static final class ErrorRegistration { |
| public final long[] errorCodes; |
| public final String[] exceptions; |
| |
| public ErrorRegistration(final long[] errorCodes, final String[] exceptions) |
| { |
| this.errorCodes = errorCodes; |
| this.exceptions = exceptions; |
| } |
| } |
| |
| static final class ErrorRegistrationStatus implements Comparable<ErrorRegistrationStatus> { |
| |
| private final ServletHandler handler; |
| public final Map<Integer, ErrorRegistration> reasonMapping = new HashMap<Integer, ErrorRegistration>(); |
| |
| public final boolean usesClientErrorCodes; |
| public final boolean usesServerErrorCodes; |
| |
| public ErrorRegistrationStatus(final ServletHandler handler) |
| { |
| this.handler = handler; |
| this.usesClientErrorCodes = hasErrorCode(handler, CLIENT_ERROR); |
| this.usesServerErrorCodes = hasErrorCode(handler, SERVER_ERROR); |
| } |
| |
| public ServletHandler getHandler() |
| { |
| return this.handler; |
| } |
| |
| @Override |
| public int compareTo(final ErrorRegistrationStatus o) |
| { |
| return this.handler.compareTo(o.getHandler()); |
| } |
| } |
| |
| private static boolean hasErrorCode(final ServletHandler handler, final String key) |
| { |
| for(final String val : handler.getServletInfo().getErrorPage()) |
| { |
| if ( key.equals(val) ) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static List<Long> hundredOf(final int start) |
| { |
| List<Long> result = new ArrayList<Long>(); |
| for (long i = start; i < start + 100; i++) |
| { |
| result.add(i); |
| } |
| return Collections.unmodifiableList(result); |
| } |
| |
| private static long[] toLongArray(final Set<Long> set) |
| { |
| long[] codes = BuilderConstants.EMPTY_LONG_ARRAY; |
| if ( !set.isEmpty() ) |
| { |
| codes = new long[set.size()]; |
| int index = 0; |
| for(final Long code : set) |
| { |
| codes[index++] = code; |
| } |
| } |
| return codes; |
| } |
| |
| private static Set<Long> toLongSet(final long[] codes) |
| { |
| final Set<Long> set = new TreeSet<Long>(); |
| for(final long c : codes) |
| { |
| set.add(c); |
| } |
| return set; |
| } |
| |
| private static String[] toStringArray(final Set<String> set) |
| { |
| String[] array = BuilderConstants.EMPTY_STRING_ARRAY; |
| if ( !set.isEmpty() ) |
| { |
| array = set.toArray(new String[set.size()]); |
| } |
| return array; |
| } |
| |
| private static Set<String> toStringSet(final String[] array) |
| { |
| final Set<String> set = new TreeSet<String>(); |
| for(final String s : array) |
| { |
| set.add(s); |
| } |
| return set; |
| } |
| |
| private static String parseErrorCodes(final Set<Long> codes, final String string) |
| { |
| if (CLIENT_ERROR.equalsIgnoreCase(string)) |
| { |
| codes.addAll(CLIENT_ERROR_CODES); |
| } |
| else if (SERVER_ERROR.equalsIgnoreCase(string)) |
| { |
| codes.addAll(SERVER_ERROR_CODES); |
| } |
| else if (ERROR_CODE_PATTERN.matcher(string).matches()) |
| { |
| codes.add(Long.parseLong(string)); |
| } |
| else |
| { |
| return string; |
| } |
| return null; |
| } |
| |
| /** |
| * Parse the registration properties of the servlet for error handling |
| * @param info The servlet info |
| * @return An error registration object if the servlet handles errors |
| */ |
| public static @Nullable ErrorRegistration getErrorRegistration(@NotNull final ServletInfo info) |
| { |
| if ( info.getErrorPage() != null ) |
| { |
| final Set<Long> errorCodes = new TreeSet<Long>(); |
| final Set<String> exceptions = new TreeSet<String>(); |
| |
| for(final String val : info.getErrorPage()) |
| { |
| final String exception = parseErrorCodes(errorCodes, val); |
| if ( exception != null ) |
| { |
| exceptions.add(exception); |
| } |
| } |
| final long[] codes = toLongArray(errorCodes); |
| final String[] exceptionsArray = toStringArray(exceptions); |
| |
| return new ErrorRegistration(codes, exceptionsArray); |
| } |
| return null; |
| } |
| |
| /** |
| * Add the servlet for error handling |
| * @param handler The servlet handler. |
| */ |
| public synchronized void addServlet(@NotNull final ServletHandler handler) |
| { |
| final ErrorRegistration reg = getErrorRegistration(handler.getServletInfo()); |
| if ( reg != null ) |
| { |
| final ErrorRegistrationStatus status = new ErrorRegistrationStatus(handler); |
| for(final long code : reg.errorCodes) |
| { |
| addErrorHandling(handler, status, code, null); |
| } |
| for(final String exception : reg.exceptions) |
| { |
| addErrorHandling(handler, status, 0, exception); |
| } |
| final List<ErrorRegistrationStatus> newList = new ArrayList<ErrorPageRegistry.ErrorRegistrationStatus>(this.status); |
| newList.add(status); |
| Collections.sort(newList); |
| this.status = newList; |
| } |
| } |
| |
| /** |
| * Remove the servlet from error handling |
| * @param info The servlet info. |
| */ |
| public synchronized void removeServlet(@NotNull final ServletInfo info, final boolean destroy) |
| { |
| final ErrorRegistration reg = getErrorRegistration(info); |
| if ( reg != null ) |
| { |
| final List<ErrorRegistrationStatus> newList = new ArrayList<ErrorPageRegistry.ErrorRegistrationStatus>(this.status); |
| final Iterator<ErrorRegistrationStatus> i = newList.iterator(); |
| while ( i.hasNext() ) |
| { |
| final ErrorRegistrationStatus status = i.next(); |
| if ( status.handler.getServletInfo().equals(info) ) |
| { |
| i.remove(); |
| break; |
| } |
| } |
| this.status = newList; |
| |
| for(final long code : reg.errorCodes) |
| { |
| removeErrorHandling(info, code, null); |
| } |
| for(final String exception : reg.exceptions) |
| { |
| removeErrorHandling(info, 0, exception); |
| } |
| } |
| } |
| |
| public synchronized void cleanup() |
| { |
| this.errorMapping.clear(); |
| this.status = Collections.emptyList(); |
| } |
| |
| private void addErrorHandling(final ServletHandler handler, final ErrorRegistrationStatus status, final long code, final String exception) |
| { |
| final String key = (exception != null ? exception : String.valueOf(code)); |
| |
| final List<ServletHandler> newList; |
| final List<ServletHandler> list = errorMapping.get(key); |
| if ( list == null ) |
| { |
| newList = Collections.singletonList(handler); |
| } |
| else |
| { |
| newList = new ArrayList<ServletHandler>(list); |
| newList.add(handler); |
| Collections.sort(newList); |
| } |
| if ( newList.get(0) == handler ) |
| { |
| // try to activate (and deactivate old handler) |
| final int result = handler.init(); |
| addReason(status, code, exception, result); |
| if ( result == -1 ) |
| { |
| if ( list != null ) |
| { |
| final ServletHandler old = list.get(0); |
| old.destroy(); |
| errorMapping.put(key, newList); |
| ErrorRegistrationStatus oldStatus = null; |
| final Iterator<ErrorRegistrationStatus> i = this.status.iterator(); |
| while ( oldStatus == null && i.hasNext() ) |
| { |
| final ErrorRegistrationStatus current = i.next(); |
| if ( current.handler.getServletInfo().equals(old.getServletInfo()) ) |
| { |
| oldStatus = current; |
| } |
| } |
| if ( oldStatus != null ) |
| { |
| removeReason(oldStatus, code, exception, -1); |
| boolean addReason = true; |
| if ( exception == null ) |
| { |
| if ( code >= 400 && code < 500 && oldStatus.usesClientErrorCodes && !status.usesClientErrorCodes ) |
| { |
| addReason = false; |
| } |
| else if ( code >= 500 && code < 600 && oldStatus.usesServerErrorCodes && !status.usesServerErrorCodes ) |
| { |
| addReason = false; |
| } |
| } |
| if ( addReason ) |
| { |
| addReason(oldStatus, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); |
| } |
| } |
| } |
| else |
| { |
| errorMapping.put(key, newList); |
| } |
| } |
| } |
| else |
| { |
| // failure |
| boolean addReason = true; |
| if ( exception == null ) |
| { |
| if ( code >= 400 && code < 500 && status.usesClientErrorCodes && !hasErrorCode(newList.get(0), CLIENT_ERROR) ) |
| { |
| addReason = false; |
| } |
| else if ( code >= 500 && code < 600 && status.usesServerErrorCodes && !hasErrorCode(newList.get(0), SERVER_ERROR) ) |
| { |
| addReason = false; |
| } |
| } |
| if ( addReason ) |
| { |
| addReason(status, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); |
| } |
| errorMapping.put(key, newList); |
| } |
| } |
| |
| /** |
| * Make an entry in the status object (which are used to create the DTOs) |
| * @param status The status object |
| * @param code Either the code |
| * @param exception or the exception |
| * @param reason The code for the failure reason or {@code -1} for success. |
| */ |
| private void addReason(final ErrorRegistrationStatus status, |
| final long code, |
| final String exception, |
| final int reason) |
| { |
| ErrorRegistration reg = status.reasonMapping.get(reason); |
| if ( reg == null ) |
| { |
| if ( exception != null ) |
| { |
| reg = new ErrorRegistration(BuilderConstants.EMPTY_LONG_ARRAY, new String[] {exception}); |
| } |
| else |
| { |
| reg = new ErrorRegistration(new long[] {code}, BuilderConstants.EMPTY_STRING_ARRAY); |
| } |
| } |
| else |
| { |
| long[] codes = reg.errorCodes; |
| String[] exceptions = reg.exceptions; |
| if ( exception != null ) |
| { |
| final Set<String> set = toStringSet(exceptions); |
| set.add(exception); |
| exceptions = toStringArray(set); |
| } |
| else |
| { |
| final Set<Long> set = toLongSet(codes); |
| set.add(code); |
| codes = toLongArray(set); |
| } |
| |
| reg = new ErrorRegistration(codes, exceptions); |
| } |
| status.reasonMapping.put(reason, reg); |
| } |
| |
| private void removeReason(final ErrorRegistrationStatus status, final long code, final String exception, final int reason) |
| { |
| ErrorRegistration reg = status.reasonMapping.get(reason); |
| if ( reg != null ) |
| { |
| long[] codes = reg.errorCodes; |
| String[] exceptions = reg.exceptions; |
| if ( exception != null ) |
| { |
| final Set<String> set = toStringSet(exceptions); |
| set.remove(exception); |
| exceptions = toStringArray(set); |
| } |
| else |
| { |
| final Set<Long> set = toLongSet(codes); |
| set.remove(code); |
| codes = toLongArray(set); |
| } |
| if ( codes.length == 0 && exceptions.length == 0 ) |
| { |
| status.reasonMapping.remove(reason); |
| } |
| else |
| { |
| status.reasonMapping.put(reason, new ErrorRegistration(codes, exceptions)); |
| } |
| } |
| } |
| |
| private void removeErrorHandling(final ServletInfo info, final long code, final String exception) |
| { |
| final String key = (exception != null ? exception : String.valueOf(code)); |
| |
| final List<ServletHandler> list = errorMapping.get(key); |
| if ( list != null ) |
| { |
| int index = 0; |
| final Iterator<ServletHandler> i = list.iterator(); |
| while ( i.hasNext() ) |
| { |
| final ServletHandler handler = i.next(); |
| if ( handler.getServletInfo().equals(info) ) |
| { |
| final List<ServletHandler> newList = new ArrayList<ServletHandler>(list); |
| newList.remove(handler); |
| |
| if ( index == 0 ) |
| { |
| handler.destroy(); |
| |
| index++; |
| while ( index < list.size() ) |
| { |
| final ServletHandler next = list.get(index); |
| ErrorRegistrationStatus nextStatus = null; |
| for(final ErrorRegistrationStatus s : this.status) |
| { |
| if ( s.handler.getServletInfo().equals(next.getServletInfo()) ) |
| { |
| nextStatus = s; |
| break; |
| } |
| } |
| this.removeReason(nextStatus, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); |
| final int reason = next.init(); |
| this.addReason(nextStatus, code, exception, reason); |
| if ( reason == -1 ) |
| { |
| break; |
| } |
| else |
| { |
| newList.remove(next); |
| } |
| } |
| } |
| if ( newList.isEmpty() ) |
| { |
| errorMapping.remove(key); |
| } |
| else |
| { |
| errorMapping.put(key, newList); |
| } |
| |
| break; |
| } |
| index++; |
| } |
| } |
| } |
| |
| /** |
| * Get the servlet handling the error (error code or exception). |
| * If an exception is provided, a handler for the exception is searched first. |
| * If no handler is found (or no exception provided) a handler for the error |
| * code is searched. |
| * @param exception Optional exception |
| * @param errorCode Error code |
| * @return The servlet handling the error or {@code null} |
| */ |
| public ServletHandler get(final Throwable exception, final int errorCode) |
| { |
| ServletHandler errorHandler = this.get(exception); |
| if (errorHandler != null) |
| { |
| return errorHandler; |
| } |
| |
| return get(errorCode); |
| } |
| |
| /** |
| * Get the servlet handling the error code |
| * @param errorCode Error code |
| * @return The servlet handling the error or {@code null} |
| */ |
| private ServletHandler get(final long errorCode) |
| { |
| final List<ServletHandler> list = this.errorMapping.get(String.valueOf(errorCode)); |
| if ( list != null ) |
| { |
| return list.get(0); |
| } |
| return null; |
| } |
| |
| /** |
| * Get the servlet handling the exception |
| * @param exception Error exception |
| * @return The servlet handling the error or {@code null} |
| */ |
| private ServletHandler get(final Throwable exception) |
| { |
| if (exception == null) |
| { |
| return null; |
| } |
| |
| ServletHandler servletHandler = null; |
| Class<?> throwableClass = exception.getClass(); |
| while ( servletHandler == null && throwableClass != null ) |
| { |
| final List<ServletHandler> list = this.errorMapping.get(throwableClass.getName()); |
| if ( list != null ) |
| { |
| servletHandler = list.get(0); |
| } |
| if ( servletHandler == null ) |
| { |
| throwableClass = throwableClass.getSuperclass(); |
| if ( !Throwable.class.isAssignableFrom(throwableClass) ) |
| { |
| throwableClass = null; |
| } |
| } |
| |
| } |
| return servletHandler; |
| } |
| |
| /** |
| * Get DTOs for error pages. |
| * @param dto The servlet context DTO |
| * @param failedErrorPageDTOs The failed error page DTOs |
| */ |
| public void getRuntimeInfo(final ServletContextDTO dto, |
| final Collection<FailedErrorPageDTO> failedErrorPageDTOs) |
| { |
| final List<ErrorPageDTO> errorPageDTOs = new ArrayList<ErrorPageDTO>(); |
| final List<ErrorRegistrationStatus> statusList = this.status; |
| for(final ErrorRegistrationStatus status : statusList) |
| { |
| for(final Map.Entry<Integer, ErrorRegistration> entry : status.reasonMapping.entrySet()) |
| { |
| final ErrorPageDTO state = ErrorPageDTOBuilder.build(status.getHandler(), entry.getKey()); |
| state.errorCodes = Arrays.copyOf(entry.getValue().errorCodes, entry.getValue().errorCodes.length); |
| state.exceptions = Arrays.copyOf(entry.getValue().exceptions, entry.getValue().exceptions.length); |
| |
| if ( entry.getKey() == -1 ) |
| { |
| errorPageDTOs.add(state); |
| } |
| else |
| { |
| failedErrorPageDTOs.add((FailedErrorPageDTO)state); |
| } |
| } |
| } |
| if ( !errorPageDTOs.isEmpty() ) |
| { |
| dto.errorPageDTOs = errorPageDTOs.toArray(new ErrorPageDTO[errorPageDTOs.size()]); |
| } |
| } |
| } |