Skip to content
This repository has been archived by the owner on Jul 11, 2022. It is now read-only.

Commit

Permalink
[1110277] RHQ_EVENT table's detail field limit of 4000 character will…
Browse files Browse the repository at this point in the history
… break when a log message contains multibyte characters (UTF-8)

This is a problem in a few places, not just events.  We're not
properly protecting against the 4000 byte limit on oracle, just
the 4000 character limit declared on the varchar2 field.
- Add DatabaseType.getString() which can truncate as needed in
  a vendor-specific way.
- Use the new getString() to store safe versions of event detail,
  alert notification log message, and calltime dataset destination.

Conflicts:
	modules/core/dbutils/src/main/java/org/rhq/core/db/DatabaseType.java
	modules/core/dbutils/src/main/java/org/rhq/core/db/OracleDatabaseType.java
	modules/core/domain/src/main/java/org/rhq/core/domain/resource/Resource.java
	modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/event/EventManagerBean.java
	modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/measurement/CallTimeDataManagerBean.java

Cherry-Pick master 1d836a7

(cherry picked from relese/jon3.2.x commit 6a5f14b)

Conflicts:
	modules/core/dbutils/src/main/java/org/rhq/core/db/DatabaseType.java
	modules/core/dbutils/src/main/java/org/rhq/core/db/OracleDatabaseType.java
	modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/alert/AlertManagerBean.java
  • Loading branch information
jshaughn authored and loleary committed Jun 26, 2014
1 parent 732521c commit 2d78c73
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 31 deletions.
@@ -1,6 +1,6 @@
/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* Copyright (C) 2005-2014 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
Expand Down Expand Up @@ -31,7 +31,6 @@

import mazz.i18n.Logger;

import org.rhq.core.db.ant.dbupgrade.SST_JavaTask;
import org.rhq.core.db.builders.CreateSequenceExprBuilder;

/**
Expand Down Expand Up @@ -117,7 +116,7 @@ public boolean matches(String vendor, String version) {
}

/**
* Given a genertic type, this will return an analogous type that is specific to this database.
* Given a generic type, this will return an analogous type that is specific to this database.
*
* @param generic_type a generic type name
*
Expand Down Expand Up @@ -273,6 +272,48 @@ public String getBooleanValue(boolean bool) {
return bool ? "true" : "false";
}

/**
* Get the Integer representation of the number type supplied by the db vendor for an integer field value.
* The default implementation simply applies a cast to the passed in number and is appropriate for DB types
* that support a native integer field type (like Postgres). Other db types should override this method
* (like Oracle).
*
* @param number
* @return
*/
public Integer getInteger(Object number) {
return (Integer) number;
}

/**
* Get the Long representation of the number type supplied by the db vendor for a long field value.
* The default implementation simply applies a cast to the passed in number and is appropriate for DB types
* that support a native long field type. Other db types should override this method (like Oracle).
*
* @param number
* @return
*/
public Long getLong(Object number) {
return (Long) number;
}

/**
* Different vendors have different rules regarding varchar/varchar2 string storage. In particular, Oracle
* has a hard limit of 4000 bytes (not characters, bytes). Make sure we trim to maxLength (in characters)
* while also meeting vendor-specific constraints.
*
* @param varchar The String to be stored as a varchar/varchar2
* @param maxLength max length of the DB field, in characters.
* @return The string, safe for storage to the DB field
*/
public String getString(String varchar, int maxLength) {
if (null == varchar || varchar.length() <= maxLength) {
return varchar;
}

return varchar.substring(0, maxLength);
}

/**
* Fill out a <code>PreparedStatement</code> correctly with a boolean.
*
Expand Down
@@ -1,6 +1,6 @@
/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* Copyright (C) 2005-2014 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
Expand All @@ -18,6 +18,7 @@
*/
package org.rhq.core.db;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
Expand Down Expand Up @@ -59,6 +60,55 @@ public String getBooleanValue(boolean bool) {
return bool ? "1" : "0";
}

/* (non-Javadoc)
* @see org.rhq.core.db.DatabaseType#getInteger(java.lang.Object)
*
* Oracle stores integer fields as Numbers and returns a BigDecimal. It is assumed <code>number</code> is actually
* an integer value, otherwise precision will be lost in this conversion.
*/
@Override
public Integer getInteger(Object number) {
BigDecimal intField = (BigDecimal) number;
return intField.intValue();
}

