/** * Copyright (C) 2009-2014 Cars and Tracks Development Project (CTDP). * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package net.ctdp.rfdynhud.util; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.Stack; import javax.swing.JOptionPane; import net.ctdp.rfdynhud.gamedata.LiveGameData; import net.ctdp.rfdynhud.properties.AbstractPropertiesKeeper; import net.ctdp.rfdynhud.properties.Property; import net.ctdp.rfdynhud.properties.PropertyLoader; import net.ctdp.rfdynhud.values.RelativePositioning; import net.ctdp.rfdynhud.widgets.WidgetsConfiguration; import net.ctdp.rfdynhud.widgets.WidgetsConfiguration.ConfigurationLoadListener; import net.ctdp.rfdynhud.widgets.__WCPrivilegedAccess; import net.ctdp.rfdynhud.widgets.base.widget.AbstractAssembledWidget; import net.ctdp.rfdynhud.widgets.base.widget.Widget; import net.ctdp.rfdynhud.widgets.base.widget.WidgetFactory; import net.ctdp.rfdynhud.widgets.base.widget.__WPrivilegedAccess; import org.jagatoo.util.errorhandling.ParsingException; import org.jagatoo.util.ini.AbstractIniParser; import org.jagatoo.util.versioning.Version; import org.openmali.vecmath2.util.ColorUtils; /** * This utility class serves to load HUD configuration files. * * @author Marvin Froehlich (CTDP) */ public class ConfigurationLoader implements PropertyLoader { public static final Object DEFAULT_PLACEHOLDER = "~DEFAULT~"; private final ConfigurationLoadListener loadListener; private String noConfigFoundMessage = "Couldn't find any overlay configuration file."; private String keyPrefix = null; private String currentKey = null; private String currentValue = null; private String effectiveKey = null; private Version sourceVersion = null; public void setNoConfigFoundMessage( String message ) { this.noConfigFoundMessage = message; } public final String getNoConfigFoundMessage() { return ( noConfigFoundMessage ); } public void setKeyPrefix( String prefix ) { this.keyPrefix = prefix; if ( keyPrefix == null ) effectiveKey = currentKey; else effectiveKey = currentKey.substring( keyPrefix.length() ); } public final String getKeyPrefix() { return ( keyPrefix ); } @Override public final String getCurrentKey() { return ( effectiveKey ); } @Override public final String getCurrentValue() { return ( currentValue ); } @Override public final Version getSourceVersion() { return ( sourceVersion ); } @Override public boolean loadProperty( Property property ) { if ( property.isMatchingKey( effectiveKey ) ) { if ( DEFAULT_PLACEHOLDER.equals( currentValue ) ) property.setValue( property.getDefaultValue() ); else property.loadValue( this, currentValue ); return ( true ); } return ( false ); } private static enum GroupType { Meta, Global, NamedColors, NamedFonts, BorderAliases, Widget, ; public static GroupType parseGroupType( String groupName ) { GroupType groupType = null; if ( groupName.equals( "Meta" ) ) groupType = GroupType.Meta; else if ( groupName.equals( "Global" ) ) groupType = GroupType.Global; else if ( groupName.equals( "NamedColors" ) ) groupType = GroupType.NamedColors; else if ( groupName.equals( "NamedFonts" ) ) groupType = GroupType.NamedFonts; else if ( groupName.equals( "BorderAliases" ) ) groupType = GroupType.BorderAliases; else if ( groupName.startsWith( "Widget::" ) ) groupType = GroupType.Widget; return ( groupType ); } } /** * Loads fully configured {@link Widget}s to a {@link WidgetsConfiguration}. * * @param file the file to parse * @param quietMode suppress warnings to the log? * * @return the design resolution as an int array or <code>null</code>. * * @throws IOException if anything went wrong. */ public static int[] readDesignResolutionFromConfiguration( File file, final boolean quietMode ) throws IOException { final int[] result = { -1, -1 }; new AbstractIniParser() { private GroupType currentGroupType = null; private boolean headerFound = false; private boolean settingBeforeGroupWarningThrown = false; @Override protected boolean onGroupParsed( int lineNr, String group ) { currentGroupType = GroupType.parseGroupType( group ); if ( currentGroupType == GroupType.Meta ) { headerFound = true; } else// if ( currentGroupType != null ) { if ( headerFound ) return ( false ); } return ( true ); } @Override protected boolean onSettingParsed( int lineNr, String group, String key, String value, String comment ) throws ParsingException { if ( currentGroupType == null ) { if ( !settingBeforeGroupWarningThrown ) { if ( !quietMode ) { //throw new ParsingException( "Found setting before the first (known) group started (line " + lineNr + ")." ); RFDHLog.exception( "WARNING: Found setting before the first (known) group started (line " + lineNr + ")." ); } settingBeforeGroupWarningThrown = true; } return ( true ); } switch ( currentGroupType ) { case Meta: /* if ( key.equals( "rfDynHUD_Version" ) ) { try { sourceVersion = Version.parseVersion( value ); } catch ( Throwable t ) { Logger.log( "WANRING: Unable to parse rfDynHUDVersion." ); sourceVersion = new Version( 0, 0, 0, null, 0 ); } } */ if ( key.equals( "Design_Resolution" ) ) { String[] parts = value.split( "x" ); if ( parts.length == 2 ) { try { result[0] = Integer.parseInt( parts[0] ); result[1] = Integer.parseInt( parts[1] ); } catch ( NumberFormatException e ) { if ( !quietMode ) RFDHLog.exception( e ); } } } break; } return ( true ); } }.parse( file ); if ( ( result[0] < 0 ) || ( result[1] < 0 ) ) return ( null ); return ( result ); } /** * Loads fully configured {@link Widget}s to a {@link WidgetsConfiguration}. * * @param widgetsConfig * @param gameData * @param isEditorMode * * @throws IOException if anything went wrong. */ public void clearConfiguration( WidgetsConfiguration widgetsConfig, LiveGameData gameData, final boolean isEditorMode ) throws IOException { __WCPrivilegedAccess.clear( widgetsConfig, gameData, isEditorMode, loadListener ); } /** * Loads fully configured {@link Widget}s to a {@link WidgetsConfiguration}. * * @param in * @param name * @param widgetsConfig * @param gameData * @param isEditorMode * * @throws IOException if anything went wrong. */ public void loadConfiguration( InputStream in, String name, final WidgetsConfiguration widgetsConfig, final LiveGameData gameData, final boolean isEditorMode ) throws IOException { clearConfiguration( widgetsConfig, gameData, isEditorMode ); currentKey = null; currentValue = null; keyPrefix = null; effectiveKey = null; sourceVersion = null; new AbstractIniParser() { private GroupType currentGroupType = null; private Widget currentWidget = null; private String widgetName = null; private boolean badWidget = false; private final Stack<Widget> partStack = new Stack<Widget>(); private final Stack<Integer> partIndexStack = new Stack<Integer>(); private Widget currentPart = null; private String partName = null; private boolean settingBeforeGroupWarningThrown = false; private String errorMessages = null; @Override protected boolean onGroupParsed( int lineNr, String group ) { if ( currentWidget != null ) { currentKey = "/"; currentValue = "propertiesLoadingFinished"; setKeyPrefix( keyPrefix ); if ( currentPart == null ) currentWidget.loadProperty( ConfigurationLoader.this, gameData ); else currentPart.loadProperty( ConfigurationLoader.this, gameData ); //__WCPrivilegedAccess.addWidget( widgetsConfig, currentWidget, true ); currentWidget = null; widgetName = null; badWidget = false; partStack.clear(); partIndexStack.clear(); currentPart = null; partName = null; } currentGroupType = GroupType.parseGroupType( group ); if ( currentGroupType == GroupType.Widget ) { widgetName = group.substring( 8 ); badWidget = false; partStack.clear(); //partStack.push( null ); partIndexStack.clear(); partIndexStack.push( -1 ); } return ( true ); } @Override protected boolean onSettingParsed( int lineNr, String group, String key, String value, String comment ) throws ParsingException { if ( currentGroupType == null ) { if ( !settingBeforeGroupWarningThrown ) { //throw new ParsingException( "Found setting before the first (known) group started (line " + lineNr + ")." ); RFDHLog.exception( "WARNING: Found setting before the first (known) group started (line " + lineNr + ")." ); settingBeforeGroupWarningThrown = true; } return ( true ); } currentKey = key; currentValue = value; setKeyPrefix( keyPrefix ); switch ( currentGroupType ) { case Meta: if ( key.equals( "rfDynHUD_Version" ) ) { try { sourceVersion = Version.parseVersion( value ); } catch ( Throwable t ) { RFDHLog.exception( "WANRING: Unable to parse rfDynHUD Version." ); sourceVersion = new Version( 0, 0, 0, null, 0 ); } } break; case Global: widgetsConfig.loadProperty( ConfigurationLoader.this ); break; case NamedColors: java.awt.Color color = ColorUtils.hexToColor( value, false ); if ( color == null ) { String msg = "ERROR: Illegal color value on line #" + lineNr + ": " + value; RFDHLog.exception( msg ); if ( errorMessages == null ) errorMessages = msg; else errorMessages += "\n" + msg; } else { widgetsConfig.addNamedColor( key, color ); } break; case NamedFonts: if ( getSourceVersion().getBuild() < 92 ) { value = value.replace( '-', FontUtils.SEPARATOR ); } java.awt.Font font = FontUtils.parseFont( value, widgetsConfig.getGameResolution().getViewportHeight(), false, false ); if ( ( font == FontUtils.FALLBACK_FONT ) || ( font == FontUtils.FALLBACK_VIRTUAL_FONT ) ) { String msg = "ERROR: Illegal font value on line #" + lineNr + ": " + value; RFDHLog.exception( msg ); if ( errorMessages == null ) errorMessages = msg; else errorMessages += "\n" + msg; } else { widgetsConfig.addNamedFont( key, value ); } break; case BorderAliases: widgetsConfig.addBorderAlias( key, value ); break; case Widget: if ( ( widgetName != null ) && ( currentWidget == null ) && !key.equals( "class" ) ) { if ( !badWidget ) //throw new ParsingException( "Cannot load the Widget \"" + widgetName + "\" (line " + lineNr + ")." ); RFDHLog.error( "WARNING: Cannot load the Widget \"" + widgetName + "\" (line " + lineNr + ")." ); badWidget = true; } else if ( key.equals( "class" ) ) { if ( widgetName != null ) { Class<Widget> clazz = WidgetFactory.getWidgetClass( value ); if ( clazz == null ) { RFDHLog.error( "Error: Unknown Widget class \"" + value + "\"." ); badWidget = true; } else { if ( AbstractAssembledWidget.class.isAssignableFrom( clazz ) ) currentWidget = WidgetFactory.createAssembledWidget( value, widgetName ); else currentWidget = WidgetFactory.createWidget( value, widgetName ); if ( currentWidget == null ) { RFDHLog.error( "Error creating Widget instance of class " + value + "." ); badWidget = true; } else { currentKey = "name"; currentValue = widgetName; setKeyPrefix( keyPrefix ); widgetName = null; if ( currentPart == null ) currentWidget.loadProperty( ConfigurationLoader.this, gameData ); else currentPart.loadProperty( ConfigurationLoader.this, gameData ); __WCPrivilegedAccess.addWidget( widgetsConfig, currentWidget, true, gameData ); } } } else if ( partName != null ) { currentPart = WidgetFactory.createWidget( value, partName ); if ( currentPart == null ) { RFDHLog.error( "Error creating Widget part instance of class " + value + "." ); badWidget = true; } else { //currentPart = ( (AbstractAssembledWidget)currentWidget ).getPart( partIndexStack.get( partIndexStack.size() - 2 ) ); __WPrivilegedAccess.addPart( currentPart, (AbstractAssembledWidget)currentWidget, gameData ); partStack.set( partStack.size() - 1, currentPart ); currentKey = "name"; currentValue = partName; setKeyPrefix( keyPrefix ); partName = null; if ( currentPart == null ) currentWidget.loadProperty( ConfigurationLoader.this, gameData ); else currentPart.loadProperty( ConfigurationLoader.this, gameData ); } } else { RFDHLog.exception( "WARNING: Cannot load the Widget configuration line " + lineNr + "." ); } } else if ( ( currentWidget == null ) || ( widgetName != null ) ) { if ( !badWidget ) { if ( currentWidget != null ) { //throw new ParsingException( "Cannot load the Widget \"" + currentWidget.getName() + "\" (line " + lineNr + ")." ); RFDHLog.error( "WARNING: Cannot load the Widget \"" + currentWidget.getName() + "\" (line " + lineNr + ")." ); badWidget = true; } else if ( widgetName != null ) { //throw new ParsingException( "Cannot load the Widget \"" + widgetName + "\" (line " + lineNr + ")." ); RFDHLog.error( "WARNING: Cannot load the Widget \"" + widgetName + "\" (line " + lineNr + ")." ); badWidget = true; } else { //throw new ParsingException( "Cannot load the a Widget (line " + lineNr + ")." ); RFDHLog.error( "WARNING: Cannot load the a Widget (line " + lineNr + ")." ); badWidget = true; } } } else if ( key.equals( "<WidgetPart>" ) ) { partName = value; partStack.push( null ); partIndexStack.set( partIndexStack.size() - 1, partIndexStack.peek() + 1 ); partIndexStack.push( -1 ); } else if ( key.equals( "</WidgetPart>" ) ) { if ( partStack.isEmpty() ) { RFDHLog.error( "WARNING: Found Widget part end at line " + lineNr + " with no part begun." ); } else { currentKey = "/"; currentValue = "propertiesLoadingFinished"; setKeyPrefix( keyPrefix ); if ( currentPart == null ) currentWidget.loadProperty( ConfigurationLoader.this, gameData ); else currentPart.loadProperty( ConfigurationLoader.this, gameData ); partStack.pop(); partIndexStack.pop(); if ( !partStack.isEmpty() ) currentPart = partStack.peek(); else currentPart = null; } } else { try { if ( currentPart == null ) currentWidget.loadProperty( ConfigurationLoader.this, gameData ); else currentPart.loadProperty( ConfigurationLoader.this, gameData ); } catch ( Throwable t ) { //throw new Error( t ); RFDHLog.error( t ); } } break; } return ( true ); } @Override protected void onParsingFinished() { if ( currentWidget != null ) { //__WCPrivilegedAccess.addWidget( widgetsConfig, currentWidget, true ); currentWidget = null; } if ( ( errorMessages != null ) && isEditorMode ) { JOptionPane.showMessageDialog( null, errorMessages, "Error loading config ini", JOptionPane.ERROR_MESSAGE ); } } }.parse( in ); currentKey = null; currentValue = null; keyPrefix = null; effectiveKey = null; for ( int i = 0; i < widgetsConfig.getNumWidgets(); i++ ) { Widget widget = widgetsConfig.getWidget( i ); AbstractPropertiesKeeper.attachKeeper( widget, true ); if ( widget instanceof AbstractAssembledWidget ) { __WPrivilegedAccess.sortWidgetParts( (AbstractAssembledWidget)widget ); } } __WCPrivilegedAccess.sortWidgets( widgetsConfig ); __WCPrivilegedAccess.setJustLoaded( widgetsConfig, gameData, isEditorMode, name, loadListener ); } private File currentlyLoadedConfigFile = null; private long lastModified = -1L; private File _loadConfiguration( File file, final WidgetsConfiguration widgetsConfig, LiveGameData gameData, boolean isEditorMode ) throws IOException { RFDHLog.println( "Loading configuration file from \"" + file.getAbsolutePath() + "\"..." ); String name = null; if ( file.getName().startsWith( gameData.getFileSystem().getConfigPath() + File.pathSeparator ) ) name = file.getName().substring( gameData.getFileSystem().getConfigPath().length() + 1 ); loadConfiguration( new FileInputStream( file ), name, widgetsConfig, gameData, isEditorMode ); __WCPrivilegedAccess.setValid( widgetsConfig, true ); currentlyLoadedConfigFile = file.getAbsoluteFile(); lastModified = currentlyLoadedConfigFile.lastModified(); return ( currentlyLoadedConfigFile ); } private File load( File currentlyLoadedConfigFile, long lastModified, File configFile, WidgetsConfiguration widgetsConfig, LiveGameData gameData, boolean isEditorMode ) throws IOException { if ( currentlyLoadedConfigFile == null ) return ( _loadConfiguration( configFile, widgetsConfig, gameData, isEditorMode ) ); if ( !configFile.equals( currentlyLoadedConfigFile ) || ( configFile.lastModified() > lastModified ) ) return ( _loadConfiguration( configFile, widgetsConfig, gameData, isEditorMode ) ); //__WCPrivilegedAccess.setValid( widgetsConfig, true ); return ( currentlyLoadedConfigFile ); } private boolean load( File configFile, WidgetsConfiguration widgetsConfig, LiveGameData gameData, boolean isEditorMode ) throws IOException { File old_currentlyLoadedConfigFile = currentlyLoadedConfigFile; long old_lastModified = lastModified; load( currentlyLoadedConfigFile, lastModified, configFile, widgetsConfig, gameData, isEditorMode ); if ( !currentlyLoadedConfigFile.equals( old_currentlyLoadedConfigFile ) || ( lastModified > old_lastModified ) ) { //__WCPrivilegedAccess.setJustLoaded( widgetsConfig, gameData ); return ( true ); } return ( false ); } /** * Loads fully configured {@link Widget}s to a {@link WidgetsConfiguration}. * * @param file * @param widgetsConfig * * @return the file, from which the configuration has been loaded. * * @throws IOException */ File forceLoadConfiguration( File file, final WidgetsConfiguration widgetsConfig, LiveGameData gameData, boolean isEditorMode ) throws IOException { currentlyLoadedConfigFile = null; lastModified = -1L; if ( load( file, widgetsConfig, gameData, isEditorMode ) ) return ( currentlyLoadedConfigFile ); return ( null ); } /** * * @param widgetsConfig * @param gameData * @param isEditorMode * @throws IOException */ void loadFactoryDefaults( WidgetsConfiguration widgetsConfig, LiveGameData gameData, boolean isEditorMode ) throws IOException { RFDHLog.println( "Loading factory default configuration." ); clearConfiguration( widgetsConfig, gameData, isEditorMode ); //__loadConfiguration( new InputStreamReader( ConfigurationLoader.class.getResourceAsStream( "/data/config/overlay.ini" ) ), widgetsConfig, gameData, isEditorMode, loadListener ); if ( ( getNoConfigFoundMessage() != null ) && ( getNoConfigFoundMessage().trim().length() > 0 ) ) { net.ctdp.rfdynhud.widgets.internal.InternalWidget internalWidget = new net.ctdp.rfdynhud.widgets.internal.InternalWidget(); internalWidget.setMessage( getNoConfigFoundMessage() ); __WCPrivilegedAccess.addWidget( widgetsConfig, internalWidget, true, gameData ); internalWidget.getPosition().setEffectivePosition( RelativePositioning.TOP_CENTER, ( widgetsConfig.getGameResolution().getViewportWidth() - internalWidget.getEffectiveWidth() ) / 2, 200 ); AbstractPropertiesKeeper.attachKeeper( internalWidget, true ); __WCPrivilegedAccess.sortWidgets( widgetsConfig ); } __WCPrivilegedAccess.setJustLoaded( widgetsConfig, gameData, isEditorMode, null, loadListener ); __WCPrivilegedAccess.setValid( widgetsConfig, true ); currentlyLoadedConfigFile = null; lastModified = -1L; } /** * Loads a configuration and searches for the file in the order, defined by the iterator. * Then it checks, if that file is newer than the already loaded one. * * @param candidatesIterator * @param widgetsConfig * @param gameData * @param isEditorMode * @param force * @param shortBreakInvalidCondition */ public void reloadConfiguration( Iterator<File> candidatesIterator, WidgetsConfiguration widgetsConfig, LiveGameData gameData, boolean isEditorMode, boolean force, boolean shortBreakInvalidCondition ) { if ( force || !widgetsConfig.isValid() ) { currentlyLoadedConfigFile = null; lastModified = -1L; } File f = null; try { if ( candidatesIterator != null ) { RFDHLog.debug( "DEBUG: (Re-)Loading overlay configuration..." ); while ( candidatesIterator.hasNext() ) { f = candidatesIterator.next(); RFDHLog.debug( "DEBUG: Trying overlay configuration file \"", f.getAbsolutePath(), "\"... ", ( f.exists() ? "found." : "not found." ) ); if ( f.exists() ) { load( f, widgetsConfig, gameData, isEditorMode ); return; } } } if ( shortBreakInvalidCondition ) { __WCPrivilegedAccess.setValid( widgetsConfig, false ); return; } if ( candidatesIterator != null || !widgetsConfig.isValid() ) { loadFactoryDefaults( widgetsConfig, gameData, isEditorMode ); } } catch ( Throwable t ) { RFDHLog.error( "Error loading overlay config file " + ( f != null ? f.getAbsolutePath() : "" ) + "." ); RFDHLog.error( t ); __WCPrivilegedAccess.setValid( widgetsConfig, false ); currentlyLoadedConfigFile = null; lastModified = -1L; } } public ConfigurationLoader( ConfigurationLoadListener loadListener ) { this.loadListener = loadListener; } }