blob: 9357f61b659ce7a74b81f4ecae64ccf593c93e93 [file] [log] [blame]
/*
* 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()]);
}
}
}