/* (non-Javadoc)
* @see org.rhq.core.db.DatabaseType#getLong(java.lang.Object)
*
* Oracle stores long fields as Numbers and returns a BigDecimal. It is assumed <code>number</code> is actually
* a long value, otherwise precision will be lost in this conversion.
*/
@Override
public Long getLong(Object number) {
BigDecimal longField = (BigDecimal) number;
return longField.longValue();
}

/* (non-Javadoc)
* @see org.rhq.core.db.DatabaseType#getString(java.lang.String, int)
*
* Oracle has a hard limit of 4000 bytes for varchar/varchar2 storage. Make sure the returned String
* is trimmed as needed to satisfy the constraint.
*/
@Override
public String getString(String varchar, int maxLength) {
if ( null == varchar ) {
return null;
}

// First meet the character limit.
String result = super.getString(varchar, maxLength);

// Now, ensure we can store the resulting number of bytes by clipping off the last character until we reach
// an acceptable number of bytes. This is not super-efficient but hopefully won't happen all that often. We can't
// just convert to bytes and clip to 4000, because it could leave an incomplete multi-byte character at the end.
while (result.getBytes().length > 4000) {
result = result.substring(0, result.length() - 1);
}

return result;
}

/**
* For Oracle databases, the boolean parameter will actually be of type "int" with a value of 0 or 1.
*
Expand Down
Expand Up @@ -60,7 +60,7 @@
query = "DELETE AlertNotificationLog anl " //
+ " WHERE anl.id IN ( SELECT an.id " //
+ " FROM Alert a " //
+ " JOIN a.alertNotificationLogs an" //
+ " JOIN a.alertNotificationLogs an" //
+ " WHERE a.id IN ( :alertIds ) )"),
@NamedQuery(name = AlertNotificationLog.QUERY_DELETE_BY_RESOURCES, //
query = "DELETE AlertNotificationLog anl " //
Expand Down Expand Up @@ -103,6 +103,12 @@ public class AlertNotificationLog implements Serializable {

public static final String QUERY_NATIVE_TRUNCATE_SQL = "TRUNCATE TABLE RHQ_ALERT_NOTIF_LOG";

/**
* this is a character limit, when stored certain vendors may require the string be clipped to
* satisfy a byte limit (postgres can store the 4000 chars, oracle only 4000 bytes).
*/
public static final int MESSAGE_MAX_LENGTH = 4000;

@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO, generator = "RHQ_ALERT_NOTIF_LOG_ID_SEQ")
@Id
Expand All @@ -120,14 +126,22 @@ public class AlertNotificationLog implements Serializable {
@Enumerated(EnumType.STRING)
private ResultState resultState;

@Column(name = "MESSAGE")
@Column(name = "MESSAGE", length = MESSAGE_MAX_LENGTH)
private String message;

/**
* This is insufficient for certain db vendors as it handles only character limit, not necessarily the
* 4000 byte limit on oracle.
*
* @deprecated the message should be trimmed sufficiently by the caller to meet any db vendor specific
* byte limits.
*/
@Deprecated
@PrePersist
@PreUpdate
public void trimMessage() {
if (message != null && message.length() > 4000) {
message = message.substring(0, 4000);
if (message != null && message.length() > MESSAGE_MAX_LENGTH) {
message = message.substring(0, MESSAGE_MAX_LENGTH);
}
}

Expand Down Expand Up @@ -211,4 +225,4 @@ public String toString() {
sb.append('}');
return sb.toString();
}
}
}
Expand Up @@ -43,8 +43,6 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import org.rhq.core.domain.resource.Resource;

/**
* A transpired event, pertaining to a particular {@link Resource}.
*
Expand Down Expand Up @@ -94,6 +92,11 @@ public class Event implements Serializable {
private static final long serialVersionUID = 1L;

public static final String TABLE_NAME = "RHQ_EVENT";

/**
* this is a character limit, when stored certain vendors may require the string be clipped to
* satisfy a byte limit (postgres can store the 4000 chars, oracle only 4000 bytes).
*/
public static final int DETAIL_MAX_LENGTH = 4000;

