/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.style;
import org.pentaho.reporting.engine.classic.core.layout.style.SimpleStyleSheet;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.serializer.SerializerHelper;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
/**
* An element style-sheet contains zero, one or many attributes that affect the appearance of report elements. For each
* attribute, there is a predefined key that can be used to access that attribute in the style sheet.
* <p/>
* Every report element has an associated style-sheet.
* <p/>
* A style-sheet maintains a list of parent style-sheets. If an attribute is not defined in a style-sheet, the code
* refers to the parent style-sheets to see if the attribute is defined there.
* <p/>
* All StyleSheet entries are checked against the StyleKeyDefinition for validity.
* <p/>
* As usual, this implementation is not synchronized, we need the performance during the reporting.
*
* @author Thomas Morgner
* @noinspection UnnecessaryUnboxing
*/
public class ElementStyleSheet extends AbstractStyleSheet implements Serializable, Cloneable {
/**
* The keys for the properties that have been explicitly set on the element.
*/
private List<StyleKey> propertyKeys;
/**
* The properties that have been explicitly set on the element.
*/
private transient Object[] properties;
/*
* In source, properties' flags are stored.
* Since there are only three possible flag values (SOURCE_UNDEFINED, SOURCE_FROM_PARENT, SOURCE_DIRECT),
* we can use only two bits for each flag instead of full byte.
*
* Therefore, to address a byte, where a pair of bits related to a property is put,
* it is necessary to divide the property's index by 4. Division by 4 is the same as right shifting by 2.
* After the byte's index has been computed, the pair's "internal" position can be shifted to the lowest bits,
* and these bits give the flag's value.
*/
private byte[] source;
private static final byte SOURCE_UNDEFINED = 0;
private static final byte SOURCE_FROM_PARENT = 1;
private static final byte SOURCE_DIRECT = 2;
/**
* Style change support.
*/
private transient StyleChangeSupport styleChangeSupport;
private long modificationCount;
private long changeTrackerHash;
private static final StyleKey[] EMPTY_KEYS = new StyleKey[ 0 ];
/**
* Creates a new element style-sheet. The style-sheet initially contains no attributes, and has no parent
* style-sheets.
*/
public ElementStyleSheet() {
this.styleChangeSupport = new StyleChangeSupport( this );
this.propertyKeys = StyleKey.getDefinedStyleKeysList();
if ( propertyKeys.isEmpty() || propertyKeys.get( 0 ) == null ) {
throw new IllegalStateException( "ReportingEngine has not been initialized properly." );
}
}
public long getChangeTracker() {
return ( changeTrackerHash << 16 ) | modificationCount;
}
private byte getFlag( int index ) {
final int packedByte = index >> 2;
final int shift = ( index & 3 ) << 1;
return (byte) ( ( source[packedByte] >> shift ) & 3 );
}
private void setFlag( int index, byte value ) {
final int packedByte = index >> 2;
final int shift = ( index & 3 ) << 1;
final int cleared = source[packedByte] & ~( 3 << shift );
source[packedByte] = (byte) ( cleared | ( value << shift ) );
}
/**
* Returns true, if the given key is locally defined, false otherwise.
*
* @param key the key to test
* @return true, if the key is local, false otherwise.
*/
public boolean isLocalKey( final StyleKey key ) {
if ( source == null ) {
return false;
}
final int identifier = key.identifier;
if ( properties.length <= identifier ) {
return false;
}
return getFlag( identifier ) == SOURCE_DIRECT;
}
private void pruneCachedEntries() {
if ( source != null && properties != null ) {
for ( int i = 0, len = properties.length; i < len; i++ ) {
if ( getFlag( i ) == SOURCE_FROM_PARENT ) {
setFlag( i, SOURCE_UNDEFINED );
properties[ i ] = null;
}
}
}
}
public final Object[] toArray() {
final List<StyleKey> keys = propertyKeys;
final int size = keys.size();
final Object[] data = new Object[ size ];
if ( source == null ) {
source = new byte[ ( size + 3 ) >> 2 ];
properties = new Object[ size ];
}
for ( int i = 0; i < size; i++ ) {
final StyleKey key = keys.get( i );
if ( key == null ) {
throw new NullPointerException();
}
final int identifier = key.identifier;
final byte sourceHint = getFlag( identifier );
if ( sourceHint == SOURCE_UNDEFINED ) {
data[ identifier ] = getStyleProperty( key );
} else {
data[ identifier ] = properties[ identifier ];
}
}
return data;
}
/**
* Returns the value of a style. If the style is not found in this style-sheet, the code looks in the parent
* style-sheets. If the style is not found in any of the parent style-sheets, then the default value (possibly
* <code>null</code>) is returned.
*
* @param key the style key.
* @param defaultValue the default value (<code>null</code> permitted).
* @return the value.
*/
public Object getStyleProperty( final StyleKey key, final Object defaultValue ) {
final int identifier = key.identifier;
if ( properties != null ) {
if ( properties.length <= identifier ) {
throw new IllegalStateException();
}
final byte source = getFlag( identifier );
if ( source != SOURCE_UNDEFINED ) {
final Object value = properties[ identifier ];
if ( value == null ) {
return defaultValue;
}
return value;
}
}
putInCache( key, null, SOURCE_FROM_PARENT );
return defaultValue;
}
/**
* Puts an object into the cache (if caching is enabled).
*
* @param key the stylekey for that object
* @param value the object.
*/
private void putInCache( final StyleKey key, final Object value, final byte sourceHint ) {
ensurePropertiesReady();
final int identifier = key.identifier;
properties[ identifier ] = value;
setFlag( identifier, sourceHint );
}
/**
* Sets a boolean style property.
*
* @param key the style key (<code>null</code> not permitted).
* @param value the value.
* @throws NullPointerException if the given key is null.
* @throws ClassCastException if the value cannot be assigned with the given key.
*/
public void setBooleanStyleProperty( final StyleKey key, final boolean value ) {
if ( value ) {
setStyleProperty( key, Boolean.TRUE );
} else {
setStyleProperty( key, Boolean.FALSE );
}
}
/**
* Sets a style property (or removes the style if the value is <code>null</code>).
*
* @param key the style key (<code>null</code> not permitted).
* @param value the value.
* @throws NullPointerException if the given key is null.
* @throws ClassCastException if the value cannot be assigned with the given key.
*/
public void setStyleProperty( final StyleKey key, final Object value ) {
if ( key == null ) {
throw new NullPointerException( "ElementStyleSheet.setStyleProperty: key is null." );
}
final int identifier = key.identifier;
if ( value == null ) {
if ( properties != null ) {
if ( properties[ identifier ] == null ) {
return;
}
// invalidate the cache ..
putInCache( key, null, SOURCE_UNDEFINED );
updateChangeTracker( key, null );
styleChangeSupport.fireStyleRemoved( key );
}
return;
}
if ( key.getValueType().isAssignableFrom( value.getClass() ) == false ) {
throw new ClassCastException( "Value for key " + key.getName()
+ " is not assignable: " + value.getClass()
+ " is not assignable from " + key.getValueType() );
}
ensurePropertiesReady();
if ( ObjectUtilities.equal( properties[ identifier ], value ) ) {
// no need to change anything ..
return;
}
// invalidate the cache ..
putInCache( key, value, SOURCE_DIRECT );
updateChangeTracker( key, value );
styleChangeSupport.fireStyleChanged( key, value );
}
private void ensurePropertiesReady() {
if ( properties == null ) {
final int definedStyleKeyCount = propertyKeys.size();
properties = new Object[ definedStyleKeyCount ];
source = new byte[ ( definedStyleKeyCount + 3 ) >> 2 ];
}
}
/**
* Creates and returns a copy of this object. After the cloning, the new StyleSheet is no longer registered with its
* parents.
*
* @return a clone of this instance.
* @see Cloneable
*/
public ElementStyleSheet clone() {
final ElementStyleSheet sc = (ElementStyleSheet) super.clone();
if ( properties != null ) {
sc.properties = properties.clone();
}
if ( source != null ) {
sc.source = source.clone();
}
// noinspection CloneCallsConstructors
sc.styleChangeSupport = new StyleChangeSupport( sc );
sc.pruneCachedEntries();
return sc;
}
public ElementStyleSheet derive( final boolean preserveId ) {
final ElementStyleSheet sc = (ElementStyleSheet) super.derive( preserveId );
if ( properties != null ) {
sc.properties = properties.clone();
}
if ( source != null ) {
sc.source = source.clone();
}
// noinspection CloneCallsConstructors
sc.styleChangeSupport = new StyleChangeSupport( sc );
sc.pruneCachedEntries();
return sc;
}
public StyleKey[] getDefinedPropertyNamesArray() {
if ( source == null ) {
return ElementStyleSheet.EMPTY_KEYS;
}
final StyleKey[] retval = propertyKeys.toArray( new StyleKey[ propertyKeys.size() ] );
for ( int i = 0, len = retval.length; i < len; i++ ) {
if ( getFlag( i ) != SOURCE_DIRECT ) {
retval[ i ] = null;
}
}
return retval;
}
/**
* Adds a {@link StyleChangeListener}.
*
* @param l the listener.
*/
public void addListener( final StyleChangeListener l ) {
styleChangeSupport.addListener( l );
}
/**
* Removes a {@link StyleChangeListener}.
*
* @param l the listener.
*/
public void removeListener( final StyleChangeListener l ) {
styleChangeSupport.removeListener( l );
}
private void updateChangeTracker( StyleKey key, Object value ) {
modificationCount += 1;
changeTrackerHash = changeTrackerHash * 31 + key.getIdentifier();
if ( value == null ) {
changeTrackerHash = changeTrackerHash * 31;
} else {
changeTrackerHash = changeTrackerHash * 31 + value.hashCode();
}
}
/**
* Helper method for serialization.
*
* @param out the output stream where to write the object.
* @throws IOException if errors occur while writing the stream.
*/
private void writeObject( final ObjectOutputStream out ) throws IOException {
out.defaultWriteObject();
if ( properties == null ) {
out.writeInt( 0 );
} else {
final int size = properties.length;
out.writeInt( size );
SerializerHelper helper = SerializerHelper.getInstance();
for ( int i = 0; i < size; i++ ) {
final Object value = properties[ i ];
helper.writeObject( value, out );
}
}
}
/**
* Helper method for serialization.
*
* @param in the input stream from where to read the serialized object.
* @throws IOException when reading the stream fails.
* @throws ClassNotFoundException if a class definition for a serialized object could not be found.
*/
private void readObject( final ObjectInputStream in ) throws IOException, ClassNotFoundException {
in.defaultReadObject();
final int size = in.readInt();
propertyKeys = StyleKey.getDefinedStyleKeysList();
styleChangeSupport = new StyleChangeSupport( this );
if ( size == 0 ) {
properties = null;
return;
}
if ( size != propertyKeys.size() ) {
throw new IOException(
"Encountered a different style-system configuration. This report cannot be deserialized." );
}
if ( propertyKeys.get( 0 ) == null ) {
throw new IllegalStateException();
}
properties = new Object[ size ];
final Object[] values = new Object[ size ];
final SerializerHelper serHelper = SerializerHelper.getInstance();
for ( int i = 0; i < size; i++ ) {
values[ i ] = serHelper.readObject( in );
}
for ( int i = 0; i < size; i++ ) {
final StyleKey key = propertyKeys.get( i );
if ( key != null ) {
final int identifier = key.identifier;
properties[ identifier ] = values[ i ];
}
}
}
/**
* Returns the property keys. This must return the same set of keys as a call to StyleSheet.getDefinedKeys(), but it
* allows us to avoid the synchronization on that call.
*
* @return the local copy of the style keys.
*/
public StyleKey[] getPropertyKeys() {
return propertyKeys.toArray( new StyleKey[ propertyKeys.size() ] );
}
public List<StyleKey> getPropertyKeyList() {
return propertyKeys;
}
public void addAll( final ElementStyleSheet sourceStyleSheet ) {
if ( sourceStyleSheet.source == null || sourceStyleSheet.properties == null ) {
return;
}
ensurePropertiesReady();
for ( int i = 0, len = sourceStyleSheet.properties.length; i < len; i++ ) {
final byte sourceFlag = sourceStyleSheet.getFlag( i );
if ( sourceFlag == SOURCE_DIRECT ) {
properties[ i ] = sourceStyleSheet.properties[ i ];
setFlag( i, sourceFlag );
}
}
}
public void addInherited( final ElementStyleSheet sourceStyleSheet ) {
if ( sourceStyleSheet.source == null || sourceStyleSheet.properties == null ) {
return;
}
ensurePropertiesReady();
for ( int i = 0, len = properties.length; i < len; i++ ) {
if ( propertyKeys.get( i ).isInheritable() == false ) {
continue;
}
final byte sourceFlag = sourceStyleSheet.getFlag( i );
if ( sourceFlag == SOURCE_DIRECT ) {
properties[ i ] = sourceStyleSheet.properties[ i ];
setFlag( i, SOURCE_FROM_PARENT );
}
}
}
public void addInherited( final SimpleStyleSheet sourceStyleSheet ) {
ensurePropertiesReady();
for ( int i = 0, len = properties.length; i < len; i++ ) {
StyleKey styleKey = propertyKeys.get( i );
if ( styleKey.isInheritable() == false ) {
continue;
}
properties[ i ] = sourceStyleSheet.getStyleProperty( styleKey, null );
setFlag( i, SOURCE_FROM_PARENT );
}
}
public void addDefault( final ElementStyleSheet sourceStyleSheet ) {
if ( sourceStyleSheet.source == null || sourceStyleSheet.properties == null ) {
return;
}
ensurePropertiesReady();
for ( int i = 0, len = properties.length; i < len; i++ ) {
final byte sourceFlag = sourceStyleSheet.getFlag( i );
if ( sourceFlag == SOURCE_DIRECT && getFlag( i ) == SOURCE_UNDEFINED ) {
properties[ i ] = sourceStyleSheet.properties[ i ];
setFlag( i, SOURCE_DIRECT );
}
}
}
public void clear() {
if ( source == null || properties == null ) {
return;
}
changeTrackerHash = 0;
modificationCount = 0;
Arrays.fill( properties, null );
// SOURCE_UNDEFINED is 0, hence it can be used for clearing all four flags
Arrays.fill( source, SOURCE_UNDEFINED );
}
public long getModificationCount() {
return modificationCount;
}
protected void setModificationCount( final long modificationCount ) {
this.modificationCount = modificationCount;
}
public long getChangeTrackerHash() {
return changeTrackerHash;
}
protected void setChangeTrackerHash( final long changeTracker ) {
this.changeTrackerHash = changeTracker;
}
public void copyFrom( final ElementStyleSheet style ) {
this.changeTrackerHash = style.changeTrackerHash;
this.modificationCount = style.modificationCount;
this.propertyKeys = style.propertyKeys;
if ( style.source != null ) {
this.source = style.source.clone();
} else if ( this.source != null ) {
Arrays.fill( this.source, SOURCE_UNDEFINED );
}
if ( style.properties != null ) {
this.properties = style.properties.clone();
} else if ( this.properties != null ) {
Arrays.fill( properties, null );
}
}
}