package net.pms.configuration; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import net.pms.PMS; import net.pms.network.UPNPHelper; import net.pms.util.FileWatcher; import org.apache.commons.configuration.CompositeConfiguration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DeviceConfiguration extends PmsConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(DeviceConfiguration.class); public static final int DEVICE = 0; public static final int RENDERER = 1; public static final int PMSCONF = 2; private PropertiesConfiguration deviceConf = null; private RendererConfiguration ref = null; private static HashMap<String, PropertiesConfiguration> deviceConfs; private static HashMap<String, String> xref; private static File deviceDir; public DeviceConfiguration() { super(0); } public DeviceConfiguration(File f, String uuid) throws ConfigurationException { super(f, uuid); inherit(this); } public DeviceConfiguration(RendererConfiguration ref) throws ConfigurationException { super(0); inherit(ref); } public DeviceConfiguration(RendererConfiguration ref, InetAddress ia) throws ConfigurationException { super(0); deviceConf = initConfiguration(ia); inherit(ref); } /** * Creates a composite configuration for this device consisting of a dedicated device * configuration plus the given reference renderer configuration and the default pms * configuration for fallback lookup. * * @param ref The reference renderer configuration. */ public void inherit(RendererConfiguration ref) throws ConfigurationException { CompositeConfiguration cconf = new CompositeConfiguration(); // Add the component configurations in order of lookup priority: // 1. The device configuration, marked as "in memory" (i.e. writeable) cconf.addConfiguration(deviceConf != null ? deviceConf : initConfiguration(null), true); // 2. The reference renderer configuration (read-only) cconf.addConfiguration(ref.getConfiguration()); // 3. The default pms configuration (read-only) PmsConfiguration baseConf = PMS.getConfiguration(); cconf.addConfiguration(baseConf.getConfiguration()); // Handle all queries (external and internal) via the composite configuration configuration = cconf; pmsConfiguration = this; configurationReader = new ConfigurationReader(configuration, true); // Sync our internal PmsConfiguration vars // TODO: create new objects here instead? tempFolder = baseConf.tempFolder; programPaths = baseConf.programPaths; filter = baseConf.filter; // Initialize our internal RendererConfiguration vars sortedHeaderMatcher = ref.sortedHeaderMatcher; // Note: intentionally omitting 'player = null' so as to preserve player state when reloading loaded = true; this.ref = ref; init(NOFILE); } @Override public void reset() { try { inherit(ref); PMS.get().updateRenderer(this); } catch (Exception e) { LOGGER.debug("Error reloading device configuration {}: {}", this, e); e.printStackTrace(); } } @Override public void setUUID(String uuid) { if (uuid != null && ! uuid.equals(this.uuid)) { this.uuid = uuid; // Switch to the custom device conf for this new uuid, if any if (deviceConfs.containsKey(uuid) && deviceConf != deviceConfs.get(uuid)) { deviceConf = initConfiguration(null); reset(); } } } public PropertiesConfiguration initConfiguration(InetAddress ia) { String id = uuid != null ? uuid : ia != null ? ia.toString().substring(1) : null; if (id != null && deviceConfs.containsKey(id)) { deviceConf = deviceConfs.get(id); LOGGER.info("Using custom device configuration {} for {}", deviceConf.getFile().getName(), id); } else { deviceConf = createPropertiesConfiguration(); } return deviceConf; } public PropertiesConfiguration getConfiguration(int index) { CompositeConfiguration c = (CompositeConfiguration) configuration; return (PropertiesConfiguration) c.getConfiguration(index); } @Override public File getFile() { if (loaded) { File f = getConfiguration(DEVICE).getFile(); return (f != null && !f.equals(NOFILE)) ? f : getConfiguration(RENDERER).getFile(); } return null; } public File getParentFile() { return getConfiguration(RENDERER).getFile(); } public boolean isValid() { if (loaded) { File f = getConfiguration(DEVICE).getFile(); if (f != null) { if (!f.exists()) { // Reset getConfiguration(DEVICE).setFile(NOFILE); getConfiguration(DEVICE).clear(); deviceConfs.remove(getId()); return false; } } return true; } return false; } public boolean isCustomized() { if (isValid()) { File f = getConfiguration(DEVICE).getFile(); return f != null && !f.equals(NOFILE); } return false; } public static File getDeviceDir() { return deviceDir; } public static void loadDeviceConfigurations(PmsConfiguration pmsConf) { deviceConfs = new HashMap<>(); xref = new HashMap<>(); deviceDir = new File(pmsConf.getProfileDirectory(), "renderers"); if (deviceDir.exists()) { LOGGER.info("Loading device configurations from " + deviceDir.getAbsolutePath()); File[] files = deviceDir.listFiles(); Arrays.sort(files); for (File f : files) { if (f.getName().endsWith(".conf")) { loadDeviceFile(f, createPropertiesConfiguration()); PMS.getFileWatcher().add(new FileWatcher.Watch(f.getPath(), reloader)); } } } } public static String[] loadDeviceFile(File f, PropertiesConfiguration conf) { String filename = f.getName(); try { conf.load(f); String s = conf.getString(DEVICE_ID, ""); if (s.isEmpty() && conf.containsKey("device")) { // Backward compatibility s = conf.getString("device", ""); } String[] ids = s.split("\\s*,\\s*"); for (String id : ids) { if (StringUtils.isNotBlank(id)) { deviceConfs.put(id, conf); LOGGER.info("Loaded device configuration {} for {}", filename, id); } } return ids; } catch (ConfigurationException ce) { LOGGER.info("Error loading device configuration: " + f.getAbsolutePath()); } return null; } public static int getDeviceUpnpMode(String id) { return getDeviceUpnpMode(id, false); } public static int getDeviceUpnpMode(String id, boolean store) { int mode = deviceConfs.containsKey(id) ? getUpnpMode(deviceConfs.get(id).getString(UPNP_ALLOW, "true")) : ALLOW; if (store && id.startsWith("uuid:")) { crossReference(id, UPNPHelper.getAddress(id)); } return mode; } public static void crossReference(String uuid, InetAddress ia) { // FIXME: this assumes one device per address String address = ia.toString().substring(1); xref.put(address, uuid); xref.put(uuid, address); if (deviceConfs.containsKey(uuid)) { deviceConfs.put(address, deviceConfs.get(uuid)); } } public static String getUuidOf(InetAddress ia) { // FIXME: this assumes one device per address return ia != null ? xref.get(ia.toString().substring(1)) : null; } public static File createDeviceFile(DeviceConfiguration r, String filename, boolean load) { File file = null; try { if (StringUtils.isBlank(filename)) { filename = getDefaultFilename(r); } else if (!filename.endsWith(".conf")) { filename += ".conf"; } file = new File(deviceDir, filename); ArrayList<String> conf = new ArrayList<>(); // Add the header and device id conf.add("#----------------------------------------------------------------------------"); conf.add("# Custom Device profile"); conf.add("# See DefaultRenderer.conf for descriptions of all possible renderer options"); conf.add("# and UMS.conf for program options."); conf.add(""); conf.add("# Options in this file override the default settings for the specific " + r.getSimpleName(r) + " device(s) listed below."); conf.add("# Specify devices by uuid (or address if no uuid), separated by commas if more than one."); conf.add(""); conf.add(DEVICE_ID + " = " + r.getId()); FileUtils.writeLines(file, "utf-8", conf, "\r\n"); if (load) { loadDeviceFile(file, r.getConfiguration(DEVICE)); } } catch (IOException ie) { LOGGER.debug("Error creating device configuration file: " + ie); } return file; } public static ArrayList<RendererConfiguration> getInheritors(RendererConfiguration renderer) { ArrayList<RendererConfiguration> devices = new ArrayList<>(); RendererConfiguration ref = (renderer instanceof DeviceConfiguration) ? ((DeviceConfiguration)renderer).ref : renderer; for (RendererConfiguration r : getConnectedRenderersConfigurations()) { if ((r instanceof DeviceConfiguration) && ((DeviceConfiguration)r).ref == ref) { devices.add(r); } } return devices; } /** * Automatic reloading */ public static final FileWatcher.Listener reloader = new FileWatcher.Listener() { @Override public void notify(String filename, String event, FileWatcher.Watch watch, boolean isDir) { File f = new File(filename); PropertiesConfiguration conf = null; HashSet<String> ids = new HashSet<>(); for (Iterator<String> iterator = deviceConfs.keySet().iterator(); iterator.hasNext();) { String id = iterator.next(); PropertiesConfiguration c = deviceConfs.get(id); if (c.getFile().equals(f)) { ids.add(id); conf = c; iterator.remove(); } } conf.clear(); ids.addAll(Arrays.asList(loadDeviceFile(f, conf))); for (RendererConfiguration r : getConnectedRenderersConfigurations()) { if ((r instanceof DeviceConfiguration) && ids.contains(((DeviceConfiguration)r).getId())) { r.reset(); } } } }; }