public static final String DELETE_BY_RESOURCES = "Event.deleteByResources";
Expand Down Expand Up @@ -122,7 +125,7 @@ public class Event implements Serializable {
@Enumerated(EnumType.STRING)
private EventSeverity severity;

@Column(name = "DETAIL", length = 4000, nullable = false)
@Column(name = "DETAIL", length = DETAIL_MAX_LENGTH, nullable = false)
private String detail;

/** The event's type (i.e. the name of its {@link EventDefinition}). */
Expand Down
Expand Up @@ -1222,7 +1222,9 @@ public String updateAncestryForResource() {
ancestry.append(parentAncestry);
}

// protect against the *very* unlikely case that this value is too big for the db
// protect against the *very* unlikely case that this value is too big for the db
// (jshaughn) this may not work for oracle's 4000 *byte* limit, but the chances are super-small
// that this will be a problem. If it ever is we'll have to check the byte count.
if (ancestry.length() < 4000) {
this.setAncestry(ancestry.toString());
}
Expand Down
@@ -1,6 +1,6 @@
/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* Copyright (C) 2005-2014 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
Expand Down Expand Up @@ -48,6 +48,8 @@
import org.jboss.annotation.IgnoreDependency;
import org.jboss.annotation.ejb.TransactionTimeout;

import org.rhq.core.db.DatabaseType;
import org.rhq.core.db.DatabaseTypeFactory;
import org.rhq.core.domain.alert.Alert;
import org.rhq.core.domain.alert.AlertCondition;
import org.rhq.core.domain.alert.AlertConditionCategory;
Expand Down Expand Up @@ -670,6 +672,7 @@ public void sendAlertNotifications(Alert alert) {

if (alertNotifications != null && alertNotifications.size() > 0) {
AlertSenderPluginManager alertSenderPluginManager = getAlertPluginManager();
DatabaseType dbType = DatabaseTypeFactory.getDefaultDatabaseType();

for (AlertNotification alertNotification : alertNotifications) {
AlertNotificationLog notificationLog = null;
Expand Down Expand Up @@ -706,6 +709,13 @@ public void sendAlertNotifications(Alert alert) {
}
}

// make sure we don't exceed the max message length for the db vendor
String message = dbType.getString(notificationLog.getMessage(),
AlertNotificationLog.MESSAGE_MAX_LENGTH);
if (null != message && !message.equals(notificationLog.getMessage())) {
notificationLog = new AlertNotificationLog(notificationLog.getAlert(),
notificationLog.getSender(), notificationLog.getResultState(), message);
}
entityManager.persist(notificationLog);
alert.addAlertNotificatinLog(notificationLog);
}
Expand Down
Expand Up @@ -178,12 +178,15 @@ public void addEventData(Map<EventSource, Set<Event>> events) {
ps.setString(paramIndex++, eventSource.getLocation());
ps.setLong(paramIndex++, event.getTimestamp());
ps.setString(paramIndex++, event.getSeverity().toString());
ps.setString(paramIndex++, event.getDetail());
String detail = dbType.getString(event.getDetail(), Event.DETAIL_MAX_LENGTH);
ps.setString(paramIndex++, detail);
ps.addBatch();
}

notifyAlertConditionCacheManager("addEventData", eventSource, eventData.toArray(new Event[eventData
.size()]));
// We may have trimmed the event detail for storage reasons, but for alerting use the
// full, potentially larger detail string.
notifyAlertConditionCacheManager("addEventData", eventSource,
eventData.toArray(new Event[eventData.size()]));
}
ps.executeBatch();
} finally {
Expand Down Expand Up @@ -337,7 +340,7 @@ public Map<EventSeverity, Integer> getEventCountsBySeverity(Subject subject, int
q.setParameter("resourceId", resourceId);
q.setParameter("start", startDate);
q.setParameter("end", endDate);
List<Object[]> rawResults = (List<Object[]>) q.getResultList();
List<Object[]> rawResults = q.getResultList();
for (Object[] rawResult : rawResults) {
EventSeverity severity = (EventSeverity) rawResult[0];
long count = (Long) rawResult[1];
Expand All @@ -354,7 +357,7 @@ public Map<EventSeverity, Integer> getEventCountsBySeverityForGroup(Subject subj
q.setParameter("groupId", groupId);
q.setParameter("start", startDate);
q.setParameter("end", endDate);
List<Object[]> rawResults = (List<Object[]>) q.getResultList();
List<Object[]> rawResults = q.getResultList();
for (Object[] rawResult : rawResults) {
EventSeverity severity = (EventSeverity) rawResult[0];
long count = (Long) rawResult[1];
Expand Down Expand Up @@ -456,14 +459,13 @@ public PageList<EventComposite> findEventComposites(Subject subject, EntityConte

public PageList<EventComposite> findEventCompositesByCriteria(Subject subject, EventCriteria criteria) {
CriteriaQueryGenerator generator = new CriteriaQueryGenerator(subject, criteria);
;
String replacementSelectList = "" //
+ " new org.rhq.core.domain.event.composite.EventComposite( " //
+ " event.detail," //
+ " event.source.resource.id," //
+ " event.source.resource.name," //
+ " event.source.resource.ancestry," //
+ " event.source.resource.resourceType.id," //
+ " event.source.resource.ancestry," //
+ " event.source.resource.resourceType.id," //
+ " event.id," //
+ " event.severity," //
+ " event.source.location," //
Expand All @@ -475,9 +477,6 @@ public PageList<EventComposite> findEventCompositesByCriteria(Subject subject, E
"source.resource", subject.getId());
}

//log.info(generator.getParameterReplacedQuery(false));
//log.info(generator.getParameterReplacedQuery(true));

CriteriaQueryRunner<EventComposite> queryRunner = new CriteriaQueryRunner<EventComposite>(criteria, generator,
entityManager);
return queryRunner.execute();
Expand Down
Expand Up @@ -54,6 +54,7 @@
import org.rhq.core.domain.measurement.MeasurementSchedule;
import org.rhq.core.domain.measurement.calltime.CallTimeData;
import org.rhq.core.domain.measurement.calltime.CallTimeDataComposite;
import org.rhq.core.domain.measurement.calltime.CallTimeDataKey;
import org.rhq.core.domain.measurement.calltime.CallTimeDataValue;
import org.rhq.core.domain.server.PersistenceUtility;
import org.rhq.core.domain.util.PageControl;
Expand Down Expand Up @@ -118,7 +119,8 @@ public class CallTimeDataManagerBean implements CallTimeDataManagerLocal, CallTi
private AlertConditionCacheManagerLocal alertConditionCacheManager;

@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public void addCallTimeData(@NotNull Set<CallTimeData> callTimeDataSet) {
public void addCallTimeData(@NotNull
Set<CallTimeData> callTimeDataSet) {
if (callTimeDataSet.isEmpty()) {
return;
}
Expand Down Expand Up @@ -313,8 +315,11 @@ public void insertCallTimeDataKeys(Set<CallTimeData> callTimeDataSet) {
ps.setInt(3, callTimeData.getScheduleId());
Set<String> callDestinations = callTimeData.getValues().keySet();
for (String callDestination : callDestinations) {
ps.setString(2, callDestination);
ps.setString(4, callDestination);
// make sure the destination string is safe for storage, clip as needed
String safeCallDestination = dbType.getString(callDestination,
CallTimeDataKey.DESTINATION_MAX_LENGTH);
ps.setString(2, safeCallDestination);
ps.setString(4, safeCallDestination);
ps.addBatch();
}
}
Expand Down Expand Up @@ -390,7 +395,10 @@ public void insertCallTimeDataValues(Set<CallTimeData> callTimeDataSet) {
ps.setDouble(4, callTimeDataValue.getMaximum());
ps.setDouble(5, callTimeDataValue.getTotal());
ps.setLong(6, callTimeDataValue.getCount());
ps.setString(8, callDestination);
// make sure the destination string is safe for storage, clip as needed
String safeCallDestination = dbType.getString(callDestination,
CallTimeDataKey.DESTINATION_MAX_LENGTH);
ps.setString(8, safeCallDestination);
ps.addBatch();
}
}
Expand All @@ -408,8 +416,8 @@ public void insertCallTimeDataValues(Set<CallTimeData> callTimeDataSet) {
insertedRowCount += results[i] == -2 ? 1 : results[i]; // If Oracle returns -2, just count 1 row;
}

notifyAlertConditionCacheManager("insertCallTimeDataValues", callTimeDataSet
.toArray(new CallTimeData[callTimeDataSet.size()]));
notifyAlertConditionCacheManager("insertCallTimeDataValues",
callTimeDataSet.toArray(new CallTimeData[callTimeDataSet.size()]));

if (insertedRowCount > 0) {
MeasurementMonitor.getMBean().incrementCalltimeValuesInserted(insertedRowCount);
Expand Down

0 comments on commit 2d78c73

Please sign in to comment.