001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2011-2012 Pentaho and others
008// All Rights Reserved.
009*/
010package mondrian.spi;
011
012import mondrian.olap.Util;
013import mondrian.rolap.BitKey;
014import mondrian.util.ByteString;
015
016import java.io.Serializable;
017import java.util.*;
018
019/**
020 * SegmentHeaders are the key objects used to retrieve the segments
021 * from the segment cache.
022 *
023 * <p>The segment header objects are immutable and fully serializable.
024 *
025 * <p>The headers have each an ID which is a SHA-256 checksum of the
026 * following properties, concatenated. See
027 * {@link SegmentHeader#getUniqueID()}
028 * <ul>
029 * <li>Schema Name</li>
030 * <li>Cube Name</li>
031 * <li>Measure Name</li>
032 * <li>For each column:</li>
033 *   <ul>
034 *   <li>Column table name</li>
035 *   <li>Column physical name</li>
036 *   <li>For each predicate value:</li>
037 *     <ul>
038 *     <li>The equivalent of
039 *     <code>String.valueof([value object])</code></li>
040 *     </ul>
041 *   </ul>
042 * </ul>
043 *
044 * @author LBoudreau
045 */
046public class SegmentHeader implements Serializable {
047    private static final long serialVersionUID = 8696439182886512850L;
048    private final int arity;
049    private final List<SegmentColumn> constrainedColumns;
050    private final List<SegmentColumn> excludedRegions;
051    public final List<String> compoundPredicates;
052    public final String measureName;
053    public final String cubeName;
054    public final String schemaName;
055    public final String rolapStarFactTableName;
056    public final BitKey constrainedColsBitKey;
057    private final int hashCode;
058    private ByteString uniqueID;
059    private String description;
060    public final ByteString schemaChecksum;
061
062    /**
063     * Creates a segment header.
064     *
065     * @param schemaName The name of the schema which this
066     * header belongs to.
067     * @param schemaChecksum Schema checksum
068     * @param cubeName The name of the cube this segment belongs to.
069     * @param measureName The name of the measure which defines
070     * this header.
071     * @param constrainedColumns An array of constrained columns
072     * objects which define the predicated of this segment header.
073     * @param compoundPredicates Compound predicates (Must not be null, but
074     * typically empty.)
075     * @param rolapStarFactTableName Star fact table name
076     * @param constrainedColsBitKey Constrained columns bit key
077     * @param excludedRegions Excluded regions. (Must not be null, but typically
078     */
079    public SegmentHeader(
080        String schemaName,
081        ByteString schemaChecksum,
082        String cubeName,
083        String measureName,
084        List<SegmentColumn> constrainedColumns,
085        List<String> compoundPredicates,
086        String rolapStarFactTableName,
087        BitKey constrainedColsBitKey,
088        List<SegmentColumn> excludedRegions)
089    {
090        this.constrainedColumns = constrainedColumns;
091        this.excludedRegions = excludedRegions;
092        this.schemaName = schemaName;
093        this.schemaChecksum = schemaChecksum;
094        assert schemaChecksum != null;
095        this.cubeName = cubeName;
096        this.measureName = measureName;
097        this.compoundPredicates = compoundPredicates;
098        this.rolapStarFactTableName = rolapStarFactTableName;
099        this.constrainedColsBitKey = constrainedColsBitKey;
100        this.arity = constrainedColumns.size();
101        // Hash code might be used extensively. Better compute
102        // it up front.
103        this.hashCode = computeHashCode();
104    }
105
106    private int computeHashCode() {
107        int hash = 42;
108        hash = Util.hash(hash, schemaName);
109        hash = Util.hash(hash, schemaChecksum);
110        hash = Util.hash(hash, cubeName);
111        hash = Util.hash(hash, measureName);
112        for (SegmentColumn col : this.constrainedColumns) {
113            hash = Util.hash(hash, col.columnExpression);
114            if (col.values != null) {
115                hash = Util.hashArray(hash, col.values.toArray());
116            }
117        }
118        for (SegmentColumn col : this.excludedRegions) {
119            hash = Util.hash(hash, col.columnExpression);
120            if (col.values != null) {
121                hash = Util.hashArray(hash, col.values.toArray());
122            }
123        }
124        hash = Util.hash(hash, compoundPredicates);
125        return hash;
126    }
127
128    public int hashCode() {
129        return hashCode;
130    }
131
132    public boolean equals(Object obj) {
133        if (!(obj instanceof SegmentHeader)) {
134            return false;
135        }
136        final SegmentHeader that = (SegmentHeader) obj;
137        return getUniqueID().equals(that.getUniqueID())
138            && excludedRegions.equals(that.excludedRegions);
139    }
140
141    /**
142     * Creates a clone of this header by replacing some of the
143     * constrained columns in the process.
144     * @param overrideValues A list of constrained columns to either
145     * replace or add to the original header.
146     * @return A clone of the header with the columns replaced.
147     */
148    public SegmentHeader clone(SegmentColumn[] overrideValues) {
149        Map<String, SegmentColumn> colsToAdd =
150            new HashMap<String, SegmentColumn>();
151        for (SegmentColumn cc : this.constrainedColumns) {
152            colsToAdd.put(cc.columnExpression, cc);
153        }
154        for (SegmentColumn override : overrideValues) {
155            colsToAdd.put(override.columnExpression, override);
156        }
157        return
158            new SegmentHeader(
159                schemaName,
160                schemaChecksum,
161                cubeName,
162                measureName,
163                new ArrayList<SegmentColumn>(colsToAdd.values()),
164                Collections.<String>emptyList(),
165                rolapStarFactTableName,
166                constrainedColsBitKey,
167                Collections.<SegmentColumn>emptyList());
168    }
169
170    /**
171     * Checks if this header can be constrained by a given region.
172     *
173     * <p>It will return false if the region covers one of the axis in
174     * its entirety.
175     *
176     * <p>It will return false if the region sits outside of the bounds
177     * of this header. This means that when performing a flush operation,
178     * the header must be scrapped altogether.
179     */
180    public boolean canConstrain(SegmentColumn[] region) {
181        boolean atLeastOnePresent = false;
182        for (SegmentColumn ccToFlush : region) {
183            SegmentColumn ccActual =
184                getConstrainedColumn(ccToFlush.columnExpression);
185            if (ccActual != null) {
186                final SegmentColumn ccActualExcl =
187                    getExcludedRegion(ccToFlush.columnExpression);
188                if (ccToFlush.values == null
189                    || (ccActualExcl != null
190                        && ccActualExcl.values != null
191                        && ccActualExcl.merge(ccToFlush).values == null))
192                {
193                    // This means that the whole axis is excluded.
194                    // Better destroy that segment.
195                    return false;
196                }
197                if (ccActual.values != null
198                    && ccActual.values.equals(ccToFlush.values))
199                {
200                    // This means that the whole axis is excluded.
201                    // Better destroy that segment.
202                    return false;
203                }
204                // We know there is at least one column on which
205                // we can constrain.
206                atLeastOnePresent = true;
207            }
208        }
209        return atLeastOnePresent;
210    }
211
212    /**
213     * Applies a set of exclusions to this segment header and returns
214     * a new segment header representing the original one to which a
215     * region has been excluded.
216     *
217     * @param region Region
218     * @return Header with constraint applied
219     */
220    public SegmentHeader constrain(SegmentColumn[] region) {
221        final Map<String, SegmentColumn> newRegions =
222            new HashMap<String, SegmentColumn>();
223        for (SegmentColumn excludedRegion : excludedRegions) {
224            newRegions.put(
225                excludedRegion.columnExpression,
226                excludedRegion);
227        }
228        for (SegmentColumn col : region) {
229            if (getConstrainedColumn(col.columnExpression) == null) {
230                continue;
231            }
232            if (newRegions.containsKey(col.columnExpression)) {
233                newRegions.put(
234                    col.columnExpression,
235                    newRegions.get(col.columnExpression)
236                        .merge(col));
237            } else {
238                newRegions.put(
239                    col.columnExpression,
240                    col);
241            }
242        }
243        assert newRegions.size() > 0;
244        return
245            new SegmentHeader(
246                schemaName,
247                schemaChecksum,
248                cubeName,
249                measureName,
250                constrainedColumns,
251                compoundPredicates,
252                rolapStarFactTableName,
253                constrainedColsBitKey,
254                new ArrayList<SegmentColumn>(newRegions.values()));
255    }
256
257    public String toString() {
258        return this.getDescription();
259    }
260
261    /**
262     * Returns the arity of this SegmentHeader.
263     * @return The arity as an integer number.
264     */
265    public int getArity() {
266        return arity;
267    }
268
269    public List<SegmentColumn> getExcludedRegions() {
270        return excludedRegions;
271    }
272
273    /**
274     * Returns a list of constrained columns which define this segment
275     * header. The caller should consider this list immutable.
276     *
277     * @return List of ConstrainedColumns
278     */
279    public List<SegmentColumn> getConstrainedColumns() {
280        return constrainedColumns;
281    }
282
283    /**
284     * Returns the constrained column object, if any, corresponding
285     * to a column name and a table name.
286     * @param columnExpression The column name we want.
287     * @return A Constrained column, or null.
288     */
289    public SegmentColumn getConstrainedColumn(
290        String columnExpression)
291    {
292        for (SegmentColumn c : constrainedColumns) {
293            if (c.columnExpression.equals(columnExpression)) {
294                return c;
295            }
296        }
297        return null;
298    }
299
300    public SegmentColumn getExcludedRegion(
301        String columnExpression)
302    {
303        for (SegmentColumn c : excludedRegions) {
304            if (c.columnExpression.equals(columnExpression)) {
305                return c;
306            }
307        }
308        return null;
309    }
310
311    public BitKey getConstrainedColumnsBitKey() {
312        return this.constrainedColsBitKey.copy();
313    }
314
315    /**
316     * Returns a unique identifier for this header. The identifier
317     * can be used for storage and will be the same across segments
318     * which have the same schema name, cube name, measure name,
319     * and for each constrained column, the same column name, table name,
320     * and predicate values.
321     * @return A unique identification string.
322     */
323    public ByteString getUniqueID() {
324        if (this.uniqueID == null) {
325            StringBuilder hashSB = new StringBuilder();
326            hashSB.append(this.schemaName);
327            hashSB.append(this.schemaChecksum);
328            hashSB.append(this.cubeName);
329            hashSB.append(this.measureName);
330            for (SegmentColumn c : constrainedColumns) {
331                hashSB.append(c.columnExpression);
332                if (c.values != null) {
333                    for (Object value : c.values) {
334                        hashSB.append(String.valueOf(value));
335                    }
336                }
337            }
338            for (SegmentColumn c : excludedRegions) {
339                hashSB.append(c.columnExpression);
340                if (c.values != null) {
341                    for (Object value : c.values) {
342                        hashSB.append(String.valueOf(value));
343                    }
344                }
345            }
346            for (String c : compoundPredicates) {
347                hashSB.append(c);
348            }
349            this.uniqueID =
350                new ByteString(Util.digestSha256(hashSB.toString()));
351        }
352        return uniqueID;
353    }
354
355    /**
356     * Returns a human readable description of this
357     * segment header.
358     * @return A string describing the header.
359     */
360    public String getDescription() {
361        if (this.description == null) {
362            StringBuilder descriptionSB = new StringBuilder();
363            descriptionSB.append("*Segment Header\n");
364            descriptionSB.append("Schema:[");
365            descriptionSB.append(this.schemaName);
366            descriptionSB.append("]\nChecksum:[");
367            descriptionSB.append(this.schemaChecksum);
368            descriptionSB.append("]\nCube:[");
369            descriptionSB.append(this.cubeName);
370            descriptionSB.append("]\nMeasure:[");
371            descriptionSB.append(this.measureName);
372            descriptionSB.append("]\n");
373            descriptionSB.append("Axes:[");
374            for (SegmentColumn c : constrainedColumns) {
375                descriptionSB.append("\n    {");
376                descriptionSB.append(c.columnExpression);
377                descriptionSB.append("=(");
378                if (c.values == null) {
379                    descriptionSB.append("* ");
380                } else {
381                    for (Object value : c.values) {
382                        descriptionSB.append("'");
383                        descriptionSB.append(value);
384                        descriptionSB.append("',");
385                    }
386                }
387                descriptionSB.deleteCharAt(descriptionSB.length() - 1);
388                descriptionSB.append(")}");
389            }
390            descriptionSB.append("]\n");
391            descriptionSB.append("Excluded Regions:[");
392            for (SegmentColumn c : excludedRegions) {
393                descriptionSB.append("\n    {");
394                descriptionSB.append(c.columnExpression);
395                descriptionSB.append("=(");
396                if (c.values == null) {
397                    descriptionSB.append("* ");
398                } else {
399                    for (Object value : c.values) {
400                        descriptionSB.append("'");
401                        descriptionSB.append(value);
402                        descriptionSB.append("',");
403                    }
404                }
405                descriptionSB.deleteCharAt(descriptionSB.length() - 1);
406                descriptionSB.append(")}");
407            }
408            descriptionSB.append("]\n");
409            descriptionSB.append("Compound Predicates:[");
410            for (String c : compoundPredicates) {
411                descriptionSB.append("\n\t{");
412                descriptionSB.append(c);
413            }
414            descriptionSB
415                .append("]\n")
416                .append("ID:[")
417                .append(getUniqueID())
418                .append("]\n");
419            this.description = descriptionSB.toString();
420        }
421        return description;
422    }
423}
424
425// End SegmentHeader.java